feat: add infra container start/stop/restart tab to C0R3 page
New POST /api/infra-container/{name}/{action} dashboard route (start/
stop/restart on hive-ci/hive-forge/hive-gateway/hive-matrix), reusing
the existing priv_client::control_infra_container helper the
infra_admin agent path already uses, plus an audit_log entry per
attempt. Adds infra_containers to the /api/state StateSnapshot (name +
live running status via systemctl is-active). New 1NFR4 sub-tab on the
C0R3 dashboard page: one row per infra container with a running/
stopped badge and start/stop/restart buttons, polled every 5s while
the sub-tab is open.
This commit is contained in:
parent
79d4c345bb
commit
ef14641b94
7 changed files with 229 additions and 5 deletions
|
|
@ -242,6 +242,68 @@ function stopContainerLoadPolling() {
|
|||
if (containerLoadTimer) { clearInterval(containerLoadTimer); containerLoadTimer = null; }
|
||||
}
|
||||
|
||||
// ─── infra containers (start/stop/restart the 4 hive infra containers) ────
|
||||
let infraTimer = null;
|
||||
|
||||
function renderInfraContainers(rows) {
|
||||
const root = $('infra-containers-section');
|
||||
if (!root) return;
|
||||
root.replaceChildren();
|
||||
if (!Array.isArray(rows) || !rows.length) {
|
||||
root.append(el('p', { class: 'meta' }, 'no infra container data'));
|
||||
return;
|
||||
}
|
||||
const ul = el('ul', { class: 'containers' });
|
||||
for (const c of rows) {
|
||||
const li = el('li', { class: 'container-row' });
|
||||
const head = el('div', { class: 'head' });
|
||||
head.append(
|
||||
el('span', { class: 'name' }, c.name),
|
||||
el('span', { class: 'badge ' + (c.running ? 'badge-ok' : 'badge-fail') },
|
||||
c.running ? 'running' : 'stopped'),
|
||||
);
|
||||
li.append(head);
|
||||
|
||||
const actions = el('div', { class: 'actions' });
|
||||
const base = '/api/infra-container/' + encodeURIComponent(c.name) + '/';
|
||||
if (c.running) {
|
||||
actions.append(form(base + 'restart', 'btn-restart', '↺ R3ST4RT',
|
||||
'restart ' + c.name + '?'));
|
||||
actions.append(form(base + 'stop', 'btn-stop', '■ ST0P',
|
||||
'stop ' + c.name + '?'));
|
||||
} else {
|
||||
actions.append(form(base + 'start', 'btn-start', '▶ ST4RT',
|
||||
'start ' + c.name + '?'));
|
||||
}
|
||||
li.append(actions);
|
||||
ul.append(li);
|
||||
}
|
||||
root.append(ul);
|
||||
}
|
||||
|
||||
async function refreshInfraContainers() {
|
||||
try {
|
||||
const resp = await fetch('/api/state');
|
||||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
const s = await resp.json();
|
||||
renderInfraContainers(s.infra_containers || []);
|
||||
} catch (e) {
|
||||
const root = $('infra-containers-section');
|
||||
if (root) {
|
||||
root.replaceChildren();
|
||||
root.append(el('p', { class: 'meta' }, 'infra container fetch failed: ' + e));
|
||||
}
|
||||
}
|
||||
}
|
||||
function startInfraPolling() {
|
||||
refreshInfraContainers();
|
||||
if (infraTimer) return;
|
||||
infraTimer = setInterval(refreshInfraContainers, 5000);
|
||||
}
|
||||
function stopInfraPolling() {
|
||||
if (infraTimer) { clearInterval(infraTimer); infraTimer = null; }
|
||||
}
|
||||
|
||||
// ─── render-all (cold load + any full re-render) ──────────────────────────
|
||||
function renderAll() {
|
||||
renderTombstones({ tombstones: tombstonesState });
|
||||
|
|
@ -294,6 +356,8 @@ async function init() {
|
|||
onShow: (id) => {
|
||||
if (id === 'load') startContainerLoadPolling();
|
||||
else stopContainerLoadPolling();
|
||||
if (id === 'infra') startInfraPolling();
|
||||
else stopInfraPolling();
|
||||
// Lazy-load stale-perms on first K3PT ST4T3 activation; always
|
||||
// re-fetch on subsequent visits in case perms changed.
|
||||
if (id === 'kept') fetchAndRenderStalePerms();
|
||||
|
|
|
|||
Loading…
Reference in a new issue