@miller-tech/uap 1.104.0 → 1.104.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +28 -0
- package/dist/dashboard/server.js.map +1 -1
- package/package.json +3 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/web/dashboard.html +1183 -0
- package/web/generator.html +902 -0
- package/web/vendor/uPlot.LICENSE +9 -0
- package/web/vendor/uPlot.iife.min.js +2 -0
- package/web/vendor/uPlot.min.css +1 -0
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>UAP Dashboard</title>
|
|
7
|
+
<link
|
|
8
|
+
rel="icon"
|
|
9
|
+
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>"
|
|
10
|
+
/>
|
|
11
|
+
<link rel="stylesheet" href="/vendor/uPlot.min.css">
|
|
12
|
+
<script src="/vendor/uPlot.iife.min.js"></script>
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #0d1117;
|
|
16
|
+
--bg2: #161b22;
|
|
17
|
+
--bg3: #21262d;
|
|
18
|
+
--fg: #c9d1d9;
|
|
19
|
+
--fg2: #8b949e;
|
|
20
|
+
--fg3: #484f58;
|
|
21
|
+
--cyan: #58a6ff;
|
|
22
|
+
--green: #3fb950;
|
|
23
|
+
--yellow: #d29922;
|
|
24
|
+
--red: #f85149;
|
|
25
|
+
--purple: #bc8cff;
|
|
26
|
+
--orange: #d18616;
|
|
27
|
+
--border: #30363d;
|
|
28
|
+
--radius: 8px;
|
|
29
|
+
}
|
|
30
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
31
|
+
body {
|
|
32
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
33
|
+
background: var(--bg);
|
|
34
|
+
color: var(--fg);
|
|
35
|
+
font-size: 13px;
|
|
36
|
+
line-height: 1.5;
|
|
37
|
+
}
|
|
38
|
+
.container { max-width: 1600px; margin: 0 auto; padding: 16px; }
|
|
39
|
+
|
|
40
|
+
/* ── Header ── */
|
|
41
|
+
header {
|
|
42
|
+
display: flex;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
align-items: center;
|
|
45
|
+
padding: 12px 0;
|
|
46
|
+
border-bottom: 1px solid var(--border);
|
|
47
|
+
margin-bottom: 16px;
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
}
|
|
51
|
+
header h1 { font-size: 18px; color: var(--cyan); font-weight: 600; }
|
|
52
|
+
.header-center {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 10px;
|
|
56
|
+
font-size: 11px;
|
|
57
|
+
color: var(--fg3);
|
|
58
|
+
}
|
|
59
|
+
.sys-badge {
|
|
60
|
+
padding: 2px 8px;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
background: var(--bg3);
|
|
63
|
+
border: 1px solid var(--border);
|
|
64
|
+
font-size: 11px;
|
|
65
|
+
}
|
|
66
|
+
.sys-badge .label { color: var(--fg3); }
|
|
67
|
+
.sys-badge .val { color: var(--fg); font-weight: 500; }
|
|
68
|
+
.sys-badge .val.cyan { color: var(--cyan); }
|
|
69
|
+
.sys-badge .val.green { color: var(--green); }
|
|
70
|
+
.sys-badge .val.yellow { color: var(--yellow); }
|
|
71
|
+
.header-right {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 12px;
|
|
75
|
+
}
|
|
76
|
+
.header-ts { color: var(--fg3); font-size: 11px; }
|
|
77
|
+
.status-dot {
|
|
78
|
+
display: inline-block;
|
|
79
|
+
width: 8px;
|
|
80
|
+
height: 8px;
|
|
81
|
+
border-radius: 50%;
|
|
82
|
+
margin-right: 6px;
|
|
83
|
+
}
|
|
84
|
+
.status-dot.connected { background: var(--green); box-shadow: 0 0 4px var(--green); }
|
|
85
|
+
.status-dot.disconnected { background: var(--red); }
|
|
86
|
+
.status-dot.polling { background: var(--yellow); }
|
|
87
|
+
.refresh-info { color: var(--fg3); font-size: 11px; }
|
|
88
|
+
|
|
89
|
+
/* ── Layout ── */
|
|
90
|
+
.hero-row {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: 1fr 1fr;
|
|
93
|
+
gap: 16px;
|
|
94
|
+
margin-bottom: 16px;
|
|
95
|
+
}
|
|
96
|
+
.metrics-row {
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-columns: repeat(3, 1fr);
|
|
99
|
+
gap: 16px;
|
|
100
|
+
margin-bottom: 16px;
|
|
101
|
+
}
|
|
102
|
+
.split-row {
|
|
103
|
+
display: grid;
|
|
104
|
+
grid-template-columns: 1fr 1fr;
|
|
105
|
+
gap: 16px;
|
|
106
|
+
margin-bottom: 16px;
|
|
107
|
+
}
|
|
108
|
+
.full-row { margin-bottom: 16px; }
|
|
109
|
+
@media (max-width: 1000px) {
|
|
110
|
+
.hero-row, .metrics-row, .split-row { grid-template-columns: 1fr; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── Panels ── */
|
|
114
|
+
.panel {
|
|
115
|
+
background: var(--bg2);
|
|
116
|
+
border: 1px solid var(--border);
|
|
117
|
+
border-radius: var(--radius);
|
|
118
|
+
padding: 16px;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
}
|
|
121
|
+
.panel h2 {
|
|
122
|
+
font-size: 14px;
|
|
123
|
+
color: var(--cyan);
|
|
124
|
+
margin-bottom: 12px;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
border-bottom: 1px solid var(--border);
|
|
127
|
+
padding-bottom: 8px;
|
|
128
|
+
}
|
|
129
|
+
.panel h3 {
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
color: var(--fg2);
|
|
132
|
+
margin: 12px 0 6px;
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
letter-spacing: 0.5px;
|
|
135
|
+
}
|
|
136
|
+
.kv { display: flex; justify-content: space-between; padding: 3px 0; }
|
|
137
|
+
.kv .label { color: var(--fg2); }
|
|
138
|
+
.kv .value { color: var(--fg); font-weight: 500; }
|
|
139
|
+
.kv .value.green { color: var(--green); }
|
|
140
|
+
.kv .value.yellow { color: var(--yellow); }
|
|
141
|
+
.kv .value.red { color: var(--red); }
|
|
142
|
+
.kv .value.cyan { color: var(--cyan); }
|
|
143
|
+
.kv .value.purple { color: var(--purple); }
|
|
144
|
+
.kv .value.orange { color: var(--orange); }
|
|
145
|
+
.table-wrap { overflow-x: auto; }
|
|
146
|
+
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
147
|
+
th {
|
|
148
|
+
text-align: left;
|
|
149
|
+
color: var(--fg2);
|
|
150
|
+
font-weight: 500;
|
|
151
|
+
padding: 4px 8px;
|
|
152
|
+
border-bottom: 1px solid var(--border);
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
}
|
|
155
|
+
td { padding: 4px 8px; border-bottom: 1px solid var(--bg3); }
|
|
156
|
+
.badge {
|
|
157
|
+
display: inline-block;
|
|
158
|
+
padding: 1px 6px;
|
|
159
|
+
border-radius: 3px;
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
}
|
|
163
|
+
.badge.required { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
164
|
+
.badge.recommended { background: rgba(210,153,34,0.15); color: var(--yellow); }
|
|
165
|
+
.badge.optional { background: rgba(72,79,88,0.15); color: var(--fg3); }
|
|
166
|
+
.badge.on { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
167
|
+
.badge.off { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
168
|
+
.badge.active { background: rgba(88,166,255,0.15); color: var(--cyan); }
|
|
169
|
+
.badge.idle { background: rgba(72,79,88,0.15); color: var(--fg3); }
|
|
170
|
+
.badge.droid { background: rgba(188,140,255,0.15); color: var(--purple); }
|
|
171
|
+
.badge.subagent { background: rgba(209,134,22,0.15); color: var(--orange); }
|
|
172
|
+
.badge.main { background: rgba(88,166,255,0.15); color: var(--cyan); }
|
|
173
|
+
.badge.file-only { background: rgba(72,79,88,0.15); color: var(--fg3); }
|
|
174
|
+
|
|
175
|
+
/* ── Bars ── */
|
|
176
|
+
.bar-container { display: flex; align-items: center; gap: 8px; margin: 3px 0; }
|
|
177
|
+
.bar-label { width: 80px; color: var(--fg2); font-size: 12px; }
|
|
178
|
+
.bar { height: 14px; border-radius: 3px; min-width: 2px; transition: width 0.3s; }
|
|
179
|
+
.bar.cyan { background: var(--cyan); }
|
|
180
|
+
.bar.green { background: var(--green); }
|
|
181
|
+
.bar.yellow { background: var(--yellow); }
|
|
182
|
+
.bar.red { background: var(--red); }
|
|
183
|
+
.bar.purple { background: var(--purple); }
|
|
184
|
+
.bar-value { color: var(--fg2); font-size: 11px; min-width: 20px; }
|
|
185
|
+
|
|
186
|
+
/* ── Charts ── */
|
|
187
|
+
.chart-container { position: relative; width: 100%; }
|
|
188
|
+
.chart-container .uplot { width: 100% !important; }
|
|
189
|
+
.chart-hero { height: 280px; }
|
|
190
|
+
.chart-spark { height: 80px; margin: 8px 0; }
|
|
191
|
+
|
|
192
|
+
/* uPlot dark theme overrides */
|
|
193
|
+
.uplot .u-title { color: var(--fg2) !important; font-size: 11px !important; }
|
|
194
|
+
.uplot .u-legend { font-size: 11px !important; }
|
|
195
|
+
.uplot .u-legend th { color: var(--fg2) !important; }
|
|
196
|
+
.uplot .u-legend td { color: var(--fg) !important; }
|
|
197
|
+
.uplot .u-series > * { padding: 2px 6px !important; }
|
|
198
|
+
.u-tooltip {
|
|
199
|
+
position: absolute;
|
|
200
|
+
z-index: 100;
|
|
201
|
+
background: var(--bg2);
|
|
202
|
+
border: 1px solid var(--border);
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
padding: 8px 12px;
|
|
205
|
+
font-size: 11px;
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
}
|
|
210
|
+
.u-tooltip .tt-time { color: var(--fg3); margin-bottom: 4px; font-size: 10px; }
|
|
211
|
+
.u-tooltip .tt-row { display: flex; gap: 8px; align-items: center; padding: 1px 0; }
|
|
212
|
+
.u-tooltip .tt-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
213
|
+
.u-tooltip .tt-label { color: var(--fg2); }
|
|
214
|
+
.u-tooltip .tt-val { color: var(--fg); font-weight: 600; margin-left: auto; }
|
|
215
|
+
|
|
216
|
+
/* ── Buttons ── */
|
|
217
|
+
.btn {
|
|
218
|
+
background: var(--bg3);
|
|
219
|
+
border: 1px solid var(--border);
|
|
220
|
+
color: var(--fg);
|
|
221
|
+
padding: 3px 8px;
|
|
222
|
+
border-radius: 4px;
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
font-size: 11px;
|
|
225
|
+
font-family: inherit;
|
|
226
|
+
}
|
|
227
|
+
.btn:hover { background: var(--border); }
|
|
228
|
+
.btn:focus-visible { outline: 2px solid var(--cyan); outline-offset: 1px; }
|
|
229
|
+
select {
|
|
230
|
+
background: var(--bg3);
|
|
231
|
+
border: 1px solid var(--border);
|
|
232
|
+
color: var(--fg);
|
|
233
|
+
padding: 2px 6px;
|
|
234
|
+
border-radius: 4px;
|
|
235
|
+
font-size: 11px;
|
|
236
|
+
font-family: inherit;
|
|
237
|
+
}
|
|
238
|
+
select:focus-visible { outline: 2px solid var(--cyan); outline-offset: 1px; }
|
|
239
|
+
|
|
240
|
+
/* ── Audit / Events ── */
|
|
241
|
+
.audit-row {
|
|
242
|
+
display: flex;
|
|
243
|
+
gap: 8px;
|
|
244
|
+
padding: 3px 0;
|
|
245
|
+
font-size: 12px;
|
|
246
|
+
border-bottom: 1px solid var(--bg3);
|
|
247
|
+
}
|
|
248
|
+
.audit-time { color: var(--fg3); width: 80px; flex-shrink: 0; font-size: 11px; }
|
|
249
|
+
.audit-icon { width: 40px; flex-shrink: 0; font-weight: 600; }
|
|
250
|
+
.audit-icon.pass { color: var(--green); }
|
|
251
|
+
.audit-icon.block { color: var(--red); }
|
|
252
|
+
.audit-op { color: var(--fg); flex: 1; }
|
|
253
|
+
.audit-policy { color: var(--fg3); width: 80px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
|
|
254
|
+
.audit-reason { color: var(--fg3); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
255
|
+
|
|
256
|
+
.event-feed { max-height: 200px; overflow-y: auto; }
|
|
257
|
+
.event-row {
|
|
258
|
+
display: flex;
|
|
259
|
+
gap: 6px;
|
|
260
|
+
padding: 2px 0;
|
|
261
|
+
font-size: 11px;
|
|
262
|
+
border-bottom: 1px solid var(--bg3);
|
|
263
|
+
animation: event-in 0.2s ease-out;
|
|
264
|
+
}
|
|
265
|
+
.event-row .event-cat {
|
|
266
|
+
padding: 0 4px;
|
|
267
|
+
border-radius: 3px;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
font-size: 10px;
|
|
270
|
+
text-transform: uppercase;
|
|
271
|
+
}
|
|
272
|
+
.event-row .event-time { color: var(--fg3); min-width: 60px; }
|
|
273
|
+
.event-row .event-msg { color: var(--fg2); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
274
|
+
@keyframes event-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
|
|
275
|
+
|
|
276
|
+
.cat-policy { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
277
|
+
.cat-memory { background: rgba(188,140,255,0.15); color: var(--purple); }
|
|
278
|
+
.cat-deploy { background: rgba(88,166,255,0.15); color: var(--cyan); }
|
|
279
|
+
.cat-agent { background: rgba(209,134,22,0.15); color: var(--orange); }
|
|
280
|
+
.cat-task { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
281
|
+
.cat-skill { background: rgba(210,153,34,0.15); color: var(--yellow); }
|
|
282
|
+
.cat-pattern { background: rgba(188,140,255,0.15); color: var(--purple); }
|
|
283
|
+
.cat-cost { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
284
|
+
.cat-system { background: rgba(72,79,88,0.3); color: var(--fg2); }
|
|
285
|
+
|
|
286
|
+
/* ── Gauge ── */
|
|
287
|
+
.gauge-track { background: var(--bg3); border-radius: 3px; height: 8px; margin: 6px 0; overflow: hidden; }
|
|
288
|
+
.gauge-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
|
|
289
|
+
|
|
290
|
+
/* ── Cost Savings ── */
|
|
291
|
+
.cost-savings {
|
|
292
|
+
background: rgba(63,185,80,0.08);
|
|
293
|
+
border: 1px solid rgba(63,185,80,0.2);
|
|
294
|
+
border-radius: var(--radius);
|
|
295
|
+
padding: 8px 12px;
|
|
296
|
+
margin-top: 8px;
|
|
297
|
+
}
|
|
298
|
+
.cost-savings .big-number { font-size: 20px; font-weight: 700; color: var(--green); }
|
|
299
|
+
.cost-savings .sub { font-size: 11px; color: var(--fg2); }
|
|
300
|
+
|
|
301
|
+
/* ── Agent rows ── */
|
|
302
|
+
.agent-row {
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
gap: 8px;
|
|
306
|
+
padding: 4px 0;
|
|
307
|
+
border-bottom: 1px solid var(--bg3);
|
|
308
|
+
font-size: 12px;
|
|
309
|
+
}
|
|
310
|
+
.agent-name { color: var(--fg); font-weight: 500; min-width: 100px; }
|
|
311
|
+
.agent-task { color: var(--fg2); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
312
|
+
.agent-tokens { color: var(--fg3); font-size: 11px; min-width: 60px; text-align: right; }
|
|
313
|
+
|
|
314
|
+
/* ── Chips ── */
|
|
315
|
+
.chip-list { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0; }
|
|
316
|
+
.chip {
|
|
317
|
+
display: inline-block;
|
|
318
|
+
padding: 2px 8px;
|
|
319
|
+
border-radius: 10px;
|
|
320
|
+
font-size: 11px;
|
|
321
|
+
border: 1px solid var(--border);
|
|
322
|
+
color: var(--fg2);
|
|
323
|
+
}
|
|
324
|
+
.chip.active { border-color: var(--cyan); color: var(--cyan); background: rgba(88,166,255,0.08); }
|
|
325
|
+
.chip.inactive { opacity: 0.5; }
|
|
326
|
+
|
|
327
|
+
/* ── Progress ── */
|
|
328
|
+
.progress-track { background: var(--bg3); border-radius: 3px; height: 6px; margin: 6px 0; overflow: hidden; }
|
|
329
|
+
.progress-fill { height: 100%; border-radius: 3px; background: var(--cyan); transition: width 0.3s; }
|
|
330
|
+
|
|
331
|
+
/* ── Kanban Board ── */
|
|
332
|
+
.kanban { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px; min-height: 200px; }
|
|
333
|
+
.kanban-col { flex: 1; min-width: 180px; max-width: 280px; display: flex; flex-direction: column; gap: 8px; }
|
|
334
|
+
.kanban-col-header {
|
|
335
|
+
display: flex; align-items: center; gap: 6px; padding: 6px 8px;
|
|
336
|
+
border-radius: 6px; font-size: 12px; font-weight: 600; background: var(--bg3);
|
|
337
|
+
}
|
|
338
|
+
.kanban-col-header .col-count { margin-left: auto; font-weight: 400; color: var(--fg3); font-size: 11px; }
|
|
339
|
+
.kanban-col-header.open { border-left: 3px solid var(--fg2); }
|
|
340
|
+
.kanban-col-header.in_progress { border-left: 3px solid var(--cyan); color: var(--cyan); }
|
|
341
|
+
.kanban-col-header.blocked { border-left: 3px solid var(--red); color: var(--red); }
|
|
342
|
+
.kanban-col-header.done { border-left: 3px solid var(--green); color: var(--green); }
|
|
343
|
+
.kanban-col-header.wont_do { border-left: 3px solid var(--fg3); color: var(--fg3); }
|
|
344
|
+
.kanban-cards { display: flex; flex-direction: column; gap: 6px; flex: 1; min-height: 40px; }
|
|
345
|
+
.kanban-card {
|
|
346
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
347
|
+
padding: 8px 10px; cursor: default;
|
|
348
|
+
transition: transform 0.2s ease, opacity 0.3s ease, box-shadow 0.2s ease;
|
|
349
|
+
animation: card-enter 0.35s ease-out;
|
|
350
|
+
}
|
|
351
|
+
.kanban-card:hover { border-color: var(--cyan); box-shadow: 0 0 8px rgba(88,166,255,0.15); transform: translateY(-1px); }
|
|
352
|
+
.kanban-card.card-exit { animation: card-exit 0.3s ease-in forwards; }
|
|
353
|
+
.kanban-card .card-id { font-size: 10px; color: var(--cyan); font-weight: 600; }
|
|
354
|
+
.kanban-card .card-title { font-size: 12px; color: var(--fg); margin: 3px 0; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
355
|
+
.kanban-card .card-meta { display: flex; align-items: center; gap: 6px; font-size: 10px; color: var(--fg3); }
|
|
356
|
+
.kanban-card .card-priority { padding: 0 4px; border-radius: 3px; font-weight: 600; font-size: 10px; }
|
|
357
|
+
.card-priority.p0 { background: rgba(248,81,73,0.2); color: var(--red); }
|
|
358
|
+
.card-priority.p1 { background: rgba(210,153,34,0.2); color: var(--yellow); }
|
|
359
|
+
.card-priority.p2 { background: rgba(88,166,255,0.15); color: var(--cyan); }
|
|
360
|
+
.card-priority.p3, .card-priority.p4 { background: rgba(72,79,88,0.2); color: var(--fg3); }
|
|
361
|
+
.kanban-empty { color: var(--fg3); font-size: 11px; font-style: italic; text-align: center; padding: 16px 4px; border: 1px dashed var(--border); border-radius: 6px; }
|
|
362
|
+
@keyframes card-enter { 0% { opacity: 0; transform: translateY(-12px) scale(0.95); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
|
|
363
|
+
@keyframes card-exit { 0% { opacity: 1; transform: translateX(0) scale(1); } 100% { opacity: 0; transform: translateX(30px) scale(0.9); } }
|
|
364
|
+
|
|
365
|
+
/* ── Toasts ── */
|
|
366
|
+
.toast-container { position: fixed; top: 16px; right: 16px; z-index: 1000; display: flex; flex-direction: column; gap: 8px; }
|
|
367
|
+
.toast { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 14px; font-size: 12px; color: var(--fg); animation: toast-in 0.2s ease-out; max-width: 320px; }
|
|
368
|
+
.toast.success { border-color: var(--green); color: var(--green); }
|
|
369
|
+
.toast.error { border-color: var(--red); color: var(--red); }
|
|
370
|
+
@keyframes toast-in { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
|
|
371
|
+
|
|
372
|
+
/* ── Collapsible ── */
|
|
373
|
+
.collapsible-header {
|
|
374
|
+
cursor: pointer;
|
|
375
|
+
user-select: none;
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
gap: 6px;
|
|
379
|
+
}
|
|
380
|
+
.collapsible-header::before {
|
|
381
|
+
content: '\25B6';
|
|
382
|
+
font-size: 10px;
|
|
383
|
+
color: var(--fg3);
|
|
384
|
+
transition: transform 0.2s;
|
|
385
|
+
}
|
|
386
|
+
.collapsible-header.open::before { transform: rotate(90deg); }
|
|
387
|
+
.collapsible-body { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
|
388
|
+
.collapsible-body.open { max-height: 600px; }
|
|
389
|
+
|
|
390
|
+
.empty { color: var(--fg3); font-style: italic; padding: 8px 0; }
|
|
391
|
+
</style>
|
|
392
|
+
</head>
|
|
393
|
+
<body>
|
|
394
|
+
<div class="toast-container" id="toast-container"></div>
|
|
395
|
+
<div class="container">
|
|
396
|
+
<header>
|
|
397
|
+
<h1>UAP Dashboard</h1>
|
|
398
|
+
<div class="header-center">
|
|
399
|
+
<span class="sys-badge"><span class="label">v</span><span class="val" id="sys-version">?</span></span>
|
|
400
|
+
<span class="sys-badge"><span class="label">branch </span><span class="val cyan" id="sys-branch">?</span></span>
|
|
401
|
+
<span class="sys-badge"><span class="label">dirty </span><span class="val" id="sys-dirty">?</span></span>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="header-right">
|
|
404
|
+
<span class="header-ts" id="data-ts"></span>
|
|
405
|
+
<div>
|
|
406
|
+
<span class="status-dot disconnected" id="ws-status" role="status" aria-label="Connection status: disconnected"></span>
|
|
407
|
+
<span class="refresh-info" id="refresh-info">Connecting...</span>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</header>
|
|
411
|
+
|
|
412
|
+
<!-- Hero Charts -->
|
|
413
|
+
<div class="hero-row">
|
|
414
|
+
<div class="panel">
|
|
415
|
+
<h2>Tasks & Agents</h2>
|
|
416
|
+
<div class="chart-container chart-hero" id="chart-tasks"></div>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="panel">
|
|
419
|
+
<h2>Compression & Memory</h2>
|
|
420
|
+
<div class="chart-container chart-hero" id="chart-compression"></div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<!-- Core Metrics -->
|
|
425
|
+
<div class="metrics-row">
|
|
426
|
+
<div class="panel">
|
|
427
|
+
<h2>Models & Routing</h2>
|
|
428
|
+
<div id="model-roles"></div>
|
|
429
|
+
<h3>Router</h3>
|
|
430
|
+
<div id="model-router"></div>
|
|
431
|
+
<h3>Cost Optimization</h3>
|
|
432
|
+
<div id="model-cost-opt"></div>
|
|
433
|
+
<h3>Routing Matrix</h3>
|
|
434
|
+
<div id="model-routing-matrix" class="table-wrap"></div>
|
|
435
|
+
<h3>Recent Routing Decisions</h3>
|
|
436
|
+
<div id="model-routes" class="table-wrap"><div class="empty">No routing decisions yet</div></div>
|
|
437
|
+
<h3>Session Usage by Model</h3>
|
|
438
|
+
<div id="model-usage" class="table-wrap"></div>
|
|
439
|
+
<div class="kv" style="margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
|
|
440
|
+
<span class="label" style="font-weight:600">Total Cost</span>
|
|
441
|
+
<span class="value green" id="model-cost">$0.0000</span>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div class="panel">
|
|
446
|
+
<h2>Memory</h2>
|
|
447
|
+
<div id="memory-panel">
|
|
448
|
+
<div class="kv"><span class="label">L1 Working</span><span class="value" id="mem-l1">-</span></div>
|
|
449
|
+
<div class="kv"><span class="label">L2 Session</span><span class="value" id="mem-l2">-</span></div>
|
|
450
|
+
<div class="kv"><span class="label">L3 Semantic</span><span class="value" id="mem-l3">-</span></div>
|
|
451
|
+
<div class="kv"><span class="label">L4 Knowledge</span><span class="value" id="mem-l4">-</span></div>
|
|
452
|
+
</div>
|
|
453
|
+
<h3>Memory Hit Rate</h3>
|
|
454
|
+
<div id="mem-hitrate-panel">
|
|
455
|
+
<div class="kv"><span class="label">Hits</span><span class="value green" id="mem-hits">0</span></div>
|
|
456
|
+
<div class="kv"><span class="label">Misses</span><span class="value red" id="mem-misses">0</span></div>
|
|
457
|
+
<div class="kv"><span class="label">Rate</span><span class="value" id="mem-hitrate">N/A</span></div>
|
|
458
|
+
<div class="gauge-track"><div class="gauge-fill" id="mem-hitrate-gauge" style="width:0%;background:var(--green)"></div></div>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="chart-container chart-spark" id="chart-hitrate"></div>
|
|
461
|
+
<h3>Context Compression</h3>
|
|
462
|
+
<div id="compression-panel">
|
|
463
|
+
<div class="kv"><span class="label">Raw</span><span class="value" id="comp-raw">0 KB</span></div>
|
|
464
|
+
<div class="kv"><span class="label">Compressed</span><span class="value" id="comp-ctx">0 KB</span></div>
|
|
465
|
+
<div class="kv"><span class="label">Savings</span><span class="value green" id="comp-savings">0%</span></div>
|
|
466
|
+
<div class="kv"><span class="label">Tool Calls</span><span class="value" id="comp-calls">0</span></div>
|
|
467
|
+
</div>
|
|
468
|
+
<h3>Recent Memory Queries</h3>
|
|
469
|
+
<div id="memory-recent-queries"><div class="empty">No recent queries</div></div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div class="panel">
|
|
473
|
+
<h2>Deploy Pipeline</h2>
|
|
474
|
+
<div id="deploy-buckets">
|
|
475
|
+
<div class="bar-container"><span class="bar-label">Queued</span><div class="bar yellow" id="deploy-queued" style="width:0%"></div><span class="bar-value" id="deploy-queued-count">0</span></div>
|
|
476
|
+
<div class="bar-container"><span class="bar-label">Batched</span><div class="bar purple" id="deploy-batched" style="width:0%"></div><span class="bar-value" id="deploy-batched-count">0</span></div>
|
|
477
|
+
<div class="bar-container"><span class="bar-label">Executing</span><div class="bar cyan" id="deploy-executing" style="width:0%"></div><span class="bar-value" id="deploy-executing-count">0</span></div>
|
|
478
|
+
<div class="bar-container"><span class="bar-label">Done</span><div class="bar green" id="deploy-done" style="width:0%"></div><span class="bar-value" id="deploy-done-count">0</span></div>
|
|
479
|
+
<div class="bar-container"><span class="bar-label">Failed</span><div class="bar red" id="deploy-failed" style="width:0%"></div><span class="bar-value" id="deploy-failed-count">0</span></div>
|
|
480
|
+
<div class="kv" style="margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
|
|
481
|
+
<span class="label">Batches</span><span class="value" id="deploy-batch-count">0</span>
|
|
482
|
+
</div>
|
|
483
|
+
<div class="kv"><span class="label">Saved Ops</span><span class="value green" id="deploy-saved-ops">0</span></div>
|
|
484
|
+
</div>
|
|
485
|
+
<h3>Deploy Trend</h3>
|
|
486
|
+
<div class="chart-container chart-spark" id="chart-deploy"></div>
|
|
487
|
+
<h3>Coordination</h3>
|
|
488
|
+
<div id="coord-panel">
|
|
489
|
+
<div class="kv"><span class="label">Active Agents</span><span class="value" id="coord-agents">0</span></div>
|
|
490
|
+
<div class="kv"><span class="label">Total Agents</span><span class="value" id="coord-total-agents">0</span></div>
|
|
491
|
+
<div class="kv"><span class="label">Completed</span><span class="value green" id="coord-completed-agents">0</span></div>
|
|
492
|
+
<div class="kv"><span class="label">Resource Claims</span><span class="value" id="coord-claims">0</span></div>
|
|
493
|
+
<div class="kv"><span class="label">Pending Deploys</span><span class="value" id="coord-deploys">0</span></div>
|
|
494
|
+
<div class="kv"><span class="label">Active Worktrees</span><span class="value cyan" id="coord-worktrees">0</span></div>
|
|
495
|
+
<div class="kv"><span class="label">Pattern Uses</span><span class="value" id="coord-pattern-hits">0</span></div>
|
|
496
|
+
<div class="kv"><span class="label">Pattern Successes</span><span class="value green" id="coord-pattern-successes">0</span></div>
|
|
497
|
+
</div>
|
|
498
|
+
<h3>Recent Agents</h3>
|
|
499
|
+
<div id="coord-agents-table" class="table-wrap"><div class="empty">No agents registered</div></div>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<!-- Session Telemetry -->
|
|
504
|
+
<div class="full-row" id="session-panel" style="display:none">
|
|
505
|
+
<div class="panel">
|
|
506
|
+
<h2>Session Telemetry <span style="font-size:11px;color:var(--fg3);font-weight:400" id="sess-label">(current)</span></h2>
|
|
507
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px">
|
|
508
|
+
<div>
|
|
509
|
+
<h3>Overview</h3>
|
|
510
|
+
<div class="kv"><span class="label">Session ID</span><span class="value cyan" id="sess-id">-</span></div>
|
|
511
|
+
<div class="kv"><span class="label">Uptime</span><span class="value" id="sess-uptime">-</span></div>
|
|
512
|
+
<div class="kv"><span class="label">Tokens Used</span><span class="value" id="sess-tokens">0</span></div>
|
|
513
|
+
<div class="kv"><span class="label">Tokens Saved</span><span class="value green" id="sess-saved">0</span></div>
|
|
514
|
+
<div class="kv"><span class="label">Tool Calls</span><span class="value" id="sess-tools">0</span></div>
|
|
515
|
+
<div class="kv"><span class="label">Policy Checks</span><span class="value" id="sess-checks">0</span></div>
|
|
516
|
+
<div class="kv"><span class="label">Policy Blocks</span><span class="value red" id="sess-blocks">0</span></div>
|
|
517
|
+
<div class="kv"><span class="label">Errors</span><span class="value red" id="sess-errors">0</span></div>
|
|
518
|
+
<div id="sess-token-io-detail"></div>
|
|
519
|
+
<div id="sess-progress-wrap" style="display:none">
|
|
520
|
+
<h3>Step Progress</h3>
|
|
521
|
+
<div class="kv"><span class="label" id="sess-step-name">-</span><span class="value" id="sess-step-count">0/0</span></div>
|
|
522
|
+
<div class="progress-track"><div class="progress-fill" id="sess-progress-bar" style="width:0%"></div></div>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
<div>
|
|
526
|
+
<h3>Cost</h3>
|
|
527
|
+
<div class="kv"><span class="label">Session Cost</span><span class="value" id="sess-cost">$0.0000</span></div>
|
|
528
|
+
<div class="kv"><span class="label">Without UAP</span><span class="value" id="sess-cost-without">$0.0000</span></div>
|
|
529
|
+
<div class="cost-savings" id="sess-savings-box">
|
|
530
|
+
<div class="big-number" id="sess-savings-pct">0%</div>
|
|
531
|
+
<div class="sub">estimated savings with UAP</div>
|
|
532
|
+
</div>
|
|
533
|
+
<h3>Agents</h3>
|
|
534
|
+
<div id="sess-agents"><div class="empty">No active agents</div></div>
|
|
535
|
+
<h3>Per-Agent Breakdown</h3>
|
|
536
|
+
<div id="sess-agent-breakdown" class="table-wrap"><div class="empty">No agent data</div></div>
|
|
537
|
+
</div>
|
|
538
|
+
<div>
|
|
539
|
+
<h3>Skills</h3>
|
|
540
|
+
<div id="sess-skills" class="chip-list"><span class="empty">No skills loaded</span></div>
|
|
541
|
+
<h3>Patterns</h3>
|
|
542
|
+
<div id="sess-patterns" class="chip-list"><span class="empty">No patterns matched</span></div>
|
|
543
|
+
<h3>Deploys</h3>
|
|
544
|
+
<div id="sess-deploys"><div class="empty">No deploys</div></div>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<!-- Compliance -->
|
|
551
|
+
<div class="split-row">
|
|
552
|
+
<div class="panel">
|
|
553
|
+
<h2>Policies</h2>
|
|
554
|
+
<div id="policy-table" class="table-wrap"></div>
|
|
555
|
+
<h3>Enforcement Stages</h3>
|
|
556
|
+
<div id="policy-stages"></div>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="panel">
|
|
559
|
+
<h2>Compliance & Audit</h2>
|
|
560
|
+
<h3>Block Rate Trend</h3>
|
|
561
|
+
<div class="chart-container chart-spark" id="chart-compliance"></div>
|
|
562
|
+
<h3>Failures by Mechanism</h3>
|
|
563
|
+
<div id="compliance-mechanisms"></div>
|
|
564
|
+
<h3>Recent Failures</h3>
|
|
565
|
+
<div id="compliance-failures" class="table-wrap"><div class="empty">No recent failures</div></div>
|
|
566
|
+
<h3>Audit Trail</h3>
|
|
567
|
+
<div id="audit-trail"></div>
|
|
568
|
+
<h3>Live Events</h3>
|
|
569
|
+
<div class="event-feed" id="live-events"><div class="empty">Connecting to event stream...</div></div>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
<!-- Session History -->
|
|
574
|
+
<div class="full-row" id="session-history-panel" style="display:none">
|
|
575
|
+
<div class="panel">
|
|
576
|
+
<h2>Session History</h2>
|
|
577
|
+
<div id="session-history-table" class="table-wrap"><div class="empty">No session history</div></div>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
<!-- Task Board -->
|
|
582
|
+
<div class="full-row" id="kanban-panel" style="display:none">
|
|
583
|
+
<div class="panel">
|
|
584
|
+
<h2>Task Board</h2>
|
|
585
|
+
<div class="kanban" id="kanban-board">
|
|
586
|
+
<div class="kanban-col" data-status="open"><div class="kanban-col-header open"><span>Open</span><span class="col-count" id="kb-open-count">0</span></div><div class="kanban-cards" id="kb-open"></div></div>
|
|
587
|
+
<div class="kanban-col" data-status="in_progress"><div class="kanban-col-header in_progress"><span>In Progress</span><span class="col-count" id="kb-progress-count">0</span></div><div class="kanban-cards" id="kb-progress"></div></div>
|
|
588
|
+
<div class="kanban-col" data-status="blocked"><div class="kanban-col-header blocked"><span>Blocked</span><span class="col-count" id="kb-blocked-count">0</span></div><div class="kanban-cards" id="kb-blocked"></div></div>
|
|
589
|
+
<div class="kanban-col" data-status="done"><div class="kanban-col-header done"><span>Done</span><span class="col-count" id="kb-done-count">0</span></div><div class="kanban-cards" id="kb-done"></div></div>
|
|
590
|
+
<div class="kanban-col" data-status="wont_do"><div class="kanban-col-header wont_do"><span>Won't Do</span><span class="col-count" id="kb-wontdo-count">0</span></div><div class="kanban-cards" id="kb-wontdo"></div></div>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
<!-- Performance (collapsible) -->
|
|
596
|
+
<div class="full-row">
|
|
597
|
+
<div class="panel">
|
|
598
|
+
<h2 class="collapsible-header" id="perf-toggle" onclick="toggleCollapsible('perf')">Performance</h2>
|
|
599
|
+
<div class="collapsible-body" id="perf-body">
|
|
600
|
+
<div id="perf-panel"><div class="empty">No performance data</div></div>
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<script>
|
|
607
|
+
const WS_URL = `ws://${location.host}`;
|
|
608
|
+
const API_URL = `http://${location.host}`;
|
|
609
|
+
let ws = null;
|
|
610
|
+
let reconnectTimer = null;
|
|
611
|
+
let reconnectDelay = 1000;
|
|
612
|
+
const MAX_RECONNECT_DELAY = 30000;
|
|
613
|
+
let chartTasks = null, chartCompression = null, chartDeploy = null, chartHitrate = null, chartCompliance = null;
|
|
614
|
+
const uplotReady = typeof uPlot !== 'undefined';
|
|
615
|
+
|
|
616
|
+
function showToast(message, type = 'info') {
|
|
617
|
+
const c = document.getElementById('toast-container');
|
|
618
|
+
const t = document.createElement('div');
|
|
619
|
+
t.className = 'toast ' + type;
|
|
620
|
+
t.textContent = message;
|
|
621
|
+
c.appendChild(t);
|
|
622
|
+
setTimeout(() => t.remove(), 3000);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function toggleCollapsible(id) {
|
|
626
|
+
document.getElementById(id + '-toggle').classList.toggle('open');
|
|
627
|
+
document.getElementById(id + '-body').classList.toggle('open');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ── WebSocket ──
|
|
631
|
+
function connect() {
|
|
632
|
+
clearTimeout(reconnectTimer);
|
|
633
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
634
|
+
ws = new WebSocket(WS_URL);
|
|
635
|
+
ws.onopen = () => {
|
|
636
|
+
reconnectDelay = 1000;
|
|
637
|
+
const dot = document.getElementById('ws-status');
|
|
638
|
+
dot.className = 'status-dot connected';
|
|
639
|
+
dot.setAttribute('aria-label', 'Connection status: live');
|
|
640
|
+
document.getElementById('refresh-info').textContent = 'Live';
|
|
641
|
+
document.title = 'UAP Dashboard - Live';
|
|
642
|
+
};
|
|
643
|
+
ws.onmessage = (e) => {
|
|
644
|
+
try { render(JSON.parse(e.data)); } catch (err) { console.error('Render error:', err); }
|
|
645
|
+
document.getElementById('refresh-info').textContent = 'Live - ' + new Date().toLocaleTimeString();
|
|
646
|
+
};
|
|
647
|
+
ws.onclose = () => {
|
|
648
|
+
document.getElementById('ws-status').className = 'status-dot disconnected';
|
|
649
|
+
document.getElementById('refresh-info').textContent = 'Disconnected - reconnecting...';
|
|
650
|
+
document.title = 'UAP Dashboard - Disconnected';
|
|
651
|
+
reconnectTimer = setTimeout(connect, reconnectDelay);
|
|
652
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
|
|
653
|
+
};
|
|
654
|
+
ws.onerror = () => ws.close();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ── SSE Event Stream ──
|
|
658
|
+
let eventSource = null;
|
|
659
|
+
const liveEvents = [];
|
|
660
|
+
function connectSSE() {
|
|
661
|
+
try {
|
|
662
|
+
eventSource = new EventSource(API_URL + '/api/events');
|
|
663
|
+
eventSource.onmessage = (e) => {
|
|
664
|
+
try {
|
|
665
|
+
const ev = JSON.parse(e.data);
|
|
666
|
+
liveEvents.unshift(ev);
|
|
667
|
+
if (liveEvents.length > 20) liveEvents.pop();
|
|
668
|
+
renderLiveEvents();
|
|
669
|
+
} catch {}
|
|
670
|
+
};
|
|
671
|
+
eventSource.onerror = () => { eventSource.close(); setTimeout(connectSSE, 5000); };
|
|
672
|
+
} catch {}
|
|
673
|
+
}
|
|
674
|
+
function renderLiveEvents() {
|
|
675
|
+
const c = document.getElementById('live-events');
|
|
676
|
+
if (!c || !liveEvents.length) return;
|
|
677
|
+
let html = '';
|
|
678
|
+
for (const ev of liveEvents) {
|
|
679
|
+
const cat = ev.category || ev.type || 'system';
|
|
680
|
+
const ts = ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString() : '';
|
|
681
|
+
const msg = ev.message || ev.detail || JSON.stringify(ev.data || '').slice(0, 80);
|
|
682
|
+
html += `<div class="event-row"><span class="event-time">${esc(ts)}</span><span class="event-cat cat-${esc(cat)}">${esc(cat)}</span><span class="event-msg">${esc(msg)}</span></div>`;
|
|
683
|
+
}
|
|
684
|
+
c.innerHTML = html;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ══════════════════════════════
|
|
688
|
+
// ── CHARTS (uPlot) ──
|
|
689
|
+
// ══════════════════════════════
|
|
690
|
+
|
|
691
|
+
const CC = {
|
|
692
|
+
done:'#3fb950', inProg:'#58a6ff', blocked:'#f85149', open:'#484f58', agents:'#bc8cff',
|
|
693
|
+
raw:'#58a6ff', ctx:'#3fb950', hitRate:'#bc8cff', deploy:'#58a6ff', blockRate:'#f85149',
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
function tsUnix(arr) { return arr.map(p => new Date(p.timestamp).getTime() / 1000); }
|
|
697
|
+
|
|
698
|
+
function addTooltip(u, colors, labels, fmtVal) {
|
|
699
|
+
const tt = document.createElement('div');
|
|
700
|
+
tt.className = 'u-tooltip'; tt.style.display = 'none';
|
|
701
|
+
u.root.appendChild(tt);
|
|
702
|
+
u.hooks.setCursor = u.hooks.setCursor || [];
|
|
703
|
+
u.hooks.setCursor.push(() => {
|
|
704
|
+
const { idx, left, top } = u.cursor;
|
|
705
|
+
if (idx == null) { tt.style.display = 'none'; return; }
|
|
706
|
+
const date = new Date(u.data[0][idx] * 1000);
|
|
707
|
+
let rows = `<div class="tt-time">${date.toLocaleTimeString()}</div>`;
|
|
708
|
+
for (let i = 1; i < u.series.length; i++) {
|
|
709
|
+
if (!u.series[i].show) continue;
|
|
710
|
+
const val = u.data[i][idx];
|
|
711
|
+
if (val == null) continue;
|
|
712
|
+
const c = colors[i-1] || '#fff', l = labels[i-1] || '';
|
|
713
|
+
const v = fmtVal ? fmtVal(val, i) : val.toLocaleString();
|
|
714
|
+
rows += `<div class="tt-row"><span class="tt-dot" style="background:${c}"></span><span class="tt-label">${l}</span><span class="tt-val">${v}</span></div>`;
|
|
715
|
+
}
|
|
716
|
+
tt.innerHTML = rows; tt.style.display = '';
|
|
717
|
+
tt.style.left = (left + 16) + 'px'; tt.style.top = Math.max(0, top - 10) + 'px';
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function mkChart(elId, ts, extractors, opts) {
|
|
722
|
+
if (!uplotReady || !ts || ts.length < 2) return null;
|
|
723
|
+
const el = document.getElementById(elId);
|
|
724
|
+
if (!el) return null;
|
|
725
|
+
el.innerHTML = '';
|
|
726
|
+
const timestamps = tsUnix(ts);
|
|
727
|
+
const data = [timestamps, ...extractors.map(fn => ts.map(fn))];
|
|
728
|
+
const u = new uPlot(opts(el.clientWidth), data, el);
|
|
729
|
+
return u;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function initChartTasks(ts) {
|
|
733
|
+
const colors = [CC.done, CC.inProg, CC.blocked, CC.open, CC.agents];
|
|
734
|
+
const labels = ['Done', 'In Progress', 'Blocked', 'Open', 'Active Agents'];
|
|
735
|
+
const extractors = [
|
|
736
|
+
p => p.tasks?.done || 0, p => p.tasks?.inProgress || 0,
|
|
737
|
+
p => p.tasks?.blocked || 0, p => p.tasks?.open || 0,
|
|
738
|
+
p => p.coordination?.activeAgents || 0,
|
|
739
|
+
];
|
|
740
|
+
const u = mkChart('chart-tasks', ts, extractors, (w) => ({
|
|
741
|
+
width: w, height: 260,
|
|
742
|
+
cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
|
|
743
|
+
scales: { x: { time: true }, y: { min: 0 }, agents: { min: 0 } },
|
|
744
|
+
axes: [
|
|
745
|
+
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' },
|
|
746
|
+
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace', size: 50 },
|
|
747
|
+
{ stroke: CC.agents, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'agents' },
|
|
748
|
+
],
|
|
749
|
+
series: [
|
|
750
|
+
{},
|
|
751
|
+
{ label: 'Done', stroke: CC.done, fill: CC.done+'30', width: 2 },
|
|
752
|
+
{ label: 'In Prog', stroke: CC.inProg, fill: CC.inProg+'30', width: 2 },
|
|
753
|
+
{ label: 'Blocked', stroke: CC.blocked, fill: CC.blocked+'20', width: 2 },
|
|
754
|
+
{ label: 'Open', stroke: CC.open, fill: CC.open+'20', width: 1, dash: [4,2] },
|
|
755
|
+
{ label: 'Agents', stroke: CC.agents, width: 2, dash: [6,3], scale: 'agents' },
|
|
756
|
+
],
|
|
757
|
+
}));
|
|
758
|
+
if (u) addTooltip(u, colors, labels);
|
|
759
|
+
return u;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function initChartCompression(ts) {
|
|
763
|
+
const colors = [CC.raw, CC.ctx, CC.hitRate];
|
|
764
|
+
const labels = ['Raw KB', 'Compressed KB', 'Hit Rate %'];
|
|
765
|
+
const parseHR = p => { const h = p.memoryHitsMisses?.hitRate; return typeof h === 'string' ? parseFloat(h)||0 : h||0; };
|
|
766
|
+
const extractors = [
|
|
767
|
+
p => Math.round((p.compression?.rawBytes||0)/1024),
|
|
768
|
+
p => Math.round((p.compression?.contextBytes||0)/1024),
|
|
769
|
+
parseHR,
|
|
770
|
+
];
|
|
771
|
+
const u = mkChart('chart-compression', ts, extractors, (w) => ({
|
|
772
|
+
width: w, height: 260,
|
|
773
|
+
cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
|
|
774
|
+
scales: { x: { time: true }, y: { min: 0 }, pct: { min: 0, max: 100 } },
|
|
775
|
+
axes: [
|
|
776
|
+
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' },
|
|
777
|
+
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace', size: 50, values: (_,v) => v.map(x => x+' KB') },
|
|
778
|
+
{ stroke: CC.hitRate, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'pct', values: (_,v) => v.map(x => x+'%') },
|
|
779
|
+
],
|
|
780
|
+
series: [
|
|
781
|
+
{},
|
|
782
|
+
{ label: 'Raw', stroke: CC.raw, fill: CC.raw+'20', width: 2 },
|
|
783
|
+
{ label: 'Compressed', stroke: CC.ctx, fill: CC.ctx+'20', width: 2 },
|
|
784
|
+
{ label: 'Hit Rate', stroke: CC.hitRate, width: 2, dash: [6,3], scale: 'pct' },
|
|
785
|
+
],
|
|
786
|
+
}));
|
|
787
|
+
if (u) addTooltip(u, colors, labels, (v,i) => i===3 ? v+'%' : v+' KB');
|
|
788
|
+
return u;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function initSparkline(elId, ts, fn, color, label) {
|
|
792
|
+
if (!uplotReady || !ts || ts.length < 2) return null;
|
|
793
|
+
const el = document.getElementById(elId);
|
|
794
|
+
if (!el) return null;
|
|
795
|
+
el.innerHTML = '';
|
|
796
|
+
const timestamps = tsUnix(ts);
|
|
797
|
+
const values = ts.map(fn);
|
|
798
|
+
const data = [timestamps, values];
|
|
799
|
+
const u = new uPlot({
|
|
800
|
+
width: el.clientWidth, height: 60,
|
|
801
|
+
cursor: { sync: { key: 'uap' }, focus: { prox: 30 }, points: { show: false } },
|
|
802
|
+
legend: { show: false },
|
|
803
|
+
scales: { x: { time: true }, y: { min: 0 } },
|
|
804
|
+
axes: [{ show: false }, { show: false }],
|
|
805
|
+
series: [{}, { stroke: color, fill: color+'25', width: 1.5 }],
|
|
806
|
+
}, data, el);
|
|
807
|
+
// Sparkline tooltip
|
|
808
|
+
const tt = document.createElement('div');
|
|
809
|
+
tt.className = 'u-tooltip'; tt.style.display = 'none';
|
|
810
|
+
u.root.appendChild(tt);
|
|
811
|
+
u.hooks.setCursor = u.hooks.setCursor || [];
|
|
812
|
+
u.hooks.setCursor.push(() => {
|
|
813
|
+
const { idx } = u.cursor;
|
|
814
|
+
if (idx == null) { tt.style.display = 'none'; return; }
|
|
815
|
+
tt.innerHTML = `<div class="tt-time">${new Date(data[0][idx]*1000).toLocaleTimeString()}</div><div class="tt-row"><span class="tt-label">${label}</span><span class="tt-val">${typeof values[idx]==='number'?values[idx].toLocaleString():values[idx]}</span></div>`;
|
|
816
|
+
tt.style.display = ''; tt.style.left = (u.cursor.left+12)+'px'; tt.style.top = '0px';
|
|
817
|
+
});
|
|
818
|
+
return u;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let chartsInit = false;
|
|
822
|
+
// Seed each uPlot legend to the LATEST data point so the values read as
|
|
823
|
+
// live numbers instead of '--' until the user hovers. Fail-safe per chart.
|
|
824
|
+
function seedLegends() {
|
|
825
|
+
[chartTasks, chartCompression, chartDeploy, chartHitrate, chartCompliance].forEach(u => {
|
|
826
|
+
try { if (u && u.data && u.data[0] && u.data[0].length) u.setLegend({ idx: u.data[0].length - 1 }); } catch (e) { /* legend seed best-effort */ }
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
function updateAllCharts(ts) {
|
|
830
|
+
if (!uplotReady || !ts || ts.length < 2) return;
|
|
831
|
+
const parseHR = p => { const h=p.memoryHitsMisses?.hitRate; return typeof h==='string'?parseFloat(h)||0:h||0; };
|
|
832
|
+
const parseBR = p => { const b=p.compliance?.blockRate; return typeof b==='string'?parseFloat(b)||0:b||0; };
|
|
833
|
+
if (!chartsInit) {
|
|
834
|
+
chartTasks = initChartTasks(ts);
|
|
835
|
+
chartCompression = initChartCompression(ts);
|
|
836
|
+
chartDeploy = initSparkline('chart-deploy', ts, p=>(p.deployBuckets?.queued||0)+(p.deployBuckets?.executing||0)+(p.deployBuckets?.batched||0), CC.deploy, 'Active Deploys');
|
|
837
|
+
chartHitrate = initSparkline('chart-hitrate', ts, parseHR, CC.hitRate, 'Hit Rate %');
|
|
838
|
+
chartCompliance = initSparkline('chart-compliance', ts, parseBR, CC.blockRate, 'Block Rate %');
|
|
839
|
+
chartsInit = true;
|
|
840
|
+
seedLegends();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// Update data
|
|
844
|
+
const timestamps = tsUnix(ts);
|
|
845
|
+
if (chartTasks) chartTasks.setData([timestamps, ts.map(p=>p.tasks?.done||0), ts.map(p=>p.tasks?.inProgress||0), ts.map(p=>p.tasks?.blocked||0), ts.map(p=>p.tasks?.open||0), ts.map(p=>p.coordination?.activeAgents||0)]);
|
|
846
|
+
if (chartCompression) chartCompression.setData([timestamps, ts.map(p=>Math.round((p.compression?.rawBytes||0)/1024)), ts.map(p=>Math.round((p.compression?.contextBytes||0)/1024)), ts.map(parseHR)]);
|
|
847
|
+
if (chartDeploy) chartDeploy.setData([timestamps, ts.map(p=>(p.deployBuckets?.queued||0)+(p.deployBuckets?.executing||0)+(p.deployBuckets?.batched||0))]);
|
|
848
|
+
if (chartHitrate) chartHitrate.setData([timestamps, ts.map(parseHR)]);
|
|
849
|
+
if (chartCompliance) chartCompliance.setData([timestamps, ts.map(parseBR)]);
|
|
850
|
+
seedLegends();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ══════════════════════════════
|
|
854
|
+
// ── MAIN RENDER ──
|
|
855
|
+
// ══════════════════════════════
|
|
856
|
+
|
|
857
|
+
function render(data) {
|
|
858
|
+
if (!data) return;
|
|
859
|
+
const _errors = [];
|
|
860
|
+
function safe(label, fn) { try { fn(); } catch(e) { _errors.push(label+': '+(e.message||e)); console.error('Render ['+label+']:', e); } }
|
|
861
|
+
|
|
862
|
+
const sys = data.system || {};
|
|
863
|
+
const tasks = data.tasks || {};
|
|
864
|
+
const coord = data.coordination || {};
|
|
865
|
+
const models = data.models || { roles:{}, sessionUsage:[], totalCost:0, strategy:'unknown' };
|
|
866
|
+
const mem = data.memory || {};
|
|
867
|
+
const l1 = mem.l1 || { entries:0, sizeKB:0 };
|
|
868
|
+
const l2 = mem.l2 || { entries:0 };
|
|
869
|
+
const l3 = mem.l3 || { status:'Unknown', uptime:'' };
|
|
870
|
+
const l4 = mem.l4 || { entities:0, relationships:0 };
|
|
871
|
+
const comp = mem.compression || { rawBytes:0, contextBytes:0, savingsPercent:'0%', totalCalls:0 };
|
|
872
|
+
const hm = mem.hitsMisses || { hits:0, misses:0, hitRate:'N/A' };
|
|
873
|
+
const policies = data.policies || [];
|
|
874
|
+
const policyFiles = data.policyFiles || [];
|
|
875
|
+
const audit = data.auditTrail || [];
|
|
876
|
+
const timeSeries = data.timeSeries || [];
|
|
877
|
+
const compliance = data.compliance || {};
|
|
878
|
+
const session = data.session || null;
|
|
879
|
+
|
|
880
|
+
safe('timestamp', () => { if (data.timestamp) setText('data-ts', new Date(data.timestamp).toLocaleTimeString()); });
|
|
881
|
+
|
|
882
|
+
safe('system', () => {
|
|
883
|
+
setText('sys-version', sys.version || '?');
|
|
884
|
+
setText('sys-branch', sys.branch || '?');
|
|
885
|
+
const el = document.getElementById('sys-dirty');
|
|
886
|
+
if (el) { const d = sys.dirty||0; el.textContent = d > 0 ? d+' files' : 'clean'; el.className = 'val '+(d > 0 ? 'yellow' : 'green'); }
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
safe('charts', () => updateAllCharts(timeSeries));
|
|
890
|
+
|
|
891
|
+
safe('coordination', () => {
|
|
892
|
+
setText('coord-agents', coord.activeAgents||0);
|
|
893
|
+
setText('coord-total-agents', coord.totalAgents||0);
|
|
894
|
+
setText('coord-completed-agents', coord.completedAgents||0);
|
|
895
|
+
setText('coord-claims', coord.activeClaims||0);
|
|
896
|
+
setText('coord-deploys', coord.pendingDeploys||0);
|
|
897
|
+
setText('coord-worktrees', coord.activeWorktrees||0);
|
|
898
|
+
setText('coord-pattern-hits', coord.patternHits||0);
|
|
899
|
+
setText('coord-pattern-successes', coord.patternSuccesses||0);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
safe('agents-table', () => {
|
|
903
|
+
const agents = coord.agents || [];
|
|
904
|
+
if (agents.length > 0) {
|
|
905
|
+
let h = '<table><tr><th>ID</th><th>Name</th><th>Status</th><th>Started</th></tr>';
|
|
906
|
+
for (const a of agents) {
|
|
907
|
+
const cls = a.status==='active'?'active':a.status==='completed'?'on':'idle';
|
|
908
|
+
h += `<tr><td style="color:var(--fg3);font-size:11px">${esc((a.id||'').slice(0,16))}</td><td>${esc(a.name||'-')}</td><td><span class="badge ${cls}">${esc(a.status||'-')}</span></td><td style="color:var(--fg3)">${esc(a.startedAt?a.startedAt.slice(0,19).replace('T',' '):'-')}</td></tr>`;
|
|
909
|
+
}
|
|
910
|
+
document.getElementById('coord-agents-table').innerHTML = h + '</table>';
|
|
911
|
+
} else { document.getElementById('coord-agents-table').innerHTML = '<div class="empty">No agents registered</div>'; }
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
safe('deploy', () => {
|
|
915
|
+
const d = data.deployBuckets || {};
|
|
916
|
+
const total = d.totalActions || (d.done||0)+(d.failed||0)+(d.queued||0)+(d.batched||0)+(d.executing||0) || 1;
|
|
917
|
+
const max = Math.max(total, 1);
|
|
918
|
+
const setBar = (id, val) => { const el=document.getElementById(id),ce=document.getElementById(id+'-count'); if(el)el.style.width=Math.min(Math.round((val/max)*100),100)+'%'; if(ce)ce.textContent=val; };
|
|
919
|
+
setBar('deploy-queued',d.queued||0); setBar('deploy-batched',d.batched||0);
|
|
920
|
+
setBar('deploy-executing',d.executing||0); setBar('deploy-done',d.done||0); setBar('deploy-failed',d.failed||0);
|
|
921
|
+
setText('deploy-batch-count',d.batchCount||0); setText('deploy-saved-ops',d.savedOps||0);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
safe('models', () => {
|
|
925
|
+
// Roles
|
|
926
|
+
let rh = '';
|
|
927
|
+
for (const [role, model] of Object.entries(models.roles||{})) {
|
|
928
|
+
const c = role==='planner'?'green':role==='executor'?'cyan':role==='reviewer'?'purple':'';
|
|
929
|
+
rh += `<div class="kv"><span class="label">${esc(capitalize(role))}</span><span class="value ${c}">${esc(modelName(String(model)))}</span></div>`;
|
|
930
|
+
}
|
|
931
|
+
document.getElementById('model-roles').innerHTML = rh;
|
|
932
|
+
|
|
933
|
+
// Router
|
|
934
|
+
document.getElementById('model-router').innerHTML = `<div class="kv"><span class="label">Strategy</span><span class="value cyan">${esc(models.strategy||'unknown')}</span></div><div class="kv"><span class="label">Available</span><span class="value">${models.availableModels?models.availableModels.length:0}</span></div><div class="kv"><span class="label">Enabled</span><span class="value ${models.enabled?'green':'yellow'}">${models.enabled?'Yes':'No'}</span></div>`;
|
|
935
|
+
|
|
936
|
+
// Cost Optimization
|
|
937
|
+
const co = models.costOptimization || {};
|
|
938
|
+
document.getElementById('model-cost-opt').innerHTML = `<div class="kv"><span class="label">Enabled</span><span class="value ${co.enabled?'green':'yellow'}">${co.enabled?'Yes':'No'}</span></div><div class="kv"><span class="label">Target Reduction</span><span class="value">${co.targetReduction?(co.targetReduction*100).toFixed(0)+'%':'N/A'}</span></div><div class="kv"><span class="label">Max Degradation</span><span class="value">${co.maxPerformanceDegradation?(co.maxPerformanceDegradation*100).toFixed(0)+'%':'N/A'}</span></div>`;
|
|
939
|
+
|
|
940
|
+
// Routing Matrix
|
|
941
|
+
const mx = Object.entries(models.routingMatrix||{});
|
|
942
|
+
if (mx.length > 0) {
|
|
943
|
+
let h = '<table><tr><th>Task Type</th><th>Planner</th><th>Executor</th></tr>';
|
|
944
|
+
for (const [tt, m] of mx) h += `<tr><td>${esc(tt)}</td><td style="color:var(--green)">${esc(modelName(m.planner||'-'))}</td><td style="color:var(--cyan)">${esc(modelName(m.executor||'-'))}</td></tr>`;
|
|
945
|
+
document.getElementById('model-routing-matrix').innerHTML = h+'</table>';
|
|
946
|
+
} else { document.getElementById('model-routing-matrix').innerHTML = '<div class="empty">No routing matrix</div>'; }
|
|
947
|
+
|
|
948
|
+
// Routing Decisions
|
|
949
|
+
const rd = models.recentRoutingDecisions || [];
|
|
950
|
+
if (rd.length > 0) {
|
|
951
|
+
let h = '<table><tr><th>Time</th><th>Model</th><th>Task</th><th>Tokens</th><th>Cost</th><th>Result</th></tr>';
|
|
952
|
+
for (const r of rd.slice(0,10)) {
|
|
953
|
+
const t = r.timestamp?r.timestamp.slice(11,19):'-';
|
|
954
|
+
h += `<tr><td style="color:var(--fg3);font-size:11px">${esc(t)}</td><td style="color:var(--purple)">${esc(modelName(r.modelUsed))}</td><td>${esc(r.taskType||'?')}</td><td>${fmt((r.tokensIn||0)+(r.tokensOut||0))}</td><td style="color:var(--green)">${typeof r.cost==='number'?'$'+r.cost.toFixed(4):'$0'}</td><td><span class="value ${r.success?'green':'red'}">${r.success?'OK':'FAIL'}</span></td></tr>`;
|
|
955
|
+
}
|
|
956
|
+
document.getElementById('model-routes').innerHTML = h+'</table>';
|
|
957
|
+
} else { document.getElementById('model-routes').innerHTML = '<div class="empty">No routing decisions yet</div>'; }
|
|
958
|
+
|
|
959
|
+
// Usage table
|
|
960
|
+
const usage = models.sessionUsage || [];
|
|
961
|
+
if (usage.length > 0) {
|
|
962
|
+
let h = '<table><tr><th>Model</th><th>Tasks</th><th>In</th><th>Out</th><th>Cost</th><th>Success</th></tr>';
|
|
963
|
+
for (const u of usage) {
|
|
964
|
+
const rate = typeof u.successRate==='number'?(u.successRate*100).toFixed(0):'0';
|
|
965
|
+
h += `<tr><td style="color:var(--cyan)">${esc(modelName(u.modelId))}</td><td>${u.taskCount||0}</td><td>${fmt(u.totalTokensIn||0)}</td><td>${fmt(u.totalTokensOut||0)}</td><td>$${(u.totalCost||0).toFixed(4)}</td><td><span class="value ${rate>=90?'green':rate>=70?'yellow':'red'}">${rate}%</span></td></tr>`;
|
|
966
|
+
}
|
|
967
|
+
document.getElementById('model-usage').innerHTML = h+'</table>';
|
|
968
|
+
} else { document.getElementById('model-usage').innerHTML = '<div class="empty">No usage recorded</div>'; }
|
|
969
|
+
setText('model-cost', '$'+(typeof models.totalCost==='number'?models.totalCost.toFixed(4):'0.0000'));
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
safe('memory', () => {
|
|
973
|
+
setText('mem-l1', l1.entries+' entries ('+l1.sizeKB+' KB)');
|
|
974
|
+
setText('mem-l2', l2.entries+' entries');
|
|
975
|
+
const l3El = document.getElementById('mem-l3');
|
|
976
|
+
if (l3El) { l3El.textContent = l3.status==='Running'?'Qdrant '+(l3.uptime||''):l3.status||'Stopped'; l3El.className = 'value '+(l3.status==='Running'?'green':'yellow'); }
|
|
977
|
+
setText('mem-l4', l4.entities+' entities, '+l4.relationships+' rels');
|
|
978
|
+
setText('mem-hits', hm.hits||0);
|
|
979
|
+
setText('mem-misses', hm.misses||0);
|
|
980
|
+
const hrStr = String(hm.hitRate||'N/A');
|
|
981
|
+
const hrVal = parseFloat(hrStr)||0;
|
|
982
|
+
setText('mem-hitrate', hrStr.includes('%')?hrStr:hrStr==='N/A'?hrStr:hrStr+'%');
|
|
983
|
+
const g = document.getElementById('mem-hitrate-gauge');
|
|
984
|
+
if (g) { g.style.width = Math.min(hrVal,100)+'%'; g.style.background = hrVal>80?'var(--green)':hrVal>50?'var(--yellow)':'var(--red)'; }
|
|
985
|
+
const hrEl = document.getElementById('mem-hitrate');
|
|
986
|
+
if (hrEl) hrEl.className = 'value '+(hrVal>80?'green':hrVal>50?'yellow':'red');
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
safe('compression', () => {
|
|
990
|
+
setText('comp-raw', Math.round((comp.rawBytes||0)/1024)+' KB');
|
|
991
|
+
setText('comp-ctx', Math.round((comp.contextBytes||0)/1024)+' KB');
|
|
992
|
+
setText('comp-savings', comp.savingsPercent||'0%');
|
|
993
|
+
setText('comp-calls', comp.totalCalls||0);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
safe('memory-queries', () => {
|
|
997
|
+
const rq = mem.recentQueries || [];
|
|
998
|
+
if (rq.length > 0) {
|
|
999
|
+
let h = '<table><tr><th>Type</th><th>Query</th><th>Time</th></tr>';
|
|
1000
|
+
for (const q of rq.slice(0,8)) h += `<tr><td><span class="badge active">${esc(q.type||'memory')}</span></td><td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(q.query||'-')}</td><td style="color:var(--fg3);font-size:11px">${esc(q.timestamp?q.timestamp.slice(11,19):'-')}</td></tr>`;
|
|
1001
|
+
document.getElementById('memory-recent-queries').innerHTML = h+'</table>';
|
|
1002
|
+
} else { document.getElementById('memory-recent-queries').innerHTML = '<div class="empty">No recent queries</div>'; }
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
safe('session', () => {
|
|
1006
|
+
const sp = document.getElementById('session-panel');
|
|
1007
|
+
if (!session) { sp.style.display = 'none'; return; }
|
|
1008
|
+
sp.style.display = '';
|
|
1009
|
+
setText('sess-id', (session.sessionId||'').slice(0,12));
|
|
1010
|
+
setText('sess-uptime', session.uptime||'-');
|
|
1011
|
+
setText('sess-tokens', fmt(session.tokensUsed||0));
|
|
1012
|
+
setText('sess-saved', fmt(session.tokensSaved||0));
|
|
1013
|
+
setText('sess-tools', session.toolCalls||0);
|
|
1014
|
+
setText('sess-checks', session.policyChecks||0);
|
|
1015
|
+
setText('sess-blocks', session.policyBlocks||0);
|
|
1016
|
+
setText('sess-errors', session.errors||0);
|
|
1017
|
+
const td = document.getElementById('sess-token-io-detail');
|
|
1018
|
+
if (td) td.innerHTML = (session.tokensIn>0||session.tokensOut>0) ? `<div class="kv"><span class="label">Tokens In</span><span class="value cyan">${fmt(session.tokensIn||0)}</span></div><div class="kv"><span class="label">Tokens Out</span><span class="value green">${fmt(session.tokensOut||0)}</span></div>` : '';
|
|
1019
|
+
setText('sess-cost', '$'+(typeof session.totalCostUsd==='number'?session.totalCostUsd.toFixed(4):'0.0000'));
|
|
1020
|
+
setText('sess-cost-without', '$'+(typeof session.estimatedCostWithoutUap==='number'?session.estimatedCostWithoutUap.toFixed(4):'0.0000'));
|
|
1021
|
+
setText('sess-savings-pct', typeof session.costSavingsPercent==='number'?session.costSavingsPercent.toFixed(0)+'%':'0%');
|
|
1022
|
+
const pw = document.getElementById('sess-progress-wrap');
|
|
1023
|
+
if (session.stepsTotal>0) { pw.style.display=''; setText('sess-step-name',session.currentStep||'Working...'); setText('sess-step-count',(session.stepsCompleted||0)+'/'+session.stepsTotal); document.getElementById('sess-progress-bar').style.width=Math.round(((session.stepsCompleted||0)/session.stepsTotal)*100)+'%'; }
|
|
1024
|
+
else { pw.style.display='none'; }
|
|
1025
|
+
|
|
1026
|
+
const agents = session.agents||[];
|
|
1027
|
+
if (agents.length > 0) {
|
|
1028
|
+
let ah=''; for (const a of agents) { const ml=a.model&&a.model!=='unknown'?' <span style="color:var(--purple);font-size:11px">'+esc(modelName(a.model))+'</span>':''; ah+=`<div class="agent-row"><span class="badge ${a.type||'main'}">${esc(a.type||'agent')}</span><span class="agent-name">${esc(a.name||a.id||'?')}${ml}</span><span class="badge ${a.status==='active'?'active':'idle'}">${esc(a.status||'idle')}</span><span class="agent-task">${esc(a.task||'-')}</span><span class="agent-tokens">${fmt(a.tokensUsed||0)} tok</span></div>`; }
|
|
1029
|
+
document.getElementById('sess-agents').innerHTML = ah;
|
|
1030
|
+
let bh='<table><tr><th>Agent</th><th>Model</th><th>Status</th><th>In</th><th>Out</th><th>Total</th><th>Cost</th></tr>';
|
|
1031
|
+
for (const a of agents) { const tI=a.tokensIn||0,tO=a.tokensOut||0,tT=tI+tO||a.tokensUsed||0; bh+=`<tr><td style="font-weight:500">${esc(a.name||a.id||'?')}</td><td style="color:var(--purple)">${esc(modelName(a.model||'?'))}</td><td><span class="badge ${a.status==='active'?'active':a.status==='done'?'on':'idle'}">${esc(a.status||'idle')}</span></td><td>${fmt(tI)}</td><td>${fmt(tO)}</td><td style="font-weight:500">${fmt(tT)}</td><td style="color:var(--green)">${typeof a.cost==='number'?'$'+a.cost.toFixed(4):'$0.0000'}</td></tr>`; }
|
|
1032
|
+
const tI=agents.reduce((s,a)=>s+(a.tokensIn||0),0),tO=agents.reduce((s,a)=>s+(a.tokensOut||0),0),tA=tI+tO||agents.reduce((s,a)=>s+(a.tokensUsed||0),0),tC=agents.reduce((s,a)=>s+(a.cost||0),0);
|
|
1033
|
+
bh+=`<tr style="border-top:2px solid var(--border);font-weight:600"><td>TOTAL</td><td></td><td></td><td style="color:var(--cyan)">${fmt(tI)}</td><td style="color:var(--green)">${fmt(tO)}</td><td style="color:var(--purple)">${fmt(tA)}</td><td style="color:var(--green)">$${tC.toFixed(4)}</td></tr></table>`;
|
|
1034
|
+
document.getElementById('sess-agent-breakdown').innerHTML = bh;
|
|
1035
|
+
} else { document.getElementById('sess-agents').innerHTML='<div class="empty">No active agents</div>'; document.getElementById('sess-agent-breakdown').innerHTML='<div class="empty">No agent data</div>'; }
|
|
1036
|
+
|
|
1037
|
+
document.getElementById('sess-skills').innerHTML = (session.skills||[]).length>0 ? session.skills.map(s=>`<span class="chip ${s.active?'active':'inactive'}" title="${esc(s.reason||'')}">${esc(s.name||'?')}</span>`).join('') : '<span class="empty">No skills loaded</span>';
|
|
1038
|
+
document.getElementById('sess-patterns').innerHTML = (session.patterns||[]).length>0 ? session.patterns.map(p=>`<span class="chip ${p.active?'active':'inactive'}" title="Weight: ${p.weight||0}">${esc(p.name||p.id||'?')}</span>`).join('') : '<span class="empty">No patterns matched</span>';
|
|
1039
|
+
const batch = session.deployBatchSummary;
|
|
1040
|
+
document.getElementById('sess-deploys').innerHTML = batch&&batch.totalActions>0 ? `<div class="kv"><span class="label">Total</span><span class="value">${batch.totalActions}</span></div><div class="kv"><span class="label">Done</span><span class="value green">${batch.done||0}</span></div><div class="kv"><span class="label">Failed</span><span class="value red">${batch.failed||0}</span></div><div class="kv"><span class="label">Saved</span><span class="value green">${batch.savedOps||0}</span></div>` : '<div class="empty">No deploys</div>';
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
safe('session-history', () => {
|
|
1044
|
+
const sessions = data.sessions||[];
|
|
1045
|
+
const hp = document.getElementById('session-history-panel');
|
|
1046
|
+
if (!sessions.length) { hp.style.display='none'; return; }
|
|
1047
|
+
hp.style.display='';
|
|
1048
|
+
let h='<table><tr><th>Session</th><th>Started</th><th>Duration</th><th>Status</th><th>Model</th><th>In</th><th>Out</th><th>Total</th><th>Cost</th><th>Agents</th><th>Tasks</th></tr>';
|
|
1049
|
+
for (const s of sessions) {
|
|
1050
|
+
const isCur=s.status==='active', hl=isCur?' style="background:rgba(139,233,253,0.08)"':'', lb=isCur?' <span style="color:var(--cyan);font-size:10px">(live)</span>':'';
|
|
1051
|
+
const tI=s.tokensIn||0,tO=s.tokensOut||0;
|
|
1052
|
+
h+=`<tr${hl}><td style="font-weight:500">${esc(s.sessionId||'-').slice(0,12)}${lb}</td><td>${s.startedAt?new Date(s.startedAt).toLocaleString():'-'}</td><td>${s.durationMs>0?Math.round(s.durationMs/60000)+'m':'-'}</td><td><span class="badge ${isCur?'active':s.status==='completed'?'on':'idle'}">${esc(s.status||'?')}</span></td><td style="color:var(--purple)">${esc(modelName(s.model||'-'))}</td><td>${fmt(tI)}</td><td>${fmt(tO)}</td><td style="font-weight:500">${fmt(tI+tO)}</td><td style="color:var(--green)">${typeof s.totalCost==='number'?'$'+s.totalCost.toFixed(4):'$0.00'}</td><td>${s.agentCount||0}</td><td>${s.taskCount||0}</td></tr>`;
|
|
1053
|
+
}
|
|
1054
|
+
const sI=sessions.reduce((a,s)=>a+(s.tokensIn||0),0),sO=sessions.reduce((a,s)=>a+(s.tokensOut||0),0),sC=sessions.reduce((a,s)=>a+(s.totalCost||0),0),sT=sessions.reduce((a,s)=>a+(s.taskCount||0),0);
|
|
1055
|
+
h+=`<tr style="border-top:2px solid var(--border);font-weight:600"><td>ALL (${sessions.length})</td><td></td><td></td><td></td><td></td><td style="color:var(--cyan)">${fmt(sI)}</td><td style="color:var(--green)">${fmt(sO)}</td><td style="color:var(--purple)">${fmt(sI+sO)}</td><td style="color:var(--green)">$${sC.toFixed(4)}</td><td></td><td>${sT}</td></tr></table>`;
|
|
1056
|
+
document.getElementById('session-history-table').innerHTML = h;
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Unified Policies
|
|
1060
|
+
safe('policies', () => {
|
|
1061
|
+
const pm = new Map();
|
|
1062
|
+
for (const p of policies) pm.set(p.name||p.id, {...p, source:'db'});
|
|
1063
|
+
for (const pf of policyFiles) { if (!pm.has(pf.name)) pm.set(pf.name, {id:pf.filename, name:pf.name, category:pf.category, level:'-', enforcementStage:'-', isActive:null, tags:[], priority:0, source:'file'}); }
|
|
1064
|
+
const all = [...pm.values()];
|
|
1065
|
+
if (all.length > 0) {
|
|
1066
|
+
let h='<table><tr><th>Name</th><th>Category</th><th>Level</th><th>Stage</th><th>Status</th><th>Actions</th></tr>';
|
|
1067
|
+
for (const p of all) {
|
|
1068
|
+
const isDb = p.source==='db';
|
|
1069
|
+
const status = isDb ? `<span class="badge ${p.isActive?'on':'off'}">${p.isActive?'ON':'OFF'}</span>` : '<span class="badge file-only">FILE</span>';
|
|
1070
|
+
const actions = isDb ? `<button class="btn" onclick="togglePolicy('${esc(p.id)}')">${p.isActive?'Disable':'Enable'}</button> <select onchange="setStage('${esc(p.id)}',this.value)">${['pre-exec','post-exec','review','always'].map(s=>`<option value="${s}" ${s===p.enforcementStage?'selected':''}>${s}</option>`).join('')}</select> <select onchange="setLevel('${esc(p.id)}',this.value)">${['REQUIRED','RECOMMENDED','OPTIONAL'].map(l=>`<option value="${l}" ${l===p.level?'selected':''}>${l}</option>`).join('')}</select>` : '<span style="color:var(--fg3);font-size:11px">file-only</span>';
|
|
1071
|
+
h+=`<tr><td style="font-weight:500">${esc(p.name||'-')}</td><td>${esc(p.category||'-')}</td><td><span class="badge ${(p.level||'').toLowerCase()}">${esc(p.level||'-')}</span></td><td>${esc(p.enforcementStage||'-')}</td><td>${status}</td><td>${actions}</td></tr>`;
|
|
1072
|
+
}
|
|
1073
|
+
document.getElementById('policy-table').innerHTML = h+'</table>';
|
|
1074
|
+
const stages = {'pre-exec':0,'post-exec':0,review:0,always:0};
|
|
1075
|
+
for (const p of all) if (p.enforcementStage && stages[p.enforcementStage]!=null) stages[p.enforcementStage]++;
|
|
1076
|
+
const maxS = Math.max(...Object.values(stages),1);
|
|
1077
|
+
let sh=''; for (const [s,c] of Object.entries(stages)) sh+=`<div class="bar-container"><span class="bar-label">${esc(s)}</span><div class="bar cyan" style="width:${Math.round((c/maxS)*200)}px"></div><span class="bar-value">${c}</span></div>`;
|
|
1078
|
+
document.getElementById('policy-stages').innerHTML = sh;
|
|
1079
|
+
} else { document.getElementById('policy-table').innerHTML='<div class="empty">No policies</div>'; document.getElementById('policy-stages').innerHTML=''; }
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
safe('compliance', () => {
|
|
1083
|
+
const fbm = compliance.failuresByMechanism || {};
|
|
1084
|
+
const me = Object.entries(fbm);
|
|
1085
|
+
if (me.length > 0) {
|
|
1086
|
+
const max = Math.max(...me.map(([,v])=>v),1);
|
|
1087
|
+
let h=''; for (const [m,c] of me.sort((a,b)=>b[1]-a[1])) h+=`<div class="bar-container"><span class="bar-label" style="width:120px">${esc(m)}</span><div class="bar red" style="width:${Math.round((c/max)*180)}px"></div><span class="bar-value">${c}</span></div>`;
|
|
1088
|
+
document.getElementById('compliance-mechanisms').innerHTML = h;
|
|
1089
|
+
} else { document.getElementById('compliance-mechanisms').innerHTML='<div class="empty">No failure mechanisms</div>'; }
|
|
1090
|
+
|
|
1091
|
+
const rf = compliance.recentFailures || [];
|
|
1092
|
+
if (rf.length > 0) {
|
|
1093
|
+
let h='<table><tr><th>Time</th><th>Policy</th><th>Op</th><th>Mechanism</th><th>Reason</th></tr>';
|
|
1094
|
+
for (const f of rf.slice(0,10)) h+=`<tr><td style="color:var(--fg3);font-size:11px">${esc(f.executedAt?f.executedAt.slice(11,19):'-')}</td><td style="color:var(--red)">${esc(f.policyName||f.policyId||'-')}</td><td>${esc(f.operation||'-')}</td><td><span class="badge required">${esc(f.defeatedMechanism||'-')}</span></td><td style="color:var(--fg3);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(f.reason||'-')}</td></tr>`;
|
|
1095
|
+
document.getElementById('compliance-failures').innerHTML = h+'</table>';
|
|
1096
|
+
} else { document.getElementById('compliance-failures').innerHTML='<div class="empty">No recent failures</div>'; }
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
safe('audit', () => {
|
|
1100
|
+
if (audit.length > 0) {
|
|
1101
|
+
let h=''; for (const e of audit.slice(0,15)) {
|
|
1102
|
+
const cls=e.allowed?'pass':'block', ts=typeof e.executedAt==='string'&&e.executedAt.length>=19?e.executedAt.slice(11,19):e.executedAt||'-';
|
|
1103
|
+
h+=`<div class="audit-row"><span class="audit-time">${esc(ts)}</span><span class="audit-icon ${cls}">${e.allowed?'PASS':'BLOCK'}</span><span class="audit-policy" title="${esc(e.policyId||'')}">${esc(e.policyId?e.policyId.slice(0,8):'')}</span><span class="audit-op">${esc(e.operation||'')}</span><span class="audit-reason">${esc(e.reason||'')}</span></div>`;
|
|
1104
|
+
}
|
|
1105
|
+
document.getElementById('audit-trail').innerHTML = h;
|
|
1106
|
+
} else { document.getElementById('audit-trail').innerHTML='<div class="empty">No audit entries</div>'; }
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
safe('kanban', () => renderKanban(tasks));
|
|
1110
|
+
|
|
1111
|
+
safe('performance', () => {
|
|
1112
|
+
const hp = (data.performance||{}).hotPaths || [];
|
|
1113
|
+
const pp = document.getElementById('perf-panel');
|
|
1114
|
+
if (!pp) return;
|
|
1115
|
+
if (hp.length > 0) {
|
|
1116
|
+
let h='<table><tr><th>Operation</th><th>Avg (ms)</th><th>P95 (ms)</th><th>Calls</th></tr>';
|
|
1117
|
+
for (const p of hp) h+=`<tr><td style="color:var(--cyan)">${esc(p.name)}</td><td class="value ${p.avgMs>500?'red':p.avgMs>100?'yellow':'green'}">${p.avgMs}</td><td class="value ${p.p95Ms>1000?'red':p.p95Ms>300?'yellow':'green'}">${p.p95Ms}</td><td>${p.count}</td></tr>`;
|
|
1118
|
+
pp.innerHTML = h+'</table>';
|
|
1119
|
+
} else { pp.innerHTML='<div class="empty">No performance data</div>'; }
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
if (_errors.length > 0) {
|
|
1123
|
+
let eb = document.getElementById('render-errors');
|
|
1124
|
+
if (!eb) { eb=document.createElement('div'); eb.id='render-errors'; eb.style.cssText='position:fixed;bottom:0;left:0;right:0;background:var(--red);color:#fff;padding:8px 16px;font-size:12px;z-index:9999;max-height:120px;overflow:auto;'; document.body.appendChild(eb); }
|
|
1125
|
+
eb.innerHTML='<strong>Render errors:</strong> '+_errors.map(e=>esc(e)).join(' | '); eb.style.display='';
|
|
1126
|
+
} else { const eb=document.getElementById('render-errors'); if(eb) eb.style.display='none'; }
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// ── API Actions ──
|
|
1130
|
+
async function togglePolicy(id) { try { const r=await fetch(`${API_URL}/api/policy/${id}/toggle`,{method:'POST'}); if(!r.ok)throw new Error(); const d=await r.json(); showToast(`Policy ${d.isActive?'enabled':'disabled'}`,'success'); } catch { showToast('Failed to toggle policy','error'); } }
|
|
1131
|
+
async function setStage(id,stage) { try { const r=await fetch(`${API_URL}/api/policy/${id}/stage`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({stage})}); if(!r.ok)throw new Error(); showToast(`Stage: ${stage}`,'success'); } catch { showToast('Failed to set stage','error'); } }
|
|
1132
|
+
async function setLevel(id,level) { try { const r=await fetch(`${API_URL}/api/policy/${id}/level`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({level})}); if(!r.ok)throw new Error(); showToast(`Level: ${level}`,'success'); } catch { showToast('Failed to set level','error'); } }
|
|
1133
|
+
|
|
1134
|
+
// ── Kanban ──
|
|
1135
|
+
const TYPE_ICONS = {task:'\u25C6',bug:'\uD83D\uDC1B',feature:'\u2728',epic:'\uD83C\uDFAF',chore:'\uD83D\uDD27',story:'\uD83D\uDCD6'};
|
|
1136
|
+
const PRIO_LABELS = ['P0','P1','P2','P3','P4'];
|
|
1137
|
+
let prevCardMap = new Map();
|
|
1138
|
+
function renderKanban(tasks) {
|
|
1139
|
+
const items=tasks.items||[], cols={open:[],in_progress:[],blocked:[],done:[],wont_do:[]}, ncm=new Map();
|
|
1140
|
+
for (const i of items) { if(cols[i.status]) cols[i.status].push(i); ncm.set(i.id,i.status); }
|
|
1141
|
+
setText('kb-open-count',cols.open.length); setText('kb-progress-count',cols.in_progress.length);
|
|
1142
|
+
setText('kb-blocked-count',cols.blocked.length); setText('kb-done-count',cols.done.length); setText('kb-wontdo-count',cols.wont_do.length);
|
|
1143
|
+
renderCol('kb-open',cols.open,ncm); renderCol('kb-progress',cols.in_progress,ncm); renderCol('kb-blocked',cols.blocked,ncm); renderCol('kb-done',cols.done,ncm); renderCol('kb-wontdo',cols.wont_do,ncm);
|
|
1144
|
+
const p=document.getElementById('kanban-panel'); if(p) p.style.display=items.length>0?'':'none';
|
|
1145
|
+
prevCardMap=ncm;
|
|
1146
|
+
}
|
|
1147
|
+
function renderCol(cid,items,ncm) {
|
|
1148
|
+
const c=document.getElementById(cid); if(!c) return;
|
|
1149
|
+
if(!items.length) { c.innerHTML='<div class="kanban-empty">No tasks</div>'; return; }
|
|
1150
|
+
const ids=new Set(items.map(i=>i.id));
|
|
1151
|
+
c.querySelectorAll('.kanban-card').forEach(el=>{ if(!ids.has(el.dataset.id)){el.classList.add('card-exit');el.addEventListener('animationend',()=>el.remove(),{once:true});} });
|
|
1152
|
+
for (const item of items) {
|
|
1153
|
+
let card=c.querySelector('.kanban-card[data-id="'+item.id+'"]');
|
|
1154
|
+
if(!card){card=document.createElement('div');card.className='kanban-card';card.dataset.id=item.id;if(prevCardMap.has(item.id)&&prevCardMap.get(item.id)!==item.status){card.style.animation='none';requestAnimationFrame(()=>{card.style.animation='';});}c.appendChild(card);}else{card.style.animation='none';}
|
|
1155
|
+
const ti=TYPE_ICONS[item.type]||'\u25C6',pl=PRIO_LABELS[item.priority]||'P2',pc='p'+(item.priority!=null?item.priority:2),as=item.assignee?' \u00B7 '+esc(item.assignee):'';
|
|
1156
|
+
card.innerHTML='<div style="display:flex;justify-content:space-between;align-items:center"><span class="card-id">'+esc(item.id)+'</span><span>'+ti+'</span></div><div class="card-title">'+esc(item.title||'Untitled')+'</div><div class="card-meta"><span class="card-priority '+pc+'">'+pl+'</span><span>'+esc(item.type)+as+'</span></div>';
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// ── Helpers ──
|
|
1161
|
+
function setText(id,val){const e=document.getElementById(id);if(e)e.textContent=String(val);}
|
|
1162
|
+
function fmt(n){if(typeof n!=='number'||isNaN(n))return'0';return Math.round(n).toLocaleString();}
|
|
1163
|
+
function capitalize(s){return s?s.charAt(0).toUpperCase()+s.slice(1):'';}
|
|
1164
|
+
const MODEL_NAMES={'opus-4.6':'Claude Opus 4.6','sonnet-4.6':'Claude Sonnet 4.6','gpt-5.4':'GPT 5.4','gpt-5.3-codex':'GPT 5.3 Codex','qwen35':'Qwen 3.5 35B A3B','qwen35-a3b':'Qwen 3.5 35B A3B'};
|
|
1165
|
+
function modelName(id){return MODEL_NAMES[id]||id||'unknown';}
|
|
1166
|
+
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML;}
|
|
1167
|
+
|
|
1168
|
+
// ── Init ──
|
|
1169
|
+
connect();
|
|
1170
|
+
connectSSE();
|
|
1171
|
+
|
|
1172
|
+
// Fallback poll
|
|
1173
|
+
setInterval(async()=>{
|
|
1174
|
+
if(ws&&ws.readyState===WebSocket.OPEN)return;
|
|
1175
|
+
try{const r=await fetch(API_URL+'/api/dashboard');if(!r.ok)return;const d=await r.json();if(d)render(d);document.getElementById('ws-status').className='status-dot polling';document.getElementById('refresh-info').textContent='Polling - '+new Date().toLocaleTimeString();document.title='UAP Dashboard - Polling';}catch{}
|
|
1176
|
+
}, 2000);
|
|
1177
|
+
|
|
1178
|
+
// Resize: re-init charts
|
|
1179
|
+
let resizeT;
|
|
1180
|
+
window.addEventListener('resize',()=>{clearTimeout(resizeT);resizeT=setTimeout(()=>{chartsInit=false;chartTasks=chartCompression=chartDeploy=chartHitrate=chartCompliance=null;},300);});
|
|
1181
|
+
</script>
|
|
1182
|
+
</body>
|
|
1183
|
+
</html>
|