nexus-prime 3.2.1 → 3.2.2

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.
@@ -75,7 +75,7 @@
75
75
  align-items: center;
76
76
  justify-content: space-between;
77
77
  gap: 1rem;
78
- padding: 1.2rem 1.6rem;
78
+ padding: 1rem 1.4rem;
79
79
  border-bottom: 1px solid var(--line);
80
80
  background: rgba(5, 8, 13, 0.76);
81
81
  backdrop-filter: blur(28px);
@@ -99,14 +99,38 @@
99
99
 
100
100
  h1 {
101
101
  margin: 0;
102
- font-size: clamp(1.35rem, 2vw, 1.9rem);
102
+ font-size: clamp(1.18rem, 1.8vw, 1.62rem);
103
103
  letter-spacing: -0.04em;
104
104
  }
105
105
 
106
106
  .subtitle {
107
107
  margin-top: 0.15rem;
108
108
  color: var(--muted);
109
- font-size: 0.92rem;
109
+ font-size: 0.84rem;
110
+ }
111
+
112
+ .banner {
113
+ margin: 0 1rem;
114
+ margin-top: 0.85rem;
115
+ padding: 0.72rem 0.95rem;
116
+ border-radius: 16px;
117
+ border: 1px solid var(--line);
118
+ background: rgba(255, 255, 255, 0.03);
119
+ color: var(--muted);
120
+ font-size: 0.76rem;
121
+ line-height: 1.45;
122
+ }
123
+
124
+ .banner.warn {
125
+ border-color: rgba(255, 209, 77, 0.22);
126
+ background: rgba(255, 209, 77, 0.08);
127
+ color: #ffe7a0;
128
+ }
129
+
130
+ .banner.bad {
131
+ border-color: rgba(255, 109, 99, 0.28);
132
+ background: rgba(255, 109, 99, 0.08);
133
+ color: #ffc1bc;
110
134
  }
111
135
 
112
136
  .header-actions {
@@ -122,11 +146,12 @@
122
146
  display: inline-flex;
123
147
  align-items: center;
124
148
  gap: 0.55rem;
125
- padding: 0.58rem 0.9rem;
149
+ padding: 0.48rem 0.8rem;
126
150
  border-radius: 999px;
127
151
  border: 1px solid var(--line);
128
152
  background: rgba(255, 255, 255, 0.03);
129
153
  color: var(--muted);
154
+ font-size: 0.82rem;
130
155
  }
131
156
 
132
157
  .status-pill .dot,
@@ -166,7 +191,7 @@
166
191
  display: grid;
167
192
  grid-template-columns: 320px minmax(0, 1fr) 410px;
168
193
  gap: 1rem;
169
- padding: 1rem 1rem 1.15rem;
194
+ padding: 0.9rem 1rem 1rem;
170
195
  }
171
196
 
172
197
  .panel {
@@ -185,7 +210,7 @@
185
210
  align-items: center;
186
211
  justify-content: space-between;
187
212
  gap: 0.8rem;
188
- padding: 1rem 1.2rem;
213
+ padding: 0.85rem 1rem;
189
214
  border-bottom: 1px solid var(--line);
190
215
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
191
216
  }
@@ -193,7 +218,7 @@
193
218
  .panel-header h2,
194
219
  .panel-header h3 {
195
220
  margin: 0;
196
- font-size: 0.82rem;
221
+ font-size: 0.74rem;
197
222
  letter-spacing: 0.14em;
198
223
  text-transform: uppercase;
199
224
  color: var(--muted);
@@ -203,13 +228,13 @@
203
228
  flex: 1;
204
229
  min-height: 0;
205
230
  overflow: auto;
206
- padding: 1rem;
231
+ padding: 0.85rem;
207
232
  scrollbar-width: thin;
208
233
  scrollbar-color: rgba(188, 204, 255, 0.22) transparent;
209
234
  }
210
235
 
211
236
  .panel-body.compact {
212
- padding: 0.9rem;
237
+ padding: 0.75rem;
213
238
  }
214
239
 
215
240
  .rail {
@@ -235,7 +260,7 @@
235
260
  border: 1px solid var(--line);
236
261
  background: var(--panel-soft);
237
262
  border-radius: var(--radius-card);
238
- padding: 0.9rem;
263
+ padding: 0.78rem;
239
264
  }
240
265
 
241
266
  .card.interactive {
@@ -259,7 +284,7 @@
259
284
  }
260
285
 
261
286
  .card-title strong {
262
- font-size: 1rem;
287
+ font-size: 0.92rem;
263
288
  font-weight: 600;
264
289
  }
265
290
 
@@ -275,12 +300,12 @@
275
300
 
276
301
  .meta {
277
302
  color: var(--muted);
278
- font-size: 0.78rem;
303
+ font-size: 0.72rem;
279
304
  line-height: 1.5;
280
305
  }
281
306
 
282
307
  .hero-metric {
283
- padding: 1rem;
308
+ padding: 0.88rem;
284
309
  border-radius: var(--radius-card);
285
310
  border: 1px solid var(--line);
286
311
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
@@ -288,16 +313,16 @@
288
313
 
289
314
  .hero-metric label {
290
315
  display: block;
291
- font-size: 0.74rem;
316
+ font-size: 0.68rem;
292
317
  color: var(--muted);
293
318
  text-transform: uppercase;
294
319
  letter-spacing: 0.12em;
295
- margin-bottom: 0.5rem;
320
+ margin-bottom: 0.38rem;
296
321
  }
297
322
 
298
323
  .hero-metric strong {
299
324
  display: block;
300
- font-size: 2rem;
325
+ font-size: 1.62rem;
301
326
  letter-spacing: -0.05em;
302
327
  margin-bottom: 0.2rem;
303
328
  }
@@ -320,7 +345,7 @@
320
345
  align-items: center;
321
346
  justify-content: space-between;
322
347
  gap: 0.75rem;
323
- padding: 0.8rem 0.9rem;
348
+ padding: 0.68rem 0.78rem;
324
349
  border-radius: 16px;
325
350
  border: 1px solid var(--line);
326
351
  background: rgba(255, 255, 255, 0.03);
@@ -335,6 +360,7 @@
335
360
 
336
361
  .client-name {
337
362
  font-weight: 600;
363
+ font-size: 0.92rem;
338
364
  }
339
365
 
340
366
  .state-chip,
@@ -343,8 +369,8 @@
343
369
  align-items: center;
344
370
  justify-content: center;
345
371
  border-radius: 999px;
346
- padding: 0.23rem 0.55rem;
347
- font-size: 0.72rem;
372
+ padding: 0.2rem 0.5rem;
373
+ font-size: 0.68rem;
348
374
  border: 1px solid var(--line);
349
375
  background: rgba(255, 255, 255, 0.05);
350
376
  text-transform: uppercase;
@@ -415,13 +441,13 @@
415
441
  }
