nexus-prime 3.2.2 → 3.3.0

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.
@@ -245,6 +245,9 @@
245
245
 
246
246
  .rail.left {
247
247
  grid-template-rows: auto auto auto auto;
248
+ overflow-y: auto;
249
+ scrollbar-width: thin;
250
+ scrollbar-color: rgba(188, 204, 255, 0.22) transparent;
248
251
  }
249
252
 
250
253
  .rail.right {
@@ -426,8 +429,8 @@
426
429
  stroke: var(--accent);
427
430
  stroke-width: 12;
428
431
  stroke-linecap: round;
429
- stroke-dasharray: 440;
430
- stroke-dashoffset: 440;
432
+ stroke-dasharray: 439.82;
433
+ stroke-dashoffset: 439.82;
431
434
  filter: drop-shadow(0 0 10px rgba(84, 255, 135, 0.32));
432
435
  transition: stroke-dashoffset 300ms ease;
433
436
  }
@@ -512,7 +515,7 @@
512
515
 
513
516
  #graph-stage {
514
517
  position: relative;
515
- min-height: 0;
518
+ min-height: 300px;
516
519
  height: 100%;
517
520
  border-radius: 22px;
518
521
  border: 1px solid var(--line);
@@ -751,6 +754,21 @@
751
754
  transform: translateX(0);
752
755
  }
753
756
 
757
+ .drawer-backdrop {
758
+ position: fixed;
759
+ inset: 0;
760
+ background: rgba(0, 0, 0, 0.4);
761
+ z-index: 19;
762
+ opacity: 0;
763
+ pointer-events: none;
764
+ transition: opacity 220ms ease;
765
+ }
766
+
767
+ .drawer-backdrop.open {
768
+ opacity: 1;
769
+ pointer-events: auto;
770
+ }
771
+
754
772
  .drawer-header {
755
773
  display: flex;
756
774
  justify-content: space-between;
@@ -797,10 +815,37 @@
797
815
  .library-grid {
798
816
  display: grid;
799
817
  gap: 0.7rem;
800
- max-height: 15rem;
818
+ max-height: 28rem;
801
819
  overflow: auto;
802
820
  }
803
821
 
822
+ .refresh-bar {
823
+ position: absolute;
824
+ top: 0;
825
+ left: 0;
826
+ height: 3px;
827
+ width: 100%;
828
+ background: linear-gradient(90deg, var(--accent), var(--blue), var(--violet));
829
+ transform: scaleX(0);
830
+ transform-origin: left;
831
+ transition: transform 400ms ease;
832
+ z-index: 5;
833
+ pointer-events: none;
834
+ opacity: 0;
835
+ }
836
+
837
+ .refresh-bar.active {
838
+ opacity: 1;
839
+ transform: scaleX(1);
840
+ transition: transform 1.8s cubic-bezier(0.4, 0, 0.2, 1);
841
+ }
842
+
843
+ .refresh-bar.done {
844
+ transform: scaleX(1);
845
+ opacity: 0;
846
+ transition: transform 200ms ease, opacity 400ms ease 200ms;
847
+ }
848
+
804
849
  .hidden {
805
850
  display: none !important;
806
851
  }
@@ -809,6 +854,20 @@
809
854
  .layout {
810
855
  grid-template-columns: 290px minmax(0, 1fr) 360px;
811
856
  }
857
+
858
+ .token-dial {
859
+ width: 140px;
860
+ height: 140px;
861
+ }
862
+
863
+ .token-dial svg {
864
+ width: 140px;
865
+ height: 140px;
866
+ }
867
+
868
+ .metrics-grid {
869
+ grid-template-columns: 1fr;
870
+ }
812
871
  }
813
872
 
814
873
  @media (max-width: 1180px) {
@@ -827,7 +886,7 @@
827
886
  }
828
887
 
829
888
  .graph-panel {
830
- min-height: 34rem;
889
+ min-height: 24rem;
831
890
  }
832
891
  }
833
892
 
