Add GPU/macvlan passthrough, container edit/update, image prune/tag

Create Container form:
- GPU passthrough dropdown (AMD/Intel via /dev/dri detection, NVIDIA
  excluded since it needs a different runtime) - device paths strictly
  validated server-side against the host's own detected list.
- Macvlan network support: selecting a macvlan network reveals a static
  IP field and hides port mappings (meaningless once the container has
  its own LAN address), matching Unraid Docker Manager's "Custom: br0"
  behavior. Networks panel gained a matching macvlan network-creation
  flow, with the parent-interface dropdown read from Unraid's own
  network.cfg so it lists exactly what Docker Manager itself offers.

Containers panel:
- Edit: reopens the create form pre-filled from the container's current
  config (image/ports/volumes/env/network/restart policy/GPU/static IP);
  saving stops+removes the old container and recreates it under the same
  settings, since podman/Docker have no in-place "modify" API for most of
  this.
- Update: same stop/remove/recreate flow, but pulls the current image
  first. "Check for Updates" compares each in-use image's local digest
  against its origin registry (Docker Hub/GHCR/self-hosted registries all
  verified live) with no podman-side feature backing it - implemented via
  the registry's own HTTP API. A small log-modal shows progress for both
  actions instead of a silent wait.
- Fixed a real bug hit live: PodmanClient's flat 15s HTTP timeout aborted
  real image pulls/container creates mid-request; bumped to 600s (nginx
  already allows up to 640s for this plugin's requests).

Images panel:
- "Prune unused" (removes every image with zero containers referencing
  it, not just dangling ones - confirmation copy says so explicitly since
  this is more aggressive than it sounds) and per-image "Tag".

Also several real UI bugs found via live screenshots: unused-image prune
having no visible effect until reloaded, table action-button columns
drifting row to row (a bare "display:flex" on a <td> was fighting the
table layout algorithm), Templates category badges dumping raw multi-tag
strings from real Unraid templates, and low-contrast search/filter
controls that were nearly invisible against the card background.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 18:34:57 +00:00
co-authored by Claude Sonnet 5
parent 8ac9cde621
commit ca62577a8b
13 changed files with 1158 additions and 70 deletions
+60 -8
View File
@@ -18,7 +18,9 @@
'<td class="tnum">' + P.escapeHtml(img.sizeFormatted) + '</td>' +
'<td class="tnum">' + created + '</td>' +
'<td class="tnum">' + img.usedBy + '</td>' +
'<td class="podman-actions"><div class="podman-actions-row"><button class="podman-btn podman-btn-icon" data-action="remove"' +
'<td class="podman-actions"><div class="podman-actions-row">' +
'<button class="podman-btn podman-btn-icon" data-action="tag" title="Add tag">&#127991;</button>' +
'<button class="podman-btn podman-btn-icon" data-action="remove"' +
(img.usedBy > 0 ? ' disabled title="In use by a container"' : ' title="Remove"') + '>&#128465;</button></div></td>' +
'</tr>';
}
@@ -55,16 +57,66 @@
});
});
P.el('images-prune-btn').addEventListener('click', function () {
// Computed client-side from the list already on screen — no extra
// round trip needed, and it lets the confirm() be specific instead
// of a generic warning. "Unused" here matches libpod's own
// definition (zero containers, running or stopped, referencing the
// image) — the same "Used By" count already shown in the table, not
// just dangling/untagged images. Found live that this can be far
// more aggressive than expected: with no containers at all, it
// removes every image on the host.
const unused = images.filter(function (img) { return img.usedBy === 0; });
if (!unused.length) {
alert('No unused images to remove — every image is referenced by at least one container.');
return;
}
const totalBytes = unused.reduce(function (sum, img) { return sum + img.sizeBytes; }, 0);
if (!confirm(
'Remove ' + unused.length + ' image(s) not used by any container (' + P.formatBytes(totalBytes) + ')?\n\n' +
'This removes any tagged image with zero containers using it, not just dangling ones.'
)) return;
const btn = this;
btn.disabled = true;
P.post('images', 'prune').then(function (result) {
btn.disabled = false;
alert('Removed ' + result.removedCount + ' image(s), reclaimed ' + P.formatBytes(result.reclaimedBytes) + '.');
return load();
}).catch(function (err) {
btn.disabled = false;
alert('Prune failed: ' + err.message);
});
});
P.el('images-tbody').addEventListener('click', function (e) {
const btn = e.target.closest('button[data-action="remove"]');
const btn = e.target.closest('button[data-action]');
if (!btn || btn.disabled) return;
const id = btn.closest('tr').dataset.id;
if (!confirm('Remove this image?')) return;
btn.disabled = true;
P.post('images', 'remove', { id: id }).then(load).catch(function (err) {
alert('Remove failed: ' + err.message);
btn.disabled = false;
});
if (btn.dataset.action === 'tag') {
P.openFormModal({
title: 'Add Tag',
submitLabel: 'Add tag',
fields: [
{ name: 'repo', label: 'Repository', required: true, placeholder: 'my-registry.local/my-image' },
{ name: 'tag', label: 'Tag', placeholder: 'latest' },
],
onSubmit: function (values) {
return P.post('images', 'tag', { id: id, repo: values.repo, tag: values.tag || 'latest' }).then(load);
},
});
return;
}
if (btn.dataset.action === 'remove') {
if (!confirm('Remove this image?')) return;
btn.disabled = true;
P.post('images', 'remove', { id: id }).then(load).catch(function (err) {
alert('Remove failed: ' + err.message);
btn.disabled = false;
});
}
});
return load();