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.
- package/dist/agents/adapters/mcp.d.ts +1 -0
- package/dist/agents/adapters/mcp.d.ts.map +1 -1
- package/dist/agents/adapters/mcp.js +43 -1
- package/dist/agents/adapters/mcp.js.map +1 -1
- package/dist/dashboard/index.html +331 -46
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +305 -201
- package/dist/dashboard/server.js.map +1 -1
- package/dist/engines/client-registry.d.ts.map +1 -1
- package/dist/engines/client-registry.js +12 -1
- package/dist/engines/client-registry.js.map +1 -1
- package/package.json +1 -1
|
@@ -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:
|
|
430
|
-
stroke-dashoffset:
|
|
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:
|
|
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:
|
|
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:
|
|
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}-${
|
|
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}-${
|
|
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()}
|
|
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
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
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
|
-
|
|
2423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|