@@ -866,7 +925,8 @@
866
925
 
867
926
  <div id="status-banner" class="banner hidden" role="status" aria-live="polite"></div>
868
927
 
869
- <main class="layout">
928
+ <main class="layout" style="position:relative;">
929
+ <div id="refresh-bar" class="refresh-bar"></div>
870
930
  <aside class="rail left">
871
931
  <section class="panel">
872
932
  <div class="panel-header">
@@ -993,6 +1053,7 @@
993
1053
  <button data-library-mode="memories" class="active">Memories</button>
994
1054
  <button data-library-mode="skills">Skills</button>
995
1055
  <button data-library-mode="workflows">Workflows</button>
1056
+ <button data-library-mode="spend">Spend</button>
996
1057
  <button data-library-mode="pod">POD</button>
997
1058
  <button data-library-mode="clients">Clients</button>
998
1059
  </div>
@@ -1096,6 +1157,7 @@
1096
1157
  <div class="empty">Touch a memory, run, workflow, skill, POD worker, or client to inspect it.</div>
1097
1158
  </div>
1098
1159
  </aside>
1160
+ <div id="drawer-backdrop" class="drawer-backdrop"></div>
1099
1161
 
1100
1162
  <script>
1101
1163
  const DASHBOARD_API_VERSION = '2';
@@ -1356,7 +1418,7 @@
1356
1418
 
