Add reproducible build system, native Unraid plugin, and WebUI
- versions.env pins podman, conmon, crun, netavark, aardvark-dns, passt, and fuse-overlayfs to verified upstream source checksums; SlackBuild recipes, scripts/build-packages.sh, checksums.sh, release.sh, and update-versions.sh implement the reproducible pipeline; GitHub Actions workflows build in a Slackware container and publish releases without committing any binaries. - plugin/podman.plg installs/updates/removes all eight packages (the seven components plus the plugin's own unraid-podman scaffolding package) via upgradepkg, using the official Unraid array-event hook mechanism (event/disks_mounted, event/stopping) instead of editing /boot/config/go. rc.podman and the sbin/ helper scripts implement storage creation, config seeding/sync, preflight checks, autostart with per-container Safe-Mode, and package verify/update/rollback. - webui/plugins/podman implements the Dashboard, Containers, Pods, Images, Volumes, Networks, Logs, Terminal, Compose, and Settings panels against the approved mockup (webui/mockups/prototype.html), talking to podman system service exclusively via PodmanClient.php (libpod REST API over the Unix socket), with two documented exceptions: Terminal's one-shot exec model and Compose's use of the podman compose CLI, since libpod has no REST equivalent for either. - docs/ARCHITECTURE.md and docs/ROADMAP.md record the design decisions and honest current status (syntax-checked, unit- and integration-tested against fake sockets/servers; not yet run against a real Unraid/Podman/Slackware system). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* javascript/app.js
|
||||
*
|
||||
* Shared runtime for the Podman plugin page: the AJAX helper every panel
|
||||
* module uses to talk to webui/plugins/podman/ajax/*.php, small DOM
|
||||
* utilities to avoid repeating the same escaping/formatting logic in ten
|
||||
* places, and the sub-tab router that shows/hides panels and lazily
|
||||
* initializes each one's module the first time it's opened.
|
||||
*
|
||||
* Loaded first (before any panel module) — see Podman.page.
|
||||
*/
|
||||
window.Podman = (function () {
|
||||
'use strict';
|
||||
|
||||
const BASE = '/plugins/podman/ajax/';
|
||||
|
||||
/**
|
||||
* Calls one ajax/<file>.php?action=<action> endpoint and resolves with
|
||||
* response.data, or rejects with an Error carrying the server's message
|
||||
* — every ajax/*.php endpoint replies with the same {ok, data|error}
|
||||
* envelope (see include/helpers.php's podman_json_response/_error), so
|
||||
* this one function is the only place that envelope shape is known.
|
||||
*
|
||||
* @param {string} file e.g. "containers"
|
||||
* @param {string} action e.g. "list"
|
||||
* @param {('GET'|'POST')} method
|
||||
* @param {object|null} body sent as JSON for POST
|
||||
* @param {object} query extra query-string params (e.g. {id: "..."})
|
||||
*/
|
||||
function call(file, action, method, body, query) {
|
||||
const params = new URLSearchParams(Object.assign({ action: action }, query || {}));
|
||||
const url = BASE + file + '.php?' + params.toString();
|
||||
|
||||
const opts = { method: method, headers: {} };
|
||||
if (body !== undefined && body !== null) {
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return fetch(url, opts)
|
||||
.then(function (res) {
|
||||
return res.json().then(function (envelope) {
|
||||
if (!envelope.ok) {
|
||||
throw new Error(envelope.error || ('Request failed (' + res.status + ')'));
|
||||
}
|
||||
return envelope.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function get(file, action, query) {
|
||||
return call(file, action, 'GET', null, query);
|
||||
}
|
||||
|
||||
function post(file, action, body) {
|
||||
return call(file, action, 'POST', body || {}, {});
|
||||
}
|
||||
|
||||
// --- DOM helpers ---------------------------------------------------------
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes <= 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
const value = bytes / Math.pow(1024, i);
|
||||
return (value >= 100 || i === 0 ? value.toFixed(0) : value.toFixed(1)) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
if (seconds == null) return '—';
|
||||
if (seconds < 60) return seconds + 's';
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (days > 0) return days + 'd ' + hours + 'h';
|
||||
if (hours > 0) return hours + 'h ' + minutes + 'm';
|
||||
return minutes + 'm';
|
||||
}
|
||||
|
||||
function formatRelativeTime(unixSeconds) {
|
||||
if (!unixSeconds) return '—';
|
||||
const diff = Math.max(0, Math.floor(Date.now() / 1000) - unixSeconds);
|
||||
if (diff < 60) return 'just now';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
/** Status string (from libpod's container "State") -> chip color class. */
|
||||
function stateChipClass(state) {
|
||||
switch (state) {
|
||||
case 'running': return 'podman-chip-good';
|
||||
case 'paused': return 'podman-chip-warn';
|
||||
case 'exited':
|
||||
case 'created': return 'podman-chip-neutral';
|
||||
default: return 'podman-chip-bad';
|
||||
}
|
||||
}
|
||||
|
||||
function loadingRow(colspan, label) {
|
||||
return '<tr><td colspan="' + colspan + '" class="podman-loading">' + escapeHtml(label || 'Loading…') + '</td></tr>';
|
||||
}
|
||||
|
||||
function errorRow(colspan, message) {
|
||||
return '<tr><td colspan="' + colspan + '" class="podman-error">' + escapeHtml(message) + '</td></tr>';
|
||||
}
|
||||
|
||||
// --- Panel router ----------------------------------------------------------
|
||||
|
||||
const panelModules = {};
|
||||
const initialized = {};
|
||||
|
||||
/** Called by each panel's own JS file (e.g. containers.js) to register itself. */
|
||||
function registerPanel(name, module) {
|
||||
panelModules[name] = module;
|
||||
}
|
||||
|
||||
function activatePanel(name) {
|
||||
document.querySelectorAll('.podman-subnav button').forEach(function (btn) {
|
||||
btn.classList.toggle('active', btn.dataset.panel === name);
|
||||
});
|
||||
document.querySelectorAll('.podman-panel').forEach(function (panel) {
|
||||
panel.classList.toggle('active', panel.id === 'podman-panel-' + name);
|
||||
});
|
||||
|
||||
const module = panelModules[name];
|
||||
if (!module) return;
|
||||
|
||||
if (!initialized[name]) {
|
||||
initialized[name] = true;
|
||||
if (typeof module.init === 'function') module.init();
|
||||
} else if (typeof module.refresh === 'function') {
|
||||
module.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function boot() {
|
||||
const subnav = document.querySelector('.podman-subnav');
|
||||
if (!subnav) return;
|
||||
|
||||
subnav.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('button[data-panel]');
|
||||
if (btn) activatePanel(btn.dataset.panel);
|
||||
});
|
||||
|
||||
const refreshBtn = el('podman-refresh-all');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function () {
|
||||
const active = document.querySelector('.podman-subnav button.active');
|
||||
if (active) activatePanel(active.dataset.panel);
|
||||
});
|
||||
}
|
||||
|
||||
// Activate whichever panel is marked active in the initial HTML
|
||||
// (Dashboard, by default — see Podman.page).
|
||||
const initial = document.querySelector('.podman-subnav button.active');
|
||||
activatePanel(initial ? initial.dataset.panel : 'dashboard');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
|
||||
return {
|
||||
get: get,
|
||||
post: post,
|
||||
escapeHtml: escapeHtml,
|
||||
el: el,
|
||||
formatBytes: formatBytes,
|
||||
formatDuration: formatDuration,
|
||||
formatRelativeTime: formatRelativeTime,
|
||||
stateChipClass: stateChipClass,
|
||||
loadingRow: loadingRow,
|
||||
errorRow: errorRow,
|
||||
registerPanel: registerPanel,
|
||||
activatePanel: activatePanel,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user