Files
unraid-podman/webui/plugins/podman/javascript/settings.js
T
maggesandClaude Sonnet 5 7f6fcb9166
Build Packages / Build .txz packages (push) Successful in 9m17s
Lint / ShellCheck (push) Successful in 12s
Lint / Validate .plg XML (push) Successful in 13s
Lint / EditorConfig (push) Successful in 6s
Fix real STORAGE_PATH bug; add Start Podman + format-disk from the WebUI
Root cause of a fresh-install "cannot reach the Podman API socket" report
(a friend's Unraid box, cache pool present and mounted): unlike
Docker-for-Unraid's docker.img path, this plugin never auto-created
STORAGE_PATH itself — only podman.img inside it. A perfectly normal,
already-mounted cache pool still failed preflight/storage-create with
"does not exist", just because its own .../system/podman subdirectory
had never been created. Fixed by walking up to the nearest existing
ancestor and checking whether it's on a different device than / (real
mount vs. nothing mounted at all) — see podman-common.sh's new
podman_path_has_real_mount_ancestor(), used by both podman-preflight.sh
and podman-storage.sh.

Settings gets a "Podman Service" card (status chip + Start/Restart,
backed by new ajax/settings.php service_status/start/restart actions
that just shell out to rc.podman) so a fresh install that failed to start
can be diagnosed and retried without SSH/terminal access at all — exactly
what was missing when this was first needed live.

Also adds "Format a Disk for Podman Storage" (new ajax/disks.php) for a
single-disk system with no cache pool at all. Only ever lists disks with
literally no existing partition/filesystem/RAID-or-ZFS-membership
signature and that aren't Unraid's boot flash — found live, twice, during
development: the boot USB (FAT, labeled "UNRAID") passed the initial
mounted-only check because this host's /boot is backed by a ZFS dataset
rather than a direct partition mount, and active RAID-member cache disks
passed a data-vs-blank *warning* rather than a hard exclusion. Both are
now excluded outright, not just flagged — see disks.php's
device_or_children_labeled_unraid() and the hasData exclusion in
list_candidate_disks(). A disk formatted this way is remounted by UUID on
every boot via a new plugin/sbin/podman-mount-managed-disk.sh, called
from plugin/event/disks_mounted before rc.podman start.