1357
1419
  if (raw.category && raw.title && raw.time) {
1358
1420
  return {
1359
- id: raw.id || `evt-${raw.time}-${Math.random().toString(36).slice(2, 8)}`,
1421
+ id: raw.id || `evt-${raw.type || raw.category}-${raw.time}-${raw.source || 'nexus-prime'}`,
1360
1422
  type: raw.type || raw.category,
1361
1423
  title: String(raw.title),
1362
1424
  source: String(raw.source || 'nexus-prime'),
@@ -1371,7 +1433,7 @@
1371
1433
  if (raw.type && raw.timestamp) {
1372
1434
  const payload = raw.data || {};
1373
1435
  return {
1374
- id: raw.id || `evt-${raw.timestamp}-${Math.random().toString(36).slice(2, 8)}`,
1436
+ id: raw.id || `evt-${raw.type}-${raw.timestamp}-${legacySource(raw.type, payload)}`,
1375
1437
  type: raw.type,
1376
1438
  title: legacyTitle(raw.type),
1377
1439
  source: legacySource(raw.type, payload),
@@ -1384,7 +1446,7 @@
1384
1446
  }
1385
1447
 
1386
1448
  return {
1387
- id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1449
+ id: `evt-legacy-${Date.now()}`,
1388
1450
  type: 'system.legacy',
1389
1451
  title: 'Legacy event',
1390
1452
  source: 'nexus-prime',
@@ -1406,6 +1468,9 @@
1406
1468
  }
1407
1469
 
1408
1470
  async function refreshAll() {
1471
+ const bar = $('refresh-bar');
1472
+ bar.classList.remove('done');
1473
+ bar.classList.add('active');
1409
1474
  const resources = [
1410
1475
  ['runs', '/api/runs?limit=20', (value) => { state.runs = Array.isArray(value) ? value : state.runs; }],
1411
1476
  ['skills', '/api/skills', (value) => { state.skills = Array.isArray(value) ? value : state.skills; }],
@@ -1416,9 +1481,14 @@
1416
1481
  ['pod', '/api/pod?limit=30', (value) => { state.pod = value || state.pod; }],
1417
1482
  ['clients', '/api/clients', (value) => { state.clients = Array.isArray(value) ? value : state.clients; }],
1418
1483
  ['events', '/api/events?limit=80', (value) => {
1419
- state.events = Array.isArray(value)
1420
- ? value.map((event) => normalizeEventCard(event)).filter(Boolean)
1421
- : state.events;
1484
+ if (Array.isArray(value)) {
1485
+ const seen = new Set();
1486
+ state.events = value.map((event) => normalizeEventCard(event)).filter((e) => {
1487
+ if (!e || seen.has(e.id)) return false;
1488
+ seen.add(e.id);
1489
+ return true;
1490
+ });
1491
+ }
1422
1492
  }],
1423
1493
  ];
1424
1494
 
@@ -1440,6 +1510,9 @@
1440
1510
  state.lastRefreshAt = Date.now();
1441
1511
  }
1442
1512
 
1513
+ reconcileBanner();
1514
+ populateBackendSelects();
1515
+
1443
1516
  const focusMemoryId = state.selected?.kind === 'memory'
1444
1517
  ? state.selected.id
1445
1518
  : state.memories[0]?.id;
@@ -1453,11 +1526,29 @@
1453
1526
  setResourceStatus('memoryNetwork', 'idle');
1454
1527
  }
1455
1528
 
1456
- reconcileBanner();
1457
- populateBackendSelects();
1529
+ bar.classList.remove('active');
1530
+ bar.classList.add('done');
1531
+ setTimeout(() => bar.classList.remove('done'), 600);
1458
1532
  render();
1459
1533
  }
1460
1534
 
1535
+ let _refreshTimer = null;
1536
+ let _refreshInFlight = false;
1537
+
1538
+ function scheduleRefresh() {
1539
+ if (_refreshTimer) return;
1540
+ _refreshTimer = setTimeout(async () => {
1541
+ _refreshTimer = null;
1542
+ if (_refreshInFlight) {
1543
+ scheduleRefresh();
1544
+ return;
1545
+ }
1546
+ _refreshInFlight = true;
1547
+ try { await refreshAll(); } catch {}
1548
+ _refreshInFlight = false;
1549
+ }, 300);
1550
+ }
1551
+
1461
1552
  async function refreshMemorySelection(memoryId, openDrawer = true) {
1462
1553
  const [detail, network] = await Promise.allSettled([
1463
1554
  fetchJson(`/api/memory/${encodeURIComponent(memoryId)}`),
@@ -1542,6 +1633,77 @@
1542
1633
  };
1543
1634
  }
1544
1635
 
1636
+ function computeSpendData() {
1637
+ const tokenEvents = state.events.filter((e) => e.category === 'tokens');
1638
+ let totalTokens = 0;
1639
+ const categoryMap = {};
1640
+ const sessions = new Set();
1641
+
1642
+ for (const event of tokenEvents) {
1643
+ const p = event.payload || {};
1644
+ const tokens = (p.inputTokens || 0) + (p.outputTokens || 0) + (p.savings || 0);
1645
+ totalTokens += tokens;
1646
+ const cat = event.type || 'unknown';
1647
+ categoryMap[cat] = (categoryMap[cat] || 0) + tokens;
1648
+ if (p.sessionId) sessions.add(p.sessionId);
1649
+ }
1650
+
1651
+ const byCategory = Object.entries(categoryMap)
1652
+ .map(([category, tokens]) => ({ category, tokens, pct: totalTokens ? Math.round((tokens / totalTokens) * 100) : 0 }))
1653
+ .sort((a, b) => b.tokens - a.tokens)
1654
+ .slice(0, 8);
1655
+
1656
+ const recentEvents = tokenEvents.slice(0, 10).map((e) => ({
1657
+ title: e.title,
1658
+ tokens: (e.payload?.inputTokens || 0) + (e.payload?.outputTokens || 0),
1659
+ time: e.time,
1660
+ }));
1661
+
1662
+ // Rough cost estimate: ~$3 per 1M input, ~$15 per 1M output (blended ~$8/1M)
1663
+ const estimatedCost = (totalTokens / 1000000 * 8).toFixed(4);
1664
+
1665
+ return { totalTokens, estimatedCost, sessionCount: sessions.size || 1, byCategory, recentEvents };
1666
+ }
1667
+
1668
+ function bindSkillFormHandlers() {
1669
+ const form = $('skill-create-form');
1670
+ if (!form) return;
1671
+
1672
+ form.addEventListener('submit', async (e) => {
1673
+ e.preventDefault();
1674
+ const name = $('skill-name-input').value.trim();
1675
+ const instructions = $('skill-instructions-input').value.trim();
1676
+ const riskClass = $('skill-risk-input').value;
1677
+ const scope = $('skill-scope-input').value;
1678
+ if (!name || !instructions) return;
1679
+
1680
+ try {
1681
+ $('skill-create-status').textContent = 'Registering skill...';
1682
+ await fetchJson('/api/skills/register', postJson({ name, instructions, riskClass, scope }));
1683
+ $('skill-create-status').textContent = `Skill "${name}" registered.`;
1684
+ $('skill-name-input').value = '';
1685
+ $('skill-instructions-input').value = '';
1686
+ await refreshAll();
1687
+ } catch (err) {
1688
+ $('skill-create-status').textContent = `Failed: ${err.message}`;
1689
+ }
1690
+ });
1691
+
1692
+ const seedBtn = $('seed-skills-btn');
1693
+ if (seedBtn) {
1694
+ seedBtn.addEventListener('click', async () => {
1695
+ try {
1696
+ $('skill-create-status').textContent = 'Seeding default skills...';
1697
+ await fetchJson('/api/skills/seed', postJson({}));
1698
+ $('skill-create-status').textContent = 'Default skills installed.';
1699
+ await refreshAll();
1700
+ } catch (err) {
1701
+ $('skill-create-status').textContent = `Seed failed: ${err.message}`;
1702
+ }
1703
+ });
1704
+ }
1705
+ }
1706
+
1545
1707
  function render() {
1546
1708
  renderBanner();
1547
1709
  renderHeader();
@@ -1598,6 +1760,12 @@
1598
1760
  `).join('')
1599
1761
  : emptyState('No clients detected.');
1600
1762
 
1763
+ $('clients-list').querySelectorAll('.card.interactive').forEach((card) => {
1764
+ card.addEventListener('click', () => {
1765
+ void openEntity(card.getAttribute('data-kind'), card.getAttribute('data-id'));
1766
+ });
1767
+ });
1768
+
1601
1769
  const tokenMetrics = computeTokenMetrics();
1602
1770
  $('token-summary').textContent = resourceFailed('events') ? 'events unavailable' : `${tokenMetrics.events} events`;
1603
1771
  $('gross-tokens').textContent = formatNumber(tokenMetrics.gross);
@@ -1648,6 +1816,12 @@
1648
1816
  </div>
1649
1817
  `).join('')
1650
1818
  : emptyState('Waiting for POD traffic.');
1819
+
1820
+ $('pod-highlights').querySelectorAll('.card.interactive').forEach((card) => {
1821
+ card.addEventListener('click', () => {
1822
+ void openEntity(card.getAttribute('data-kind'), card.getAttribute('data-id'));
1823
+ });
1824
+ });
1651
1825
  }
1652
1826
 
1653
1827
  function buildGraphModel() {
@@ -1820,6 +1994,7 @@
1820
1994
  memories: 'Memory Snapshots',
1821
1995
  skills: 'Skills',
1822
1996
  workflows: 'Workflows',
1997
+ spend: 'Tool Spend Tracker',
1823
1998
  pod: 'POD Signals',
1824
1999
  clients: 'Connected Ecosystem',
1825
2000
  };
@@ -1842,7 +2017,45 @@
1842
2017
  `).join('')
1843
2018
  : emptyState('No memory stored yet.');
1844
2019
  } else if (state.libraryMode === 'skills') {
1845
- container.innerHTML = resourceFailed('skills') && !state.skills.length
2020
+ const skillForm = `
2021
+ <details class="card" style="border-color: rgba(84,255,135,0.2); background: rgba(84,255,135,0.04);">
2022
+ <summary style="cursor:pointer; font-size:0.82rem; font-weight:600; color:var(--accent);">+ Create Skill</summary>
2023
+ <form id="skill-create-form" class="control-grid" style="margin-top:0.6rem;">
2024
+ <div class="field">
2025
+ <label for="skill-name-input">Name</label>
2026
+ <input id="skill-name-input" type="text" placeholder="my-custom-skill" required>
2027
+ </div>
2028
+ <div class="field">
2029
+ <label for="skill-instructions-input">Instructions</label>
2030
+ <textarea id="skill-instructions-input" placeholder="What this skill does and when to trigger it..." required></textarea>
2031
+ </div>
2032
+ <div class="inline-fields">
2033
+ <div class="field">
2034
+ <label for="skill-risk-input">Risk Class</label>
2035
+ <select id="skill-risk-input">
2036
+ <option value="read">Read</option>
2037
+ <option value="orchestrate" selected>Orchestrate</option>
2038
+ <option value="mutate">Mutate</option>
2039
+ </select>
2040
+ </div>
2041
+ <div class="field">
2042
+ <label for="skill-scope-input">Scope</label>
2043
+ <select id="skill-scope-input">
2044
+ <option value="session" selected>Session</option>
2045
+ <option value="worker">Worker</option>
2046
+ <option value="global">Global</option>
2047
+ </select>
2048
+ </div>
2049
+ </div>
2050
+ <div class="action-bar">
2051
+ <button type="submit" class="primary-button">Register Skill</button>
2052
+ <button type="button" id="seed-skills-btn" class="ghost-button">Seed Defaults</button>
2053
+ </div>
2054
+ <div id="skill-create-status" class="meta"></div>
2055
+ </form>
2056
+ </details>
2057
+ `;
2058
+ const skillList = resourceFailed('skills') && !state.skills.length
1846
2059
  ? emptyState('Skills endpoint unavailable.')
1847
2060
  : state.skills.length
1848
2061
  ? state.skills.map((skill) => `
@@ -1859,7 +2072,9 @@
1859
2072
  </div>
1860
2073
  </div>
1861
2074
  `).join('')
1862
- : emptyState('No skills loaded.');
2075
+ : emptyState('No skills loaded. Click "Seed Defaults" to install starter skills.');
2076
+ container.innerHTML = skillForm + skillList;
2077
+ bindSkillFormHandlers();
1863
2078
  } else if (state.libraryMode === 'workflows') {
1864
2079
  container.innerHTML = resourceFailed('workflows') && !state.workflows.length
1865
2080
  ? emptyState('Workflows endpoint unavailable.')
@@ -1891,7 +2106,45 @@
1891
2106
  </div>
1892
2107
  `).join('')
