nexus-prime 3.2.1 → 3.2.3
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/dashboard/index.html +590 -138
- package/dist/dashboard/server.d.ts +15 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +433 -224
- package/dist/dashboard/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
align-items: center;
|
|
76
76
|
justify-content: space-between;
|
|
77
77
|
gap: 1rem;
|
|
78
|
-
padding:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
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.
|
|
237
|
+
padding: 0.75rem;
|
|
213
238
|
}
|
|
214
239
|
|
|
215
240
|
.rail {
|
|
@@ -220,6 +245,9 @@
|
|
|
220
245
|
|
|
221
246
|
.rail.left {
|
|
222
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;
|
|
223
251
|
}
|
|
224
252
|
|
|
225
253
|
.rail.right {
|
|
@@ -235,7 +263,7 @@
|
|
|
235
263
|
border: 1px solid var(--line);
|
|
236
264
|
background: var(--panel-soft);
|
|
237
265
|
border-radius: var(--radius-card);
|
|
238
|
-
padding: 0.
|
|
266
|
+
padding: 0.78rem;
|
|
239
267
|
}
|
|
240
268
|
|
|
241
269
|
.card.interactive {
|
|
@@ -259,7 +287,7 @@
|
|
|
259
287
|
}
|
|
260
288
|
|
|
261
289
|
.card-title strong {
|
|
262
|
-
font-size:
|
|
290
|
+
font-size: 0.92rem;
|
|
263
291
|
font-weight: 600;
|
|
264
292
|
}
|
|
265
293
|
|
|
@@ -275,12 +303,12 @@
|
|
|
275
303
|
|
|
276
304
|
.meta {
|
|
277
305
|
color: var(--muted);
|
|
278
|
-
font-size: 0.
|
|
306
|
+
font-size: 0.72rem;
|
|
279
307
|
line-height: 1.5;
|
|
280
308
|
}
|
|
281
309
|
|
|
282
310
|
.hero-metric {
|
|
283
|
-
padding:
|
|
311
|
+
padding: 0.88rem;
|
|
284
312
|
border-radius: var(--radius-card);
|
|
285
313
|
border: 1px solid var(--line);
|
|
286
314
|
background: linear-gradient(160deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
|
|
@@ -288,16 +316,16 @@
|
|
|
288
316
|
|
|
289
317
|
.hero-metric label {
|
|
290
318
|
display: block;
|
|
291
|
-
font-size: 0.
|
|
319
|
+
font-size: 0.68rem;
|
|
292
320
|
color: var(--muted);
|
|
293
321
|
text-transform: uppercase;
|
|
294
322
|
letter-spacing: 0.12em;
|
|
295
|
-
margin-bottom: 0.
|
|
323
|
+
margin-bottom: 0.38rem;
|
|
296
324
|
}
|
|
297
325
|
|
|
298
326
|
.hero-metric strong {
|
|
299
327
|
display: block;
|
|
300
|
-
font-size:
|
|
328
|
+
font-size: 1.62rem;
|
|
301
329
|
letter-spacing: -0.05em;
|
|
302
330
|
margin-bottom: 0.2rem;
|
|
303
331
|
}
|
|
@@ -320,7 +348,7 @@
|
|
|
320
348
|
align-items: center;
|
|
321
349
|
justify-content: space-between;
|
|
322
350
|
gap: 0.75rem;
|
|
323
|
-
padding: 0.
|
|
351
|
+
padding: 0.68rem 0.78rem;
|
|
324
352
|
border-radius: 16px;
|
|
325
353
|
border: 1px solid var(--line);
|
|
326
354
|
background: rgba(255, 255, 255, 0.03);
|
|
@@ -335,6 +363,7 @@
|
|
|
335
363
|
|
|
336
364
|
.client-name {
|
|
337
365
|
font-weight: 600;
|
|
366
|
+
font-size: 0.92rem;
|
|
338
367
|
}
|
|
339
368
|
|
|
340
369
|
.state-chip,
|
|
@@ -343,8 +372,8 @@
|
|
|
343
372
|
align-items: center;
|
|
344
373
|
justify-content: center;
|
|
345
374
|
border-radius: 999px;
|
|
346
|
-
padding: 0.
|
|
347
|
-
font-size: 0.
|
|
375
|
+
padding: 0.2rem 0.5rem;
|
|
376
|
+
font-size: 0.68rem;
|
|
348
377
|
border: 1px solid var(--line);
|
|
349
378
|
background: rgba(255, 255, 255, 0.05);
|
|
350
379
|
text-transform: uppercase;
|
|
@@ -400,8 +429,8 @@
|
|
|
400
429
|
stroke: var(--accent);
|
|
401
430
|
stroke-width: 12;
|
|
402
431
|
stroke-linecap: round;
|
|
403
|
-
stroke-dasharray:
|
|
404
|
-
stroke-dashoffset:
|
|
432
|
+
stroke-dasharray: 439.82;
|
|
433
|
+
stroke-dashoffset: 439.82;
|
|
405
434
|
filter: drop-shadow(0 0 10px rgba(84, 255, 135, 0.32));
|
|
406
435
|
transition: stroke-dashoffset 300ms ease;
|
|
407
436
|
}
|
|
@@ -415,13 +444,13 @@
|
|
|
415
444
|
}
|
|
416
445
|
|
|
417
446
|
.dial-value {
|
|
418
|
-
font-size:
|
|
447
|
+
font-size: 1.68rem;
|
|
419
448
|
font-weight: 700;
|
|
420
449
|
letter-spacing: -0.06em;
|
|
421
450
|
}
|
|
422
451
|
|
|
423
452
|
.dial-caption {
|
|
424
|
-
font-size: 0.
|
|
453
|
+
font-size: 0.66rem;
|
|
425
454
|
color: var(--muted);
|
|
426
455
|
text-transform: uppercase;
|
|
427
456
|
letter-spacing: 0.12em;
|
|
@@ -453,10 +482,11 @@
|
|
|
453
482
|
.primary-button {
|
|
454
483
|
border: 0;
|
|
455
484
|
border-radius: 999px;
|
|
456
|
-
padding: 0.
|
|
485
|
+
padding: 0.42rem 0.68rem;
|
|
457
486
|
background: transparent;
|
|
458
487
|
color: var(--muted);
|
|
459
488
|
transition: background 160ms ease, color 160ms ease, transform 160ms ease;
|
|
489
|
+
font-size: 0.72rem;
|
|
460
490
|
}
|
|
461
491
|
|
|
462
492
|
.segmented button.active,
|
|
@@ -485,7 +515,7 @@
|
|
|
485
515
|
|
|
486
516
|
#graph-stage {
|
|
487
517
|
position: relative;
|
|
488
|
-
min-height:
|
|
518
|
+
min-height: 300px;
|
|
489
519
|
height: 100%;
|
|
490
520
|
border-radius: 22px;
|
|
491
521
|
border: 1px solid var(--line);
|
|
@@ -513,7 +543,7 @@
|
|
|
513
543
|
|
|
514
544
|
.node-label {
|
|
515
545
|
font-family: var(--mono);
|
|
516
|
-
font-size:
|
|
546
|
+
font-size: 9.5px;
|
|
517
547
|
fill: rgba(246, 247, 251, 0.86);
|
|
518
548
|
pointer-events: none;
|
|
519
549
|
}
|
|
@@ -551,7 +581,7 @@
|
|
|
551
581
|
|
|
552
582
|
.graph-note {
|
|
553
583
|
color: var(--muted);
|
|
554
|
-
font-size: 0.
|
|
584
|
+
font-size: 0.72rem;
|
|
555
585
|
}
|
|
556
586
|
|
|
557
587
|
.graph-footer {
|
|
@@ -572,11 +602,11 @@
|
|
|
572
602
|
display: inline-flex;
|
|
573
603
|
align-items: center;
|
|
574
604
|
gap: 0.35rem;
|
|
575
|
-
padding: 0.
|
|
605
|
+
padding: 0.24rem 0.48rem;
|
|
576
606
|
border-radius: 999px;
|
|
577
607
|
border: 1px solid var(--line);
|
|
578
608
|
background: rgba(255, 255, 255, 0.05);
|
|
579
|
-
font-size: 0.
|
|
609
|
+
font-size: 0.68rem;
|
|
580
610
|
}
|
|
581
611
|
|
|
582
612
|
.entity-header {
|
|
@@ -589,7 +619,7 @@
|
|
|
589
619
|
|
|
590
620
|
.entity-header h3 {
|
|
591
621
|
margin: 0;
|
|
592
|
-
font-size: 0.
|
|
622
|
+
font-size: 0.82rem;
|
|
593
623
|
letter-spacing: 0.08em;
|
|
594
624
|
text-transform: uppercase;
|
|
595
625
|
color: var(--muted);
|
|
@@ -599,13 +629,14 @@
|
|
|
599
629
|
display: flex;
|
|
600
630
|
flex-wrap: wrap;
|
|
601
631
|
gap: 0.35rem;
|
|
632
|
+
justify-content: flex-end;
|
|
602
633
|
}
|
|
603
634
|
|
|
604
635
|
.event-card {
|
|
605
636
|
border-radius: 18px;
|
|
606
637
|
border: 1px solid var(--line);
|
|
607
638
|
background: rgba(255, 255, 255, 0.03);
|
|
608
|
-
padding: 0.
|
|
639
|
+
padding: 0.78rem;
|
|
609
640
|
border-left: 3px solid var(--muted);
|
|
610
641
|
}
|
|
611
642
|
|
|
@@ -623,7 +654,7 @@
|
|
|
623
654
|
|
|
624
655
|
.event-head strong {
|
|
625
656
|
display: block;
|
|
626
|
-
font-size: 0.
|
|
657
|
+
font-size: 0.88rem;
|
|
627
658
|
}
|
|
628
659
|
|
|
629
660
|
.event-head .meta {
|
|
@@ -631,24 +662,24 @@
|
|
|
631
662
|
}
|
|
632
663
|
|
|
633
664
|
details.raw {
|
|
634
|
-
margin-top: 0.
|
|
665
|
+
margin-top: 0.45rem;
|
|
635
666
|
}
|
|
636
667
|
|
|
637
668
|
details.raw summary {
|
|
638
669
|
cursor: pointer;
|
|
639
670
|
color: var(--muted);
|
|
640
|
-
font-size: 0.
|
|
671
|
+
font-size: 0.68rem;
|
|
641
672
|
text-transform: uppercase;
|
|
642
673
|
letter-spacing: 0.08em;
|
|
643
674
|
}
|
|
644
675
|
|
|
645
676
|
pre {
|
|
646
677
|
overflow: auto;
|
|
647
|
-
padding: 0.
|
|
678
|
+
padding: 0.68rem;
|
|
648
679
|
border-radius: 14px;
|
|
649
680
|
background: rgba(0, 0, 0, 0.3);
|
|
650
681
|
border: 1px solid var(--line);
|
|
651
|
-
font-size: 0.
|
|
682
|
+
font-size: 0.7rem;
|
|
652
683
|
line-height: 1.5;
|
|
653
684
|
margin: 0.55rem 0 0;
|
|
654
685
|
white-space: pre-wrap;
|
|
@@ -666,7 +697,7 @@
|
|
|
666
697
|
}
|
|
667
698
|
|
|
668
699
|
.field label {
|
|
669
|
-
font-size: 0.
|
|
700
|
+
font-size: 0.68rem;
|
|
670
701
|
color: var(--muted);
|
|
671
702
|
text-transform: uppercase;
|
|
672
703
|
letter-spacing: 0.08em;
|
|
@@ -676,7 +707,7 @@
|
|
|
676
707
|
.field select,
|
|
677
708
|
.field textarea {
|
|
678
709
|
width: 100%;
|
|
679
|
-
padding: 0.
|
|
710
|
+
padding: 0.62rem 0.72rem;
|
|
680
711
|
border-radius: 14px;
|
|
681
712
|
border: 1px solid var(--line);
|
|
682
713
|
background: rgba(255, 255, 255, 0.03);
|
|
@@ -723,6 +754,21 @@
|
|
|
723
754
|
transform: translateX(0);
|
|
724
755
|
}
|
|
725
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
|
+
|
|
726
772
|
.drawer-header {
|
|
727
773
|
display: flex;
|
|
728
774
|
justify-content: space-between;
|
|
@@ -741,12 +787,12 @@
|
|
|
741
787
|
border: 1px solid var(--line);
|
|
742
788
|
background: rgba(255, 255, 255, 0.03);
|
|
743
789
|
border-radius: 18px;
|
|
744
|
-
padding: 0.
|
|
790
|
+
padding: 0.78rem;
|
|
745
791
|
}
|
|
746
792
|
|
|
747
793
|
.drawer-section h4 {
|
|
748
794
|
margin: 0 0 0.55rem;
|
|
749
|
-
font-size: 0.
|
|
795
|
+
font-size: 0.74rem;
|
|
750
796
|
color: var(--muted);
|
|
751
797
|
text-transform: uppercase;
|
|
752
798
|
letter-spacing: 0.08em;
|
|
@@ -769,10 +815,20 @@
|
|
|
769
815
|
.library-grid {
|
|
770
816
|
display: grid;
|
|
771
817
|
gap: 0.7rem;
|
|
772
|
-
max-height:
|
|
818
|
+
max-height: 28rem;
|
|
773
819
|
overflow: auto;
|
|
774
820
|
}
|
|
775
821
|
|
|
822
|
+
.loading-pulse {
|
|
823
|
+
opacity: 0.5;
|
|
824
|
+
animation: pulse 1.2s ease-in-out infinite;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
@keyframes pulse {
|
|
828
|
+
0%, 100% { opacity: 0.5; }
|
|
829
|
+
50% { opacity: 0.3; }
|
|
830
|
+
}
|
|
831
|
+
|
|
776
832
|
.hidden {
|
|
777
833
|
display: none !important;
|
|
778
834
|
}
|
|
@@ -799,7 +855,7 @@
|
|
|
799
855
|
}
|
|
800
856
|
|
|
801
857
|
.graph-panel {
|
|
802
|
-
min-height:
|
|
858
|
+
min-height: 24rem;
|
|
803
859
|
}
|
|
804
860
|
}
|
|
805
861
|
|
|
@@ -836,6 +892,8 @@
|
|
|
836
892
|
</div>
|
|
837
893
|
</header>
|
|
838
894
|
|
|
895
|
+
<div id="status-banner" class="banner hidden" role="status" aria-live="polite"></div>
|
|
896
|
+
|
|
839
897
|
<main class="layout">
|
|
840
898
|
<aside class="rail left">
|
|
841
899
|
<section class="panel">
|
|
@@ -1066,8 +1124,16 @@
|
|
|
1066
1124
|
<div class="empty">Touch a memory, run, workflow, skill, POD worker, or client to inspect it.</div>
|
|
1067
1125
|
</div>
|
|
1068
1126
|
</aside>
|
|
1127
|
+
<div id="drawer-backdrop" class="drawer-backdrop"></div>
|
|
1069
1128
|
|
|
1070
1129
|
<script>
|
|
1130
|
+
const DASHBOARD_API_VERSION = '2';
|
|
1131
|
+
const REQUIRED_CAPABILITIES = ['runs', 'memory', 'pod', 'clients', 'events', 'stream'];
|
|
1132
|
+
|
|
1133
|
+
function createResourceState() {
|
|
1134
|
+
return { status: 'idle', error: '' };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1071
1137
|
const state = {
|
|
1072
1138
|
runs: [],
|
|
1073
1139
|
skills: [],
|
|
@@ -1086,6 +1152,20 @@
|
|
|
1086
1152
|
selected: null,
|
|
1087
1153
|
streamConnected: false,
|
|
1088
1154
|
lastRefreshAt: 0,
|
|
1155
|
+
banner: null,
|
|
1156
|
+
resources: {
|
|
1157
|
+
health: createResourceState(),
|
|
1158
|
+
backends: createResourceState(),
|
|
1159
|
+
runs: createResourceState(),
|
|
1160
|
+
skills: createResourceState(),
|
|
1161
|
+
workflows: createResourceState(),
|
|
1162
|
+
memory: createResourceState(),
|
|
1163
|
+
pod: createResourceState(),
|
|
1164
|
+
clients: createResourceState(),
|
|
1165
|
+
events: createResourceState(),
|
|
1166
|
+
memoryDetail: createResourceState(),
|
|
1167
|
+
memoryNetwork: createResourceState(),
|
|
1168
|
+
},
|
|
1089
1169
|
};
|
|
1090
1170
|
|
|
1091
1171
|
const graphModes = {
|
|
@@ -1147,61 +1227,323 @@
|
|
|
1147
1227
|
.filter(Boolean);
|
|
1148
1228
|
}
|
|
1149
1229
|
|
|
1230
|
+
function setResourceStatus(name, status, error = '') {
|
|
1231
|
+
state.resources[name] = { status, error };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function getResourceStatus(name) {
|
|
1235
|
+
return state.resources[name] || createResourceState();
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function resourceFailed(name) {
|
|
1239
|
+
return getResourceStatus(name).status === 'error';
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function countFailedResources() {
|
|
1243
|
+
return Object.values(state.resources).filter((resource) => resource.status === 'error').length;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function setBanner(kind, message) {
|
|
1247
|
+
state.banner = message ? { kind, message } : null;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function clearBanner() {
|
|
1251
|
+
state.banner = null;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function normalizeError(error) {
|
|
1255
|
+
return error instanceof Error ? error.message : String(error || 'Unknown error');
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function reconcileBanner() {
|
|
1259
|
+
const healthStatus = getResourceStatus('health');
|
|
1260
|
+
if (healthStatus.status === 'error') {
|
|
1261
|
+
setBanner('bad', 'Connected to stale dashboard server or an incompatible API surface. Open the latest dashboard URL printed by the MCP process.');
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const version = state.health?.dashboardApiVersion;
|
|
1266
|
+
const capabilities = state.health?.capabilities || {};
|
|
1267
|
+
const missingCapabilities = REQUIRED_CAPABILITIES.filter((capability) => capabilities[capability] !== true);
|
|
1268
|
+
|
|
1269
|
+
if (!version || version !== DASHBOARD_API_VERSION || missingCapabilities.length) {
|
|
1270
|
+
setBanner('bad', `Dashboard compatibility mismatch detected. Expected API v${DASHBOARD_API_VERSION}; missing capabilities: ${missingCapabilities.join(', ') || 'version marker'}.`);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const failed = Object.entries(state.resources)
|
|
1275
|
+
.filter(([name, resource]) => resource.status === 'error' && !['memoryDetail', 'memoryNetwork'].includes(name))
|
|
1276
|
+
.map(([name]) => name);
|
|
1277
|
+
|
|
1278
|
+
if (failed.length) {
|
|
1279
|
+
setBanner('warn', `Dashboard is partially degraded. Unavailable surfaces: ${failed.join(', ')}.`);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
clearBanner();
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function emptyState(message) {
|
|
1287
|
+
return `<div class="empty">${escapeHtml(message)}</div>`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function normalizeLegacyCategory(type) {
|
|
1291
|
+
if (String(type).startsWith('memory.')) return 'memory';
|
|
1292
|
+
if (String(type).startsWith('pod.')) return 'pod';
|
|
1293
|
+
if (String(type).startsWith('phantom.')) return 'runtime';
|
|
1294
|
+
if (String(type).startsWith('client.')) return 'clients';
|
|
1295
|
+
if (String(type).startsWith('skill.')) return 'skills';
|
|
1296
|
+
if (String(type).startsWith('workflow.')) return 'workflows';
|
|
1297
|
+
if (String(type).startsWith('tokens.') || String(type).startsWith('cas.') || String(type).startsWith('kv.')) return 'tokens';
|
|
1298
|
+
return 'system';
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function normalizeLegacySeverity(type, payload) {
|
|
1302
|
+
if (type === 'guardrail.check') return payload?.passed ? 'good' : 'bad';
|
|
1303
|
+
if (['phantom.merge', 'phantom.merge.complete', 'workflow.run'].includes(type)) {
|
|
1304
|
+
return payload?.status === 'failed' ? 'bad' : 'good';
|
|
1305
|
+
}
|
|
1306
|
+
if (type === 'client.inferred') return 'warn';
|
|
1307
|
+
if (type === 'dashboard.action' && payload?.status === 'failed') return 'bad';
|
|
1308
|
+
if (type === 'pod.signal') return 'info';
|
|
1309
|
+
return 'info';
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function legacyTitle(type) {
|
|
1313
|
+
return {
|
|
1314
|
+
'system.boot': 'Runtime boot',
|
|
1315
|
+
'memory.store': 'Memory stored',
|
|
1316
|
+
'memory.recall': 'Memory recall',
|
|
1317
|
+
'pod.signal': 'POD signal',
|
|
1318
|
+
'tokens.optimized': 'Tokens optimized',
|
|
1319
|
+
'phantom.worker.start': 'Worker start',
|
|
1320
|
+
'phantom.worker.complete': 'Worker complete',
|
|
1321
|
+
'phantom.merge.complete': 'Merge complete',
|
|
1322
|
+
'phantom.merge': 'Merge decision',
|
|
1323
|
+
'guardrail.check': 'Guardrail check',
|
|
1324
|
+
'ghost.pass': 'Ghost pass',
|
|
1325
|
+
'graph.query': 'Graph query',
|
|
1326
|
+
'darwin.cycle': 'Darwin cycle',
|
|
1327
|
+
'session.dna': 'Session DNA',
|
|
1328
|
+
'skill.register': 'Skill registered',
|
|
1329
|
+
'skill.deploy': 'Skill deployed',
|
|
1330
|
+
'skill.revoke': 'Skill revoked',
|
|
1331
|
+
'workflow.deploy': 'Workflow deployed',
|
|
1332
|
+
'workflow.run': 'Workflow run',
|
|
1333
|
+
'client.heartbeat': 'Client heartbeat',
|
|
1334
|
+
'client.inferred': 'Client inferred',
|
|
1335
|
+
'client.status': 'Client status',
|
|
1336
|
+
'dashboard.action': 'Dashboard action',
|
|
1337
|
+
'nexusnet.publish': 'NexusNet publish',
|
|
1338
|
+
'nexusnet.sync': 'NexusNet sync',
|
|
1339
|
+
'entanglement.create': 'Entanglement created',
|
|
1340
|
+
'entanglement.collapse': 'Entanglement collapsed',
|
|
1341
|
+
'entanglement.correlate': 'Entanglement correlated',
|
|
1342
|
+
'cas.encode': 'CAS encode',
|
|
1343
|
+
'cas.decode': 'CAS decode',
|
|
1344
|
+
'cas.pattern_learned': 'CAS pattern',
|
|
1345
|
+
'kv.merge': 'KV merge',
|
|
1346
|
+
'kv.adapt': 'KV adapt',
|
|
1347
|
+
'kv.consensus': 'KV consensus',
|
|
1348
|
+
}[type] || String(type || 'event');
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function legacySource(type, payload) {
|
|
1352
|
+
if (String(type).startsWith('client.')) return String(payload?.displayName || payload?.clientId || 'client');
|
|
1353
|
+
if (type === 'pod.signal') return String(payload?.workerId || 'pod');
|
|
1354
|
+
if (String(type).startsWith('phantom.')) return String(payload?.workerId || payload?.winner || 'runtime');
|
|
1355
|
+
if (String(type).startsWith('skill.')) return String(payload?.skillId || payload?.name || 'skill');
|
|
1356
|
+
if (String(type).startsWith('workflow.')) return String(payload?.workflowId || 'workflow');
|
|
1357
|
+
return 'nexus-prime';
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function legacySummary(type, payload) {
|
|
1361
|
+
switch (type) {
|
|
1362
|
+
case 'memory.store':
|
|
1363
|
+
return `Priority ${payload?.priority ?? 'n/a'} · ${(payload?.tags || []).join(', ') || 'no tags'}`;
|
|
1364
|
+
case 'memory.recall':
|
|
1365
|
+
return `Recalled ${payload?.count ?? 0} memories for "${payload?.query || ''}"`;
|
|
1366
|
+
case 'pod.signal':
|
|
1367
|
+
return String(payload?.content || 'POD signal received');
|
|
1368
|
+
case 'tokens.optimized':
|
|
1369
|
+
return `Saved ${payload?.savings ?? 0} tokens across ${payload?.files ?? 0} files`;
|
|
1370
|
+
case 'phantom.worker.start':
|
|
1371
|
+
return `${payload?.approach || 'worker'} started for ${payload?.goal || 'task'}`;
|
|
1372
|
+
case 'phantom.worker.complete':
|
|
1373
|
+
return `Confidence ${payload?.confidence ?? 0}`;
|
|
1374
|
+
case 'phantom.merge':
|
|
1375
|
+
return `${payload?.action || 'merge'} · ${payload?.winner || 'unknown winner'}`;
|
|
1376
|
+
case 'dashboard.action':
|
|
1377
|
+
return `${payload?.action || 'action'} → ${payload?.status || 'unknown'}`;
|
|
1378
|
+
default:
|
|
1379
|
+
return typeof payload === 'string' ? payload : JSON.stringify(payload || {});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function normalizeEventCard(raw) {
|
|
1384
|
+
if (!raw) return null;
|
|
1385
|
+
|
|
1386
|
+
if (raw.category && raw.title && raw.time) {
|
|
1387
|
+
return {
|
|
1388
|
+
id: raw.id || `evt-${raw.type || raw.category}-${raw.time}-${raw.source || 'nexus-prime'}`,
|
|
1389
|
+
type: raw.type || raw.category,
|
|
1390
|
+
title: String(raw.title),
|
|
1391
|
+
source: String(raw.source || 'nexus-prime'),
|
|
1392
|
+
time: Number(raw.time) || Date.now(),
|
|
1393
|
+
severity: ['good', 'info', 'warn', 'bad'].includes(raw.severity) ? raw.severity : 'info',
|
|
1394
|
+
category: raw.category,
|
|
1395
|
+
summary: String(raw.summary || ''),
|
|
1396
|
+
payload: raw.payload ?? raw,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (raw.type && raw.timestamp) {
|
|
1401
|
+
const payload = raw.data || {};
|
|
1402
|
+
return {
|
|
1403
|
+
id: raw.id || `evt-${raw.type}-${raw.timestamp}-${legacySource(raw.type, payload)}`,
|
|
1404
|
+
type: raw.type,
|
|
1405
|
+
title: legacyTitle(raw.type),
|
|
1406
|
+
source: legacySource(raw.type, payload),
|
|
1407
|
+
time: Number(raw.timestamp) || Date.now(),
|
|
1408
|
+
severity: normalizeLegacySeverity(raw.type, payload),
|
|
1409
|
+
category: normalizeLegacyCategory(raw.type),
|
|
1410
|
+
summary: legacySummary(raw.type, payload),
|
|
1411
|
+
payload,
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return {
|
|
1416
|
+
id: `evt-legacy-${Date.now()}`,
|
|
1417
|
+
type: 'system.legacy',
|
|
1418
|
+
title: 'Legacy event',
|
|
1419
|
+
source: 'nexus-prime',
|
|
1420
|
+
time: Date.now(),
|
|
1421
|
+
severity: 'info',
|
|
1422
|
+
category: 'system',
|
|
1423
|
+
summary: typeof raw === 'string' ? raw : 'Malformed event payload received',
|
|
1424
|
+
payload: raw,
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1150
1428
|
async function fetchJson(url, options) {
|
|
1151
1429
|
const res = await fetch(url, options);
|
|
1430
|
+
const text = await res.text();
|
|
1152
1431
|
if (!res.ok) {
|
|
1153
|
-
throw new Error(`${res.status} ${res.statusText}`);
|
|
1432
|
+
throw new Error(`${res.status} ${res.statusText}${text ? ` · ${text.slice(0, 120)}` : ''}`);
|
|
1154
1433
|
}
|
|
1155
|
-
return
|
|
1434
|
+
return text ? JSON.parse(text) : null;
|
|
1156
1435
|
}
|
|
1157
1436
|
|
|
1158
1437
|
async function refreshAll() {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1438
|
+
document.querySelector('.layout')?.classList.add('loading-pulse');
|
|
1439
|
+
const resources = [
|
|
1440
|
+
['runs', '/api/runs?limit=20', (value) => { state.runs = Array.isArray(value) ? value : state.runs; }],
|
|
1441
|
+
['skills', '/api/skills', (value) => { state.skills = Array.isArray(value) ? value : state.skills; }],
|
|
1442
|
+
['workflows', '/api/workflows', (value) => { state.workflows = Array.isArray(value) ? value : state.workflows; }],
|
|
1443
|
+
['backends', '/api/backends', (value) => { state.backends = value || state.backends; }],
|
|
1444
|
+
['health', '/api/health', (value) => { state.health = value || state.health; }],
|
|
1445
|
+
['memory', '/api/memory?limit=40', (value) => { state.memories = Array.isArray(value) ? value : state.memories; }],
|
|
1446
|
+
['pod', '/api/pod?limit=30', (value) => { state.pod = value || state.pod; }],
|
|
1447
|
+
['clients', '/api/clients', (value) => { state.clients = Array.isArray(value) ? value : state.clients; }],
|
|
1448
|
+
['events', '/api/events?limit=80', (value) => {
|
|
1449
|
+
if (Array.isArray(value)) {
|
|
1450
|
+
const seen = new Set();
|
|
1451
|
+
state.events = value.map((event) => normalizeEventCard(event)).filter((e) => {
|
|
1452
|
+
if (!e || seen.has(e.id)) return false;
|
|
1453
|
+
seen.add(e.id);
|
|
1454
|
+
return true;
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}],
|
|
1458
|
+
];
|
|
1170
1459
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1460
|
+
const results = await Promise.allSettled(resources.map(([, url]) => fetchJson(url)));
|
|
1461
|
+
let refreshed = false;
|
|
1462
|
+
|
|
1463
|
+
results.forEach((result, index) => {
|
|
1464
|
+
const [name, , assign] = resources[index];
|
|
1465
|
+
if (result.status === 'fulfilled') {
|
|
1466
|
+
assign(result.value);
|
|
1467
|
+
setResourceStatus(name, 'ready');
|
|
1468
|
+
refreshed = true;
|
|
1469
|
+
} else {
|
|
1470
|
+
setResourceStatus(name, 'error', normalizeError(result.reason));
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
if (refreshed) {
|
|
1475
|
+
state.lastRefreshAt = Date.now();
|
|
1476
|
+
}
|
|
1181
1477
|
|
|
1182
1478
|
const focusMemoryId = state.selected?.kind === 'memory'
|
|
1183
1479
|
? state.selected.id
|
|
1184
1480
|
: state.memories[0]?.id;
|
|
1185
|
-
|
|
1481
|
+
|
|
1482
|
+
if (focusMemoryId && !resourceFailed('memory')) {
|
|
1186
1483
|
await refreshMemorySelection(focusMemoryId, false);
|
|
1187
|
-
} else {
|
|
1484
|
+
} else if (!focusMemoryId) {
|
|
1188
1485
|
state.memoryDetail = null;
|
|
1189
1486
|
state.memoryNetwork = { nodes: [], links: [], focusId: null };
|
|
1487
|
+
setResourceStatus('memoryDetail', 'idle');
|
|
1488
|
+
setResourceStatus('memoryNetwork', 'idle');
|
|
1190
1489
|
}
|
|
1191
1490
|
|
|
1491
|
+
document.querySelector('.layout')?.classList.remove('loading-pulse');
|
|
1492
|
+
reconcileBanner();
|
|
1192
1493
|
populateBackendSelects();
|
|
1193
1494
|
render();
|
|
1194
1495
|
}
|
|
1195
1496
|
|
|
1497
|
+
let _refreshTimer = null;
|
|
1498
|
+
let _refreshInFlight = false;
|
|
1499
|
+
|
|
1500
|
+
function scheduleRefresh() {
|
|
1501
|
+
if (_refreshTimer) return;
|
|
1502
|
+
_refreshTimer = setTimeout(async () => {
|
|
1503
|
+
_refreshTimer = null;
|
|
1504
|
+
if (_refreshInFlight) {
|
|
1505
|
+
scheduleRefresh();
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
_refreshInFlight = true;
|
|
1509
|
+
try { await refreshAll(); } catch {}
|
|
1510
|
+
_refreshInFlight = false;
|
|
1511
|
+
}, 300);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1196
1514
|
async function refreshMemorySelection(memoryId, openDrawer = true) {
|
|
1197
|
-
const [detail, network] = await Promise.
|
|
1515
|
+
const [detail, network] = await Promise.allSettled([
|
|
1198
1516
|
fetchJson(`/api/memory/${encodeURIComponent(memoryId)}`),
|
|
1199
1517
|
fetchJson(`/api/memory/${encodeURIComponent(memoryId)}/network?depth=2&limit=18`),
|
|
1200
1518
|
]);
|
|
1201
|
-
|
|
1202
|
-
|
|
1519
|
+
|
|
1520
|
+
if (detail.status === 'fulfilled') {
|
|
1521
|
+
state.memoryDetail = detail.value;
|
|
1522
|
+
setResourceStatus('memoryDetail', 'ready');
|
|
1523
|
+
} else {
|
|
1524
|
+
state.memoryDetail = state.memories.find((memory) => memory.id === memoryId) || null;
|
|
1525
|
+
setResourceStatus('memoryDetail', 'error', normalizeError(detail.reason));
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (network.status === 'fulfilled') {
|
|
1529
|
+
state.memoryNetwork = network.value || { nodes: [], links: [], focusId: memoryId };
|
|
1530
|
+
setResourceStatus('memoryNetwork', 'ready');
|
|
1531
|
+
} else {
|
|
1532
|
+
state.memoryNetwork = {
|
|
1533
|
+
focusId: memoryId,
|
|
1534
|
+
nodes: state.memories.slice(0, 12).map((memory) => ({
|
|
1535
|
+
id: memory.id,
|
|
1536
|
+
label: memory.excerpt,
|
|
1537
|
+
entityType: 'memory',
|
|
1538
|
+
tier: memory.tier,
|
|
1539
|
+
})),
|
|
1540
|
+
links: [],
|
|
1541
|
+
};
|
|
1542
|
+
setResourceStatus('memoryNetwork', 'error', normalizeError(network.reason));
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1203
1545
|
if (openDrawer) {
|
|
1204
|
-
state.selected = { kind: 'memory', id: memoryId, data:
|
|
1546
|
+
state.selected = { kind: 'memory', id: memoryId, data: state.memoryDetail };
|
|
1205
1547
|
}
|
|
1206
1548
|
}
|
|
1207
1549
|
|
|
@@ -1254,6 +1596,7 @@
|
|
|
1254
1596
|
}
|
|
1255
1597
|
|
|
1256
1598
|
function render() {
|
|
1599
|
+
renderBanner();
|
|
1257
1600
|
renderHeader();
|
|
1258
1601
|
renderLeftRail();
|
|
1259
1602
|
renderGraph();
|
|
@@ -1262,19 +1605,38 @@
|
|
|
1262
1605
|
renderDrawer();
|
|
1263
1606
|
}
|
|
1264
1607
|
|
|
1608
|
+
function renderBanner() {
|
|
1609
|
+
const banner = $('status-banner');
|
|
1610
|
+
if (!state.banner?.message) {
|
|
1611
|
+
banner.className = 'banner hidden';
|
|
1612
|
+
banner.textContent = '';
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
banner.className = `banner ${state.banner.kind}`;
|
|
1617
|
+
banner.textContent = state.banner.message;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1265
1620
|
function renderHeader() {
|
|
1266
1621
|
$('header-version').textContent = `v${state.health.release?.packageVersion || '?'}`;
|
|
1267
1622
|
const pill = $('sync-pill');
|
|
1268
|
-
|
|
1269
|
-
pill.classList.toggle('
|
|
1623
|
+
const failedResources = countFailedResources();
|
|
1624
|
+
pill.classList.toggle('live', state.streamConnected && failedResources === 0);
|
|
1625
|
+
pill.classList.toggle('warn', !state.streamConnected || failedResources > 0);
|
|
1270
1626
|
$('sync-label').textContent = state.streamConnected
|
|
1271
|
-
?
|
|
1272
|
-
|
|
1627
|
+
? failedResources
|
|
1628
|
+
? `Degraded · ${failedResources} surfaces unavailable`
|
|
1629
|
+
: `Synchronized · ${formatAgo(state.lastRefreshAt)}`
|
|
1630
|
+
: failedResources
|
|
1631
|
+
? `REST degraded · ${failedResources} unavailable`
|
|
1632
|
+
: 'Stream reconnecting';
|
|
1273
1633
|
}
|
|
1274
1634
|
|
|
1275
1635
|
function renderLeftRail() {
|
|
1276
|
-
$('clients-summary').textContent = `${state.clients.length} visible`;
|
|
1277
|
-
$('clients-list').innerHTML = state.clients.length
|
|
1636
|
+
$('clients-summary').textContent = resourceFailed('clients') ? 'unavailable' : `${state.clients.length} visible`;
|
|
1637
|
+
$('clients-list').innerHTML = resourceFailed('clients') && !state.clients.length
|
|
1638
|
+
? emptyState('Clients endpoint unavailable.')
|
|
1639
|
+
: state.clients.length
|
|
1278
1640
|
? state.clients.map((client) => `
|
|
1279
1641
|
<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
1642
|
<div class="client-info">
|
|
@@ -1287,37 +1649,53 @@
|
|
|
1287
1649
|
<span class="state-chip ${stateClass(client.state)}">${escapeHtml(client.state)}</span>
|
|
1288
1650
|
</div>
|
|
1289
1651
|
`).join('')
|
|
1290
|
-
: '
|
|
1652
|
+
: emptyState('No clients detected.');
|
|
1653
|
+
|
|
1654
|
+
$('clients-list').querySelectorAll('.card.interactive').forEach((card) => {
|
|
1655
|
+
card.addEventListener('click', () => {
|
|
1656
|
+
void openEntity(card.getAttribute('data-kind'), card.getAttribute('data-id'));
|
|
1657
|
+
});
|
|
1658
|
+
});
|
|
1291
1659
|
|
|
1292
1660
|
const tokenMetrics = computeTokenMetrics();
|
|
1293
|
-
$('token-summary').textContent = `${tokenMetrics.events} events`;
|
|
1661
|
+
$('token-summary').textContent = resourceFailed('events') ? 'events unavailable' : `${tokenMetrics.events} events`;
|
|
1294
1662
|
$('gross-tokens').textContent = formatNumber(tokenMetrics.gross);
|
|
1295
1663
|
$('saved-tokens').textContent = formatNumber(tokenMetrics.saved);
|
|
1296
1664
|
$('net-tokens').textContent = formatNumber(tokenMetrics.forwarded);
|
|
1297
|
-
$('memory-count').textContent = formatNumber(state.memories.length);
|
|
1665
|
+
$('memory-count').textContent = resourceFailed('memory') && !state.memories.length ? 'n/a' : formatNumber(state.memories.length);
|
|
1298
1666
|
$('dial-value').textContent = `${tokenMetrics.ratio}%`;
|
|
1299
1667
|
const circumference = 2 * Math.PI * 70;
|
|
1300
1668
|
const offset = circumference - (circumference * tokenMetrics.ratio / 100);
|
|
1301
1669
|
$('dial-progress').style.strokeDashoffset = `${offset}`;
|
|
1302
1670
|
|
|
1303
|
-
$('runs-count').textContent = formatNumber(state.runs.length);
|
|
1671
|
+
$('runs-count').textContent = resourceFailed('runs') && !state.runs.length ? 'n/a' : formatNumber(state.runs.length);
|
|
1304
1672
|
const latestRun = state.runs[0];
|
|
1305
|
-
$('latest-run-state').textContent = latestRun
|
|
1673
|
+
$('latest-run-state').textContent = resourceFailed('runs') && !latestRun
|
|
1674
|
+
? 'Runs endpoint unavailable'
|
|
1675
|
+
: latestRun
|
|
1306
1676
|
? `${latestRun.state} · ${latestRun.selectedBackends?.memoryBackend || 'memory n/a'}`
|
|
1307
1677
|
: 'No runs yet';
|
|
1308
1678
|
$('artifact-count').textContent = `${state.skills.length} / ${state.workflows.length}`;
|
|
1309
|
-
$('artifact-summary').textContent =
|
|
1310
|
-
|
|
1311
|
-
|
|
1679
|
+
$('artifact-summary').textContent = resourceFailed('skills') || resourceFailed('workflows')
|
|
1680
|
+
? 'Skill or workflow endpoint unavailable'
|
|
1681
|
+
: `${state.skills.filter((item) => item.rolloutStatus === 'promoted').length} promoted skills · ${state.workflows.filter((item) => item.rolloutStatus === 'promoted').length} promoted workflows`;
|
|
1682
|
+
$('docs-health').textContent = resourceFailed('health')
|
|
1683
|
+
? 'Unknown'
|
|
1684
|
+
: state.health.docs?.pagesWorkflowValid ? 'Healthy' : 'Needs attention';
|
|
1685
|
+
$('ci-health').textContent = resourceFailed('health')
|
|
1686
|
+
? 'Health endpoint unavailable'
|
|
1687
|
+
: state.health.docs?.pagesWorkflowValid
|
|
1312
1688
|
? `Pages valid · ${state.health.ci?.eventHistory || 0} events`
|
|
1313
1689
|
: 'Pages syntax or docs deployment needs attention';
|
|
1314
1690
|
|
|
1315
1691
|
const memory = state.health.memory || {};
|
|
1316
|
-
$('memory-tier-summary').textContent = `${memory.cortex || 0} cortex`;
|
|
1692
|
+
$('memory-tier-summary').textContent = resourceFailed('health') ? 'health unavailable' : `${memory.cortex || 0} cortex`;
|
|
1317
1693
|
|
|
1318
1694
|
const pod = state.pod || {};
|
|
1319
|
-
$('pod-summary').textContent = `${(pod.activeWorkers || []).length} workers`;
|
|
1320
|
-
$('pod-highlights').innerHTML = pod.activeWorkers
|
|
1695
|
+
$('pod-summary').textContent = resourceFailed('pod') ? 'unavailable' : `${(pod.activeWorkers || []).length} workers`;
|
|
1696
|
+
$('pod-highlights').innerHTML = resourceFailed('pod') && !(pod.activeWorkers || []).length
|
|
1697
|
+
? emptyState('POD endpoint unavailable.')
|
|
1698
|
+
: pod.activeWorkers?.length
|
|
1321
1699
|
? pod.activeWorkers.slice(0, 4).map((worker) => `
|
|
1322
1700
|
<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
1701
|
<div class="card-title">
|
|
@@ -1328,7 +1706,13 @@
|
|
|
1328
1706
|
<div class="list-inline">${(worker.tags || []).slice(0, 4).map((tag) => chip(tag)).join('')}</div>
|
|
1329
1707
|
</div>
|
|
1330
1708
|
`).join('')
|
|
1331
|
-
: '
|
|
1709
|
+
: emptyState('Waiting for POD traffic.');
|
|
1710
|
+
|
|
1711
|
+
$('pod-highlights').querySelectorAll('.card.interactive').forEach((card) => {
|
|
1712
|
+
card.addEventListener('click', () => {
|
|
1713
|
+
void openEntity(card.getAttribute('data-kind'), card.getAttribute('data-id'));
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1332
1716
|
}
|
|
1333
1717
|
|
|
1334
1718
|
function buildGraphModel() {
|
|
@@ -1374,7 +1758,14 @@
|
|
|
1374
1758
|
return { nodes, links };
|
|
1375
1759
|
}
|
|
1376
1760
|
|
|
1377
|
-
const nodes = (state.memoryNetwork.nodes || []).
|
|
1761
|
+
const nodes = (state.memoryNetwork.nodes || []).length
|
|
1762
|
+
? (state.memoryNetwork.nodes || []).map((node) => ({ ...node }))
|
|
1763
|
+
: state.memories.slice(0, 18).map((memory) => ({
|
|
1764
|
+
id: memory.id,
|
|
1765
|
+
label: memory.excerpt,
|
|
1766
|
+
entityType: 'memory',
|
|
1767
|
+
tier: memory.tier,
|
|
1768
|
+
}));
|
|
1378
1769
|
const links = (state.memoryNetwork.links || []).map((link) => ({ ...link }));
|
|
1379
1770
|
return { nodes, links };
|
|
1380
1771
|
}
|
|
@@ -1394,7 +1785,19 @@
|
|
|
1394
1785
|
if (!model.nodes.length) {
|
|
1395
1786
|
svg.innerHTML = '';
|
|
1396
1787
|
empty.classList.remove('hidden');
|
|
1788
|
+
empty.textContent = state.graphMode === 'memory'
|
|
1789
|
+
? resourceFailed('memory')
|
|
1790
|
+
? 'Memory endpoint unavailable.'
|
|
1791
|
+
: 'No memory stored yet.'
|
|
1792
|
+
: state.graphMode === 'runs'
|
|
1793
|
+
? resourceFailed('runs')
|
|
1794
|
+
? 'Runs endpoint unavailable.'
|
|
1795
|
+
: 'No run graph data yet.'
|
|
1796
|
+
: resourceFailed('pod')
|
|
1797
|
+
? 'POD endpoint unavailable.'
|
|
1798
|
+
: 'No POD topology data yet.';
|
|
1397
1799
|
$('graph-stats').innerHTML = '';
|
|
1800
|
+
$('graph-note').textContent = empty.textContent;
|
|
1398
1801
|
return;
|
|
1399
1802
|
}
|
|
1400
1803
|
|
|
@@ -1469,6 +1872,9 @@
|
|
|
1469
1872
|
state.graphMode === 'runs' ? chip(`${state.runs.length} recent runs`) : '',
|
|
1470
1873
|
state.graphMode === 'pod' ? chip(`${(state.pod.activeWorkers || []).length} pod workers`) : '',
|
|
1471
1874
|
].filter(Boolean).join('');
|
|
1875
|
+
$('graph-note').textContent = state.graphMode === 'memory' && resourceFailed('memoryNetwork')
|
|
1876
|
+
? 'Memory network endpoint unavailable. Showing snapshot fallback topology.'
|
|
1877
|
+
: config.subtitle;
|
|
1472
1878
|
}
|
|
1473
1879
|
|
|
1474
1880
|
function renderLibrary() {
|
|
@@ -1486,7 +1892,9 @@
|
|
|
1486
1892
|
const container = $('library-list');
|
|
1487
1893
|
|
|
1488
1894
|
if (state.libraryMode === 'memories') {
|
|
1489
|
-
container.innerHTML = state.memories.length
|
|
1895
|
+
container.innerHTML = resourceFailed('memory') && !state.memories.length
|
|
1896
|
+
? emptyState('Memory endpoint unavailable.')
|
|
1897
|
+
: state.memories.length
|
|
1490
1898
|
? state.memories.map((memory) => `
|
|
1491
1899
|
<div class="card interactive ${state.selected?.kind === 'memory' && state.selected.id === memory.id ? 'active' : ''}" data-kind="memory" data-id="${escapeHtml(memory.id)}">
|
|
1492
1900
|
<div class="card-title">
|
|
@@ -1497,9 +1905,11 @@
|
|
|
1497
1905
|
<div class="list-inline">${(memory.tags || []).slice(0, 5).map((tag) => chip(tag)).join('')}</div>
|
|
1498
1906
|
</div>
|
|
1499
1907
|
`).join('')
|
|
1500
|
-
: '
|
|
1908
|
+
: emptyState('No memory stored yet.');
|
|
1501
1909
|
} else if (state.libraryMode === 'skills') {
|
|
1502
|
-
container.innerHTML = state.skills.length
|
|
1910
|
+
container.innerHTML = resourceFailed('skills') && !state.skills.length
|
|
1911
|
+
? emptyState('Skills endpoint unavailable.')
|
|
1912
|
+
: state.skills.length
|
|
1503
1913
|
? state.skills.map((skill) => `
|
|
1504
1914
|
<div class="card interactive ${state.selected?.kind === 'skill' && state.selected.id === skill.skillId ? 'active' : ''}" data-kind="skill" data-id="${escapeHtml(skill.skillId)}">
|
|
1505
1915
|
<div class="card-title">
|
|
@@ -1514,9 +1924,11 @@
|
|
|
1514
1924
|
</div>
|
|
1515
1925
|
</div>
|
|
1516
1926
|
`).join('')
|
|
1517
|
-
: '
|
|
1927
|
+
: emptyState('No skills loaded.');
|
|
1518
1928
|
} else if (state.libraryMode === 'workflows') {
|
|
1519
|
-
container.innerHTML = state.workflows.length
|
|
1929
|
+
container.innerHTML = resourceFailed('workflows') && !state.workflows.length
|
|
1930
|
+
? emptyState('Workflows endpoint unavailable.')
|
|
1931
|
+
: state.workflows.length
|
|
1520
1932
|
? state.workflows.map((workflow) => `
|
|
1521
1933
|
<div class="card interactive ${state.selected?.kind === 'workflow' && state.selected.id === workflow.workflowId ? 'active' : ''}" data-kind="workflow" data-id="${escapeHtml(workflow.workflowId)}">
|
|
1522
1934
|
<div class="card-title">
|
|
@@ -1527,9 +1939,11 @@
|
|
|
1527
1939
|
<div class="list-inline">${chip(`domain:${workflow.domain}`)}${chip(`verify:${workflow.effectiveness?.verificationPasses || 0}`)}</div>
|
|
1528
1940
|
</div>
|
|
1529
1941
|
`).join('')
|
|
1530
|
-
: '
|
|
1942
|
+
: emptyState('No workflows loaded.');
|
|
1531
1943
|
} else if (state.libraryMode === 'pod') {
|
|
1532
|
-
container.innerHTML = state.pod.messages
|
|
1944
|
+
container.innerHTML = resourceFailed('pod') && !(state.pod.messages || []).length
|
|
1945
|
+
? emptyState('POD endpoint unavailable.')
|
|
1946
|
+
: state.pod.messages?.length
|
|
1533
1947
|
? state.pod.messages.map((message) => `
|
|
1534
1948
|
<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
1949
|
<div class="card-title">
|
|
@@ -1541,9 +1955,11 @@
|
|
|
1541
1955
|
<div class="list-inline">${(message.tags || []).map((tag) => chip(tag)).join('')}</div>
|
|
1542
1956
|
</div>
|
|
1543
1957
|
`).join('')
|
|
1544
|
-
: '
|
|
1958
|
+
: emptyState('No POD signals captured.');
|
|
1545
1959
|
} else {
|
|
1546
|
-
container.innerHTML = state.clients.length
|
|
1960
|
+
container.innerHTML = resourceFailed('clients') && !state.clients.length
|
|
1961
|
+
? emptyState('Clients endpoint unavailable.')
|
|
1962
|
+
: state.clients.length
|
|
1547
1963
|
? state.clients.map((client) => `
|
|
1548
1964
|
<div class="card interactive ${state.selected?.kind === 'client' && state.selected.id === client.clientId ? 'active' : ''}" data-kind="client" data-id="${escapeHtml(client.clientId)}">
|
|
1549
1965
|
<div class="card-title">
|
|
@@ -1554,7 +1970,7 @@
|
|
|
1554
1970
|
<div class="list-inline">${(client.evidence || []).map((evidence) => chip(evidence)).join('')}</div>
|
|
1555
1971
|
</div>
|
|
1556
1972
|
`).join('')
|
|
1557
|
-
: '
|
|
1973
|
+
: emptyState('No clients detected.');
|
|
1558
1974
|
}
|
|
1559
1975
|
|
|
1560
1976
|
container.querySelectorAll('.card.interactive').forEach((card) => {
|
|
@@ -1567,7 +1983,7 @@
|
|
|
1567
1983
|
}
|
|
1568
1984
|
|
|
1569
1985
|
function renderEvents() {
|
|
1570
|
-
$('events-summary').textContent = `${state.events.length} signals`;
|
|
1986
|
+
$('events-summary').textContent = resourceFailed('events') ? 'events unavailable' : `${state.events.length} signals`;
|
|
1571
1987
|
document.querySelectorAll('#event-filters button').forEach((button) => {
|
|
1572
1988
|
button.classList.toggle('active', button.dataset.eventFilter === state.eventFilter);
|
|
1573
1989
|
});
|
|
@@ -1593,13 +2009,17 @@
|
|
|
1593
2009
|
</details>
|
|
1594
2010
|
</article>
|
|
1595
2011
|
`).join('')
|
|
1596
|
-
: '
|
|
2012
|
+
: resourceFailed('events')
|
|
2013
|
+
? emptyState('Events endpoint unavailable. Live stream may still populate this panel.')
|
|
2014
|
+
: emptyState('No events for this filter.');
|
|
1597
2015
|
}
|
|
1598
2016
|
|
|
1599
2017
|
function renderDrawer() {
|
|
1600
2018
|
const drawer = $('drawer');
|
|
2019
|
+
const backdrop = $('drawer-backdrop');
|
|
1601
2020
|
if (!state.selected || !state.selected.data) {
|
|
1602
2021
|
drawer.classList.remove('open');
|
|
2022
|
+
backdrop.classList.remove('open');
|
|
1603
2023
|
$('drawer-title').textContent = 'Inspector';
|
|
1604
2024
|
$('drawer-subtitle').textContent = 'Select a node or card.';
|
|
1605
2025
|
$('drawer-body').innerHTML = '<div class="empty">Touch a memory, run, workflow, skill, POD worker, or client to inspect it.</div>';
|
|
@@ -1607,6 +2027,7 @@
|
|
|
1607
2027
|
}
|
|
1608
2028
|
|
|
1609
2029
|
drawer.classList.add('open');
|
|
2030
|
+
backdrop.classList.add('open');
|
|
1610
2031
|
const { kind, data } = state.selected;
|
|
1611
2032
|
$('drawer-title').textContent = formatDrawerTitle(kind, data);
|
|
1612
2033
|
$('drawer-subtitle').textContent = formatDrawerSubtitle(kind, data);
|
|
@@ -1900,29 +2321,33 @@
|
|
|
1900
2321
|
|
|
1901
2322
|
async function openEntity(kind, id) {
|
|
1902
2323
|
if (!kind || !id) return;
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2324
|
+
try {
|
|
2325
|
+
if (kind === 'memory') {
|
|
2326
|
+
await refreshMemorySelection(id, true);
|
|
2327
|
+
} else if (kind === 'run') {
|
|
2328
|
+
const run = state.runs.find((item) => item.runId === id) || await fetchJson(`/api/runs/${encodeURIComponent(id)}`);
|
|
2329
|
+
if (run?.error) return;
|
|
2330
|
+
state.selected = { kind: 'run', id, data: run };
|
|
2331
|
+
} else if (kind === 'skill') {
|
|
2332
|
+
const skill = state.skills.find((item) => item.skillId === id);
|
|
2333
|
+
if (!skill) return;
|
|
2334
|
+
state.selected = { kind: 'skill', id, data: skill };
|
|
2335
|
+
} else if (kind === 'workflow') {
|
|
2336
|
+
const workflow = state.workflows.find((item) => item.workflowId === id);
|
|
2337
|
+
if (!workflow) return;
|
|
2338
|
+
state.selected = { kind: 'workflow', id, data: workflow };
|
|
2339
|
+
} else if (kind === 'pod-worker') {
|
|
2340
|
+
const worker = await fetchJson(`/api/pod/${encodeURIComponent(id)}`);
|
|
2341
|
+
state.selected = { kind: 'pod-worker', id, data: worker };
|
|
2342
|
+
} else if (kind === 'client') {
|
|
2343
|
+
const client = state.clients.find((item) => item.clientId === id);
|
|
2344
|
+
if (!client) return;
|
|
2345
|
+
state.selected = { kind: 'client', id, data: client };
|
|
2346
|
+
}
|
|
2347
|
+
render();
|
|
2348
|
+
} catch (error) {
|
|
2349
|
+
$('control-status').textContent = `Failed to load ${kind}: ${error.message || 'unknown error'}`;
|
|
1924
2350
|
}
|
|
1925
|
-
render();
|
|
1926
2351
|
}
|
|
1927
2352
|
|
|
1928
2353
|
function shortLabel(label) {
|
|
@@ -2043,6 +2468,18 @@
|
|
|
2043
2468
|
render();
|
|
2044
2469
|
});
|
|
2045
2470
|
|
|
2471
|
+
$('drawer-backdrop').addEventListener('click', () => {
|
|
2472
|
+
state.selected = null;
|
|
2473
|
+
render();
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
document.addEventListener('keydown', (e) => {
|
|
2477
|
+
if (e.key === 'Escape' && state.selected) {
|
|
2478
|
+
state.selected = null;
|
|
2479
|
+
render();
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
|
|
2046
2483
|
document.querySelectorAll('#graph-modes button').forEach((button) => {
|
|
2047
2484
|
button.addEventListener('click', async () => {
|
|
2048
2485
|
state.graphMode = button.dataset.graphMode;
|
|
@@ -2065,24 +2502,40 @@
|
|
|
2065
2502
|
});
|
|
2066
2503
|
}
|
|
2067
2504
|
|
|
2505
|
+
let _stream = null;
|
|
2506
|
+
let _reconnectDelay = 1000;
|
|
2507
|
+
let _reconnectTimer = null;
|
|
2508
|
+
|
|
2068
2509
|
function connectStream() {
|
|
2069
|
-
|
|
2070
|
-
|
|
2510
|
+
if (_stream) { _stream.close(); _stream = null; }
|
|
2511
|
+
if (_reconnectTimer) { clearTimeout(_reconnectTimer); _reconnectTimer = null; }
|
|
2512
|
+
|
|
2513
|
+
_stream = new EventSource('/stream');
|
|
2514
|
+
_stream.onopen = () => {
|
|
2071
2515
|
state.streamConnected = true;
|
|
2516
|
+
_reconnectDelay = 1000;
|
|
2072
2517
|
renderHeader();
|
|
2073
2518
|
};
|
|
2074
|
-
|
|
2519
|
+
_stream.onerror = () => {
|
|
2075
2520
|
state.streamConnected = false;
|
|
2076
2521
|
renderHeader();
|
|
2522
|
+
if (_stream) { _stream.close(); _stream = null; }
|
|
2523
|
+
_reconnectTimer = setTimeout(connectStream, _reconnectDelay);
|
|
2524
|
+
_reconnectDelay = Math.min(_reconnectDelay * 2, 30000);
|
|
2077
2525
|
};
|
|
2078
|
-
|
|
2526
|
+
_stream.onmessage = (event) => {
|
|
2079
2527
|
try {
|
|
2080
|
-
const
|
|
2081
|
-
if (
|
|
2082
|
-
|
|
2528
|
+
const raw = JSON.parse(event.data);
|
|
2529
|
+
if (raw?.connected) return;
|
|
2530
|
+
const payload = normalizeEventCard(raw);
|
|
2531
|
+
if (!payload) return;
|
|
2532
|
+
if (!state.events.some((e) => e.id === payload.id)) {
|
|
2533
|
+
state.events.unshift(payload);
|
|
2534
|
+
}
|
|
2083
2535
|
state.events = state.events.slice(0, 120);
|
|
2536
|
+
setResourceStatus('events', 'ready');
|
|
2084
2537
|
if (['runtime', 'memory', 'pod', 'skills', 'workflows', 'clients'].includes(payload.category)) {
|
|
2085
|
-
|
|
2538
|
+
scheduleRefresh();
|
|
2086
2539
|
} else {
|
|
2087
2540
|
renderEvents();
|
|
2088
2541
|
renderHeader();
|
|
@@ -2095,14 +2548,13 @@
|
|
|
2095
2548
|
|
|
2096
2549
|
async function bootstrap() {
|
|
2097
2550
|
attachStaticHandlers();
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
}
|
|
2551
|
+
await refreshAll();
|
|
2552
|
+
$('control-status').textContent = state.banner?.message
|
|
2553
|
+
? state.banner.message
|
|
2554
|
+
: 'Dashboard actions stay local and route through the runtime.';
|
|
2103
2555
|
connectStream();
|
|
2104
2556
|
setInterval(() => {
|
|
2105
|
-
|
|
2557
|
+
scheduleRefresh();
|
|
2106
2558
|
}, 20000);
|
|
2107
2559
|
}
|
|
2108
2560
|
|