Unrelated fix bundled in: scripts/lib/slackbuild-common.sh now sets
SOURCE_DATE_EPOCH (derived from the repo's last commit) before calling
makepkg, so two separate builds of the same commit produce byte-identical
.txz files — makepkg already supports this (`--clamp-mtime` when
$SOURCE_DATE_EPOCH is set, confirmed by reading a real host's
/sbin/makepkg) but nothing was setting the variable, so release.yml's
"rebuild in CI and verify it matches the committed checksums" step was
guaranteed to fail on the first package it checked alphabetically
(observed live: aardvark-dns).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:55:14 +00:00

217 lines
9.8 KiB
JavaScript

/**
* javascript/settings.js
*
* Settings panel: reads/writes unraid-podman's own configuration via
* ajax/settings.php (podman.cfg + the autostart list) — not a
* PodmanClient/libpod concern, see that file's header comment.
*/
(function () {
'use strict';
const P = window.Podman;
let autostartNames = [];
function renderAutostart() {
const tbody = P.el('autostart-tbody');
tbody.innerHTML = autostartNames.length
? autostartNames.map(function (name, i) {
return '<tr data-index="' + i + '">' +
'<td class="tnum">' + (i + 1) + '</td>' +
'<td>' + P.escapeHtml(name) + '</td>' +
'<td class="podman-actions"><div class="podman-actions-row">' +
'<button class="podman-btn podman-btn-icon" data-action="up"' + (i === 0 ? ' disabled' : '') + ' title="Move up">&#8593;</button>' +
'<button class="podman-btn podman-btn-icon" data-action="down"' + (i === autostartNames.length - 1 ? ' disabled' : '') + ' title="Move down">&#8595;</button>' +
'<button class="podman-btn podman-btn-icon" data-action="remove" title="Remove from autostart">&#128465;</button>' +
'</div></td></tr>';
}).join('')
: '<tr><td colspan="3" class="podman-empty-note">No containers in the autostart chain.</td></tr>';
}
function saveAutostart() {
return P.post('settings', 'autostart_save', { names: autostartNames }).catch(function (err) {
alert('Could not save autostart order: ' + err.message);
});
}
function fillForm(settings) {
P.el('settings-storage-path').value = settings.storagePath;
P.el('settings-storage-size').value = settings.storageImageSizeGb;
P.el('settings-enabled').checked = settings.enabled;
P.el('settings-stop-timeout').value = settings.stopTimeoutSeconds;
autostartNames = settings.autostart.slice();
renderAutostart();
const versions = settings.packageVersions || {};
const order = ['PODMAN', 'CONMON', 'CRUN', 'NETAVARK', 'AARDVARK_DNS', 'PASST', 'FUSE_OVERLAYFS'];
const chipsHtml = order.map(function (k) {
const name = k.toLowerCase().replace(/_/g, '-');
const version = versions[k + '_INSTALLED_VERSION'];
return '<span class="podman-version-chip">' + P.escapeHtml(name) + ' <b>' + P.escapeHtml(version || '?') + '</b></span>';
}).join('');
P.el('settings-package-versions').innerHTML = chipsHtml || '<span class="podman-empty-note">No version manifest found.</span>';
}
function load() {
return P.get('settings', 'get').then(fillForm).catch(function (err) {
alert('Could not load settings: ' + err.message);
});
}
// --- Podman service status/start/restart ----------------------------------
function renderServiceResult(data) {
const chip = P.el('settings-service-chip');
chip.className = 'podman-chip ' + (data.running ? 'podman-chip-good' : 'podman-chip-bad');
chip.innerHTML = '<span class="d"></span>' + (data.running ? 'Running' : 'Not running');
const log = P.el('settings-service-log');
if (data.output) {
log.style.display = '';
log.textContent = data.output;
log.scrollTop = log.scrollHeight;
}
}
function refreshServiceStatus() {
const buttons = [P.el('settings-service-status-btn'), P.el('settings-service-start-btn'), P.el('settings-service-restart-btn')];
buttons.forEach(function (b) { b.disabled = true; });
return P.get('settings', 'service_status').then(renderServiceResult).catch(function (err) {
P.el('settings-service-chip').className = 'podman-chip podman-chip-bad';
P.el('settings-service-chip').innerHTML = '<span class="d"></span>Unknown';
P.el('settings-service-log').style.display = '';
P.el('settings-service-log').textContent = err.message;
}).finally(function () {
buttons.forEach(function (b) { b.disabled = false; });
});
}
function runServiceAction(action) {
const buttons = [P.el('settings-service-status-btn'), P.el('settings-service-start-btn'), P.el('settings-service-restart-btn')];
buttons.forEach(function (b) { b.disabled = true; });
P.el('settings-service-log').style.display = '';
P.el('settings-service-log').textContent = 'Working…';
return P.post('settings', action, {}).then(renderServiceResult).catch(function (err) {
P.el('settings-service-log').style.display = '';
P.el('settings-service-log').textContent = err.message;
}).finally(function () {
buttons.forEach(function (b) { b.disabled = false; });
});
}
// --- Format a disk for podman storage --------------------------------------
function openFormatDiskModal() {
const backdrop = document.createElement('div');
backdrop.className = 'podman-modal-backdrop';
backdrop.innerHTML = '' +
'<div class="podman-modal" role="dialog" aria-modal="true">' +
'<div class="podman-modal-head"><h3>Format a Disk for Podman Storage</h3></div>' +
'<div class="podman-modal-body">' +
'<div class="podman-modal-field"><label>Disk</label><select id="fd-device"><option value="">Loading…</option></select>' +
'<div class="hint" id="fd-warning">Only disks with no existing partitions or filesystem are listed — anything already in use, part of the array/cache, or the boot flash is never shown here.</div></div>' +
'<div class="podman-modal-field">' +
'<label style="display:flex; align-items:flex-start; gap:8px; font-weight:400;">' +
'<input type="checkbox" id="fd-confirm" style="margin-top:3px;">' +
'<span>I understand this permanently erases all data on this disk, with no undo.</span>' +
'</label></div>' +
'</div>' +
'<div class="podman-modal-actions">' +
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">Cancel</button>' +
'<button type="button" class="podman-btn podman-btn-ghost podman-btn-danger" data-role="submit" disabled>Format Disk</button>' +
'</div></div>';
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
const select = backdrop.querySelector('#fd-device');
const warning = backdrop.querySelector('#fd-warning');
const confirmBox = backdrop.querySelector('#fd-confirm');
const submitBtn = backdrop.querySelector('[data-role="submit"]');
function updateSubmitEnabled() {
submitBtn.disabled = !(select.value && confirmBox.checked);
}
P.get('disks', 'list_candidates').then(function (list) {
select.innerHTML = list.length
? list.map(function (d) {
return '<option value="' + P.escapeHtml(d.device) + '">' + P.escapeHtml(d.device) +
' — ' + P.escapeHtml(d.sizeFormatted) + (d.model ? ' (' + P.escapeHtml(d.model) + ')' : '') + '</option>';
}).join('')
: '<option value="">No eligible blank disks found</option>';
updateSubmitEnabled();
}).catch(function (err) {
select.innerHTML = '<option value="">Error loading disks</option>';
warning.textContent = err.message;
});
select.addEventListener('change', updateSubmitEnabled);
confirmBox.addEventListener('change', updateSubmitEnabled);
function close() { backdrop.remove(); }
submitBtn.addEventListener('click', function () {
if (!select.value || !confirmBox.checked) return;
if (!confirm('Format ' + select.value + '? This cannot be undone.')) return;
submitBtn.disabled = true;
submitBtn.textContent = 'Formatting…';
P.post('disks', 'format', { device: select.value }).then(function (data) {
close();
P.el('settings-storage-path').value = data.mountPath;
alert('Formatted and mounted at ' + data.mountPath + '. Storage path has been filled in below — click "Save Settings" to use it, then Restart Podman.');
}).catch(function (err) {
submitBtn.disabled = false;
submitBtn.textContent = 'Format Disk';
alert('Format failed: ' + err.message);
});
});
backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close);
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); });
}
function save() {
const body = {
storagePath: P.el('settings-storage-path').value.trim(),
storageImageSizeGb: parseInt(P.el('settings-storage-size').value, 10) || 20,
enabled: P.el('settings-enabled').checked,
stopTimeoutSeconds: parseInt(P.el('settings-stop-timeout').value, 10) || 10,
};
return P.post('settings', 'save', body).then(function () {
alert('Saved. Restart podman (rc.podman restart) to apply storage/enabled changes.');
}).catch(function (err) {
alert('Save failed: ' + err.message);
});
}
function init() {
P.el('settings-save-btn').addEventListener('click', save);
P.el('autostart-tbody').addEventListener('click', function (e) {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const i = parseInt(btn.closest('tr').dataset.index, 10);
const action = btn.dataset.action;
if (action === 'remove') {
autostartNames.splice(i, 1);
} else if (action === 'up' && i > 0) {
[autostartNames[i - 1], autostartNames[i]] = [autostartNames[i], autostartNames[i - 1]];
} else if (action === 'down' && i < autostartNames.length - 1) {
[autostartNames[i + 1], autostartNames[i]] = [autostartNames[i], autostartNames[i + 1]];
}
renderAutostart();
saveAutostart();
});
P.el('settings-service-status-btn').addEventListener('click', refreshServiceStatus);
P.el('settings-service-start-btn').addEventListener('click', function () { runServiceAction('service_start'); });
P.el('settings-service-restart-btn').addEventListener('click', function () { runServiceAction('service_restart'); });
P.el('settings-format-disk-btn').addEventListener('click', openFormatDiskModal);
refreshServiceStatus();
return load();
}
P.registerPanel('settings', { init: init, refresh: load });
})();