podman_asset_version() has never actually cache-busted anything, all session — found live: it kept returning '0' for every asset regardless of a hard browser reload OR a full php-fpm restart (both ruled out explicitly before looking further), which meant the mechanism itself was broken, not caching around it. Root cause: Unraid's PageBuilder runs every .page file's PHP through eval() (webGui/include/DefaultPageLayout/evalContent.php literally does `eval($evalContent)`), and __DIR__/__FILE__ inside eval()'d code resolve to the eval() CALL SITE's own directory, not to Podman.page's real location — a standard PHP gotcha. So `__DIR__ . $relPath` was always pointing at a nonexistent path under webGui/include/DefaultPageLayout/, is_file() always failed, and the function always fell back to '0'. Every "do a hard refresh" instruction given throughout this session worked only because Ctrl+Shift+R bypasses the browser's cache directly — completely unrelated to this (non-functional) query-string mechanism. Likely also the real explanation for containers getting stopped unexpectedly during live testing just now: a stale, mismatched cached JS/HTML combination made a "Start Podman" click actually run as a full restart cycle (confirmed via plugin.log: a complete, uninterrupted stop-then-start at that exact timestamp) — restarted manually afterward. Fixed by hardcoding /usr/local/emhttp/plugins/podman instead of __DIR__ — consistent with the rest of the codebase already assuming this exact install path elsewhere (include/Config.php, plugin/podman.plg's event hook paths). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
353 lines
18 KiB
Plaintext
353 lines
18 KiB
Plaintext
Menu="Tasks:66"
|
|
Type="xmenu"
|
|
Tabs="false"
|
|
Title="Podman"
|
|
Icon="podman"
|
|
---
|
|
<?php
|
|
/**
|
|
* Podman.page — Unraid Dynamix-style GUI page for unraid-podman.
|
|
*
|
|
* This file only renders the page shell: the header, the sub-navigation
|
|
* for the ten sections, and one (mostly empty) container per panel. Every
|
|
* panel fills itself in via AJAX after load — see javascript/app.js's
|
|
* router and each javascript/<panel>.js module — so this file has no
|
|
* business logic of its own and does not talk to PodmanClient directly.
|
|
* See docs/ARCHITECTURE.md section 18 for the overall WebUI design and
|
|
* webui/mockups/prototype.html for the approved visual reference this
|
|
* page's markup mirrors (same structure, same CSS classes, real data
|
|
* instead of static samples).
|
|
*/
|
|
|
|
/**
|
|
* Cache-busts every static asset with its own on-disk mtime. Unraid's
|
|
* webserver sends no explicit no-cache headers for /plugins/ static
|
|
* files, so without this, browsers can keep serving a stale app.js/
|
|
* podman.css for a long time after a plugin update.
|
|
*
|
|
* Deliberately a hardcoded absolute path, NOT __DIR__ — Unraid's own
|
|
* PageBuilder runs every .page file's PHP through eval() (see
|
|
* webGui/include/DefaultPageLayout/evalContent.php: "eval($evalContent)"),
|
|
* and __DIR__/__FILE__ inside eval()'d code resolve to the eval() CALL
|
|
* SITE (that file's own directory), not to Podman.page's real location —
|
|
* a standard PHP eval() gotcha. Confirmed live: this made
|
|
* podman_asset_version() return '0' for every single asset, always,
|
|
* completely independent of any browser or PHP-FPM caching (ruled both
|
|
* out first: neither a hard-reload nor a php-fpm restart changed the
|
|
* result) — the version query string was never actually cache-busting
|
|
* anything all session; every "hard refresh fixed it" moment was the
|
|
* browser's own Ctrl+Shift+R bypass, not this mechanism. The rest of
|
|
* this codebase already hardcodes this same install path elsewhere
|
|
* (e.g. include/Config.php's $bootDir, plugin/podman.plg's event hook
|
|
* paths) — Unraid plugins always install to /usr/local/emhttp/plugins/
|
|
* <name>, so this isn't a new class of fragility.
|
|
*/
|
|
function podman_asset_version(string $relPath): string
|
|
{
|
|
$full = '/usr/local/emhttp/plugins/podman' . $relPath;
|
|
return is_file($full) ? (string) filemtime($full) : '0';
|
|
}
|
|
?>
|
|
<link rel="stylesheet" type="text/css" href="/plugins/podman/styles/podman.css?v=<?=podman_asset_version('/styles/podman.css')?>">
|
|
|
|
<div class="podman-plugin">
|
|
|
|
<header class="podman-pagehead">
|
|
<div class="titlewrap">
|
|
<div class="icon" aria-hidden="true">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 2l8 4.5v9L12 20l-8-4.5v-9L12 2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M12 11l8-4.5M12 11v9M12 11L4 6.5" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
|
|
</div>
|
|
<div>
|
|
<h1>Podman</h1>
|
|
<div class="meta" id="podman-header-meta">Connecting…</div>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex; gap:8px;">
|
|
<button class="podman-btn" id="podman-refresh-all">↻ Refresh</button>
|
|
</div>
|
|
</header>
|
|
|
|
<nav class="podman-subnav">
|
|
<button class="active" data-panel="dashboard">Dashboard</button>
|
|
<button data-panel="containers">Containers</button>
|
|
<button data-panel="templates">Templates</button>
|
|
<button data-panel="pods">Pods</button>
|
|
<button data-panel="images">Images</button>
|
|
<button data-panel="volumes">Volumes</button>
|
|
<button data-panel="networks">Networks</button>
|
|
<button data-panel="logs">Logs</button>
|
|
<button data-panel="terminal">Terminal</button>
|
|
<button data-panel="compose">Compose</button>
|
|
<button data-panel="settings">Settings</button>
|
|
</nav>
|
|
|
|
<main class="podman-main">
|
|
|
|
<!-- ============================= DASHBOARD ============================= -->
|
|
<section class="podman-panel active" id="podman-panel-dashboard">
|
|
<div class="podman-grid podman-stat-grid">
|
|
<div class="podman-stat"><div class="label">Running</div><div class="value tnum"><span id="stat-running">—</span> <small id="stat-running-total"></small></div></div>
|
|
<div class="podman-stat"><div class="label">Pods</div><div class="value tnum" id="stat-pods">—</div></div>
|
|
<div class="podman-stat"><div class="label">Images</div><div class="value tnum" id="stat-images">—</div></div>
|
|
<div class="podman-stat"><div class="label">Volumes</div><div class="value tnum" id="stat-volumes">—</div></div>
|
|
<div class="podman-stat"><div class="label">Networks</div><div class="value tnum" id="stat-networks">—</div></div>
|
|
<div class="podman-stat"><div class="label">Images on Disk</div><div class="value tnum" id="stat-images-size">—</div></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= CONTAINERS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-containers">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<input class="podman-search" id="containers-search" type="text" placeholder="Search containers by name or image…">
|
|
<div class="podman-segmented" id="containers-filterset">
|
|
<button class="active" data-filter="all" id="containers-count-all">All</button>
|
|
<button data-filter="running" id="containers-count-running">Running</button>
|
|
<button data-filter="stopped" id="containers-count-stopped">Stopped</button>
|
|
</div>
|
|
<button class="podman-btn podman-btn-primary" id="containers-check-updates-btn" style="margin-left:auto;">Check for Updates</button>
|
|
<button class="podman-btn podman-btn-primary" id="containers-update-all-btn">Update All</button>
|
|
<button class="podman-btn podman-btn-primary" id="containers-create-btn">+ New Container</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Status</th><th>Name</th><th>Image</th><th>CPU/Mem</th><th>Ports</th><th>Uptime</th><th></th></tr></thead>
|
|
<tbody id="containers-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= TEMPLATES ============================= -->
|
|
<section class="podman-panel" id="podman-panel-templates"></section>
|
|
|
|
<!-- ============================= PODS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-pods"></section>
|
|
|
|
<!-- ============================= IMAGES ============================= -->
|
|
<section class="podman-panel" id="podman-panel-images">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<input class="podman-search" type="text" placeholder="Search images…" disabled title="Client-side filtering not yet wired up for Images">
|
|
<button class="podman-btn" id="images-prune-btn" style="margin-left:auto;">Prune unused</button>
|
|
<button class="podman-btn" id="images-pull-btn">⬇ Pull Image</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Repository</th><th>Tag</th><th>Image ID</th><th>Size</th><th>Created</th><th>Used By</th><th></th></tr></thead>
|
|
<tbody id="images-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= VOLUMES ============================= -->
|
|
<section class="podman-panel" id="podman-panel-volumes">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<div style="flex:1;"></div>
|
|
<button class="podman-btn podman-btn-primary" id="volumes-create-btn">+ New Volume</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Name</th><th>Driver</th><th>Mountpoint</th><th>Used By</th><th></th></tr></thead>
|
|
<tbody id="volumes-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="podman-empty-note" style="border-top:1px solid var(--border); text-align:left; padding:12px 18px;">
|
|
App data mapped from Unraid shares (e.g. <span class="mono">/mnt/user/appdata/…</span>) is managed as bind mounts per-container and does not appear here.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= NETWORKS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-networks">
|
|
<div class="podman-card">
|
|
<div class="podman-toolbar">
|
|
<div style="flex:1;"></div>
|
|
<button class="podman-btn podman-btn-primary" id="networks-create-btn">+ New Network</button>
|
|
</div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>Name</th><th>Driver</th><th>Subnet</th><th>Gateway</th><th>Containers</th><th></th></tr></thead>
|
|
<tbody id="networks-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= LOGS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-logs">
|
|
<div class="podman-card">
|
|
<div class="podman-logs-layout">
|
|
<div class="podman-logs-side" id="logs-sidebar"></div>
|
|
<div>
|
|
<div class="podman-toolbar">
|
|
<input class="podman-search" id="logs-filter" type="text" placeholder="Filter log output…" style="max-width:280px;">
|
|
<span class="podman-segmented" id="logs-follow-toggle">
|
|
<button class="active" data-follow="true">Follow</button>
|
|
<button data-follow="false">Paused</button>
|
|
</span>
|
|
</div>
|
|
<div class="podman-log-pane" id="log-pane" role="log"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= TERMINAL ============================= -->
|
|
<section class="podman-panel" id="podman-panel-terminal">
|
|
<div class="podman-card">
|
|
<div class="podman-card-head"><h2>Live Terminal</h2></div>
|
|
<div class="podman-card-pad">
|
|
<div class="podman-term-launcher">
|
|
<label>Container <select class="podman-term-select" id="term-container-select"></select></label>
|
|
<label>Shell
|
|
<select class="podman-term-select" id="term-shell-select">
|
|
<option value="bash" selected>bash</option>
|
|
<option value="sh">sh</option>
|
|
</select>
|
|
</label>
|
|
<button class="podman-btn podman-btn-primary" id="term-open-btn">▶ Open Terminal</button>
|
|
<button class="podman-btn podman-btn-ghost podman-btn-danger" id="term-disconnect-btn" disabled>■ Disconnect</button>
|
|
</div>
|
|
<div id="term-frame-wrap">
|
|
<p class="podman-empty-note">Pick a running container and click "Open Terminal" — the same live, fully interactive terminal Unraid's own Docker "Console" button opens (arrow-key history, tab completion, vim, etc. all work).</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= COMPOSE ============================= -->
|
|
<section class="podman-panel" id="podman-panel-compose">
|
|
<div class="podman-card">
|
|
<div class="podman-compose-layout">
|
|
<div class="podman-compose-side">
|
|
<div class="podman-toolbar" style="border-bottom:1px solid var(--border); padding:10px;">
|
|
<button class="podman-btn podman-btn-primary" id="compose-new-btn" style="width:100%; justify-content:center;">+ New Project</button>
|
|
</div>
|
|
<div id="compose-sidebar"></div>
|
|
</div>
|
|
<div>
|
|
<div class="podman-toolbar">
|
|
<strong id="compose-title" style="flex:1;">—</strong>
|
|
<button class="podman-btn podman-btn-ghost podman-btn-danger" id="compose-action-delete">Delete</button>
|
|
<button class="podman-btn" id="compose-action-pull">⬇ Pull</button>
|
|
<button class="podman-btn" id="compose-action-down">■ Down</button>
|
|
<button class="podman-btn podman-btn-primary" id="compose-action-up">▶ Up</button>
|
|
<button class="podman-btn podman-btn-primary" id="compose-action-save">Save</button>
|
|
</div>
|
|
<textarea class="podman-yaml podman-yaml-editor mono" id="compose-yaml" spellcheck="false"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================= SETTINGS ============================= -->
|
|
<section class="podman-panel" id="podman-panel-settings">
|
|
<div class="podman-settings-actions">
|
|
<span class="hint" id="settings-save-hint">Changes to storage/enabled/timeout need <span class="mono">rc.podman restart</span> to take effect.</span>
|
|
<button class="podman-btn podman-btn-primary" id="settings-save-btn">Save Settings</button>
|
|
</div>
|
|
<div class="podman-grid">
|
|
<div class="podman-card">
|
|
<div class="podman-card-head">
|
|
<div><h2>Podman Service</h2><div class="sub">Start, stop, restart, or check the podman.sock backend — no terminal needed.</div></div>
|
|
</div>
|
|
<div class="podman-card-pad">
|
|
<div class="podman-service-row">
|
|
<span class="podman-chip podman-chip-neutral" id="settings-service-chip"><span class="d"></span>Checking…</span>
|
|
<button class="podman-btn" id="settings-service-status-btn">Refresh Status</button>
|
|
<button class="podman-btn podman-btn-primary" id="settings-service-start-btn">▶ Start Podman</button>
|
|
<button class="podman-btn podman-btn-ghost podman-btn-danger" id="settings-service-stop-btn">■ Stop Podman</button>
|
|
<button class="podman-btn" id="settings-service-restart-btn">↻ Restart Podman</button>
|
|
</div>
|
|
<div class="hint">Stop/Restart first stop all running containers (each gets its own configured grace period) — not just the API service.</div>
|
|
<div class="podman-log-pane" id="settings-service-log" style="display:none;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="podman-card">
|
|
<div class="podman-card-head">
|
|
<div><h2>Storage</h2><div class="sub">Where podman keeps images, containers and volumes on disk.</div></div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-storage-path">Storage path</label>
|
|
<div>
|
|
<input type="text" id="settings-storage-path" class="mono">
|
|
<div class="hint">Cache pool or dedicated disk — never a path under /mnt/user (FUSE).</div>
|
|
</div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label>No cache pool?</label>
|
|
<div>
|
|
<button class="podman-btn" id="settings-format-disk-btn">Format a Disk for Podman Storage…</button>
|
|
<div class="hint">Formats an unused disk with XFS and mounts it, for a single-disk system with no cache pool set up yet.</div>
|
|
</div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-storage-size">podman.img size</label>
|
|
<div>
|
|
<div class="podman-input-suffix"><input type="number" id="settings-storage-size" min="1"> <span>GB</span></div>
|
|
<div class="hint">Overlay filesystem image size. Only applies the first time podman initializes storage at this path.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="podman-card">
|
|
<div class="podman-card-head">
|
|
<div><h2>Autostart & Lifecycle</h2><div class="sub">What runs when the array starts, and how containers shut down.</div></div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-enabled">Start podman on array start</label>
|
|
<div>
|
|
<label class="podman-switch">
|
|
<input type="checkbox" id="settings-enabled"><span class="podman-switch-track"><span class="podman-switch-thumb"></span></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label for="settings-stop-timeout">Container stop timeout</label>
|
|
<div>
|
|
<div class="podman-input-suffix"><input type="number" id="settings-stop-timeout" min="0"> <span>seconds</span></div>
|
|
<div class="hint">Grace period before a stop/restart escalates to SIGKILL.</div>
|
|
</div>
|
|
</div>
|
|
<div class="podman-field-row">
|
|
<label>Autostart order</label>
|
|
<div>
|
|
<div class="podman-table-wrap">
|
|
<table>
|
|
<thead><tr><th>#</th><th>Container</th><th></th></tr></thead>
|
|
<tbody id="autostart-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="hint">Saved immediately on reorder/remove — no separate save step.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="podman-card">
|
|
<div class="podman-card-head">
|
|
<div><h2>Installed Packages</h2><div class="sub">Versions currently installed on this system.</div></div>
|
|
</div>
|
|
<div class="podman-card-pad">
|
|
<div class="podman-version-chips" id="settings-package-versions">—</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<?php
|
|
foreach ([
|
|
'app', 'dashboard', 'containers', 'templates', 'pods', 'images', 'volumes',
|
|
'networks', 'logs', 'terminal', 'compose', 'settings',
|
|
] as $podmanJsModule) {
|
|
$podmanJsPath = "/javascript/{$podmanJsModule}.js";
|
|
echo '<script src="/plugins/podman' . $podmanJsPath . '?v=' . podman_asset_version($podmanJsPath) . '"></script>' . "\n";
|
|
}
|
|
?>
|