instar 1.3.815 → 1.3.817
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/glance.js +688 -7
- package/dashboard/index.html +164 -649
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +12 -4
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +13 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/scaffold/templates.js +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +68 -57
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +1 -1
- package/upgrades/1.3.816.md +23 -0
- package/upgrades/1.3.817.md +22 -0
- package/upgrades/eli16/plain-enrollment-cancel.md +9 -0
- package/upgrades/side-effects/external-operation-unknown-verbs.md +85 -0
- package/upgrades/side-effects/plain-enrollment-cancel.md +79 -0
package/dashboard/index.html
CHANGED
|
@@ -657,6 +657,15 @@
|
|
|
657
657
|
color: var(--text); cursor: pointer;
|
|
658
658
|
}
|
|
659
659
|
.glance-record-action:hover { border-color: var(--accent-dim); }
|
|
660
|
+
/* Phase 3: the #1429 editable nickname field (in a machine's Layer-3 record) and
|
|
661
|
+
the warn-toned Layer-2 rows (a machine or safety check that needs a look). */
|
|
662
|
+
.glance-record-input {
|
|
663
|
+
padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px;
|
|
664
|
+
background: var(--bg); color: var(--text); font-size: 13px; max-width: 260px;
|
|
665
|
+
}
|
|
666
|
+
.glance-record-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
|
|
667
|
+
.glance-record-input[data-rename-error] { border-color: var(--orange); }
|
|
668
|
+
.glance-list-row[data-tone="warn"] { border-left: 3px solid var(--orange); }
|
|
660
669
|
|
|
661
670
|
.terminal-header {
|
|
662
671
|
display: flex;
|
|
@@ -3529,28 +3538,15 @@
|
|
|
3529
3538
|
<button onclick="loadRoutingMap()" style="padding:6px 12px">Refresh</button>
|
|
3530
3539
|
</div>
|
|
3531
3540
|
<div class="tab-purpose">
|
|
3532
|
-
Where
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
flags a door that's <b>metered</b> (a paid API, skipped for now), <b>money-gated</b>, or
|
|
3536
|
-
that must never take untrusted input. Read-only: it shows the map, it changes nothing.
|
|
3537
|
-
</div>
|
|
3538
|
-
|
|
3539
|
-
<!-- The four lanes (chains) -->
|
|
3540
|
-
<div id="routingMapChainsCard" style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:12px">
|
|
3541
|
-
<div style="font-weight:600">Lanes (fallback order)</div>
|
|
3542
|
-
<div id="routingMapChainsBody" style="font-size:13px;overflow-x:auto">
|
|
3543
|
-
<div style="color:var(--text-dim)">Loading...</div>
|
|
3544
|
-
</div>
|
|
3541
|
+
Where the agent's background AI work runs — the headline says which model it uses first
|
|
3542
|
+
and what backs it up; tap a tile to see a job group's ordered list of models, tap one for
|
|
3543
|
+
the full setup. Read-only: it shows the map, it changes nothing.
|
|
3545
3544
|
</div>
|
|
3546
3545
|
|
|
3547
|
-
<!--
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
<div style="color:var(--text-dim)">Loading...</div>
|
|
3552
|
-
</div>
|
|
3553
|
-
</div>
|
|
3546
|
+
<!-- Glance floors F10/F11 (topic 29836, Phase 3): the shared component renders the
|
|
3547
|
+
headline + per-lane tiles + drill-down here. "Lane / nature / door" became plain
|
|
3548
|
+
words at the glance; the ordered model lists and full config moved one drill down. -->
|
|
3549
|
+
<div id="routingMapGlance" class="glance-root"></div>
|
|
3554
3550
|
</div>
|
|
3555
3551
|
|
|
3556
3552
|
<!-- Spend tab — the read-only Routing Control Room spend/caps view (Increment A,
|
|
@@ -3579,28 +3575,11 @@
|
|
|
3579
3575
|
go-live control are a later increment.
|
|
3580
3576
|
</div>
|
|
3581
3577
|
|
|
3582
|
-
<!--
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
</div>
|
|
3588
|
-
|
|
3589
|
-
<!-- Per door/model spend -->
|
|
3590
|
-
<div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
|
|
3591
|
-
<div style="font-weight:600">By door & model</div>
|
|
3592
|
-
<div id="spendRowsBody" style="font-size:13px;overflow-x:auto">
|
|
3593
|
-
<div style="color:var(--text-dim)">Loading...</div>
|
|
3594
|
-
</div>
|
|
3595
|
-
</div>
|
|
3596
|
-
|
|
3597
|
-
<!-- Paid-door caps -->
|
|
3598
|
-
<div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
|
|
3599
|
-
<div style="font-weight:600">Paid-door caps</div>
|
|
3600
|
-
<div id="spendCapsBody" style="font-size:13px;overflow-x:auto">
|
|
3601
|
-
<div style="color:var(--text-dim)">Loading...</div>
|
|
3602
|
-
</div>
|
|
3603
|
-
</div>
|
|
3578
|
+
<!-- Glance floors F10/F11 (topic 29836, Phase 3): the shared component renders
|
|
3579
|
+
the headline + totals tiles + drill-down here. "Metered / paid door / reflows"
|
|
3580
|
+
became plain words at the glance ("pay-per-use"); the per-model math and the
|
|
3581
|
+
paid-door caps moved one drill down. -->
|
|
3582
|
+
<div id="spendGlance" class="glance-root"></div>
|
|
3604
3583
|
</div>
|
|
3605
3584
|
|
|
3606
3585
|
<!-- Evidence Tab (WikiClaim Phase 4) — per-entity evidence + reverse "what cites this?" view.
|
|
@@ -3842,13 +3821,11 @@
|
|
|
3842
3821
|
<h2 class="ph-title">Machines</h2>
|
|
3843
3822
|
<div id="machinesStamp" class="ph-stamp" aria-live="polite"></div>
|
|
3844
3823
|
</div>
|
|
3845
|
-
<p class="ph-intro">Every computer this agent
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
<div id="machinesList"></div>
|
|
3851
|
-
</section>
|
|
3824
|
+
<p class="tab-purpose ph-intro">Every computer this agent runs on, at a glance — the headline says whether they're all online and healthy; tap a tile to see which machines, tap one for its full detail (and to rename it). The "dispatcher" is the one machine currently handing out new conversations.</p>
|
|
3825
|
+
<!-- Glance floors F10/F11 (topic 29836, Phase 3): the shared component renders the
|
|
3826
|
+
headline + tiles + drill-down here. Rebuilt from the old insider "guards" line.
|
|
3827
|
+
Nickname editing (issue #1429) lives one drill down, commits on Enter/blur. -->
|
|
3828
|
+
<div id="machinesGlance" class="glance-root"></div>
|
|
3852
3829
|
</div>
|
|
3853
3830
|
|
|
3854
3831
|
<div id="evidenceTab" class="tab-panel" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
|
|
@@ -4003,28 +3980,18 @@
|
|
|
4003
3980
|
</div>
|
|
4004
3981
|
</div>
|
|
4005
3982
|
|
|
4006
|
-
<!-- Health Tab (was Systems)
|
|
3983
|
+
<!-- Health Tab (was Systems) — glance floors F10/F11 (topic 29836, Phase 3).
|
|
3984
|
+
The old ~390-word subsystem prose became a plain headline + tiles; each
|
|
3985
|
+
subsystem's full description / processes / metrics live one drill down.
|
|
3986
|
+
The shared component renders the headline + tiles + drill-down here. -->
|
|
4007
3987
|
<div class="systems-container" id="systemsTab" style="display:none">
|
|
4008
3988
|
<div class="systems-main">
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
<
|
|
4012
|
-
<h2>Health</h2>
|
|
4013
|
-
<button class="systems-refresh" onclick="loadSystems()">Refresh</button>
|
|
4014
|
-
</div>
|
|
4015
|
-
<div class="tab-purpose" style="margin-bottom:16px">Your agent's health and running processes — the server and each running session at a glance.</div>
|
|
4016
|
-
<div id="systemsBanner">
|
|
4017
|
-
<div style="padding:20px;color:var(--text-dim);text-align:center">Loading...</div>
|
|
4018
|
-
</div>
|
|
4019
|
-
<div id="systemsIssues"></div>
|
|
4020
|
-
<div id="systemsCapabilities"></div>
|
|
4021
|
-
<div id="systemsEvents"></div>
|
|
4022
|
-
</div>
|
|
4023
|
-
<!-- Detail view (single capability) -->
|
|
4024
|
-
<div class="cap-detail-view" id="systemsDetailView">
|
|
4025
|
-
<button class="cap-detail-back" onclick="showSystemsOverview()">◀ Back to Health</button>
|
|
4026
|
-
<div id="systemsDetailContent"></div>
|
|
3989
|
+
<div class="systems-header">
|
|
3990
|
+
<h2>Health</h2>
|
|
3991
|
+
<button class="systems-refresh" onclick="loadSystems()">Refresh</button>
|
|
4027
3992
|
</div>
|
|
3993
|
+
<div class="tab-purpose" style="margin-bottom:16px">Your agent's health at a glance — the headline says whether everything's operational; tap a tile to see which subsystems, tap one for its full detail.</div>
|
|
3994
|
+
<div id="systemsGlance" class="glance-root"></div>
|
|
4028
3995
|
</div>
|
|
4029
3996
|
</div>
|
|
4030
3997
|
|
|
@@ -5562,107 +5529,77 @@
|
|
|
5562
5529
|
return resp.json();
|
|
5563
5530
|
}
|
|
5564
5531
|
|
|
5565
|
-
// ── Machines Tab (
|
|
5532
|
+
// ── Machines Tab — glance floors F10/F11 (topic 29836, Phase 3) + issue #1429 ──
|
|
5533
|
+
// Rebuilt on the shared glance component: a plain headline over Online /
|
|
5534
|
+
// Attention needed / Dispatcher / Safety-checks tiles (Layer 1); each drills into
|
|
5535
|
+
// the machines or the named safety checks in plain words (Layer 2); each row opens
|
|
5536
|
+
// the full record (Layer 3) where the raw guard posture used to sit on the front
|
|
5537
|
+
// page. Nicknames are user-authored → they NEVER appear at Layer 1; the editable
|
|
5538
|
+
// field lives in the machine record and commits only on Enter/blur with optimistic
|
|
5539
|
+
// echo (#1429). The 15s poll HOLDS an open drill (F9) so a rename in progress is
|
|
5540
|
+
// never clobbered; when the drill closes the next poll reloads server authority.
|
|
5566
5541
|
let __machinesTimer = null;
|
|
5567
5542
|
function startMachines() { loadMachines(); if (!__machinesTimer) __machinesTimer = setInterval(loadMachines, 15000); }
|
|
5568
5543
|
function stopMachines() { if (__machinesTimer) { clearInterval(__machinesTimer); __machinesTimer = null; } }
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
const ram = hw.totalMemBytes ? Math.round(hw.totalMemBytes / 1073741824) + ' GB' : '';
|
|
5574
|
-
return [chip, cores, ram].filter(Boolean).join(' · ') || 'specs not reported yet';
|
|
5575
|
-
}
|
|
5576
|
-
function machineBusyWord(m) {
|
|
5577
|
-
if (m.loadAvg == null) return '';
|
|
5578
|
-
if (m.loadAvg < 1) return 'idle';
|
|
5579
|
-
if (m.loadAvg < 4) return 'working';
|
|
5580
|
-
return 'busy';
|
|
5581
|
-
}
|
|
5582
|
-
function machineGuardsLine(m) {
|
|
5583
|
-
// Guard posture (GUARD-POSTURE-ENDPOINT-SPEC §2.3): last-known summary
|
|
5584
|
-
// with RECEIVER-side age. No posture ever received → honest "unknown",
|
|
5585
|
-
// never "0 on / 0 off". Only problem classes get named; dark-default
|
|
5586
|
-
// offs stay quiet (alarm-fatigue is how the next Mini stays off while
|
|
5587
|
-
// displayed).
|
|
5588
|
-
const p = m.guardPosture;
|
|
5589
|
-
if (!p) return 'guards: unknown';
|
|
5590
|
-
const problems = [];
|
|
5591
|
-
if (p.offDeviant) problems.push(p.offDeviant + ' off that should be on');
|
|
5592
|
-
if (p.offRuntimeDivergent) problems.push(p.offRuntimeDivergent + ' off despite settings');
|
|
5593
|
-
if (p.onStale) problems.push(p.onStale + ' frozen');
|
|
5594
|
-
if (p.missing) problems.push(p.missing + ' missing');
|
|
5595
|
-
if (p.errored) problems.push(p.errored + ' erroring');
|
|
5596
|
-
if (p.divergedPendingRestart) problems.push(p.divergedPendingRestart + ' awaiting restart');
|
|
5597
|
-
const okPart = 'guards: ' + ((p.onConfirmed ?? 0) + (p.onUnverified ?? 0) + (p.onDryRun ?? 0)) + ' on' +
|
|
5598
|
-
(p.onConfirmed ? ' (' + p.onConfirmed + ' confirmed)' : '');
|
|
5599
|
-
let age = '';
|
|
5600
|
-
if (m.guardPostureReceivedAt) {
|
|
5601
|
-
const ms = Date.now() - Date.parse(m.guardPostureReceivedAt);
|
|
5602
|
-
if (isFinite(ms) && ms >= 0) {
|
|
5603
|
-
const mins = Math.round(ms / 60000);
|
|
5604
|
-
age = mins < 2 ? 'just now' : mins < 90 ? 'as of ' + mins + 'm ago'
|
|
5605
|
-
: mins < 2880 ? 'as of ' + Math.round(mins / 60) + 'h ago'
|
|
5606
|
-
: 'as of ' + Math.round(mins / 1440) + 'd ago';
|
|
5607
|
-
}
|
|
5608
|
-
}
|
|
5609
|
-
return [okPart, problems.length ? '⚠ ' + problems.join(', ') : '', age].filter(Boolean).join(' · ');
|
|
5610
|
-
}
|
|
5611
|
-
function renderMachineCard(m, routerHolder) {
|
|
5612
|
-
const online = !!m.online;
|
|
5613
|
-
const dot = !online ? 'off' : (m.clockSkewStatus === 'suspect-clock-removed' ? 'warn' : 'ok');
|
|
5614
|
-
const statusWord = !online ? 'offline' : (m.clockSkewStatus === 'suspect-clock-removed' ? 'clock out of sync — paused for new conversations' : 'online');
|
|
5615
|
-
const isRouter = !!(routerHolder && m.machineId === routerHolder);
|
|
5616
|
-
const nick = m.nickname || m.machineId;
|
|
5617
|
-
const busy = machineBusyWord(m);
|
|
5618
|
-
const sessions = (m.activeSessionCount != null) ? (m.activeSessionCount + (m.maxSessions ? '/' + m.maxSessions : '') + ' conversation' + (m.activeSessionCount === 1 ? '' : 's')) : '';
|
|
5619
|
-
const tail = [statusWord, busy, sessions].filter(Boolean).map(s => escapeHtml(s)).join(' · ');
|
|
5620
|
-
return '<div class="machine-card">' +
|
|
5621
|
-
'<div class="machine-head">' +
|
|
5622
|
-
'<span class="machine-dot ' + dot + '" title="' + escapeHtml(statusWord) + '"></span>' +
|
|
5623
|
-
'<input class="machine-nick" value="' + escapeHtml(nick) + '" data-mid="' + escapeHtml(m.machineId) + '" ' +
|
|
5624
|
-
'onkeydown="if(event.key===\'Enter\'){this.blur();}" onblur="saveMachineNickname(this)" aria-label="machine nickname" />' +
|
|
5625
|
-
(isRouter ? '<span class="machine-badge">dispatcher</span>' : '') +
|
|
5626
|
-
'</div>' +
|
|
5627
|
-
'<div class="machine-meta">' + escapeHtml(machineSpecLine(m.hardware)) + '</div>' +
|
|
5628
|
-
'<div class="machine-meta">' + tail + '</div>' +
|
|
5629
|
-
'<div class="machine-meta">' + escapeHtml(machineGuardsLine(m)) + '</div>' +
|
|
5630
|
-
'</div>';
|
|
5631
|
-
}
|
|
5632
|
-
async function saveMachineNickname(input) {
|
|
5633
|
-
const mid = input.getAttribute('data-mid');
|
|
5634
|
-
const nickname = input.value.trim();
|
|
5544
|
+
|
|
5545
|
+
async function saveMachineNickname(machineId, nickname, input, prev) {
|
|
5546
|
+
// Optimistic: the input already shows the typed value. PATCH the registry; on
|
|
5547
|
+
// failure revert the optimistic echo and surface an honest inline note.
|
|
5635
5548
|
try {
|
|
5636
|
-
await apiFetch('/pool/machines/' + encodeURIComponent(
|
|
5549
|
+
await apiFetch('/pool/machines/' + encodeURIComponent(machineId), {
|
|
5637
5550
|
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname }),
|
|
5638
5551
|
});
|
|
5639
|
-
|
|
5552
|
+
if (input) { input.removeAttribute('data-rename-error'); input.removeAttribute('title'); }
|
|
5640
5553
|
} catch (e) {
|
|
5641
|
-
|
|
5642
|
-
|
|
5554
|
+
if (input) {
|
|
5555
|
+
if (prev != null) input.value = prev; // revert the optimistic echo
|
|
5556
|
+
const msg = (e && e.message) ? e.message : 'rename failed';
|
|
5557
|
+
input.setAttribute('data-rename-error', msg);
|
|
5558
|
+
input.setAttribute('title', 'Rename failed — ' + msg);
|
|
5559
|
+
}
|
|
5643
5560
|
}
|
|
5644
5561
|
}
|
|
5562
|
+
|
|
5645
5563
|
async function loadMachines() {
|
|
5564
|
+
const glanceRoot = document.getElementById('machinesGlance');
|
|
5646
5565
|
const stamp = document.getElementById('machinesStamp');
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5566
|
+
if (!glanceRoot) return;
|
|
5567
|
+
|
|
5568
|
+
const glance = await loadGlanceModule();
|
|
5569
|
+
if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
|
|
5570
|
+
|
|
5571
|
+
let pool = null;
|
|
5650
5572
|
try {
|
|
5651
|
-
|
|
5652
|
-
if (stamp) stamp.textContent = new Date().toLocaleTimeString();
|
|
5653
|
-
if (!data.enabled) {
|
|
5654
|
-
if (headline) headline.innerHTML = '<div class="ph-headline">Running on a single machine right now — nothing to spread across yet.</div>';
|
|
5655
|
-
list.innerHTML = '';
|
|
5656
|
-
return;
|
|
5657
|
-
}
|
|
5658
|
-
const machines = data.machines || [];
|
|
5659
|
-
const onlineCount = machines.filter(m => m.online).length;
|
|
5660
|
-
if (headline) headline.innerHTML = '<div class="ph-headline">' + onlineCount + ' of ' + machines.length + ' machine' + (machines.length === 1 ? '' : 's') + ' online</div>';
|
|
5661
|
-
const routerHolder = data.router && data.router.holder;
|
|
5662
|
-
list.innerHTML = machines.length ? machines.map(m => renderMachineCard(m, routerHolder)).join('') : '<div class="machine-meta">No machines paired yet.</div>';
|
|
5573
|
+
pool = await apiFetch('/pool');
|
|
5663
5574
|
} catch (e) {
|
|
5664
|
-
|
|
5575
|
+
glanceRoot.textContent = '';
|
|
5576
|
+
const note = document.createElement('div');
|
|
5577
|
+
note.className = 'glance-empty';
|
|
5578
|
+
note.textContent = 'Could not load machines right now — refresh to retry.';
|
|
5579
|
+
glanceRoot.appendChild(note);
|
|
5580
|
+
return;
|
|
5665
5581
|
}
|
|
5582
|
+
if (stamp) stamp.textContent = new Date().toLocaleTimeString();
|
|
5583
|
+
|
|
5584
|
+
if (!pool.enabled) {
|
|
5585
|
+
glanceRoot.textContent = '';
|
|
5586
|
+
const note = document.createElement('div');
|
|
5587
|
+
note.className = 'glance-empty';
|
|
5588
|
+
note.textContent = 'Running on a single machine right now — nothing to spread across yet.';
|
|
5589
|
+
glanceRoot.appendChild(note);
|
|
5590
|
+
return;
|
|
5591
|
+
}
|
|
5592
|
+
|
|
5593
|
+
// Named safety checks (GET /guards, this machine) enrich the "Safety checks"
|
|
5594
|
+
// tile; a failure omits that tile — it never blocks the machines view.
|
|
5595
|
+
let guards = null;
|
|
5596
|
+
try { guards = await apiFetch('/guards'); }
|
|
5597
|
+
catch { /* @silent-fallback-ok — guards only enrich the glance; on failure the Safety-checks tile is omitted and machines still render */ }
|
|
5598
|
+
|
|
5599
|
+
const spec = glance.machinesGlanceSpec(document, pool, guards, {
|
|
5600
|
+
onRename: (id, nickname, input, prev) => saveMachineNickname(id, nickname, input, prev),
|
|
5601
|
+
});
|
|
5602
|
+
glance.renderGlance(document, glanceRoot, spec);
|
|
5666
5603
|
}
|
|
5667
5604
|
|
|
5668
5605
|
// ── Tokens Tab ──────────────────────────────────────────────────
|
|
@@ -6023,213 +5960,75 @@
|
|
|
6023
5960
|
}
|
|
6024
5961
|
}
|
|
6025
5962
|
|
|
5963
|
+
// ── Routing Map Tab — glance floors F10/F11 (topic 29836, Phase 3) ──
|
|
5964
|
+
// The nature-axis routing map rebuilt on the shared glance component. "Lane /
|
|
5965
|
+
// nature / door" became plain words at the glance: a headline naming the primary
|
|
5966
|
+
// model + backup, one tile per job group + a Job-types tile (Layer 1); each lane
|
|
5967
|
+
// drills into its ordered fallback list of plain model names (Layer 2); each row
|
|
5968
|
+
// opens the full door/model config (Layer 3). Data from /intelligence/routing/chains;
|
|
5969
|
+
// 503 = no AI provider configured → an honest note, never a crash.
|
|
6026
5970
|
async function loadRoutingMap() {
|
|
6027
|
-
const
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
const
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
if (p.injectionSafe === false) f.push('unsafe for untrusted input');
|
|
6035
|
-
if (p.claudeBanned) f.push('no-Claude');
|
|
6036
|
-
return f.length ? ' <span style="color:var(--text-dim)">(' + f.map(escapeHtml).join(' · ') + ')</span>' : '';
|
|
6037
|
-
};
|
|
6038
|
-
const posLine = (p, i) => '<div style="padding:2px 0">' +
|
|
6039
|
-
'<span style="color:var(--text-dim)">' + (i + 1) + '.</span> ' +
|
|
6040
|
-
'<b>' + escapeHtml(p.door) + '</b> → ' + escapeHtml(p.modelId) + flags(p) + '</div>';
|
|
6041
|
-
const th = (t, right) => '<th style="padding:6px 8px;' + (right ? 'text-align:right' : 'text-align:left') + '">' + t + '</th>';
|
|
6042
|
-
const td = (v, right) => '<td style="padding:6px 8px;vertical-align:top;' + (right ? 'text-align:right' : '') + '">' + v + '</td>';
|
|
5971
|
+
const glanceRoot = document.getElementById('routingMapGlance');
|
|
5972
|
+
if (!glanceRoot) return;
|
|
5973
|
+
|
|
5974
|
+
const glance = await loadGlanceModule();
|
|
5975
|
+
if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
|
|
5976
|
+
|
|
5977
|
+
let map = null;
|
|
6043
5978
|
try {
|
|
6044
|
-
|
|
6045
|
-
const chains = data.chains || [];
|
|
6046
|
-
chainsEl.innerHTML =
|
|
6047
|
-
'<div style="font-size:11px;color:var(--text-dim);margin-bottom:8px">Enforced default framework: <b>' +
|
|
6048
|
-
escapeHtml(String(data.defaultFramework || '—')) + '</b> · injection signal: ' +
|
|
6049
|
-
escapeHtml(String(data.injectionExposureSource || '—')) + '</div>' +
|
|
6050
|
-
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px">' +
|
|
6051
|
-
chains.map((c) =>
|
|
6052
|
-
'<div style="border:1px solid var(--border);border-radius:6px;padding:10px">' +
|
|
6053
|
-
'<div style="font-weight:600;margin-bottom:4px">' + escapeHtml(c.chain) + '</div>' +
|
|
6054
|
-
(c.positions || []).map(posLine).join('') +
|
|
6055
|
-
'</div>').join('') +
|
|
6056
|
-
'</div>';
|
|
6057
|
-
|
|
6058
|
-
const comps = (data.components || []).slice().sort((a, b) => {
|
|
6059
|
-
// Mapped (has a chain) first, then by component name.
|
|
6060
|
-
if (!!a.chain !== !!b.chain) return a.chain ? -1 : 1;
|
|
6061
|
-
return String(a.component).localeCompare(String(b.component));
|
|
6062
|
-
});
|
|
6063
|
-
if (comps.length === 0) {
|
|
6064
|
-
compsEl.innerHTML = '<div style="color:var(--text-dim)">No components mapped.</div>';
|
|
6065
|
-
return;
|
|
6066
|
-
}
|
|
6067
|
-
const yn = (v) => v === true ? 'yes' : (v === false ? 'no' : '—');
|
|
6068
|
-
compsEl.innerHTML =
|
|
6069
|
-
'<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:760px">' +
|
|
6070
|
-
'<thead><tr style="color:var(--text-dim)">' +
|
|
6071
|
-
th('Job-kind') + th('Category') + th('Lane') + th('Enforced now') +
|
|
6072
|
-
th('Critical gate') + th('Injection exposure') + th('Fallback order') +
|
|
6073
|
-
'</tr></thead><tbody>' +
|
|
6074
|
-
comps.map((c) => {
|
|
6075
|
-
const route = (c.route && c.route.length)
|
|
6076
|
-
? c.route.map(posLine).join('')
|
|
6077
|
-
: '<span style="color:var(--text-dim)">legacy category routing (no nature lane)</span>';
|
|
6078
|
-
const lane = c.chain
|
|
6079
|
-
? escapeHtml(c.chain) + ' <span style="color:var(--text-dim)">(' + escapeHtml(String(c.nature)) + ')</span>'
|
|
6080
|
-
: '<span style="color:var(--text-dim)">—</span>';
|
|
6081
|
-
// Prefer the richer FD5b exposure classification; fall back to the untrusted-input flag.
|
|
6082
|
-
let exposure;
|
|
6083
|
-
if (c.injectionExposure) {
|
|
6084
|
-
const ch = c.injectionExposure.channels || {};
|
|
6085
|
-
const chans = ['user', 'model', 'tool'].filter((k) => ch[k]);
|
|
6086
|
-
exposure = c.injectionExposure.exposed
|
|
6087
|
-
? 'exposed' + (chans.length ? ' <span style="color:var(--text-dim)">(' + chans.join('/') + ')</span>' : '')
|
|
6088
|
-
: 'not exposed' + (c.injectionExposure.reason ? ' <span style="color:var(--text-dim)">(' + escapeHtml(c.injectionExposure.reason) + ')</span>' : '');
|
|
6089
|
-
} else if (c.untrustedInput === true) {
|
|
6090
|
-
exposure = 'exposed';
|
|
6091
|
-
} else if (c.untrustedInput === false) {
|
|
6092
|
-
exposure = 'not exposed' + (c.untrustedInputReason ? ' <span style="color:var(--text-dim)">(' + escapeHtml(c.untrustedInputReason) + ')</span>' : '');
|
|
6093
|
-
} else {
|
|
6094
|
-
exposure = '—';
|
|
6095
|
-
}
|
|
6096
|
-
return '<tr style="border-top:1px solid var(--border)">' +
|
|
6097
|
-
td('<b>' + escapeHtml(c.component) + '</b>') +
|
|
6098
|
-
td(escapeHtml(String(c.category || '—'))) +
|
|
6099
|
-
td(lane) +
|
|
6100
|
-
td(escapeHtml(String(c.enforcedFramework || '—'))) +
|
|
6101
|
-
td(c.criticalGate ? '<b>yes</b>' : 'no') +
|
|
6102
|
-
td(exposure) +
|
|
6103
|
-
td(route) +
|
|
6104
|
-
'</tr>';
|
|
6105
|
-
}).join('') +
|
|
6106
|
-
'</tbody></table>';
|
|
5979
|
+
map = await apiFetch('/intelligence/routing/chains');
|
|
6107
5980
|
} catch (err) {
|
|
6108
5981
|
const m = err && err.message ? String(err.message) : String(err);
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
5982
|
+
glanceRoot.textContent = '';
|
|
5983
|
+
const note = document.createElement('div');
|
|
5984
|
+
note.className = 'glance-empty';
|
|
5985
|
+
note.textContent = /503|unavailable|not initialized/i.test(m)
|
|
5986
|
+
? 'The routing map isn’t available on this agent (no AI provider configured).'
|
|
5987
|
+
: 'Could not load the routing map right now — refresh to retry.';
|
|
5988
|
+
glanceRoot.appendChild(note);
|
|
5989
|
+
return;
|
|
6115
5990
|
}
|
|
5991
|
+
|
|
5992
|
+
const spec = glance.routingMapGlanceSpec(document, map);
|
|
5993
|
+
glance.renderGlance(document, glanceRoot, spec);
|
|
6116
5994
|
}
|
|
6117
5995
|
|
|
6118
|
-
//
|
|
6119
|
-
//
|
|
5996
|
+
// ── Spend Tab — glance floors F10/F11 (topic 29836, Phase 3) ──
|
|
5997
|
+
// The Routing Control Room read-only spend/caps view rebuilt on the shared glance
|
|
5998
|
+
// component. "Metered / paid door / reflows" became plain words at the glance:
|
|
5999
|
+
// "pay-per-use", a plain headline, and totals tiles (Layer 1); the per-model cost
|
|
6000
|
+
// + usage rows and the paid-door caps drill down (Layer 2), full pricing/caps
|
|
6001
|
+
// detail at Layer 3. Data from /routing-spend/summary + /routing-spend/caps;
|
|
6002
|
+
// 503 = the dev-gated view is dark on the fleet → an honest note, never a crash.
|
|
6120
6003
|
async function loadRoutingSpend() {
|
|
6121
|
-
const
|
|
6122
|
-
|
|
6123
|
-
const capsEl = document.getElementById('spendCapsBody');
|
|
6004
|
+
const glanceRoot = document.getElementById('spendGlance');
|
|
6005
|
+
if (!glanceRoot) return;
|
|
6124
6006
|
const grain = (document.getElementById('spendGrain') || {}).value || 'day';
|
|
6125
|
-
|
|
6126
|
-
const
|
|
6127
|
-
|
|
6128
|
-
|
|
6007
|
+
|
|
6008
|
+
const glance = await loadGlanceModule();
|
|
6009
|
+
if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
|
|
6010
|
+
|
|
6011
|
+
let summary = null;
|
|
6012
|
+
let caps = null;
|
|
6129
6013
|
try {
|
|
6130
|
-
|
|
6014
|
+
[summary, caps] = await Promise.all([
|
|
6131
6015
|
apiFetch('/routing-spend/summary?grain=' + encodeURIComponent(grain)),
|
|
6132
6016
|
apiFetch('/routing-spend/caps'),
|
|
6133
6017
|
]);
|
|
6134
|
-
|
|
6135
|
-
// Headline totals
|
|
6136
|
-
const t = summary.totals || {};
|
|
6137
|
-
const unpriced = (Number(t.unpricedTokensIn) || 0) + (Number(t.unpricedTokensOut) || 0);
|
|
6138
|
-
const stat = (label, val) => '<div style="min-width:120px"><div style="font-size:11px;color:var(--text-dim)">' + label + '</div><div style="font-size:18px;font-weight:600">' + val + '</div></div>';
|
|
6139
|
-
headEl.innerHTML =
|
|
6140
|
-
'<div style="display:flex;gap:20px;flex-wrap:wrap">' +
|
|
6141
|
-
stat('Gross', usd(t.grossUsd)) +
|
|
6142
|
-
stat('Net (of subsidy/credits)', usd(t.netUsd)) +
|
|
6143
|
-
stat('Committed (cap-enforced)', usd(t.committedUsd)) +
|
|
6144
|
-
(typeof t.amortizedSubscriptionUsd === 'number' ? stat('Subscriptions (amortized)', '~' + usd(t.amortizedSubscriptionUsd)) : '') +
|
|
6145
|
-
stat('Tokens in', num(t.tokensIn)) +
|
|
6146
|
-
stat('Tokens out', num(t.tokensOut)) +
|
|
6147
|
-
(unpriced > 0 ? stat('Unpriced tokens', num(unpriced)) : '') +
|
|
6148
|
-
'</div>' +
|
|
6149
|
-
'<div style="font-size:11px;color:var(--text-dim);margin-top:10px;line-height:1.4">' +
|
|
6150
|
-
escapeHtml(String(summary.note || '')) + '<br>' +
|
|
6151
|
-
escapeHtml(String(summary.horizonNote || '')) +
|
|
6152
|
-
(summary.providerGroundingNote ? '<br>' + escapeHtml(String(summary.providerGroundingNote)) : '') +
|
|
6153
|
-
(summary.reportingBasis ? '<br>Basis: ' + escapeHtml(String(summary.reportingBasis.note || '')) : '') +
|
|
6154
|
-
'</div>';
|
|
6155
|
-
|
|
6156
|
-
// Per door/model rows
|
|
6157
|
-
const rows = summary.rows || [];
|
|
6158
|
-
if (rows.length === 0) {
|
|
6159
|
-
rowsEl.innerHTML = '<div style="color:var(--text-dim)">No internal-LLM usage recorded in this window yet.</div>';
|
|
6160
|
-
} else {
|
|
6161
|
-
rowsEl.innerHTML =
|
|
6162
|
-
'<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:720px">' +
|
|
6163
|
-
'<thead><tr style="color:var(--text-dim)">' +
|
|
6164
|
-
th('Door') + th('Model') + th('Class') + th('Tokens in', true) + th('Tokens out', true) +
|
|
6165
|
-
th('Gross', true) + th('Net', true) + th('Price basis') +
|
|
6166
|
-
'</tr></thead><tbody>' +
|
|
6167
|
-
rows.map((r) => {
|
|
6168
|
-
const cls = r.doorClass === 'metered'
|
|
6169
|
-
? 'metered' + (r.notLiveYet ? ' · not live' : '')
|
|
6170
|
-
: escapeHtml(String(r.doorClass));
|
|
6171
|
-
let basis;
|
|
6172
|
-
if (r.priceBasis === 'subscription-zero') basis = '<span style="color:var(--text-dim)">subscription — not per-token billed</span>';
|
|
6173
|
-
else if (r.priceBasis === 'no-matching-point') basis = '<span style="color:var(--red)">unpriced (no matching price)</span>';
|
|
6174
|
-
else basis = escapeHtml(String(r.priceBasis)) + (r.priceStale ? ' <span style="color:var(--red)">(stale)</span>' : '');
|
|
6175
|
-
const netCell = r.priceBasis === 'subscription-zero'
|
|
6176
|
-
? (typeof r.amortizedSubscriptionUsd === 'number'
|
|
6177
|
-
? '<span title="' + escapeHtml(String(r.amortizationDerivation || '')) + '">~' + usd(r.amortizedSubscriptionUsd) + ' <span style="color:var(--text-dim);font-size:10px">amortized ⓘ</span></span>'
|
|
6178
|
-
: '<span style="color:var(--text-dim)">$0 (subscription)</span>')
|
|
6179
|
-
: usd(r.netUsd);
|
|
6180
|
-
return '<tr style="border-top:1px solid var(--border)">' +
|
|
6181
|
-
td('<b>' + escapeHtml(String(r.door)) + '</b>') +
|
|
6182
|
-
td(escapeHtml(String(r.modelId))) +
|
|
6183
|
-
td(escapeHtml(cls)) +
|
|
6184
|
-
td(num(r.tokensIn), true) +
|
|
6185
|
-
td(num(r.tokensOut), true) +
|
|
6186
|
-
td(usd(r.grossUsd), true) +
|
|
6187
|
-
td(netCell, true) +
|
|
6188
|
-
td(basis) +
|
|
6189
|
-
'</tr>';
|
|
6190
|
-
}).join('') +
|
|
6191
|
-
'</tbody></table>' +
|
|
6192
|
-
(function () {
|
|
6193
|
-
// "Show the math": each door's amortization derivation, verbatim, once.
|
|
6194
|
-
const der = {}; rows.forEach((r) => { if (r.amortizationDerivation) der[r.door] = r.amortizationDerivation; });
|
|
6195
|
-
const lines = Object.values(der);
|
|
6196
|
-
return lines.length
|
|
6197
|
-
? '<div style="font-size:11px;color:var(--text-dim);margin-top:8px;line-height:1.5"><b>Amortization math:</b><br>' + lines.map((l) => escapeHtml(String(l))).join('<br>') + '</div>'
|
|
6198
|
-
: '';
|
|
6199
|
-
})();
|
|
6200
|
-
}
|
|
6201
|
-
|
|
6202
|
-
// Paid-door caps
|
|
6203
|
-
const keys = caps.keys || [];
|
|
6204
|
-
capsEl.innerHTML =
|
|
6205
|
-
'<div style="font-size:11px;color:var(--text-dim);margin-bottom:8px">' + escapeHtml(String(caps.note || '')) + '</div>' +
|
|
6206
|
-
'<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:640px">' +
|
|
6207
|
-
'<thead><tr style="color:var(--text-dim)">' +
|
|
6208
|
-
th('Key') + th('Provider') + th('Door') + th('Daily cap', true) + th('Lifetime cap', true) +
|
|
6209
|
-
th('Committed', true) + th('Status') +
|
|
6210
|
-
'</tr></thead><tbody>' +
|
|
6211
|
-
keys.map((k) =>
|
|
6212
|
-
'<tr style="border-top:1px solid var(--border)">' +
|
|
6213
|
-
td('<b>' + escapeHtml(String(k.keyRef)) + '</b>') +
|
|
6214
|
-
td(escapeHtml(String(k.provider))) +
|
|
6215
|
-
td(escapeHtml(String(k.door))) +
|
|
6216
|
-
td(usd(k.dailyCapUsd), true) +
|
|
6217
|
-
td(usd(k.lifetimeCapUsd), true) +
|
|
6218
|
-
td(usd(k.committedDayUsd) + ' / ' + usd(k.committedLifetimeUsd), true) +
|
|
6219
|
-
td('<span style="color:var(--text-dim)">' + escapeHtml(String(k.goLiveState)) + (k.frozen ? ' · frozen' : '') + '</span>') +
|
|
6220
|
-
'</tr>').join('') +
|
|
6221
|
-
'</tbody></table>' +
|
|
6222
|
-
'<div style="font-size:11px;color:var(--text-dim);margin-top:8px">' + escapeHtml(String(caps.singleMachineNote || '')) + '</div>';
|
|
6223
6018
|
} catch (err) {
|
|
6224
6019
|
const m = err && err.message ? String(err.message) : String(err);
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6020
|
+
glanceRoot.textContent = '';
|
|
6021
|
+
const note = document.createElement('div');
|
|
6022
|
+
note.className = 'glance-empty';
|
|
6023
|
+
note.textContent = /503|not enabled|unavailable/i.test(m)
|
|
6024
|
+
? 'The spend view isn’t enabled on this agent (it’s a development-agent surface, dark on the fleet).'
|
|
6025
|
+
: 'Could not load spend right now — refresh to retry.';
|
|
6026
|
+
glanceRoot.appendChild(note);
|
|
6027
|
+
return;
|
|
6232
6028
|
}
|
|
6029
|
+
|
|
6030
|
+
const spec = glance.spendGlanceSpec(document, summary, caps);
|
|
6031
|
+
glance.renderGlance(document, glanceRoot, spec);
|
|
6233
6032
|
}
|
|
6234
6033
|
|
|
6235
6034
|
async function loadResources() {
|
|
@@ -9281,313 +9080,29 @@
|
|
|
9281
9080
|
}
|
|
9282
9081
|
}
|
|
9283
9082
|
|
|
9284
|
-
// ──
|
|
9285
|
-
|
|
9286
|
-
|
|
9083
|
+
// ── Health Tab (was Systems) — glance floors F10/F11 (topic 29836, Phase 3) ──
|
|
9084
|
+
// The 390-word subsystem prose became a plain headline over Subsystems / Need
|
|
9085
|
+
// attention / Recent events tiles (Layer 1); each subsystem drills into its full
|
|
9086
|
+
// description, processes, and metrics (Layer 3). All /systems/status data is
|
|
9087
|
+
// preserved — it moved one or two clicks down instead of dumping on the front page.
|
|
9287
9088
|
async function loadSystems() {
|
|
9089
|
+
const glanceRoot = document.getElementById('systemsGlance');
|
|
9090
|
+
if (!glanceRoot) return;
|
|
9091
|
+
const glance = await loadGlanceModule();
|
|
9092
|
+
if (!glance) { glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.'; return; }
|
|
9093
|
+
let systems = null;
|
|
9288
9094
|
try {
|
|
9289
|
-
|
|
9290
|
-
showSystemsOverview();
|
|
9291
|
-
renderSystemsOverview();
|
|
9292
|
-
} catch (e) {
|
|
9293
|
-
document.getElementById('systemsBanner').innerHTML =
|
|
9294
|
-
'<div style="padding:20px;color:var(--red);text-align:center">Failed to load systems status</div>';
|
|
9295
|
-
}
|
|
9296
|
-
}
|
|
9297
|
-
|
|
9298
|
-
function renderSystemsOverview() {
|
|
9299
|
-
if (!systemsData) return;
|
|
9300
|
-
const { uptime, health, healthSummary, activeCapabilities, issues, recentEvents } = systemsData;
|
|
9301
|
-
const uptimeStr = formatUptimeClient(uptime);
|
|
9302
|
-
|
|
9303
|
-
// Health Banner
|
|
9304
|
-
const bannerClass = health === 'healthy' ? 'healthy' : health === 'error' ? 'error' : 'degraded';
|
|
9305
|
-
const bannerIcon = health === 'healthy' ? '✓' : '⚠';
|
|
9306
|
-
document.getElementById('systemsBanner').innerHTML = `
|
|
9307
|
-
<div class="health-banner ${bannerClass}">
|
|
9308
|
-
<span class="health-icon">${bannerIcon}</span>
|
|
9309
|
-
<div class="health-text">
|
|
9310
|
-
<div class="health-title">${esc(healthSummary)}</div>
|
|
9311
|
-
<div class="health-meta">Uptime: ${esc(uptimeStr)} · ${activeCapabilities.length} capabilities active${issues.length > 0 ? ' · ' + issues.length + ' issue' + (issues.length > 1 ? 's' : '') : ''}</div>
|
|
9312
|
-
</div>
|
|
9313
|
-
</div>`;
|
|
9314
|
-
|
|
9315
|
-
// Issues (only if any)
|
|
9316
|
-
const issuesEl = document.getElementById('systemsIssues');
|
|
9317
|
-
if (issues.length > 0) {
|
|
9318
|
-
issuesEl.innerHTML = '<div class="issues-panel">' + issues.map(iss => {
|
|
9319
|
-
const cls = iss.severity === 'warning' ? ' warning' : '';
|
|
9320
|
-
const ic = iss.severity === 'warning' ? '⚠' : '❌';
|
|
9321
|
-
return `<div class="issue-card${cls}">
|
|
9322
|
-
<span class="issue-card-icon">${ic}</span>
|
|
9323
|
-
<div class="issue-card-content">
|
|
9324
|
-
<div class="issue-card-label">${esc(iss.label)}</div>
|
|
9325
|
-
<div class="issue-card-desc">${esc(iss.description)}</div>
|
|
9326
|
-
</div>
|
|
9327
|
-
</div>`;
|
|
9328
|
-
}).join('') + '</div>';
|
|
9329
|
-
} else {
|
|
9330
|
-
issuesEl.innerHTML = '';
|
|
9331
|
-
}
|
|
9332
|
-
|
|
9333
|
-
// Card Grid
|
|
9334
|
-
const capsEl = document.getElementById('systemsCapabilities');
|
|
9335
|
-
if (activeCapabilities.length > 0) {
|
|
9336
|
-
const cards = activeCapabilities.map(cap => {
|
|
9337
|
-
const dotClass = cap.status === 'error' ? 'error' : 'active';
|
|
9338
|
-
const statusClass = cap.status === 'error' ? ' error' : '';
|
|
9339
|
-
const previewStats = buildPreviewStats(cap.stats || {});
|
|
9340
|
-
return `<div class="cap-card${statusClass}" onclick="showCapabilityDetail('${esc(cap.id)}')">
|
|
9341
|
-
<div class="cap-card-top">
|
|
9342
|
-
<span class="cap-dot ${dotClass}"></span>
|
|
9343
|
-
<span class="cap-card-name">${esc(cap.label)}</span>
|
|
9344
|
-
<span class="cap-card-arrow">▶</span>
|
|
9345
|
-
</div>
|
|
9346
|
-
<div class="cap-card-desc">${esc(cap.description)}</div>
|
|
9347
|
-
${previewStats ? '<div class="cap-card-stats">' + previewStats + '</div>' : ''}
|
|
9348
|
-
<div class="cap-card-metric">${esc(cap.metric)}</div>
|
|
9349
|
-
</div>`;
|
|
9350
|
-
}).join('');
|
|
9351
|
-
capsEl.innerHTML = `<div class="cap-section-title">Active Subsystems</div><div class="cap-grid">${cards}</div>`;
|
|
9352
|
-
} else {
|
|
9353
|
-
capsEl.innerHTML = '';
|
|
9354
|
-
}
|
|
9355
|
-
|
|
9356
|
-
// Events
|
|
9357
|
-
const eventsEl = document.getElementById('systemsEvents');
|
|
9358
|
-
if (recentEvents.length > 0) {
|
|
9359
|
-
const rows = recentEvents.map(e => {
|
|
9360
|
-
const time = e.timestamp ? new Date(e.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
|
|
9361
|
-
return `<div class="systems-event-row"><span class="systems-event-time">${esc(time)}</span><span class="systems-event-text">${esc(e.narrative || e.subsystem || '')}</span></div>`;
|
|
9362
|
-
}).join('');
|
|
9363
|
-
eventsEl.innerHTML = `<div class="events-disclosure" id="eventsDisclosure">
|
|
9364
|
-
<button class="events-disclosure-toggle" onclick="document.getElementById('eventsDisclosure').classList.toggle('expanded')">
|
|
9365
|
-
<span class="events-disclosure-arrow">▶</span> Recent Events (${recentEvents.length})
|
|
9366
|
-
</button>
|
|
9367
|
-
<div class="events-disclosure-body">${rows}</div></div>`;
|
|
9368
|
-
} else {
|
|
9369
|
-
eventsEl.innerHTML = '';
|
|
9370
|
-
}
|
|
9371
|
-
}
|
|
9372
|
-
|
|
9373
|
-
function buildPreviewStats(stats) {
|
|
9374
|
-
if (!stats || typeof stats !== 'object') return '';
|
|
9375
|
-
const map = {
|
|
9376
|
-
recoveries: 'Recovered', interventions: 'Auto-fixed', activeCases: 'Active',
|
|
9377
|
-
coherencePassed: 'Checks OK', coherenceFailed: 'Issues', memoryPercent: 'Memory',
|
|
9378
|
-
enabledJobs: 'Jobs', activeJobSessions: 'Running', queueLength: 'Queued',
|
|
9379
|
-
weeklyUsage: 'Weekly', fiveHourRate: '5h Rate',
|
|
9380
|
-
topicMappings: 'Topics', totalMessages: 'Messages',
|
|
9381
|
-
proposals: 'Proposals', gaps: 'Gaps', learningsApplied: 'Applied',
|
|
9382
|
-
};
|
|
9383
|
-
const chips = [];
|
|
9384
|
-
for (const [key, label] of Object.entries(map)) {
|
|
9385
|
-
if (stats[key] != null && stats[key] !== 0 && stats[key] !== false) {
|
|
9386
|
-
const suf = ['weeklyUsage','fiveHourRate','memoryPercent'].includes(key) ? '%' : '';
|
|
9387
|
-
chips.push(`<span class="cap-stat"><span class="cap-stat-value">${esc(String(stats[key]))}${suf}</span>${esc(label)}</span>`);
|
|
9388
|
-
}
|
|
9389
|
-
if (chips.length >= 3) break;
|
|
9390
|
-
}
|
|
9391
|
-
return chips.join('');
|
|
9392
|
-
}
|
|
9393
|
-
|
|
9394
|
-
function showCapabilityDetail(capId) {
|
|
9395
|
-
if (!systemsData) return;
|
|
9396
|
-
const cap = systemsData.activeCapabilities.find(c => c.id === capId);
|
|
9397
|
-
if (!cap) return;
|
|
9398
|
-
document.getElementById('systemsOverview').style.display = 'none';
|
|
9399
|
-
document.getElementById('systemsDetailView').classList.add('active');
|
|
9400
|
-
const content = document.getElementById('systemsDetailContent');
|
|
9401
|
-
|
|
9402
|
-
const statCards = buildStatCards(cap.stats || {}, capId);
|
|
9403
|
-
const processRows = (cap.processes || []).map(p => {
|
|
9404
|
-
const dotColor = p.status === 'running' ? 'var(--accent)' : 'var(--red)';
|
|
9405
|
-
return `<div class="cap-process-row">
|
|
9406
|
-
<span class="cap-process-dot" style="background:${dotColor}"></span>
|
|
9407
|
-
<span class="cap-process-name">${esc(p.name)}</span>
|
|
9408
|
-
<span class="cap-process-status">${esc(p.status === 'running' ? 'Running' : 'Error')}</span>
|
|
9409
|
-
</div>`;
|
|
9410
|
-
}).join('');
|
|
9411
|
-
|
|
9412
|
-
const lastAct = cap.lastActivity
|
|
9413
|
-
? '<div class="cap-last-activity">Last activity: ' + esc(timeAgo(cap.lastActivity)) + '</div>' : '';
|
|
9414
|
-
|
|
9415
|
-
content.innerHTML = `
|
|
9416
|
-
<div class="cap-detail-header">
|
|
9417
|
-
<div class="cap-detail-title">
|
|
9418
|
-
<span class="cap-dot ${cap.status === 'error' ? 'error' : 'active'}"></span>
|
|
9419
|
-
<h3>${esc(cap.label)}</h3>
|
|
9420
|
-
</div>
|
|
9421
|
-
<div class="cap-detail-description">${esc(cap.description)}</div>
|
|
9422
|
-
${lastAct}
|
|
9423
|
-
</div>
|
|
9424
|
-
${statCards ? '<div class="cap-detail-section"><div class="cap-detail-section-title">Metrics</div><div class="cap-stats-grid">' + statCards + '</div><div id="capContentBrowser"></div></div>' : ''}
|
|
9425
|
-
<div class="cap-detail-section">
|
|
9426
|
-
<div class="cap-detail-section-title">Components (${(cap.processes || []).length})</div>
|
|
9427
|
-
<div class="cap-process-list">${processRows}</div>
|
|
9428
|
-
</div>
|
|
9429
|
-
<div class="cap-detail-section">
|
|
9430
|
-
<div class="cap-detail-section-title">Status</div>
|
|
9431
|
-
<div style="font-size:13px;color:var(--accent);font-weight:500;padding:8px 0">${esc(cap.metric)}</div>
|
|
9432
|
-
</div>`;
|
|
9433
|
-
}
|
|
9434
|
-
|
|
9435
|
-
// Map stat keys to browsable content endpoints
|
|
9436
|
-
const BROWSABLE_STATS = {
|
|
9437
|
-
// Evolution
|
|
9438
|
-
learnings: { endpoint: '/evolution/learnings', label: 'Learnings', renderer: 'renderLearnings' },
|
|
9439
|
-
proposals: { endpoint: '/evolution/proposals', label: 'Proposals', renderer: 'renderProposals' },
|
|
9440
|
-
gaps: { endpoint: '/evolution/gaps', label: 'Capability Gaps', renderer: 'renderGaps' },
|
|
9441
|
-
actions: { endpoint: '/evolution/actions', label: 'Actions', renderer: 'renderActions' },
|
|
9442
|
-
overdueActions: { endpoint: '/evolution/actions/overdue', label: 'Overdue Actions', renderer: 'renderActions' },
|
|
9443
|
-
// Health
|
|
9444
|
-
coherenceChecks: { endpoint: '/health/coherence', label: 'Coherence Report', renderer: 'renderCoherence' },
|
|
9445
|
-
coherencePassed: { endpoint: '/health/coherence', label: 'Coherence Report', renderer: 'renderCoherence' },
|
|
9446
|
-
coherenceFailed: { endpoint: '/health/coherence', label: 'Coherence Report', renderer: 'renderCoherence' },
|
|
9447
|
-
// Recovery
|
|
9448
|
-
totalTriages: { endpoint: '/triage/history?limit=20', label: 'Triage History', renderer: 'renderTriageHistory' },
|
|
9449
|
-
interventions: { endpoint: '/watchdog/status', label: 'Watchdog Status', renderer: 'renderWatchdog' },
|
|
9450
|
-
recoveries: { endpoint: '/watchdog/status', label: 'Watchdog Status', renderer: 'renderWatchdog' },
|
|
9451
|
-
// Jobs
|
|
9452
|
-
totalJobs: { endpoint: '/jobs', label: 'Jobs', renderer: 'renderJobs' },
|
|
9453
|
-
enabledJobs: { endpoint: '/jobs', label: 'Jobs', renderer: 'renderJobs' },
|
|
9454
|
-
// Telegram
|
|
9455
|
-
totalMessages: { endpoint: '/telegram/log-stats', label: 'Message Stats', renderer: 'renderTelegramStats' },
|
|
9456
|
-
topicMappings: { endpoint: '/telegram/log-stats', label: 'Message Stats', renderer: 'renderTelegramStats' },
|
|
9457
|
-
};
|
|
9458
|
-
|
|
9459
|
-
function buildStatCards(stats, capId) {
|
|
9460
|
-
if (!stats || typeof stats !== 'object') return '';
|
|
9461
|
-
const map = {
|
|
9462
|
-
interventions: 'Auto-fixes', recoveries: 'Recovered', sessionDeaths: 'Crashes',
|
|
9463
|
-
llmOverrides: 'AI Decisions', activeCases: 'Active Issues', totalTriages: 'Investigated',
|
|
9464
|
-
cooldowns: 'Cooldowns',
|
|
9465
|
-
coherenceChecks: 'Consistency Checks', coherencePassed: 'Passed', coherenceFailed: 'Issues Found', coherenceCorrected: 'Auto-corrected',
|
|
9466
|
-
memoryPercent: 'Memory %', memoryFreeGB: 'Free GB', memoryTotalGB: 'Total GB',
|
|
9467
|
-
trackedProcesses: 'Processes', orphanProcesses: 'Cleanup Needed', totalMemoryMB: 'Memory MB',
|
|
9468
|
-
totalJobs: 'Total Jobs', enabledJobs: 'Enabled', queueLength: 'Queued', activeJobSessions: 'Running',
|
|
9469
|
-
weeklyUsage: 'Weekly %', fiveHourRate: '5h Rate %',
|
|
9470
|
-
topicMappings: 'Topics', pendingStalls: 'Needs Attention', totalMessages: 'Messages',
|
|
9471
|
-
proposals: 'Proposals', learnings: 'Learnings', learningsApplied: 'Applied',
|
|
9472
|
-
gaps: 'Gaps', actions: 'Actions', overdueActions: 'Overdue',
|
|
9473
|
-
};
|
|
9474
|
-
const cards = [];
|
|
9475
|
-
for (const [key, label] of Object.entries(map)) {
|
|
9476
|
-
if (stats[key] != null && typeof stats[key] === 'number') {
|
|
9477
|
-
const browsable = BROWSABLE_STATS[key];
|
|
9478
|
-
const clickAttr = browsable ? ` onclick="browseStatContent('${key}')" style="cursor:pointer"` : '';
|
|
9479
|
-
const browseHint = browsable ? '<div style="font-size:9px;color:var(--accent);margin-top:2px">click to view</div>' : '';
|
|
9480
|
-
cards.push(`<div class="cap-stat-card"${clickAttr}><div class="cap-stat-card-value">${stats[key]}</div><div class="cap-stat-card-label">${esc(label)}</div>${browseHint}</div>`);
|
|
9481
|
-
}
|
|
9482
|
-
}
|
|
9483
|
-
return cards.join('');
|
|
9484
|
-
}
|
|
9485
|
-
|
|
9486
|
-
async function browseStatContent(statKey) {
|
|
9487
|
-
const info = BROWSABLE_STATS[statKey];
|
|
9488
|
-
if (!info) return;
|
|
9489
|
-
const panel = document.getElementById('capContentBrowser');
|
|
9490
|
-
if (!panel) return;
|
|
9491
|
-
panel.innerHTML = '<div style="padding:12px;color:var(--text-dim);font-size:12px">Loading ' + esc(info.label) + '...</div>';
|
|
9492
|
-
try {
|
|
9493
|
-
const data = await apiFetch(info.endpoint);
|
|
9494
|
-
panel.innerHTML = '<div class="cap-content-panel">' +
|
|
9495
|
-
'<div class="cap-content-header"><span>' + esc(info.label) + '</span><button onclick="document.getElementById(\'capContentBrowser\').innerHTML=\'\'" style="border:none;background:none;color:var(--text-dim);cursor:pointer;font-size:14px" title="Close" aria-label="Close">×</button></div>' +
|
|
9496
|
-
'<div class="cap-content-body">' + renderBrowsableContent(info.renderer, data) + '</div></div>';
|
|
9095
|
+
systems = await apiFetch('/systems/status');
|
|
9497
9096
|
} catch (e) {
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
case 'renderLearnings': {
|
|
9505
|
-
const items = data.learnings || data || [];
|
|
9506
|
-
if (!Array.isArray(items) || items.length === 0) return '<div class="cap-content-empty">No learnings recorded</div>';
|
|
9507
|
-
return items.map(l => `<div class="cap-content-item">
|
|
9508
|
-
<div class="cap-content-item-title">${esc(l.title || l.insight || l.id || 'Untitled')}</div>
|
|
9509
|
-
${l.description || l.context ? '<div class="cap-content-item-body">' + esc(l.description || l.context || '') + '</div>' : ''}
|
|
9510
|
-
${l.category ? '<span class="cap-content-tag">' + esc(l.category) + '</span>' : ''}
|
|
9511
|
-
${l.applied ? '<span class="cap-content-tag applied">Applied</span>' : ''}
|
|
9512
|
-
${l.createdAt ? '<div class="cap-content-item-time">' + esc(timeAgo(l.createdAt)) + '</div>' : ''}
|
|
9513
|
-
</div>`).join('');
|
|
9514
|
-
}
|
|
9515
|
-
case 'renderProposals': {
|
|
9516
|
-
const items = data.proposals || data || [];
|
|
9517
|
-
if (!Array.isArray(items) || items.length === 0) return '<div class="cap-content-empty">No proposals</div>';
|
|
9518
|
-
return items.map(p => `<div class="cap-content-item">
|
|
9519
|
-
<div class="cap-content-item-title">${esc(p.title || p.id || 'Untitled')}</div>
|
|
9520
|
-
${p.description ? '<div class="cap-content-item-body">' + esc(p.description) + '</div>' : ''}
|
|
9521
|
-
${p.status ? '<span class="cap-content-tag">' + esc(p.status) + '</span>' : ''}
|
|
9522
|
-
${p.type ? '<span class="cap-content-tag">' + esc(p.type) + '</span>' : ''}
|
|
9523
|
-
</div>`).join('');
|
|
9524
|
-
}
|
|
9525
|
-
case 'renderGaps': {
|
|
9526
|
-
const items = data.gaps || data || [];
|
|
9527
|
-
if (!Array.isArray(items) || items.length === 0) return '<div class="cap-content-empty">No gaps detected</div>';
|
|
9528
|
-
return items.map(g => `<div class="cap-content-item">
|
|
9529
|
-
<div class="cap-content-item-title">${esc(g.title || g.capability || g.id || 'Untitled')}</div>
|
|
9530
|
-
${g.description ? '<div class="cap-content-item-body">' + esc(g.description) + '</div>' : ''}
|
|
9531
|
-
${g.severity ? '<span class="cap-content-tag">' + esc(g.severity) + '</span>' : ''}
|
|
9532
|
-
</div>`).join('');
|
|
9533
|
-
}
|
|
9534
|
-
case 'renderActions': {
|
|
9535
|
-
const items = data.actions || data || [];
|
|
9536
|
-
if (!Array.isArray(items) || items.length === 0) return '<div class="cap-content-empty">No actions</div>';
|
|
9537
|
-
return items.map(a => `<div class="cap-content-item">
|
|
9538
|
-
<div class="cap-content-item-title">${esc(a.title || a.action || a.id || 'Untitled')}</div>
|
|
9539
|
-
${a.status ? '<span class="cap-content-tag">' + esc(a.status) + '</span>' : ''}
|
|
9540
|
-
${a.dueAt ? '<div class="cap-content-item-time">Due: ' + esc(timeAgo(a.dueAt)) + '</div>' : ''}
|
|
9541
|
-
</div>`).join('');
|
|
9542
|
-
}
|
|
9543
|
-
case 'renderCoherence': {
|
|
9544
|
-
const report = data.lastReport || data;
|
|
9545
|
-
if (!report || !report.checks) return '<div class="cap-content-empty">No coherence data</div>';
|
|
9546
|
-
return report.checks.map(c => `<div class="cap-content-item" style="border-left:3px solid ${c.passed ? 'var(--accent)' : 'var(--red)'}">
|
|
9547
|
-
<div class="cap-content-item-title">${c.passed ? '✓' : '✗'} ${esc(c.name)}</div>
|
|
9548
|
-
<div class="cap-content-item-body">${esc(c.message)}</div>
|
|
9549
|
-
</div>`).join('');
|
|
9550
|
-
}
|
|
9551
|
-
case 'renderTriageHistory': {
|
|
9552
|
-
const items = data.history || data || [];
|
|
9553
|
-
if (!Array.isArray(items) || items.length === 0) return '<div class="cap-content-empty">No triage history</div>';
|
|
9554
|
-
return items.map(t => `<div class="cap-content-item">
|
|
9555
|
-
<div class="cap-content-item-title">Topic ${t.topicId} — ${esc(t.sessionName || 'unknown')}</div>
|
|
9556
|
-
<div class="cap-content-item-body">${t.result ? esc(JSON.stringify(t.result.actionsTaken || t.result, null, 0).substring(0, 200)) : ''}</div>
|
|
9557
|
-
${t.timestamp ? '<div class="cap-content-item-time">' + esc(timeAgo(t.timestamp)) + '</div>' : ''}
|
|
9558
|
-
</div>`).join('');
|
|
9559
|
-
}
|
|
9560
|
-
case 'renderWatchdog': {
|
|
9561
|
-
const hist = data.interventionHistory || [];
|
|
9562
|
-
if (hist.length === 0) return '<div class="cap-content-empty">No interventions recorded</div>';
|
|
9563
|
-
return hist.map(h => `<div class="cap-content-item">
|
|
9564
|
-
<div class="cap-content-item-title">PID ${h.stuckPid || '?'} — ${esc(h.level || h.outcome || 'intervention')}</div>
|
|
9565
|
-
${h.outcome ? '<span class="cap-content-tag">' + esc(h.outcome) + '</span>' : ''}
|
|
9566
|
-
${h.timestamp ? '<div class="cap-content-item-time">' + esc(timeAgo(h.timestamp)) + '</div>' : ''}
|
|
9567
|
-
</div>`).join('');
|
|
9568
|
-
}
|
|
9569
|
-
case 'renderJobs': {
|
|
9570
|
-
const jobs = data.jobs || data || [];
|
|
9571
|
-
if (!Array.isArray(jobs) || jobs.length === 0) return '<div class="cap-content-empty">No jobs</div>';
|
|
9572
|
-
return jobs.slice(0, 20).map(j => `<div class="cap-content-item">
|
|
9573
|
-
<div class="cap-content-item-title">${esc(j.slug || j.name || 'unnamed')}</div>
|
|
9574
|
-
<div class="cap-content-item-body">${esc(j.cron || '')} ${j.enabled === false ? '(disabled)' : ''}</div>
|
|
9575
|
-
</div>`).join('');
|
|
9576
|
-
}
|
|
9577
|
-
case 'renderTelegramStats': {
|
|
9578
|
-
return `<div class="cap-content-item">
|
|
9579
|
-
<div class="cap-content-item-title">Message Log</div>
|
|
9580
|
-
<div class="cap-content-item-body">Total messages: ${data.totalMessages || '?'}<br>Log size: ${data.logSizeBytes ? Math.round(data.logSizeBytes / 1024) + ' KB' : '?'}</div>
|
|
9581
|
-
</div>`;
|
|
9582
|
-
}
|
|
9583
|
-
default:
|
|
9584
|
-
return '<div class="cap-content-empty">No viewer for this content</div>';
|
|
9097
|
+
glanceRoot.textContent = '';
|
|
9098
|
+
const note = document.createElement('div');
|
|
9099
|
+
note.className = 'glance-empty';
|
|
9100
|
+
note.textContent = 'Could not load the health view right now — refresh to retry.';
|
|
9101
|
+
glanceRoot.appendChild(note);
|
|
9102
|
+
return;
|
|
9585
9103
|
}
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
function showSystemsOverview() {
|
|
9589
|
-
document.getElementById('systemsOverview').style.display = '';
|
|
9590
|
-
document.getElementById('systemsDetailView').classList.remove('active');
|
|
9104
|
+
const spec = glance.healthGlanceSpec(document, systems);
|
|
9105
|
+
glance.renderGlance(document, glanceRoot, spec);
|
|
9591
9106
|
}
|
|
9592
9107
|
|
|
9593
9108
|
function timeAgo(isoStr) {
|