Rework Dashboard, add toasts, container folders/icons/WebUI links, detail tabs
Lint / ShellCheck (push) Successful in 11s
Lint / Validate .plg XML (push) Successful in 10s
Lint / EditorConfig (push) Successful in 6s

Dashboard:
- Plain-count stat tiles (Running/Stopped/Pods/Images/Volumes/Networks)
  separated from a single "Resource Usage" card (CPU/Memory/Swap/Storage
  meter rows) instead of forcing both into one tile grid, which produced
  awkward spanning-tile/dead-cell layouts.
- Fixed CPU usage never changing (libpod's own cpuUtilization is computed
  once and never resampled) by computing it from /proc/stat deltas instead.
- Fixed memory usage reading far too high by using /proc/meminfo's
  MemAvailable instead of libpod's raw (non-reclaimable-aware) memFree.
- Added an Autostart Queue table reusing podman-autostart.sh's own
  failure-counter files.
- Dashboard and Containers now auto-refresh every ~2s (paused when the
  tab is hidden or a modal is open).

Toasts:
- Real success/warn/error/info toast notifications replacing every
  alert() used for one-way feedback, across every panel.

Container detail modal:
- 5 new tabs: Resources, Logs, Console, Events, Healthcheck.

Containers panel:
- Folders to group containers (name + icon), stored in the plugin's own
  folders.json — a folder's header always shows an icon+name+status chip
  per member, matching Unraid's own Docker page folders. "Move to
  Folder" becomes "Remove from Folder" once a container is already
  grouped.
- Containers can carry an icon URL and a WebUI URL (small button next to
  the name), both stored as container labels and auto-filled from
  templates where applicable.
- Settings: an "Add container" control for the Autostart order table.

Fixes:
- Context menus now measure their own rendered size and flip above the
  anchor when there isn't room below, instead of running off-screen.
- Containers table now uses table-layout:fixed with explicit column
  widths — auto layout was shifting every column (and the header) on
  every folder expand/collapse, and briefly again when a flex wrapper
  was mistakenly placed directly on a <td>.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 21:32:34 +00:00
co-authored by Claude Sonnet 5
parent 1ca78e7115
commit 23898ff62e
20 changed files with 1416 additions and 100 deletions
+101 -3
View File
@@ -126,6 +126,60 @@ window.Podman = (function () {
return '<tr><td colspan="' + colspan + '" class="podman-error">' + escapeHtml(message) + '</td></tr>';
}
// --- Toast notifications ----------------------------------------------------
//
// Replaces alert() for one-way feedback ("Saved.", "Removed 3 image(s)",
// "Save failed: ..."). Confirmations stay native confirm() — a toast is
// for telling the user something happened, not for asking them a
// yes/no question. Command output that can run to hundreds of lines
// (e.g. `podman compose up`) stays in the existing openLogModal()
// pattern instead — a toast has to stay short and auto-dismiss, which
// doesn't fit a scrolling log.
const TOAST_ICON = { success: '✓', warn: '!', error: '✕', info: '' };
const TOAST_DURATION_MS = { success: 4000, warn: 5000, error: 7000, info: 4000 };
let toastContainer = null;
function toastRoot() {
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'podman-toast-container';
(document.querySelector('.podman-plugin') || document.body).appendChild(toastContainer);
}
return toastContainer;
}
/**
* @param {string} message
* @param {('success'|'warn'|'error'|'info')} [type='info']
*/
function toast(message, type) {
const kind = TOAST_ICON[type] ? type : 'info';
const root = toastRoot();
const node = document.createElement('div');
node.className = 'podman-toast podman-toast-' + kind;
node.innerHTML =
'<span class="ico">' + TOAST_ICON[kind] + '</span>' +
'<span class="msg"></span>' +
'<button type="button" class="close" aria-label="Dismiss">&#10005;</button>';
node.querySelector('.msg').textContent = message;
root.appendChild(node);
let dismissed = false;
function dismiss() {
if (dismissed) return;
dismissed = true;
node.classList.add('leaving');
// Not animationend — that never fires when prefers-reduced-motion
// disables the animation (podman.css sets animation:none for it),
// which would leave the toast stuck on screen forever.
setTimeout(function () { node.remove(); }, 160);
}
node.querySelector('.close').addEventListener('click', dismiss);
setTimeout(dismiss, TOAST_DURATION_MS[kind]);
}
// --- Modal form dialog -----------------------------------------------------
/**
@@ -299,12 +353,32 @@ window.Podman = (function () {
(item.disabled ? ' disabled' : '') + '>' + escapeHtml(item.label) + '</button>';
}).join('');
(document.querySelector('.podman-plugin') || document.body).appendChild(menu);
// Viewport-relative (see the "position: fixed" comment on
// .podman-context-menu in podman.css) — no scrollY/scrollX added.
// Measured AFTER appending (not assumed) since the menu's height
// varies with its item count — a long menu (e.g. a container's row
// menu with Details/Pause/Kill/Rename/Edit/Remove) opened from a row
// near the bottom of the viewport used to run off-screen with no way
// to reach its last few items. Flips above the anchor instead when
// there isn't enough room below, and clamps horizontally the same way.
const rect = anchorEl.getBoundingClientRect();
menu.style.top = (rect.bottom + 4) + 'px';
menu.style.left = (rect.right - 180) + 'px';
(document.querySelector('.podman-plugin') || document.body).appendChild(menu);
const menuRect = menu.getBoundingClientRect();
const margin = 8;
let top = rect.bottom + 4;
if (top + menuRect.height > window.innerHeight - margin) {
top = rect.top - menuRect.height - 4;
}
top = Math.max(margin, top);
let left = rect.right - menuRect.width;
left = Math.min(left, window.innerWidth - menuRect.width - margin);
left = Math.max(margin, left);
menu.style.top = top + 'px';
menu.style.left = left + 'px';
// menu.children includes the separator <div>s too, so indexing into it
// directly (by a counter that only advances for real items) drifts by
@@ -399,6 +473,29 @@ window.Podman = (function () {
// (Dashboard, by default — see Podman.page).
const initial = document.querySelector('.podman-subnav button.active');
activatePanel(initial ? initial.dataset.panel : 'dashboard');
setInterval(autoRefreshTick, AUTO_REFRESH_INTERVAL_MS);
}
// Only panels that opt in via `autoRefresh: true` (Dashboard, Containers)
// get polled — most panels (Settings, Compose, Terminal, ...) have
// in-progress forms or connections an unexpected refresh would disrupt.
// Paused while the tab is hidden (nothing to look at) and while any
// modal is open (a full-panel re-render mid-edit would be jarring),
// rather than fighting those cases with more state.
const AUTO_REFRESH_INTERVAL_MS = 2000;
function autoRefreshTick() {
if (document.hidden) return;
if (document.querySelector('.podman-modal-backdrop')) return;
const activeBtn = document.querySelector('.podman-subnav button.active');
if (!activeBtn) return;
const name = activeBtn.dataset.panel;
const module = panelModules[name];
if (module && module.autoRefresh && initialized[name] && typeof module.refresh === 'function') {
module.refresh();
}
}
document.addEventListener('DOMContentLoaded', boot);
@@ -414,6 +511,7 @@ window.Podman = (function () {
stateChipClass: stateChipClass,
loadingRow: loadingRow,
errorRow: errorRow,
toast: toast,
openFormModal: openFormModal,
openLogModal: openLogModal,
openContextMenu: openContextMenu,