rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
9ea6d75
·
1 Parent(s): 1c5baae

fix(admin): KI-162 + KI-163 — simplify LLM Chain UI + truncate visitor names

Browse files

KI-162 — LLM Chain admin tab redesigned to two simple tables:
- "What's Live" — 3 rows (Brain Fast / Brain Main / Judge) × current model + probed-age
- "What's Available" — NIM-only dedup pool with Healthy + Used By columns
Dropped 124 lines of stale renderLlmHealthChain (chain cards + AVAILABLE/
UNAVAILABLE chips / PRIMARY pills). Added isRetiredModel() filter that
drops any groq:/openrouter:/or: entries even if a stale payload still
carries them.

KI-163 — visitor table no longer blows up vertically when a session
captured a multi-word garbage name (e.g. "Currently Not Having Any" pre-
KI-156 Fix B). Added max-width + nowrap + ellipsis to .name-cell and
.slug-cell, with hover tooltip via title= for the full value. Also
deleted 4 garbage profile JSONs from 40-data/profiles/ ("Just Me" ×2,
"Rohit Sara", "Not Having Any Pre-existing") that pre-dated the name
stop-list fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. frontend/public/admin/llm-control.html +219 -225
frontend/public/admin/llm-control.html CHANGED
@@ -347,121 +347,60 @@
347
  .chain-table .icon-btn { padding: 2px 7px; font-size: 12px; line-height: 1.2; }
348
  .chain-table tbody tr.is-primary td { background: rgba(63, 185, 80, 0.05); }
349
 
350
- /* KI-086: LLM Health & Credits section */
351
- .llm-health-chains {
352
- display: grid;
353
- grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
354
- gap: 12px;
355
- }
356
- .llm-health-chain {
357
- padding: 12px;
358
- background: #0b0d12;
359
- border: 1px solid var(--border);
360
- border-radius: 8px;
361
- }
362
- .llm-health-chain .chain-head {
363
- display: flex; align-items: center; gap: 8px;
364
- margin-bottom: 10px;
365
- }
366
- .llm-health-chain .chain-head .role-badge { margin-right: 4px; }
367
- .llm-health-chain .role-block {
368
- margin-bottom: 8px;
369
- padding-bottom: 8px;
370
- border-bottom: 1px dashed var(--border);
371
- }
372
- .llm-health-chain .role-block:last-of-type { border-bottom: none; }
373
- .llm-health-chain .role-block .role-label {
374
- font-size: 10px;
375
- text-transform: uppercase;
376
- letter-spacing: 0.08em;
377
- color: var(--muted);
378
- margin-bottom: 4px;
379
  }
380
- .llm-health-chain .role-block .model-name {
381
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, monospace;
 
382
  font-size: 12px;
383
- word-break: break-all;
384
  }
385
- .llm-health-chain .role-block .stats-line {
386
- margin-top: 4px;
387
- font-size: 11px;
 
 
388
  color: var(--muted);
 
 
 
389
  }
390
- .llm-health-chain .role-block .stats-line .sep {
391
- margin: 0 6px;
392
- color: #3a4250;
393
- }
394
- .llm-health-chain .credit-banner {
395
- margin-top: 8px;
396
- padding: 8px 10px;
397
- background: rgba(248, 81, 73, 0.12);
398
- border: 1px solid rgba(248, 81, 73, 0.45);
399
- border-radius: 6px;
400
- font-size: 12px;
401
- color: var(--red);
402
- }
403
- .llm-health-chain .none-elected {
404
- color: var(--yellow);
405
- font-style: italic;
406
- font-size: 12px;
407
- }
408
-
409
- /* KI-122 — "Currently in use" header + per-member availability pills */
410
- .llm-health-chain .in-use-line {
411
- display: flex;
412
- align-items: center;
413
- gap: 8px;
414
- flex-wrap: wrap;
415
- padding: 8px 10px;
416
- margin-bottom: 12px;
417
- background: rgba(63, 185, 80, 0.06);
418
- border: 1px solid rgba(63, 185, 80, 0.25);
419
- border-radius: 6px;
420
- }
421
- .llm-health-chain .in-use-line .role-label {
422
- margin-bottom: 0;
423
- color: var(--text);
424
  }
425
- .llm-health-chain .in-use-line .model-name {
 
 
426
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, monospace;
427
- font-size: 12px;
428
  word-break: break-all;
429
  }