1893
2108
  : emptyState('No POD signals captured.');
1894
- } else {
2109
+ } else if (state.libraryMode === 'spend') {
2110
+ const spendData = computeSpendData();
2111
+ container.innerHTML = `
2112
+ <div class="metrics-grid">
2113
+ <div class="hero-metric">
2114
+ <label>Total Tokens</label>
2115
+ <strong>${formatNumber(spendData.totalTokens)}</strong>
2116
+ <div class="meta">Across ${spendData.sessionCount} sessions</div>
2117
+ </div>
2118
+ <div class="hero-metric">
2119
+ <label>Estimated Cost</label>
2120
+ <strong>$${spendData.estimatedCost}</strong>
2121
+ <div class="meta">Based on avg token pricing</div>
2122
+ </div>
2123
+ </div>
2124
+ <div style="margin-top:0.75rem;">
2125
+ <div class="entity-header"><h3>By Category</h3></div>
2126
+ ${spendData.byCategory.length ? spendData.byCategory.map((cat) => `
2127
+ <div class="card" style="margin-bottom:0.5rem;">
2128
+ <div class="card-title">
2129
+ <strong>${escapeHtml(cat.category)}</strong>
2130
+ <span class="mono" style="font-size:0.78rem;">${formatNumber(cat.tokens)} tokens</span>
2131
+ </div>
2132
+ <div style="height:4px; border-radius:2px; background:rgba(255,255,255,0.08); overflow:hidden;">
2133
+ <div style="height:100%; width:${cat.pct}%; background:var(--accent); border-radius:2px;"></div>
2134
+ </div>
2135
+ </div>
2136
+ `).join('') : emptyState('No token events recorded yet.')}
2137
+ </div>
2138
+ <div style="margin-top:0.75rem;">
2139
+ <div class="entity-header"><h3>Recent Activity</h3></div>
2140
+ ${spendData.recentEvents.length ? spendData.recentEvents.map((ev) => `
2141
+ <div class="card" style="margin-bottom:0.4rem;">
2142
+ <div class="meta">${escapeHtml(ev.title)} · ${formatNumber(ev.tokens)} tokens · ${formatAgo(ev.time)}</div>
2143
+ </div>
2144
+ `).join('') : emptyState('No recent token activity.')}
2145
+ </div>
2146
+ `;
2147
+ } else if (state.libraryMode === 'clients') {
1895
2148
  container.innerHTML = resourceFailed('clients') && !state.clients.length
1896
2149
  ? emptyState('Clients endpoint unavailable.')
1897
2150
  : state.clients.length
@@ -1951,8 +2204,10 @@
1951
2204
 
1952
2205
  function renderDrawer() {
1953
2206
  const drawer = $('drawer');
2207
+ const backdrop = $('drawer-backdrop');
1954
2208
  if (!state.selected || !state.selected.data) {
1955
2209
  drawer.classList.remove('open');
2210
+ backdrop.classList.remove('open');
1956
2211
  $('drawer-title').textContent = 'Inspector';
1957
2212
  $('drawer-subtitle').textContent = 'Select a node or card.';
1958
2213
  $('drawer-body').innerHTML = '<div class="empty">Touch a memory, run, workflow, skill, POD worker, or client to inspect it.</div>';
@@ -1960,6 +2215,7 @@
1960
2215
  }
1961
2216
 
1962
2217
  drawer.classList.add('open');
2218
+ backdrop.classList.add('open');
1963
2219
  const { kind, data } = state.selected;
1964
2220
  $('drawer-title').textContent = formatDrawerTitle(kind, data);
1965
2221
  $('drawer-subtitle').textContent = formatDrawerSubtitle(kind, data);
@@ -2253,29 +2509,33 @@
2253
2509
 
2254
2510
  async function openEntity(kind, id) {
2255
2511
  if (!kind || !id) return;
2256
- if (kind === 'memory') {
2257
- await refreshMemorySelection(id, true);
2258
- } else if (kind === 'run') {
2259
- const run = state.runs.find((item) => item.runId === id) || await fetchJson(`/api/runs/${encodeURIComponent(id)}`);
2260
- if (run?.error) return;
2261
- state.selected = { kind: 'run', id, data: run };
2262
- } else if (kind === 'skill') {
2263
- const skill = state.skills.find((item) => item.skillId === id);
2264
- if (!skill) return;
2265
- state.selected = { kind: 'skill', id, data: skill };
2266
- } else if (kind === 'workflow') {
2267
- const workflow = state.workflows.find((item) => item.workflowId === id);
2268
- if (!workflow) return;
2269
- state.selected = { kind: 'workflow', id, data: workflow };
2270
- } else if (kind === 'pod-worker') {
2271
- const worker = await fetchJson(`/api/pod/${encodeURIComponent(id)}`);
2272
- state.selected = { kind: 'pod-worker', id, data: worker };
2273
- } else if (kind === 'client') {
2274
- const client = state.clients.find((item) => item.clientId === id);
2275
- if (!client) return;
2276
- state.selected = { kind: 'client', id, data: client };
2512
+ try {
2513
+ if (kind === 'memory') {
2514
+ await refreshMemorySelection(id, true);
2515
+ } else if (kind === 'run') {
2516
+ const run = state.runs.find((item) => item.runId === id) || await fetchJson(`/api/runs/${encodeURIComponent(id)}`);
2517
+ if (run?.error) return;
2518
+ state.selected = { kind: 'run', id, data: run };
2519
+ } else if (kind === 'skill') {
2520
+ const skill = state.skills.find((item) => item.skillId === id);
2521
+ if (!skill) return;
2522
+ state.selected = { kind: 'skill', id, data: skill };
2523
+ } else if (kind === 'workflow') {
2524
+ const workflow = state.workflows.find((item) => item.workflowId === id);
2525
+ if (!workflow) return;
2526
+ state.selected = { kind: 'workflow', id, data: workflow };
2527
+ } else if (kind === 'pod-worker') {
2528
+ const worker = await fetchJson(`/api/pod/${encodeURIComponent(id)}`);
2529
+ state.selected = { kind: 'pod-worker', id, data: worker };
2530
+ } else if (kind === 'client') {
2531
+ const client = state.clients.find((item) => item.clientId === id);
2532
+ if (!client) return;
2533
+ state.selected = { kind: 'client', id, data: client };
2534
+ }
2535
+ render();
2536
+ } catch (error) {
2537
+ $('control-status').textContent = `Failed to load ${kind}: ${error.message || 'unknown error'}`;
2277
2538
  }
2278
- render();
2279
2539
  }
2280
2540
 
2281
2541
  function shortLabel(label) {
@@ -2396,6 +2656,18 @@
2396
2656
  render();
2397
2657
  });
2398
2658
 
2659
+ $('drawer-backdrop').addEventListener('click', () => {
2660
+ state.selected = null;
2661
+ render();
2662
+ });
2663
+
2664
+ document.addEventListener('keydown', (e) => {
2665
+ if (e.key === 'Escape' && state.selected) {
2666
+ state.selected = null;
2667
+ render();
2668
+ }
2669
+ });
2670
+
2399
2671
  document.querySelectorAll('#graph-modes button').forEach((button) => {
2400
2672
  button.addEventListener('click', async () => {
2401
2673
  state.graphMode = button.dataset.graphMode;
@@ -2418,27 +2690,40 @@
2418
2690
  });
2419
2691
  }
2420
2692
 
2693
+ let _stream = null;
2694
+ let _reconnectDelay = 1000;
2695
+ let _reconnectTimer = null;
2696
+
2421
2697
  function connectStream() {
2422
- const stream = new EventSource('/stream');
2423
- stream.onopen = () => {
2698
+ if (_stream) { _stream.close(); _stream = null; }
2699
+ if (_reconnectTimer) { clearTimeout(_reconnectTimer); _reconnectTimer = null; }
2700
+
2701
+ _stream = new EventSource('/stream');
2702
+ _stream.onopen = () => {
2424
2703
  state.streamConnected = true;
2704
+ _reconnectDelay = 1000;
2425
2705
  renderHeader();
2426
2706
  };
2427
- stream.onerror = () => {
2707
+ _stream.onerror = () => {
2428
2708
  state.streamConnected = false;
2429
2709
  renderHeader();
2710
+ if (_stream) { _stream.close(); _stream = null; }
2711
+ _reconnectTimer = setTimeout(connectStream, _reconnectDelay);
2712
+ _reconnectDelay = Math.min(_reconnectDelay * 2, 30000);
2430
2713
  };
2431
- stream.onmessage = (event) => {
2714
+ _stream.onmessage = (event) => {
2432
2715
  try {
2433
2716
  const raw = JSON.parse(event.data);
2434
2717
  if (raw?.connected) return;
2435
2718
  const payload = normalizeEventCard(raw);
2436
2719
  if (!payload) return;
2437
- state.events.unshift(payload);
2720
+ if (!state.events.some((e) => e.id === payload.id)) {
2721
+ state.events.unshift(payload);
2722
+ }
2438
2723
  state.events = state.events.slice(0, 120);
2439
2724
  setResourceStatus('events', 'ready');
2440
2725
  if (['runtime', 'memory', 'pod', 'skills', 'workflows', 'clients'].includes(payload.category)) {
2441
- void refreshAll().catch(() => {});
2726
+ scheduleRefresh();
2442
2727
  } else {
2443
2728
  renderEvents();
2444
2729
  renderHeader();
@@ -2457,7 +2742,7 @@
2457
2742
  : 'Dashboard actions stay local and route through the runtime.';
2458
2743
  connectStream();
2459
2744
  setInterval(() => {
2460
- void refreshAll().catch(() => {});
2745
+ scheduleRefresh();
2461
2746
  }, 20000);
2462
2747
  }
2463
2748
 
@@ -11,6 +11,7 @@ interface DashboardServerOptions {
11
11
  }
12
12
  export declare class DashboardServer {
13
13
  private server;
14
+ private cachedDashboardHtml;
14
15
  private clients;
15
16
  private unsubscribeBus;
16
17
  private runtimeProvider?;
@@ -32,6 +33,7 @@ export declare class DashboardServer {
32
33
  private serveDashboard;
33
34
  private serveSSE;
34
35
  private broadcast;
36
+ private getCorsOrigin;
35
37
  private respondOptions;
36
38
  private respondJson;
37
39
  private readJsonBody;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAkB7D,UAAU,sBAAsB;IAC5B,eAAe,CAAC,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;IACpD,cAAc,CAAC,EAAE,MAAM,YAAY,GAAG,SAAS,CAAC;IAChD,gBAAgB,CAAC,EAAE,MAAM,OAAO,EAAE,CAAC;IACnC,sBAAsB,CAAC,EAAE,MAAM,cAAc,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAyCD,qBAAa,eAAe;IACxB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,eAAe,CAAC,CAAoC;IAC5D,OAAO,CAAC,cAAc,CAAC,CAAiC;IACxD,OAAO,CAAC,gBAAgB,CAAC,CAAkB;IAC3C,OAAO,CAAC,sBAAsB,CAAC,CAAmC;IAClE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,GAAE,sBAA2B;IAgBhD,KAAK,IAAI,IAAI;IAkBb,IAAI,IAAI,IAAI;IAuBZ,UAAU,IAAI,MAAM,GAAG,IAAI;YAIb,UAAU;YA4BV,cAAc;IA8O5B,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,QAAQ;IA6BhB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,WAAW;YASL,YAAY;IAa1B,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,aAAa;IA6DrB,OAAO,CAAC,QAAQ;YAIF,cAAc;IAwD5B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,YAAY;YAsBN,sBAAsB;IAqBpC,OAAO,CAAC,YAAY;CAoBvB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAkB7D,UAAU,sBAAsB;IAC5B,eAAe,CAAC,EAAE,MAAM,eAAe,GAAG,SAAS,CAAC;IACpD,cAAc,CAAC,EAAE,MAAM,YAAY,GAAG,SAAS,CAAC;IAChD,gBAAgB,CAAC,EAAE,MAAM,OAAO,EAAE,CAAC;IACnC,sBAAsB,CAAC,EAAE,MAAM,cAAc,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAyCD,qBAAa,eAAe;IACxB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,eAAe,CAAC,CAAoC;IAC5D,OAAO,CAAC,cAAc,CAAC,CAAiC;IACxD,OAAO,CAAC,gBAAgB,CAAC,CAAkB;IAC3C,OAAO,CAAC,sBAAsB,CAAC,CAAmC;IAClE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,aAAa,CAAuC;IAC5D,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,GAAE,sBAA2B;IAgBhD,KAAK,IAAI,IAAI;IAkBb,IAAI,IAAI,IAAI;IAuBZ,UAAU,IAAI,MAAM,GAAG,IAAI;YAIb,UAAU;YA4BV,cAAc;IAqT5B,OAAO,CAAC,cAAc;IAkCtB,OAAO,CAAC,QAAQ;IA6BhB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,WAAW;YASL,YAAY;IAoB1B,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,aAAa;IA6DrB,OAAO,CAAC,QAAQ;YAIF,cAAc;IAwD5B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,YAAY;YAsBN,sBAAsB;IAqBpC,OAAO,CAAC,YAAY;CAoBvB"}