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.
@@ -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 each internal job-kind sends its AI calls. Every background check, gate, and
3533
- sentinel has a <b>lane</b> (its "nature") and a fallback list of <b>doors</b> — the ways
3534
- it can reach a model. This shows, per lane, the ordered door + model list it tries, and
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
- <!-- Per job-kind mapping -->
3548
- <div style="border:1px solid var(--border);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px">
3549
- <div style="font-weight:600">By job-kind</div>
3550
- <div id="routingMapComponentsBody" style="font-size:13px;overflow-x:auto">
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
- <!-- Headline totals -->
3583
- <div id="spendHeadlineCard" style="border:1px solid var(--border);border-radius:8px;padding:16px">
3584
- <div id="spendHeadlineBody" style="font-size:13px;overflow-x:auto">
3585
- <div style="color:var(--text-dim)">Loading...</div>
3586
- </div>
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 &amp; 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 is set up on. Each one gets a friendly name you can change — and once it has a name, you can tell the agent "move this conversation to the mini" and it knows which machine you mean. The agent shares its conversations across these machines so it can handle more at once.</p>
3846
- <div id="machinesHeadline" class="ph-headline-wrap" aria-live="polite"></div>
3847
- <section class="ph-section">
3848
- <h3 class="ph-h">The machines</h3>
3849
- <p class="ph-h-sub">Click a name to rename it. The "dispatcher" is the one machine currently handing out new conversations.</p>
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
- <!-- Overview (card grid) -->
4010
- <div id="systemsOverview">
4011
- <div class="systems-header">
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()">&#9664; 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 (Multi-Machine Session Pool §L2) ─────────────────
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
- function machineSpecLine(hw) {
5570
- if (!hw) return 'specs not reported yet';
5571
- const chip = (hw.cpuModel && hw.cpuModel !== 'unknown') ? hw.cpuModel : ((hw.platform || '') + (hw.arch ? ' ' + hw.arch : '')).trim();
5572
- const cores = hw.cpuCores ? hw.cpuCores + ' cores' : '';
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(mid), {
5549
+ await apiFetch('/pool/machines/' + encodeURIComponent(machineId), {
5637
5550
  method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname }),
5638
5551
  });
5639
- loadMachines();
5552
+ if (input) { input.removeAttribute('data-rename-error'); input.removeAttribute('title'); }
5640
5553
  } catch (e) {
5641
- const hl = document.getElementById('machinesHeadline');
5642
- if (hl) hl.innerHTML = '<div class="ph-headline">Couldn\'t rename that machine: ' + escapeHtml(e.message) + '</div>';
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
- const list = document.getElementById('machinesList');
5648
- const headline = document.getElementById('machinesHeadline');
5649
- if (!list) return;
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
- const data = await apiFetch('/pool');
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
- if (headline) headline.innerHTML = '<div class="ph-headline">Couldn\'t load machines: ' + escapeHtml(e.message) + '</div>';
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 chainsEl = document.getElementById('routingMapChainsBody');
6028
- const compsEl = document.getElementById('routingMapComponentsBody');
6029
- // Render one door+model position as a compact chip line.
6030
- const flags = (p) => {
6031
- const f = [];
6032
- if (p.doorClass === 'metered') f.push('metered' + (p.skippedInIncrementA ? ' · skipped now' : ''));
6033
- if (p.moneyGated) f.push('money-gated');
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
- const data = await apiFetch('/intelligence/routing/chains');
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
- if (/503|unavailable|not initialized/i.test(m)) {
6110
- chainsEl.innerHTML = '<div style="color:var(--text-dim)">The routing map isn\'t available on this agent (no AI provider configured).</div>';
6111
- } else {
6112
- chainsEl.innerHTML = '<div style="color:var(--red)">Error: ' + escapeHtml(m) + '</div>';
6113
- }
6114
- compsEl.innerHTML = '';
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
- // Routing Control Room — read-only spend/caps view (Increment A). Data from
6119
- // /routing-spend/summary + /routing-spend/caps. Honest $0 / not-live-yet labelling.
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 headEl = document.getElementById('spendHeadlineBody');
6122
- const rowsEl = document.getElementById('spendRowsBody');
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
- const usd = (n) => '$' + (Number(n) || 0).toFixed(4);
6126
- const num = (n) => (Number(n) || 0).toLocaleString();
6127
- const th = (t, right) => '<th style="padding:6px 8px;' + (right ? 'text-align:right' : 'text-align:left') + '">' + t + '</th>';
6128
- const td = (v, right) => '<td style="padding:6px 8px;vertical-align:top;' + (right ? 'text-align:right' : '') + '">' + v + '</td>';
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
- const [summary, caps] = await Promise.all([
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
- if (/503|not enabled|unavailable/i.test(m)) {
6226
- headEl.innerHTML = '<div style="color:var(--text-dim)">The spend view isn\'t enabled on this agent (it\'s a development-agent surface, dark on the fleet).</div>';
6227
- } else {
6228
- headEl.innerHTML = '<div style="color:var(--red)">Error: ' + escapeHtml(m) + '</div>';
6229
- }
6230
- rowsEl.innerHTML = '';
6231
- capsEl.innerHTML = '';
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
- // ── Systems Tab ──────────────────────────────────────────────
9285
- let systemsData = null;
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
- systemsData = await apiFetch('/systems/status');
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' ? '&#10003;' : '&#9888;';
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)} &middot; ${activeCapabilities.length} capabilities active${issues.length > 0 ? ' &middot; ' + 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' ? '&#9888;' : '&#10060;';
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">&#9654;</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">&#9654;</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">&times;</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
- panel.innerHTML = '<div style="padding:12px;color:var(--red);font-size:12px">Failed to load: ' + esc(e.message) + '</div>';
9499
- }
9500
- }
9501
-
9502
- function renderBrowsableContent(renderer, data) {
9503
- switch (renderer) {
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 ? '&check;' : '&cross;'} ${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) {