430
- .llm-health-chain .chain-members-list {
431
- display: flex;
432
- flex-direction: column;
433
- gap: 4px;
434
- margin: 6px 0 12px;
435
- padding: 8px 10px;
436
- background: #050709;
437
- border: 1px solid var(--border);
438
- border-radius: 6px;
439
- }
440
- .llm-health-chain .chain-member-row {
441
- display: flex;
442
- align-items: center;
443
- gap: 8px;
444
- font-size: 11px;
445
- }
446
- .llm-health-chain .chain-member-row .model-name {
447
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, monospace;
448
- word-break: break-all;
449
  }
450
- .avail-pill {
 
451
  display: inline-block;
452
- padding: 2px 8px;
453
- border-radius: 999px;
454
- font-size: 10px;
455
- font-weight: 700;
456
- letter-spacing: 0.04em;
457
- border: 1px solid transparent;
 
 
 
458
  white-space: nowrap;
 
459
  }
460
- .avail-pill.pill-inuse { background: rgba(63, 185, 80, 0.20); color: var(--green); border-color: rgba(63, 185, 80, 0.55); }
461
- .avail-pill.pill-available { background: rgba(63, 185, 80, 0.10); color: var(--green); border-color: rgba(63, 185, 80, 0.35); }
462
- .avail-pill.pill-gated { background: rgba(139, 149, 164, 0.15);color: var(--muted); border-color: rgba(139, 149, 164, 0.40); }
463
- .avail-pill.pill-degraded { background: rgba(210, 153, 34, 0.15); color: var(--yellow); border-color: rgba(210, 153, 34, 0.45); }
464
- .avail-pill.pill-down { background: rgba(248, 81, 73, 0.15); color: var(--red); border-color: rgba(248, 81, 73, 0.45); }
465
 
466
  /* Status badges — three colors: ✓ green / ⚠ amber / ✗ red */
467
  .health-badge {
@@ -617,11 +556,21 @@
617
  }
618
 
619
  /* Profiles table */
620
- .profile-table td.name-cell { font-weight: 500; }
 
 
 
 
 
 
621
  .profile-table td.slug-cell {
622
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, monospace;
623
  font-size: 12px;
624
  color: var(--muted);
 
 
 
 
625
  }
626
  .profile-table td.num-cell { text-align: right; font-variant-numeric: tabular-nums; }
627
  .profile-table td.time-cell { white-space: nowrap; font-size: 12px; }
@@ -722,13 +671,13 @@
722
  <!-- KI-086: LLM Health & Credits snapshot (KI-080..KI-085 telemetry) -->
723
  <div class="card" id="llm-health-card">
724
  <div class="row-between" style="margin-bottom: 12px;">
725
- <h2>LLM health &amp; credits <span class="small muted" id="llm-health-snapshot-ts"></span></h2>
726
  <div class="actions">
727
  <button id="btn-refresh-llm-health">Refresh</button>
728
  </div>
729
  </div>
730
- <!-- Section A: per-chain election cards -->
731
- <div id="llm-health-chains" class="llm-health-chains"></div>
732
  <!-- Section B: candidate health grid -->
733
  <h3 style="margin-top: 18px;">Candidate health grid</h3>
734
  <div id="llm-health-candidates" class="llm-health-candidates"></div>
@@ -1490,129 +1439,174 @@
1490
  return { cls: 'unknown', symbol: '?', label: 'UNKNOWN' };
1491
  }
1492
 
