dashboard: K3PT ST4T3 section + agent links open in new tab
new section between containers and questions: lists every name with a state dir under /var/lib/hyperhive/agents/ that doesn't correspond to a live container. shows state size + last-modified age + whether claude creds are kept. two actions per row: - R3V1V3 — queues a spawn approval with the same name (operator approves to recreate; spawn flow reuses prior config + claude creds, no re-login needed) - PURG3 — wipes the agent's state + applied dirs (post /purge-tombstone/ endpoint; refuses if a live container with that name still exists) dashboard also opens agent links in new tabs now (target=_blank + rel=noopener) so the operator's overview tab stays put when they dive into an agent.
This commit is contained in:
parent
8344dd9ab7
commit
5ee65d2f15
6 changed files with 212 additions and 3 deletions
|
|
@ -126,7 +126,7 @@
|
|||
// ── line 1: identity ─────────────────────────────────────────
|
||||
const head = el('div', { class: 'head' });
|
||||
head.append(
|
||||
el('a', { class: 'name', href: url }, c.name),
|
||||
el('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name),
|
||||
el('span', { class: c.is_manager ? 'role role-m1nd' : 'role role-ag3nt' },
|
||||
c.is_manager ? 'm1nd' : 'ag3nt'),
|
||||
);
|
||||
|
|
@ -135,7 +135,8 @@
|
|||
el('span', { class: 'spinner' }, '◐'), ' ', c.pending + '…'));
|
||||
} else if (c.needs_login) {
|
||||
head.append(el('a',
|
||||
{ class: 'badge badge-warn', href: url }, 'needs login →'));
|
||||
{ class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
|
||||
'needs login →'));
|
||||
}
|
||||
if (c.needs_update) {
|
||||
head.append(form(
|
||||
|
|
@ -182,6 +183,66 @@
|
|||
root.append(ul);
|
||||
}
|
||||
|
||||
function renderTombstones(s) {
|
||||
const root = $('tombstones-section');
|
||||
root.innerHTML = '';
|
||||
if (!s.tombstones || !s.tombstones.length) {
|
||||
root.append(el('p', { class: 'empty' }, 'no kept state — clean'));
|
||||
return;
|
||||
}
|
||||
const fmtBytes = (n) => {
|
||||
if (n < 1024) return n + ' B';
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
};
|
||||
const fmtAge = (ts) => {
|
||||
if (!ts) return '?';
|
||||
const d = Math.floor((Date.now() / 1000 - ts) / 86400);
|
||||
if (d <= 0) return 'today';
|
||||
if (d === 1) return '1 day ago';
|
||||
return d + ' days ago';
|
||||
};
|
||||
const ul = el('ul', { class: 'containers' });
|
||||
for (const t of s.tombstones) {
|
||||
const li = el('li', { class: 'container-row tombstone' });
|
||||
const head = el('div', { class: 'head' });
|
||||
head.append(
|
||||
el('span', { class: 'name' }, t.name),
|
||||
el('span', { class: 'badge badge-muted' }, 'destroyed'),
|
||||
);
|
||||
if (t.has_creds) {
|
||||
head.append(el('span', { class: 'badge badge-muted' }, 'creds kept'));
|
||||
}
|
||||
head.append(el('span', { class: 'meta' },
|
||||
`${fmtBytes(t.state_bytes)} · ${fmtAge(t.last_seen)}`));
|
||||
li.append(head);
|
||||
|
||||
const actions = el('div', { class: 'actions' });
|
||||
// Reuse the existing spawn form pattern via /request-spawn — operator
|
||||
// can queue an approval that recreates the agent with the same name
|
||||
// and reuses the kept state.
|
||||
const respawn = el('form', {
|
||||
method: 'POST', action: '/request-spawn',
|
||||
class: 'inline', 'data-async': '',
|
||||
'data-confirm': 'queue spawn approval for ' + t.name + '? state will be reused.',
|
||||
});
|
||||
respawn.append(
|
||||
el('input', { type: 'hidden', name: 'name', value: t.name }),
|
||||
el('button', { type: 'submit', class: 'btn btn-start' }, '⊕ R3V1V3'),
|
||||
);
|
||||
actions.append(respawn);
|
||||
actions.append(form(
|
||||
'/purge-tombstone/' + t.name, 'btn-destroy', 'PURG3',
|
||||
'PURGE ' + t.name + '? config history, claude creds, /state/ notes '
|
||||
+ 'are all WIPED. no undo.',
|
||||
));
|
||||
li.append(actions);
|
||||
ul.append(li);
|
||||
}
|
||||
root.append(ul);
|
||||
}
|
||||
|
||||
function renderQuestions(s) {
|
||||
const root = $('questions-section');
|
||||
root.innerHTML = '';
|
||||
|
|
@ -331,6 +392,7 @@
|
|||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
const s = await resp.json();
|
||||
renderContainers(s);
|
||||
renderTombstones(s);
|
||||
renderQuestions(s);
|
||||
renderInbox(s);
|
||||
renderApprovals(s);
|
||||
|
|
|
|||
|
|
@ -133,6 +133,16 @@ a:hover {
|
|||
color: var(--amber); border-color: var(--amber);
|
||||
text-shadow: 0 0 6px rgba(250, 179, 135, 0.5);
|
||||
}
|
||||
.badge-muted {
|
||||
color: var(--muted); border-color: var(--purple-dim);
|
||||
background: rgba(127, 132, 156, 0.08);
|
||||
}
|
||||
.container-row.tombstone {
|
||||
border-style: dashed;
|
||||
background: rgba(24, 24, 37, 0.35);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.container-row.tombstone .name { color: var(--muted); }
|
||||
.pending-state {
|
||||
color: var(--amber);
|
||||
font-size: 0.85em;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@
|
|||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ K3PT ST4T3 ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<div id="tombstones-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ M1ND H4S QU3STI0NS ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<div id="questions-section">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue