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:
iris 2026-07-11 20:28:27 +02:00 committed by mara
commit ef14641b94
7 changed files with 229 additions and 5 deletions

View file

@ -16,11 +16,12 @@
/core.html) carved out of the dashboard's old SYST3M tab so the
dashboard tab strip stays lean. Same minimal chrome as
/logs.html — a `← home` back-link to the H0M3 hub + a
createTabStrip sub-tab nav. Two sub-tabs: kept state (tombstones)
and container load. Rebuild queue + meta inputs have moved to
createTabStrip sub-tab nav. Three sub-tabs: kept state (tombstones),
container load, and infra containers (start/stop/restart the four
hive infra containers). Rebuild queue + meta inputs have moved to
/builds.html (the build lifecycle hub). Default tab: K3PT ST4T3.
The section <div> ids (tombstones-section, container-load-section)
match what core.js's renderers target. -->
The section <div> ids (tombstones-section, container-load-section,
infra-containers-section) match what core.js's renderers target. -->
<header class="page-header">
<a class="page-back" href="/">← home</a>
<nav class="hive-tabbar core-tabbar" id="core-tabbar" role="tablist">
@ -32,6 +33,10 @@
aria-controls="core-pane-load" data-tab="load">
<span class="core-tab-label">C0NT41N3R L04D</span>
</a>
<a class="hive-tab" id="core-tab-infra" href="#infra" role="tab"
aria-controls="core-pane-infra" data-tab="infra">
<span class="core-tab-label">1NFR4</span>
</a>
</nav>
</header>
@ -62,6 +67,19 @@
</div>
</section>
<!-- 1NFR4: start / stop / restart the four hive infrastructure
containers (hive-ci, hive-forge, hive-gateway, hive-matrix)
directly from the dashboard, mirroring what an infra_admin
agent's `restart` tool already reaches. Status polled every 5s
while this sub-tab is open, same cadence as C0NT41N3R L04D. -->
<section class="core-pane" id="core-pane-infra" data-tab-pane="infra"
role="tabpanel" aria-labelledby="core-tab-infra">
<p class="meta">hive infrastructure containers — ci, forge, gateway, matrix. actions are logged to the AUDIT log same as agent-driven restarts.</p>
<div id="infra-containers-section">
<p class="meta">loading…</p>
</div>
</section>
</main>
<script type="module" src="/static/core.js" defer></script>

View file

@ -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();