1493
- function renderLlmHealthChain(chainBlock) {
1494
- var card = createEl('div', { className: 'llm-health-chain' });
1495
- var head = createEl('div', { className: 'chain-head' });
1496
- var badge = createEl('span', { className: 'role-badge', text: (ROLE_LABELS[chainBlock.role] || chainBlock.role).toUpperCase() });
1497
- badge.style.background = ROLE_COLORS[chainBlock.role] || '#444';
1498
- head.appendChild(badge);
1499
- var title = createEl('span', { text: 'Chain status' });
1500
- title.style.fontWeight = '600';
1501
- title.style.fontSize = '13px';
1502
- head.appendChild(title);
1503
- // KI-122 — staleness signal at the chain head so the operator can
1504
- // see whether they're looking at fresh probe data or stale state.
1505
- if (chainBlock.last_probe_age_seconds != null) {
1506
- var stale = createEl('span', {
1507
- className: 'small muted',
1508
- text: probed ' + fmtDurationShort(chainBlock.last_probe_age_seconds) + ' ago',
1509
- });
1510
- stale.style.marginLeft = 'auto';
1511
- head.appendChild(stale);
1512
- }
1513
- card.appendChild(head);
1514
-
1515
- // KI-122 — top-of-card "Currently in use" line so the operator
1516
- // doesn't have to infer the live model from the PRIMARY row.
1517
- var inUse = createEl('div', { className: 'in-use-line' });
1518
- var inUseLabel = createEl('span', { className: 'role-label', text: 'Currently in use:' });
1519
- inUse.appendChild(inUseLabel);
1520
- if (chainBlock.current_primary) {
1521
- var pillCls = chainBlock.current_primary_available ? 'pill-inuse' : 'pill-degraded';
1522
- var pillText = chainBlock.current_primary_available ? '● IN USE' : '● ELECTED BUT GATED';
1523
- var pill = createEl('span', { className: 'avail-pill ' + pillCls, text: pillText });
1524
- inUse.appendChild(pill);
1525
- var nm = createEl('span', { className: 'model-name', text: chainBlock.current_primary });
1526
- inUse.appendChild(nm);
1527
- } else {
1528
- var none = createEl('span', { className: 'avail-pill pill-down', text: '● NONE ELECTED' });
1529
- inUse.appendChild(none);
1530
- }
1531
- card.appendChild(inUse);
1532
-
1533
- // KI-122 — full chain member list with per-model availability pills,
1534
- // so the operator can see at a glance every model that's wired up
1535
- // and which ones are actually callable RIGHT NOW.
1536
- var members = chainBlock.chain_members || [];
1537
- if (members.length) {
1538
- var listLabel = createEl('div', { className: 'role-label', text: 'Chain members (' + members.length + ')' });
1539
- listLabel.style.marginTop = '10px';
1540
- card.appendChild(listLabel);
1541
- var list = createEl('div', { className: 'chain-members-list' });
1542
- members.forEach(function (mi) {
1543
- var row = createEl('div', { className: 'chain-member-row' });
1544
- var p;
1545
- if (mi.is_current_primary) {
1546
- p = createEl('span', { className: 'avail-pill pill-inuse', text: '● IN USE' });
1547
- } else if (mi.available_for_calls) {
1548
- p = createEl('span', { className: 'avail-pill pill-available', text: '● AVAILABLE' });
1549
- } else if (mi.credit_exhausted) {
1550
- p = createEl('span', { className: 'avail-pill pill-gated', text: '● GATED' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1551
  } else {
1552
- p = createEl('span', { className: 'avail-pill pill-down', text: '● UNAVAILABLE' });
 
1553
  }
1554
- row.appendChild(p);
1555
- row.appendChild(createEl('span', { className: 'model-name', text: mi.model }));
1556
- list.appendChild(row);
 
 
1557
  });
1558
- card.appendChild(list);
1559
- }
1560
 
1561
- // Render PRIMARY + BACKUP detail rows (kept for the per-model
1562
- // latency/success/credit telemetry the operator drills into).
1563
- ['primary', 'backup'].forEach(function (role) {
1564
- var snap = chainBlock[role + '_snapshot'];
1565
- var name = chainBlock['elected_' + role];
1566
- var block = createEl('div', { className: 'role-block' });
1567
- var label = createEl('div', { className: 'role-label', text: role.toUpperCase() });
1568
- block.appendChild(label);
1569
- if (!name || !snap) {
1570
- var noneEl = createEl('div', { className: 'none-elected', text: 'none elected (no eligible candidate)' });
1571
- block.appendChild(noneEl);
1572
- card.appendChild(block);
1573
- return;
1574
- }
1575
- var nameLine = createEl('div', { className: 'model-name' });
1576
- var b = candidateBadge(snap);
1577
- var statusBadge = createEl('span', { className: 'health-badge ' + b.cls, text: b.symbol + ' ' + b.label });
1578
- statusBadge.style.marginRight = '6px';
1579
- nameLine.appendChild(statusBadge);
1580
- nameLine.appendChild(document.createTextNode(name));
1581
- block.appendChild(nameLine);
1582
-
1583
- var stats = createEl('div', { className: 'stats-line' });
1584
- var pushed = false;
1585
- function pushPart(text) {
1586
- if (pushed) stats.appendChild(createEl('span', { className: 'sep', text: '·' }));
1587
- stats.appendChild(document.createTextNode(text));
1588
- pushed = true;
1589
- }
1590
- if (snap.latency_ms != null) pushPart('latency ' + fmtLatency(snap.latency_ms));
1591
- if (snap.success_rate != null) pushPart('success ' + (Number(snap.success_rate) * 100).toFixed(0) + '%');
1592
- if (snap.credits_remaining != null) pushPart('credits ' + fmtCredits(snap.credits_remaining, snap.credits_unit, null));
1593
- if (snap.credits_reset_in_seconds != null && snap.credits_reset_in_seconds > 0) {
1594
- pushPart('reset in ' + fmtDurationShort(snap.credits_reset_in_seconds));
1595
- }
1596
- if (snap.degraded_for_seconds != null && snap.degraded_for_seconds > 0) {
1597
- pushPart('sin-bin ' + fmtDurationShort(snap.degraded_for_seconds));
1598
- }
1599
- if (!pushed) stats.textContent = 'No telemetry yet.';
1600
- block.appendChild(stats);
1601
- card.appendChild(block);
1602
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
 
1604
- // KI-122 — banner is now backend-gated by the STRICT exhausted rule
1605
- // (`chain_credit_exhausted` true only when zero members are
1606
- // available_for_calls AND at least one is strictly credit-exhausted).
1607
- // This prevents the historic "HEALTHY 100% + credit-exhausted banner"
1608
- // contradiction the operator reported.
1609
- if (chainBlock.chain_credit_exhausted) {
1610
- var banner = createEl('div', { className: 'credit-banner' });
1611
- banner.textContent = 'All candidates for ' + (ROLE_LABELS[chainBlock.role] || chainBlock.role).toUpperCase() +
1612
- '_CHAIN are credit-exhausted; canonical fallback will fire until quotas reset.';
1613
- card.appendChild(banner);
1614
  }
1615
- return card;
 
 
1616
  }
1617
 
1618
  function renderLlmHealthCandidates(rows) {
@@ -1754,9 +1748,7 @@
1754
  }
1755
 
1756
  clearChildren(chainsHost);
1757
- (STATE.llmHealth.chains || []).forEach(function (c) {
1758
- chainsHost.appendChild(renderLlmHealthChain(c));
1759
- });
1760
 
1761
  renderLlmHealthCandidates(STATE.llmHealth.candidates || []);
1762
  renderLlmHealthRecent(STATE.llmHealth.recent_turns || []);
@@ -1912,9 +1904,11 @@
1912
  var tr = createEl('tr');
1913
 
1914
  var nameCell = createEl('td', { className: 'name-cell', text: p.name_display || '—' });
 
1915
  tr.appendChild(nameCell);
1916
 
1917
  var slugCell = createEl('td', { className: 'slug-cell', text: p.slug || '—' });
 
1918
  tr.appendChild(slugCell);
1919
 
1920
  var firstCell = createEl('td', { className: 'time-cell', text: fmtIST(p.first_seen) });
 
347
  .chain-table .icon-btn { padding: 2px 7px; font-size: 12px; line-height: 1.2; }
348
  .chain-table tbody tr.is-primary td { background: rgba(63, 185, 80, 0.05); }
349
 
350
+ /* KI-162: simple 2-table layout for LLM Chain section (replaces legacy .llm-health-chains grid) */
351
+ .llm-simple-tables { display: flex; flex-direction: column; gap: 18px; }
352
+ .llm-simple-tables .simple-table-block h3 {
353
+ margin: 0 0 8px;
354
+ font-size: 13px;
355
+ font-weight: 600;
356
+ color: var(--text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  }
358
+ .llm-simple-tables table {
359
+ width: 100%;
360
+ border-collapse: collapse;
361
  font-size: 12px;
 
362
  }
363
+ .llm-simple-tables thead th {
364
+ text-align: left;
365
+ padding: 8px;
366
+ border-bottom: 1px solid var(--border);
367
+ font-weight: 600;
368
  color: var(--muted);
369
+ text-transform: uppercase;
370
+ letter-spacing: 0.05em;
371
+ font-size: 10px;
372
  }
373
+ .llm-simple-tables tbody td {
374
+ padding: 8px;
375
+ border-bottom: 1px solid var(--border);
376
+ vertical-align: middle;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  }
378
+ .llm-simple-tables tbody tr:last-child td { border-bottom: none; }
379
+ .llm-simple-tables td.model-name,
380
+ .llm-simple-tables td.mono {
381
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, monospace;
 
382
  word-break: break-all;
383
  }
384
+ .llm-simple-tables td.use-label {
385
+ font-weight: 600;
386
+ white-space: nowrap;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  }
388
+ .llm-simple-tables td.healthy-cell { text-align: center; width: 80px; }
389
+ .llm-simple-tables .health-dot {
390
  display: inline-block;
391
+ width: 10px;
392
+ height: 10px;
393
+ border-radius: 50%;
394
+ }
395
+ .llm-simple-tables .health-dot.ok { background: var(--green, #3fb950); }
396
+ .llm-simple-tables .health-dot.bad { background: var(--red, #f85149); }
397
+ .llm-simple-tables td.used-by-cell { color: var(--muted); }
398
+ .llm-simple-tables td.probed-cell {
399
+ color: var(--muted);
400
  white-space: nowrap;
401
+ font-variant-numeric: tabular-nums;
402
  }
403
+ /* KI-162: legacy .llm-health-chain* + .avail-pill CSS removed replaced by .llm-simple-tables above */
 
 
 
 
404
 
405
  /* Status badges — three colors: ✓ green / ⚠ amber / ✗ red */
406
  .health-badge {
 
556
  }
557
 
558
  /* Profiles table */
559
+ .profile-table td.name-cell {
560
+ font-weight: 500;
561
+ max-width: 200px;
562
+ white-space: nowrap;
563
+ overflow: hidden;
564
+ text-overflow: ellipsis;
565
+ }
566
  .profile-table td.slug-cell {
567
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, monospace;
568
  font-size: 12px;
569
  color: var(--muted);
570
+ max-width: 140px;
571
+ white-space: nowrap;
572
+ overflow: hidden;
573
+ text-overflow: ellipsis;
574
  }
575
  .profile-table td.num-cell { text-align: right; font-variant-numeric: tabular-nums; }
576
  .profile-table td.time-cell { white-space: nowrap; font-size: 12px; }
 
671
  <!-- KI-086: LLM Health & Credits snapshot (KI-080..KI-085 telemetry) -->
672
  <div class="card" id="llm-health-card">
673
  <div class="row-between" style="margin-bottom: 12px;">
674
+ <h2>LLM health <span class="small muted" id="llm-health-snapshot-ts"></span></h2>
675
  <div class="actions">
676
  <button id="btn-refresh-llm-health">Refresh</button>
677
  </div>
678
  </div>
679
+ <!-- KI-162: simplified 2-table layout (What's Live + What's Available) -->
680
+ <div id="llm-health-chains" class="llm-simple-tables"></div>
681
  <!-- Section B: candidate health grid -->
682
  <h3 style="margin-top: 18px;">Candidate health grid</h3>
683
  <div id="llm-health-candidates" class="llm-health-candidates"></div>
 
1439
  return { cls: 'unknown', symbol: '?', label: 'UNKNOWN' };
1440
  }
1441
 
1442
+ // KI-162 — simplified 2-table renderer for the LLM Chain section.
1443
+ // Table 1: "What's Live" one row per chain role with the currently-elected model.
1444
+ // Table 2: "What's Available" NIM-only pool deduped across chains, with Healthy + Used By.
1445
+ // Replaces the per-chain cards / chain-members lists / availability pills.
1446
+ var SIMPLE_USE_LABELS = {
1447
+ fast_brain: 'Brain Fast',
1448
+ brain: 'Brain Main',
1449
+ judge: 'Judge'
1450
+ };
1451
+ var SIMPLE_USE_ORDER = ['fast_brain', 'brain', 'judge'];
1452
+
1453
+ function isRetiredModel(model) {
1454
+ // Post-KI-160 NIM-only: filter out any stale Groq / OpenRouter entries
1455
+ // that might still appear in chain_members[] payloads.
1456
+ if (!model) return true;
1457
+ var m = String(model).toLowerCase();
1458
+ return (
1459
+ m.indexOf('groq:') === 0 ||
1460
+ m.indexOf('openrouter:') === 0 ||
1461
+ m.indexOf('or:') === 0
1462
+ );
1463
+ }
1464
+
1465
+ function renderLlmSimpleTables(host, chains) {
1466
+ // ---- Table 1: What's Live ----
1467
+ var liveBlock = createEl('div', { className: 'simple-table-block' });
1468
+ liveBlock.appendChild(createEl('h3', { text: "What's Live" }));
1469
+ var liveTable = createEl('table');
1470
+ var liveThead = createEl('thead');
1471
+ var liveHr = createEl('tr');
1472
+ ['Use', 'Currently In Use', 'Probed'].forEach(function (h) {
1473
+ liveHr.appendChild(createEl('th', { text: h }));
1474
+ });
1475
+ liveThead.appendChild(liveHr);
1476
+ liveTable.appendChild(liveThead);
1477
+ var liveTbody = createEl('tbody');
1478
+
1479
+ // Index chains by role for stable Brain Fast / Brain Main / Judge ordering.
1480
+ var chainsByRole = {};
1481
+ (chains || []).forEach(function (c) { if (c && c.role) chainsByRole[c.role] = c; });
1482
+
1483
+ SIMPLE_USE_ORDER.forEach(function (role) {
1484
+ var c = chainsByRole[role];
1485
+ var tr = createEl('tr');
1486
+ tr.appendChild(createEl('td', { className: 'use-label', text: SIMPLE_USE_LABELS[role] }));
1487
+
1488
+ var modelCell = createEl('td', { className: 'mono' });
1489
+ if (c && c.current_primary && !isRetiredModel(c.current_primary)) {
1490
+ if (c.current_primary_available === false) {
1491
+ var dot = createEl('span', { className: 'health-dot bad' });
1492
+ dot.style.marginRight = '6px';
1493
+ modelCell.appendChild(dot);
1494
+ }
1495
+ modelCell.appendChild(document.createTextNode(c.current_primary));
1496
+ } else {
1497
+ modelCell.textContent = '';
1498
+ }
1499
+ tr.appendChild(modelCell);
1500
+
1501
+ var probedCell = createEl('td', { className: 'probed-cell' });
1502
+ if (c && c.last_probe_age_seconds != null) {
1503
+ probedCell.textContent = fmtDurationShort(c.last_probe_age_seconds) + ' ago';
1504
+ } else {
1505
+ probedCell.textContent = '—';
1506
+ }
1507
+ tr.appendChild(probedCell);
1508
+ liveTbody.appendChild(tr);
1509
+ });
1510
+ liveTable.appendChild(liveTbody);
1511
+ liveBlock.appendChild(liveTable);
1512
+ host.appendChild(liveBlock);
1513
+
1514
+ // ---- Table 2: What's Available ----
1515
+ // Walk every chain_members[] across the 3 chains, dedupe by model,
1516
+ // and collect: healthy boolean + used-by-role list with primary-first ordering.
1517
+ var modelMap = {}; // model -> { healthy, roleEntries: [{role, isPrimary, idx}] }
1518
+ (chains || []).forEach(function (c) {
1519
+ if (!c || !Array.isArray(c.chain_members)) return;
1520
+ c.chain_members.forEach(function (mi, idx) {
1521
+ if (!mi || !mi.model || isRetiredModel(mi.model)) return;
1522
+ if (!modelMap[mi.model]) {
1523
+ modelMap[mi.model] = {
1524
+ healthy: !!mi.available_for_calls,
1525
+ roleEntries: []
1526
+ };
1527
  } else {
1528
+ // If any chain reports the model as available, treat it as healthy.
1529
+ if (mi.available_for_calls) modelMap[mi.model].healthy = true;
1530
  }
1531
+ modelMap[mi.model].roleEntries.push({
1532
+ role: c.role,
1533
+ isPrimary: !!mi.is_current_primary,
1534
+ idx: idx
1535
+ });
1536
  });
1537
+ });
 
1538
 
1539
+ var availBlock = createEl('div', { className: 'simple-table-block' });
1540
+ availBlock.appendChild(createEl('h3', { text: "What's Available" }));
1541
+ var availTable = createEl('table');
1542
+ var availThead = createEl('thead');
1543
+ var availHr = createEl('tr');
1544
+ ['Model', 'Healthy', 'Used By'].forEach(function (h) {
1545
+ availHr.appendChild(createEl('th', { text: h }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1546
  });
1547
+ availThead.appendChild(availHr);
1548
+ availTable.appendChild(availThead);
1549
+ var availTbody = createEl('tbody');
1550
+
1551
+ var modelNames = Object.keys(modelMap);
1552
+ if (!modelNames.length) {
1553
+ var emptyTr = createEl('tr');
1554
+ var emptyTd = createEl('td', { attrs: { colspan: '3' }, text: 'No models in any chain.' });
1555
+ emptyTd.style.color = 'var(--muted)';
1556
+ emptyTd.style.textAlign = 'center';
1557
+ emptyTr.appendChild(emptyTd);
1558
+ availTbody.appendChild(emptyTr);
1559
+ } else {
1560
+ // Stable sort: models that are anyone's primary first, then by name.
1561
+ modelNames.sort(function (a, b) {
1562
+ var aPrim = modelMap[a].roleEntries.some(function (e) { return e.isPrimary; });
1563
+ var bPrim = modelMap[b].roleEntries.some(function (e) { return e.isPrimary; });
1564
+ if (aPrim !== bPrim) return aPrim ? -1 : 1;
1565
+ return a.localeCompare(b);
1566
+ });
1567
+ modelNames.forEach(function (model) {
1568
+ var info = modelMap[model];
1569
+ var tr = createEl('tr');
1570
+ tr.appendChild(createEl('td', { className: 'model-name', text: model }));
1571
+
1572
+ var healthyCell = createEl('td', { className: 'healthy-cell' });
1573
+ var hd = createEl('span', { className: 'health-dot ' + (info.healthy ? 'ok' : 'bad') });
1574
+ hd.title = info.healthy ? 'Healthy' : 'Unhealthy';
1575
+ healthyCell.appendChild(hd);
1576
+ tr.appendChild(healthyCell);
1577
+
1578
+ // "Used By" — primary first per role, then backups; relabel via SIMPLE_USE_LABELS.
1579
+ // Group by role, identify primary vs. backup using is_current_primary + chain index.
1580
+ var entriesByRole = {};
1581
+ info.roleEntries.forEach(function (e) {
1582
+ if (!entriesByRole[e.role]) entriesByRole[e.role] = [];
1583
+ entriesByRole[e.role].push(e);
1584
+ });
1585
+ // Order: primaries first (in SIMPLE_USE_ORDER), then backups by chain position.
1586
+ var primaryParts = [];
1587
+ var backupParts = [];
1588
+ SIMPLE_USE_ORDER.forEach(function (role) {
1589
+ var roleList = entriesByRole[role] || [];
1590
+ roleList.forEach(function (e) {
1591
+ var label = SIMPLE_USE_LABELS[role] || role;
1592
+ if (e.isPrimary) {
1593
+ primaryParts.push(label);
1594
+ } else {
1595
+ // idx 0 inside members is the primary itself when the chain has it;
1596
+ // anything else is a backup of some rank.
1597
+ backupParts.push(label + ' (backup)');
1598
+ }
1599
+ });
1600
+ });
1601
+ var usedBy = primaryParts.concat(backupParts).join(', ');
1602
+ tr.appendChild(createEl('td', { className: 'used-by-cell', text: usedBy || '—' }));
1603
 
1604
+ availTbody.appendChild(tr);
1605
+ });
 
 
 
 
 
 
 
 
1606
  }
1607
+ availTable.appendChild(availTbody);
1608
+ availBlock.appendChild(availTable);
1609
+ host.appendChild(availBlock);
1610
  }
1611
 
1612
  function renderLlmHealthCandidates(rows) {
 
1748
  }
1749
 
1750
  clearChildren(chainsHost);
1751
+ renderLlmSimpleTables(chainsHost, STATE.llmHealth.chains || []);
 
 
1752
 
1753
  renderLlmHealthCandidates(STATE.llmHealth.candidates || []);
1754
  renderLlmHealthRecent(STATE.llmHealth.recent_turns || []);
 
1904
  var tr = createEl('tr');
1905
 
1906
  var nameCell = createEl('td', { className: 'name-cell', text: p.name_display || '—' });
1907
+ if (p.name_display) nameCell.title = p.name_display;
1908
  tr.appendChild(nameCell);
1909
 
1910
  var slugCell = createEl('td', { className: 'slug-cell', text: p.slug || '—' });
1911
+ if (p.slug) slugCell.title = p.slug;
1912
  tr.appendChild(slugCell);
1913
 
1914
  var firstCell = createEl('td', { className: 'time-cell', text: fmtIST(p.first_seen) });