416
442
 
417
443
  .dial-value {
418
- font-size: 2rem;
444
+ font-size: 1.68rem;
419
445
  font-weight: 700;
420
446
  letter-spacing: -0.06em;
421
447
  }
422
448
 
423
449
  .dial-caption {
424
- font-size: 0.72rem;
450
+ font-size: 0.66rem;
425
451
  color: var(--muted);
426
452
  text-transform: uppercase;
427
453
  letter-spacing: 0.12em;
@@ -453,10 +479,11 @@
453
479
  .primary-button {
454
480
  border: 0;
455
481
  border-radius: 999px;
456
- padding: 0.5rem 0.8rem;
482
+ padding: 0.42rem 0.68rem;
457
483
  background: transparent;
458
484
  color: var(--muted);
459
485
  transition: background 160ms ease, color 160ms ease, transform 160ms ease;
486
+ font-size: 0.72rem;
460
487
  }
461
488
 
462
489
  .segmented button.active,
@@ -513,7 +540,7 @@
513
540
 
514
541
  .node-label {
515
542
  font-family: var(--mono);
516
- font-size: 11px;
543
+ font-size: 9.5px;
517
544
  fill: rgba(246, 247, 251, 0.86);
518
545
  pointer-events: none;
519
546
  }
@@ -551,7 +578,7 @@
551
578
 
552
579
  .graph-note {
553
580
  color: var(--muted);
554
- font-size: 0.8rem;
581
+ font-size: 0.72rem;
555
582
  }
556
583
 
557
584
  .graph-footer {
@@ -572,11 +599,11 @@
572
599
  display: inline-flex;
573
600
  align-items: center;
574
601
  gap: 0.35rem;
575
- padding: 0.28rem 0.55rem;
602
+ padding: 0.24rem 0.48rem;
576
603
  border-radius: 999px;
577
604
  border: 1px solid var(--line);
578
605
  background: rgba(255, 255, 255, 0.05);
579
- font-size: 0.74rem;
606
+ font-size: 0.68rem;
580
607
  }
581
608
 
582
609
  .entity-header {
@@ -589,7 +616,7 @@
589
616
 
590
617
  .entity-header h3 {
591
618
  margin: 0;
592
- font-size: 0.9rem;
619
+ font-size: 0.82rem;
593
620
  letter-spacing: 0.08em;
594
621
  text-transform: uppercase;
595
622
  color: var(--muted);
@@ -599,13 +626,14 @@
599
626
  display: flex;
600
627
  flex-wrap: wrap;
601
628
  gap: 0.35rem;
629
+ justify-content: flex-end;
602
630
  }
603
631
 
604
632
  .event-card {
605
633
  border-radius: 18px;
606
634
  border: 1px solid var(--line);
607
635
  background: rgba(255, 255, 255, 0.03);
608
- padding: 0.9rem;
636
+ padding: 0.78rem;
609
637
  border-left: 3px solid var(--muted);
610
638
  }
611
639
 
@@ -623,7 +651,7 @@
623
651
 
624
652
  .event-head strong {
625
653
  display: block;
626
- font-size: 0.96rem;
654
+ font-size: 0.88rem;
627
655
  }
628
656
 
629
657
  .event-head .meta {
@@ -631,24 +659,24 @@
631
659
  }
632
660
 
633
661
  details.raw {
634
- margin-top: 0.6rem;
662
+ margin-top: 0.45rem;
635
663
  }
636
664
 
637
665
  details.raw summary {
638
666
  cursor: pointer;
639
667
  color: var(--muted);
640
- font-size: 0.74rem;
668
+ font-size: 0.68rem;
641
669
  text-transform: uppercase;
642
670
  letter-spacing: 0.08em;
643
671
  }
644
672
 
645
673
  pre {
646
674
  overflow: auto;
647
- padding: 0.8rem;
675
+ padding: 0.68rem;
648
676
  border-radius: 14px;
649
677
  background: rgba(0, 0, 0, 0.3);
650
678
  border: 1px solid var(--line);
651
- font-size: 0.75rem;
679
+ font-size: 0.7rem;
652
680
  line-height: 1.5;
653
681
  margin: 0.55rem 0 0;
654
682
  white-space: pre-wrap;
@@ -666,7 +694,7 @@
666
694
  }
667
695
 
668
696
  .field label {
669
- font-size: 0.74rem;
697
+ font-size: 0.68rem;
670
698
  color: var(--muted);
671
699
  text-transform: uppercase;
672
700
  letter-spacing: 0.08em;
@@ -676,7 +704,7 @@
676
704
  .field select,
677
705
  .field textarea {
678
706
  width: 100%;
679
- padding: 0.7rem 0.78rem;
707
+ padding: 0.62rem 0.72rem;
680
708
  border-radius: 14px;
681
709
  border: 1px solid var(--line);
682
710
  background: rgba(255, 255, 255, 0.03);
@@ -741,12 +769,12 @@
741
769
  border: 1px solid var(--line);
742
770
  background: rgba(255, 255, 255, 0.03);
743
771
  border-radius: 18px;
744
- padding: 0.9rem;
772
+ padding: 0.78rem;
745
773
  }
746
774
 
747
775
  .drawer-section h4 {
748
776
  margin: 0 0 0.55rem;
749
- font-size: 0.82rem;
777
+ font-size: 0.74rem;
750
778
  color: var(--muted);
751
779
  text-transform: uppercase;
752
780
  letter-spacing: 0.08em;
@@ -836,6 +864,8 @@
836
864
  </div>
837
865
  </header>
838
866
 
867
+ <div id="status-banner" class="banner hidden" role="status" aria-live="polite"></div>
868
+
839
869
  <main class="layout">
840
870
  <aside class="rail left">
841
871
  <section class="panel">
@@ -1068,6 +1098,13 @@
1068
1098
  </aside>
1069
1099
 
1070
1100
  <script>
1101
+ const DASHBOARD_API_VERSION = '2';
1102
+ const REQUIRED_CAPABILITIES = ['runs', 'memory', 'pod', 'clients', 'events', 'stream'];
1103
+
1104
+ function createResourceState() {
1105
+ return { status: 'idle', error: '' };
1106
+ }
1107
+
1071
1108
  const state = {
1072
1109
  runs: [],
1073
1110
  skills: [],
@@ -1086,6 +1123,20 @@
1086
1123
  selected: null,
1087
1124
  streamConnected: false,
1088
1125
  lastRefreshAt: 0,
1126
+ banner: null,
1127
+ resources: {
1128
+ health: createResourceState(),
1129
+ backends: createResourceState(),
1130
+ runs: createResourceState(),
1131
+ skills: createResourceState(),
1132
+ workflows: createResourceState(),
1133
+ memory: createResourceState(),
1134
+ pod: createResourceState(),
1135
+ clients: createResourceState(),
1136
+ events: createResourceState(),
1137
+ memoryDetail: createResourceState(),
1138
+ memoryNetwork: createResourceState(),
1139
+ },
1089
1140
  };
1090
1141
 
1091
1142
  const graphModes = {
@@ -1147,61 +1198,299 @@
1147
1198
  .filter(Boolean);
1148
1199
  }
1149
1200
 
1201
+ function setResourceStatus(name, status, error = '') {
1202
+ state.resources[name] = { status, error };
1203
+ }
1204
+
1205
+ function getResourceStatus(name) {
1206
+ return state.resources[name] || createResourceState();
1207
+ }
1208
+
1209
+ function resourceFailed(name) {
1210
+ return getResourceStatus(name).status === 'error';
1211
+ }
1212
+
1213
+ function countFailedResources() {
1214
+ return Object.values(state.resources).filter((resource) => resource.status === 'error').length;
1215
+ }
1216
+
1217
+ function setBanner(kind, message) {
1218
+ state.banner = message ? { kind, message } : null;
1219
+ }
1220
+
1221
+ function clearBanner() {
1222
+ state.banner = null;
1223
+ }
1224
+
1225
+ function normalizeError(error) {
1226
+ return error instanceof Error ? error.message : String(error || 'Unknown error');
1227
+ }
1228
+
1229
+ function reconcileBanner() {
1230
+ const healthStatus = getResourceStatus('health');
1231
+ if (healthStatus.status === 'error') {
1232
+ setBanner('bad', 'Connected to stale dashboard server or an incompatible API surface. Open the latest dashboard URL printed by the MCP process.');
1233
+ return;
1234
+ }
1235
+
1236
+ const version = state.health?.dashboardApiVersion;
1237
+ const capabilities = state.health?.capabilities || {};
1238
+ const missingCapabilities = REQUIRED_CAPABILITIES.filter((capability) => capabilities[capability] !== true);
1239
+
1240
+ if (!version || version !== DASHBOARD_API_VERSION || missingCapabilities.length) {
1241
+ setBanner('bad', `Dashboard compatibility mismatch detected. Expected API v${DASHBOARD_API_VERSION}; missing capabilities: ${missingCapabilities.join(', ') || 'version marker'}.`);
1242
+ return;
1243
+ }
1244
+
1245
+ const failed = Object.entries(state.resources)
1246
+ .filter(([name, resource]) => resource.status === 'error' && !['memoryDetail', 'memoryNetwork'].includes(name))
1247
+ .map(([name]) => name);
1248
+
1249
+ if (failed.length) {
1250
+ setBanner('warn', `Dashboard is partially degraded. Unavailable surfaces: ${failed.join(', ')}.`);
1251
+ return;
1252
+ }
1253
+
1254
+ clearBanner();
1255
+ }
1256
+
1257
+ function emptyState(message) {
1258
+ return `<div class="empty">${escapeHtml(message)}</div>`;
1259
+ }
1260
+
1261
+ function normalizeLegacyCategory(type) {
1262
+ if (String(type).startsWith('memory.')) return 'memory';
1263
+ if (String(type).startsWith('pod.')) return 'pod';
1264
+ if (String(type).startsWith('phantom.')) return 'runtime';
1265
+ if (String(type).startsWith('client.')) return 'clients';
1266
+ if (String(type).startsWith('skill.')) return 'skills';
1267
+ if (String(type).startsWith('workflow.')) return 'workflows';
1268
+ if (String(type).startsWith('tokens.') || String(type).startsWith('cas.') || String(type).startsWith('kv.')) return 'tokens';
1269
+ return 'system';
1270
+ }
1271
+
1272
+ function normalizeLegacySeverity(type, payload) {
1273
+ if (type === 'guardrail.check') return payload?.passed ? 'good' : 'bad';
1274
+ if (['phantom.merge', 'phantom.merge.complete', 'workflow.run'].includes(type)) {
1275
+ return payload?.status === 'failed' ? 'bad' : 'good';
1276
+ }
1277
+ if (type === 'client.inferred') return 'warn';
1278
+ if (type === 'dashboard.action' && payload?.status === 'failed') return 'bad';
1279
+ if (type === 'pod.signal') return 'info';
1280
+ return 'info';
1281
+ }
1282
+
1283
+ function legacyTitle(type) {
1284
+ return {
1285
+ 'system.boot': 'Runtime boot',
1286
+ 'memory.store': 'Memory stored',
1287
+ 'memory.recall': 'Memory recall',
1288
+ 'pod.signal': 'POD signal',
1289
+ 'tokens.optimized': 'Tokens optimized',
1290
+ 'phantom.worker.start': 'Worker start',
1291
+ 'phantom.worker.complete': 'Worker complete',
1292
+ 'phantom.merge.complete': 'Merge complete',
1293
+ 'phantom.merge': 'Merge decision',
1294
+ 'guardrail.check': 'Guardrail check',
1295
+ 'ghost.pass': 'Ghost pass',
1296
+ 'graph.query': 'Graph query',
1297
+ 'darwin.cycle': 'Darwin cycle',
1298
+ 'session.dna': 'Session DNA',
1299
+ 'skill.register': 'Skill registered',
1300
+ 'skill.deploy': 'Skill deployed',
1301
+ 'skill.revoke': 'Skill revoked',
1302
+ 'workflow.deploy': 'Workflow deployed',
1303
+ 'workflow.run': 'Workflow run',
1304
+ 'client.heartbeat': 'Client heartbeat',
1305
+ 'client.inferred': 'Client inferred',
1306
+ 'client.status': 'Client status',
1307
+ 'dashboard.action': 'Dashboard action',
1308
+ 'nexusnet.publish': 'NexusNet publish',
1309
+ 'nexusnet.sync': 'NexusNet sync',
1310
+ 'entanglement.create': 'Entanglement created',
1311
+ 'entanglement.collapse': 'Entanglement collapsed',
1312
+ 'entanglement.correlate': 'Entanglement correlated',
1313
+ 'cas.encode': 'CAS encode',
1314
+ 'cas.decode': 'CAS decode',
1315
+ 'cas.pattern_learned': 'CAS pattern',
1316
+ 'kv.merge': 'KV merge',
1317
+ 'kv.adapt': 'KV adapt',
1318
+ 'kv.consensus': 'KV consensus',
1319
+ }[type] || String(type || 'event');
1320
+ }
1321
+
1322
+ function legacySource(type, payload) {
1323
+ if (String(type).startsWith('client.')) return String(payload?.displayName || payload?.clientId || 'client');
1324
+ if (type === 'pod.signal') return String(payload?.workerId || 'pod');
1325
+ if (String(type).startsWith('phantom.')) return String(payload?.workerId || payload?.winner || 'runtime');
1326
+ if (String(type).startsWith('skill.')) return String(payload?.skillId || payload?.name || 'skill');
1327
+ if (String(type).startsWith('workflow.')) return String(payload?.workflowId || 'workflow');
1328
+ return 'nexus-prime';
1329
+ }
1330
+
1331
+ function legacySummary(type, payload) {
1332
+ switch (type) {
1333
+ case 'memory.store':
1334
+ return `Priority ${payload?.priority ?? 'n/a'} · ${(payload?.tags || []).join(', ') || 'no tags'}`;
1335
+ case 'memory.recall':
1336
+ return `Recalled ${payload?.count ?? 0} memories for "${payload?.query || ''}"`;
1337
+ case 'pod.signal':
1338
+ return String(payload?.content || 'POD signal received');
1339
+ case 'tokens.optimized':
1340
+ return `Saved ${payload?.savings ?? 0} tokens across ${payload?.files ?? 0} files`;
1341
+ case 'phantom.worker.start':
1342
+ return `${payload?.approach || 'worker'} started for ${payload?.goal || 'task'}`;
1343
+ case 'phantom.worker.complete':
1344
+ return `Confidence ${payload?.confidence ?? 0}`;
1345
+ case 'phantom.merge':
1346
+ return `${payload?.action || 'merge'} · ${payload?.winner || 'unknown winner'}`;
1347
+ case 'dashboard.action':
1348
+ return `${payload?.action || 'action'} → ${payload?.status || 'unknown'}`;
1349
+ default:
1350
+ return typeof payload === 'string' ? payload : JSON.stringify(payload || {});
1351
+ }
1352
+ }
1353
+
1354
+ function normalizeEventCard(raw) {
1355
+ if (!raw) return null;
1356
+
1357
+ if (raw.category && raw.title && raw.time) {
1358
+ return {
1359
+ id: raw.id || `evt-${raw.time}-${Math.random().toString(36).slice(2, 8)}`,
1360
+ type: raw.type || raw.category,
1361
+ title: String(raw.title),
1362
+ source: String(raw.source || 'nexus-prime'),
1363
+ time: Number(raw.time) || Date.now(),
1364
+ severity: ['good', 'info', 'warn', 'bad'].includes(raw.severity) ? raw.severity : 'info',
1365
+ category: raw.category,
1366
+ summary: String(raw.summary || ''),
1367
+ payload: raw.payload ?? raw,
1368
+ };
1369
+ }
1370
+
1371
+ if (raw.type && raw.timestamp) {
1372
+ const payload = raw.data || {};
1373
+ return {
1374
+ id: raw.id || `evt-${raw.timestamp}-${Math.random().toString(36).slice(2, 8)}`,
1375
+ type: raw.type,
1376
+ title: legacyTitle(raw.type),
1377
+ source: legacySource(raw.type, payload),
1378
+ time: Number(raw.timestamp) || Date.now(),
1379
+ severity: normalizeLegacySeverity(raw.type, payload),
1380
+ category: normalizeLegacyCategory(raw.type),
1381
+ summary: legacySummary(raw.type, payload),
1382
+ payload,
1383
+ };
1384
+ }
1385
+
1386
+ return {
1387
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1388
+ type: 'system.legacy',
1389
+ title: 'Legacy event',
1390
+ source: 'nexus-prime',
1391
+ time: Date.now(),
1392
+ severity: 'info',
1393
+ category: 'system',
1394
+ summary: typeof raw === 'string' ? raw : 'Malformed event payload received',
1395
+ payload: raw,
1396
+ };
1397
+ }
1398
+
1150
1399
  async function fetchJson(url, options) {
1151
1400
  const res = await fetch(url, options);
1401
+ const text = await res.text();
1152
1402
  if (!res.ok) {
1153
- throw new Error(`${res.status} ${res.statusText}`);
1403
+ throw new Error(`${res.status} ${res.statusText}${text ? ` · ${text.slice(0, 120)}` : ''}`);
1154
1404
  }
1155
- return res.json();
1405
+ return text ? JSON.parse(text) : null;
1156
1406
  }
1157
1407
 
1158
1408
  async function refreshAll() {
1159
- const [runs, skills, workflows, backends, health, memories, pod, clients, events] = await Promise.all([
1160
- fetchJson('/api/runs?limit=20'),
1161
- fetchJson('/api/skills'),
1162
- fetchJson('/api/workflows'),
1163
- fetchJson('/api/backends'),
1164
- fetchJson('/api/health'),
1165
- fetchJson('/api/memory?limit=40'),
1166
- fetchJson('/api/pod?limit=30'),
1167
- fetchJson('/api/clients'),
1168
- fetchJson('/api/events?limit=80'),
1169
- ]);
1409
+ const resources = [
1410
+ ['runs', '/api/runs?limit=20', (value) => { state.runs = Array.isArray(value) ? value : state.runs; }],
1411
+ ['skills', '/api/skills', (value) => { state.skills = Array.isArray(value) ? value : state.skills; }],
1412
+ ['workflows', '/api/workflows', (value) => { state.workflows = Array.isArray(value) ? value : state.workflows; }],
1413
+ ['backends', '/api/backends', (value) => { state.backends = value || state.backends; }],
1414
+ ['health', '/api/health', (value) => { state.health = value || state.health; }],
1415
+ ['memory', '/api/memory?limit=40', (value) => { state.memories = Array.isArray(value) ? value : state.memories; }],
1416
+ ['pod', '/api/pod?limit=30', (value) => { state.pod = value || state.pod; }],
1417
+ ['clients', '/api/clients', (value) => { state.clients = Array.isArray(value) ? value : state.clients; }],
1418
+ ['events', '/api/events?limit=80', (value) => {
1419
+ state.events = Array.isArray(value)
1420
+ ? value.map((event) => normalizeEventCard(event)).filter(Boolean)
1421
+ : state.events;
1422
+ }],
1423
+ ];
1424
+
1425
+ const results = await Promise.allSettled(resources.map(([, url]) => fetchJson(url)));
1426
+ let refreshed = false;
1427
+
1428
+ results.forEach((result, index) => {
1429
+ const [name, , assign] = resources[index];
1430
+ if (result.status === 'fulfilled') {
1431
+ assign(result.value);
1432
+ setResourceStatus(name, 'ready');
1433
+ refreshed = true;
1434
+ } else {
1435
+ setResourceStatus(name, 'error', normalizeError(result.reason));
1436
+ }
1437
+ });
1170
1438
 
1171
- state.runs = Array.isArray(runs) ? runs : [];
1172
- state.skills = Array.isArray(skills) ? skills : [];
1173
- state.workflows = Array.isArray(workflows) ? workflows : [];
1174
- state.backends = backends || {};
1175
- state.health = health || {};
1176
- state.memories = Array.isArray(memories) ? memories : [];
1177
- state.pod = pod || state.pod;
1178
- state.clients = Array.isArray(clients) ? clients : [];
1179
- state.events = Array.isArray(events) ? events : [];
1180
- state.lastRefreshAt = Date.now();
1439
+ if (refreshed) {
1440
+ state.lastRefreshAt = Date.now();
1441
+ }
1181
1442
 
1182
1443
  const focusMemoryId = state.selected?.kind === 'memory'
1183
1444
  ? state.selected.id
1184
1445
  : state.memories[0]?.id;
1185
- if (focusMemoryId) {
1446
+
1447
+ if (focusMemoryId && !resourceFailed('memory')) {
1186
1448
  await refreshMemorySelection(focusMemoryId, false);
1187
- } else {
1449
+ } else if (!focusMemoryId) {
1188
1450
  state.memoryDetail = null;
1189
1451
  state.memoryNetwork = { nodes: [], links: [], focusId: null };
1452
+ setResourceStatus('memoryDetail', 'idle');
1453
+ setResourceStatus('memoryNetwork', 'idle');
1190
1454
  }
1191
1455
 
1456
+ reconcileBanner();
1192
1457
  populateBackendSelects();
1193
1458
  render();
1194
1459
  }
1195
1460
 
1196
1461
  async function refreshMemorySelection(memoryId, openDrawer = true) {
1197
- const [detail, network] = await Promise.all([
1462
+ const [detail, network] = await Promise.allSettled([
1198
1463
  fetchJson(`/api/memory/${encodeURIComponent(memoryId)}`),
1199
1464
  fetchJson(`/api/memory/${encodeURIComponent(memoryId)}/network?depth=2&limit=18`),
1200
1465
  ]);
1201
- state.memoryDetail = detail;
1202
- state.memoryNetwork = network;
1466
+
1467
+ if (detail.status === 'fulfilled') {
1468
+ state.memoryDetail = detail.value;
1469
+ setResourceStatus('memoryDetail', 'ready');
1470
+ } else {
1471
+ state.memoryDetail = state.memories.find((memory) => memory.id === memoryId) || null;
1472
+ setResourceStatus('memoryDetail', 'error', normalizeError(detail.reason));
1473
+ }
1474
+
1475
+ if (network.status === 'fulfilled') {
1476
+ state.memoryNetwork = network.value || { nodes: [], links: [], focusId: memoryId };
1477
+ setResourceStatus('memoryNetwork', 'ready');
1478
+ } else {
1479
+ state.memoryNetwork = {
1480
+ focusId: memoryId,
1481
+ nodes: state.memories.slice(0, 12).map((memory) => ({
1482
+ id: memory.id,
1483
+ label: memory.excerpt,
1484
+ entityType: 'memory',
1485
+ tier: memory.tier,
1486
+ })),
1487
+ links: [],
1488
+ };
1489
+ setResourceStatus('memoryNetwork', 'error', normalizeError(network.reason));
1490
+ }
1491
+
1203
1492
  if (openDrawer) {
1204
- state.selected = { kind: 'memory', id: memoryId, data: detail };
1493
+ state.selected = { kind: 'memory', id: memoryId, data: state.memoryDetail };
1205
1494
  }
1206
1495
  }
1207
1496
 
@@ -1254,6 +1543,7 @@
1254
1543
  }
1255
1544
 
1256
1545
  function render() {
1546
+ renderBanner();
1257
1547
  renderHeader();
1258
1548
  renderLeftRail();
1259
1549
  renderGraph();
@@ -1262,19 +1552,38 @@
1262
1552
  renderDrawer();
1263
1553
  }
1264
1554
 
1555
+ function renderBanner() {
1556
+ const banner = $('status-banner');
1557
+ if (!state.banner?.message) {
1558
+ banner.className = 'banner hidden';
1559
+ banner.textContent = '';
1560
+ return;
1561
+ }
1562
+
1563
+ banner.className = `banner ${state.banner.kind}`;
1564
+ banner.textContent = state.banner.message;
1565
+ }
1566
+
1265
1567
  function renderHeader() {
1266
1568
  $('header-version').textContent = `v${state.health.release?.packageVersion || '?'}`;
1267
1569
  const pill = $('sync-pill');
1268
- pill.classList.toggle('live', state.streamConnected);
1269
- pill.classList.toggle('warn', !state.streamConnected);
1570
+ const failedResources = countFailedResources();
1571
+ pill.classList.toggle('live', state.streamConnected && failedResources === 0);
1572
+ pill.classList.toggle('warn', !state.streamConnected || failedResources > 0);
1270
1573
  $('sync-label').textContent = state.streamConnected
1271
- ? `Synchronized · ${formatAgo(state.lastRefreshAt)}`
1272
- : 'Stream reconnecting';
1574
+ ? failedResources
1575
+ ? `Degraded · ${failedResources} surfaces unavailable`
1576
+ : `Synchronized · ${formatAgo(state.lastRefreshAt)}`
1577
+ : failedResources
1578
+ ? `REST degraded · ${failedResources} unavailable`
1579
+ : 'Stream reconnecting';
1273
1580
  }
1274
1581
 
1275
1582
  function renderLeftRail() {
1276
- $('clients-summary').textContent = `${state.clients.length} visible`;
1277
- $('clients-list').innerHTML = state.clients.length
1583
+ $('clients-summary').textContent = resourceFailed('clients') ? 'unavailable' : `${state.clients.length} visible`;
1584
+ $('clients-list').innerHTML = resourceFailed('clients') && !state.clients.length
1585
+ ? emptyState('Clients endpoint unavailable.')
1586
+ : state.clients.length
1278
1587
  ? state.clients.map((client) => `
1279
1588
  <div class="client-row card interactive ${state.selected?.kind === 'client' && state.selected.id === client.clientId ? 'active' : ''}" data-kind="client" data-id="${escapeHtml(client.clientId)}">
1280
1589
  <div class="client-info">
@@ -1287,37 +1596,47 @@
1287
1596
  <span class="state-chip ${stateClass(client.state)}">${escapeHtml(client.state)}</span>
1288
1597
  </div>
1289
1598
  `).join('')
1290
- : '<div class="empty">No clients detected.</div>';
1599
+ : emptyState('No clients detected.');
1291
1600
 
1292
1601
  const tokenMetrics = computeTokenMetrics();
1293
- $('token-summary').textContent = `${tokenMetrics.events} events`;
1602
+ $('token-summary').textContent = resourceFailed('events') ? 'events unavailable' : `${tokenMetrics.events} events`;
1294
1603
  $('gross-tokens').textContent = formatNumber(tokenMetrics.gross);
1295
1604
  $('saved-tokens').textContent = formatNumber(tokenMetrics.saved);
1296
1605
  $('net-tokens').textContent = formatNumber(tokenMetrics.forwarded);
1297
- $('memory-count').textContent = formatNumber(state.memories.length);
1606
+ $('memory-count').textContent = resourceFailed('memory') && !state.memories.length ? 'n/a' : formatNumber(state.memories.length);
1298
1607
  $('dial-value').textContent = `${tokenMetrics.ratio}%`;
1299
1608
  const circumference = 2 * Math.PI * 70;
1300
1609
  const offset = circumference - (circumference * tokenMetrics.ratio / 100);
1301
1610
  $('dial-progress').style.strokeDashoffset = `${offset}`;
1302
1611
 
1303
- $('runs-count').textContent = formatNumber(state.runs.length);
1612
+ $('runs-count').textContent = resourceFailed('runs') && !state.runs.length ? 'n/a' : formatNumber(state.runs.length);
1304
1613
  const latestRun = state.runs[0];
1305
- $('latest-run-state').textContent = latestRun
1614
+ $('latest-run-state').textContent = resourceFailed('runs') && !latestRun
1615
+ ? 'Runs endpoint unavailable'
1616
+ : latestRun
1306
1617
  ? `${latestRun.state} · ${latestRun.selectedBackends?.memoryBackend || 'memory n/a'}`
1307
1618
  : 'No runs yet';
1308
1619
  $('artifact-count').textContent = `${state.skills.length} / ${state.workflows.length}`;
1309
- $('artifact-summary').textContent = `${state.skills.filter((item) => item.rolloutStatus === 'promoted').length} promoted skills · ${state.workflows.filter((item) => item.rolloutStatus === 'promoted').length} promoted workflows`;
1310
- $('docs-health').textContent = state.health.docs?.pagesWorkflowValid ? 'Healthy' : 'Needs attention';
1311
- $('ci-health').textContent = state.health.docs?.pagesWorkflowValid
1620
+ $('artifact-summary').textContent = resourceFailed('skills') || resourceFailed('workflows')
1621
+ ? 'Skill or workflow endpoint unavailable'
1622
+ : `${state.skills.filter((item) => item.rolloutStatus === 'promoted').length} promoted skills · ${state.workflows.filter((item) => item.rolloutStatus === 'promoted').length} promoted workflows`;
1623
+ $('docs-health').textContent = resourceFailed('health')
1624
+ ? 'Unknown'
1625
+ : state.health.docs?.pagesWorkflowValid ? 'Healthy' : 'Needs attention';
1626
+ $('ci-health').textContent = resourceFailed('health')
1627
+ ? 'Health endpoint unavailable'
1628
+ : state.health.docs?.pagesWorkflowValid
1312
1629
  ? `Pages valid · ${state.health.ci?.eventHistory || 0} events`
1313
1630
  : 'Pages syntax or docs deployment needs attention';
1314
1631
 
1315
1632
  const memory = state.health.memory || {};
1316
- $('memory-tier-summary').textContent = `${memory.cortex || 0} cortex`;
1633
+ $('memory-tier-summary').textContent = resourceFailed('health') ? 'health unavailable' : `${memory.cortex || 0} cortex`;
1317
1634
 
1318
1635
  const pod = state.pod || {};
1319
- $('pod-summary').textContent = `${(pod.activeWorkers || []).length} workers`;
1320
- $('pod-highlights').innerHTML = pod.activeWorkers?.length
1636
+ $('pod-summary').textContent = resourceFailed('pod') ? 'unavailable' : `${(pod.activeWorkers || []).length} workers`;
1637
+ $('pod-highlights').innerHTML = resourceFailed('pod') && !(pod.activeWorkers || []).length
1638
+ ? emptyState('POD endpoint unavailable.')
1639
+ : pod.activeWorkers?.length
1321
1640
  ? pod.activeWorkers.slice(0, 4).map((worker) => `
1322
1641
  <div class="card interactive ${state.selected?.kind === 'pod-worker' && state.selected.id === worker.workerId ? 'active' : ''}" data-kind="pod-worker" data-id="${escapeHtml(worker.workerId)}">
1323
1642
  <div class="card-title">
@@ -1328,7 +1647,7 @@
1328
1647
  <div class="list-inline">${(worker.tags || []).slice(0, 4).map((tag) => chip(tag)).join('')}</div>
1329
1648
  </div>
1330
1649
  `).join('')
1331
- : '<div class="empty">Waiting for POD traffic.</div>';
1650
+ : emptyState('Waiting for POD traffic.');
1332
1651
  }
1333
1652
 
1334
1653
  function buildGraphModel() {
@@ -1374,7 +1693,14 @@
1374
1693
  return { nodes, links };
1375
1694
  }
1376
1695
 
1377
- const nodes = (state.memoryNetwork.nodes || []).map((node) => ({ ...node }));
1696
+ const nodes = (state.memoryNetwork.nodes || []).length
1697
+ ? (state.memoryNetwork.nodes || []).map((node) => ({ ...node }))
1698
+ : state.memories.slice(0, 18).map((memory) => ({
1699
+ id: memory.id,
1700
+ label: memory.excerpt,
1701
+ entityType: 'memory',
1702
+ tier: memory.tier,
1703
+ }));
1378
1704
  const links = (state.memoryNetwork.links || []).map((link) => ({ ...link }));
1379
1705
  return { nodes, links };
1380
1706
  }
@@ -1394,7 +1720,19 @@
1394
1720
  if (!model.nodes.length) {
1395
1721
  svg.innerHTML = '';
1396
1722
  empty.classList.remove('hidden');
1723
+ empty.textContent = state.graphMode === 'memory'
1724
+ ? resourceFailed('memory')
1725
+ ? 'Memory endpoint unavailable.'
1726
+ : 'No memory stored yet.'
1727
+ : state.graphMode === 'runs'
1728
+ ? resourceFailed('runs')
1729
+ ? 'Runs endpoint unavailable.'
1730
+ : 'No run graph data yet.'
1731
+ : resourceFailed('pod')
1732
+ ? 'POD endpoint unavailable.'
1733
+ : 'No POD topology data yet.';
1397
1734
  $('graph-stats').innerHTML = '';
1735
+ $('graph-note').textContent = empty.textContent;
1398
1736
  return;
1399
1737
  }
1400
1738
 
@@ -1469,6 +1807,9 @@
1469
1807
  state.graphMode === 'runs' ? chip(`${state.runs.length} recent runs`) : '',
1470
1808
  state.graphMode === 'pod' ? chip(`${(state.pod.activeWorkers || []).length} pod workers`) : '',
1471
1809
  ].filter(Boolean).join('');
1810
+ $('graph-note').textContent = state.graphMode === 'memory' && resourceFailed('memoryNetwork')
1811
+ ? 'Memory network endpoint unavailable. Showing snapshot fallback topology.'
1812
+ : config.subtitle;
1472
1813
  }
1473
1814
 
1474
1815
  function renderLibrary() {
@@ -1486,7 +1827,9 @@
1486
1827
  const container = $('library-list');
1487
1828
 
1488
1829
  if (state.libraryMode === 'memories') {
1489
- container.innerHTML = state.memories.length
1830
+ container.innerHTML = resourceFailed('memory') && !state.memories.length
1831
+ ? emptyState('Memory endpoint unavailable.')
1832
+ : state.memories.length
1490
1833
  ? state.memories.map((memory) => `
1491
1834
  <div class="card interactive ${state.selected?.kind === 'memory' && state.selected.id === memory.id ? 'active' : ''}" data-kind="memory" data-id="${escapeHtml(memory.id)}">
1492
1835
  <div class="card-title">
@@ -1497,9 +1840,11 @@
1497
1840
  <div class="list-inline">${(memory.tags || []).slice(0, 5).map((tag) => chip(tag)).join('')}</div>
1498
1841
  </div>
1499
1842
  `).join('')
1500
- : '<div class="empty">No memories yet.</div>';
1843
+ : emptyState('No memory stored yet.');
1501
1844
  } else if (state.libraryMode === 'skills') {
1502
- container.innerHTML = state.skills.length
1845
+ container.innerHTML = resourceFailed('skills') && !state.skills.length
1846
+ ? emptyState('Skills endpoint unavailable.')
1847
+ : state.skills.length
1503
1848
  ? state.skills.map((skill) => `
1504
1849
  <div class="card interactive ${state.selected?.kind === 'skill' && state.selected.id === skill.skillId ? 'active' : ''}" data-kind="skill" data-id="${escapeHtml(skill.skillId)}">
1505
1850
  <div class="card-title">
@@ -1514,9 +1859,11 @@
1514
1859
  </div>
1515
1860
  </div>
1516
1861
  `).join('')
1517
- : '<div class="empty">No skills loaded.</div>';
1862
+ : emptyState('No skills loaded.');
1518
1863
  } else if (state.libraryMode === 'workflows') {
1519
- container.innerHTML = state.workflows.length
1864
+ container.innerHTML = resourceFailed('workflows') && !state.workflows.length
1865
+ ? emptyState('Workflows endpoint unavailable.')
1866
+ : state.workflows.length
1520
1867
  ? state.workflows.map((workflow) => `
1521
1868
  <div class="card interactive ${state.selected?.kind === 'workflow' && state.selected.id === workflow.workflowId ? 'active' : ''}" data-kind="workflow" data-id="${escapeHtml(workflow.workflowId)}">
1522
1869
  <div class="card-title">
@@ -1527,9 +1874,11 @@
1527
1874
  <div class="list-inline">${chip(`domain:${workflow.domain}`)}${chip(`verify:${workflow.effectiveness?.verificationPasses || 0}`)}</div>
1528
1875
  </div>
1529
1876
  `).join('')
1530
- : '<div class="empty">No workflows loaded.</div>';
1877
+ : emptyState('No workflows loaded.');
1531
1878
  } else if (state.libraryMode === 'pod') {
1532
- container.innerHTML = state.pod.messages?.length
1879
+ container.innerHTML = resourceFailed('pod') && !(state.pod.messages || []).length
1880
+ ? emptyState('POD endpoint unavailable.')
1881
+ : state.pod.messages?.length
1533
1882
  ? state.pod.messages.map((message) => `
1534
1883
  <div class="card interactive ${state.selected?.kind === 'pod-worker' && state.selected.id === message.workerId ? 'active' : ''}" data-kind="pod-worker" data-id="${escapeHtml(message.workerId)}">
1535
1884
  <div class="card-title">
@@ -1541,9 +1890,11 @@
1541
1890
  <div class="list-inline">${(message.tags || []).map((tag) => chip(tag)).join('')}</div>
1542
1891
  </div>
1543
1892
  `).join('')
1544
- : '<div class="empty">No POD signals captured.</div>';
1893
+ : emptyState('No POD signals captured.');
1545
1894
  } else {
1546
- container.innerHTML = state.clients.length
1895
+ container.innerHTML = resourceFailed('clients') && !state.clients.length
1896
+ ? emptyState('Clients endpoint unavailable.')
1897
+ : state.clients.length
1547
1898
  ? state.clients.map((client) => `
1548
1899
  <div class="card interactive ${state.selected?.kind === 'client' && state.selected.id === client.clientId ? 'active' : ''}" data-kind="client" data-id="${escapeHtml(client.clientId)}">
1549
1900
  <div class="card-title">
@@ -1554,7 +1905,7 @@
1554
1905
  <div class="list-inline">${(client.evidence || []).map((evidence) => chip(evidence)).join('')}</div>
1555
1906
  </div>
1556
1907
  `).join('')
1557
- : '<div class="empty">No clients detected.</div>';
1908
+ : emptyState('No clients detected.');
1558
1909
  }
1559
1910
 
1560
1911
  container.querySelectorAll('.card.interactive').forEach((card) => {
@@ -1567,7 +1918,7 @@
1567
1918
  }
1568
1919
 
1569
1920
  function renderEvents() {
1570
- $('events-summary').textContent = `${state.events.length} signals`;
1921
+ $('events-summary').textContent = resourceFailed('events') ? 'events unavailable' : `${state.events.length} signals`;
1571
1922
  document.querySelectorAll('#event-filters button').forEach((button) => {
1572
1923
  button.classList.toggle('active', button.dataset.eventFilter === state.eventFilter);
1573
1924
  });
@@ -1593,7 +1944,9 @@
1593
1944
  </details>
1594
1945
  </article>
1595
1946
  `).join('')
1596
- : '<div class="empty">No events for this filter.</div>';
1947
+ : resourceFailed('events')
1948
+ ? emptyState('Events endpoint unavailable. Live stream may still populate this panel.')
1949
+ : emptyState('No events for this filter.');
1597
1950
  }
1598
1951
 
1599
1952
  function renderDrawer() {
@@ -2077,10 +2430,13 @@
2077
2430
  };
2078
2431
  stream.onmessage = (event) => {
2079
2432
  try {
2080
- const payload = JSON.parse(event.data);
2081
- if (payload?.connected) return;
2433
+ const raw = JSON.parse(event.data);
2434
+ if (raw?.connected) return;
2435
+ const payload = normalizeEventCard(raw);
2436
+ if (!payload) return;
2082
2437
  state.events.unshift(payload);
2083
2438
  state.events = state.events.slice(0, 120);
2439
+ setResourceStatus('events', 'ready');
2084
2440
  if (['runtime', 'memory', 'pod', 'skills', 'workflows', 'clients'].includes(payload.category)) {
2085
2441
  void refreshAll().catch(() => {});
2086
2442
  } else {
@@ -2095,11 +2451,10 @@
2095
2451
 
2096
2452
  async function bootstrap() {
2097
2453
  attachStaticHandlers();
2098
- try {
2099
- await refreshAll();
2100
- } catch (error) {
2101
- $('control-status').textContent = `Initial load failed: ${error.message}`;
2102
- }
2454
+ await refreshAll();
2455
+ $('control-status').textContent = state.banner?.message
2456
+ ? state.banner.message
2457
+ : 'Dashboard actions stay local and route through the runtime.';
2103
2458
  connectStream();
2104
2459
  setInterval(() => {
2105
2460
  void refreshAll().catch(() => {});