@miller-tech/uap 1.133.2 โ 1.135.1
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/bin/cli.js +4 -1
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/config-command.d.ts +49 -0
- package/dist/cli/config-command.d.ts.map +1 -0
- package/dist/cli/config-command.js +465 -0
- package/dist/cli/config-command.js.map +1 -0
- package/dist/cli/config-wizard.d.ts +13 -0
- package/dist/cli/config-wizard.d.ts.map +1 -0
- package/dist/cli/config-wizard.js +134 -0
- package/dist/cli/config-wizard.js.map +1 -0
- package/dist/cli/deliver.js +11 -2
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/guided-setup.d.ts.map +1 -1
- package/dist/cli/guided-setup.js +11 -0
- package/dist/cli/guided-setup.js.map +1 -1
- package/dist/cli/policy.d.ts.map +1 -1
- package/dist/cli/policy.js +34 -0
- package/dist/cli/policy.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +22 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/config/policy-recommendations.d.ts +29 -0
- package/dist/config/policy-recommendations.d.ts.map +1 -0
- package/dist/config/policy-recommendations.js +108 -0
- package/dist/config/policy-recommendations.js.map +1 -0
- package/dist/config/settings-registry.d.ts +65 -0
- package/dist/config/settings-registry.d.ts.map +1 -0
- package/dist/config/settings-registry.js +334 -0
- package/dist/config/settings-registry.js.map +1 -0
- package/dist/dashboard/controls.d.ts +66 -0
- package/dist/dashboard/controls.d.ts.map +1 -0
- package/dist/dashboard/controls.js +188 -0
- package/dist/dashboard/controls.js.map +1 -0
- package/dist/dashboard/data-service.d.ts +20 -0
- package/dist/dashboard/data-service.d.ts.map +1 -1
- package/dist/dashboard/data-service.js +47 -0
- package/dist/dashboard/data-service.js.map +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +81 -5
- package/dist/dashboard/server.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts +5 -0
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +11 -0
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/run-state.d.ts +12 -0
- package/dist/delivery/run-state.d.ts.map +1 -1
- package/dist/delivery/run-state.js +58 -1
- package/dist/delivery/run-state.js.map +1 -1
- package/docs/DASHBOARD_UPLIFT_SPEC.md +171 -0
- package/docs/INDEX.md +2 -0
- package/docs/guides/POLICY_SELECTION.md +98 -0
- package/docs/reference/CONFIGURATION_REFERENCE.md +688 -0
- package/package.json +1 -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/tools/agents/scripts/anthropic_proxy.py +164 -12
- package/web/dash/core.js +398 -0
- package/web/dash/styles.css +307 -0
- package/web/dash/tab-deliver.js +12 -0
- package/web/dash/tab-memory.js +12 -0
- package/web/dash/tab-models.js +12 -0
- package/web/dash/tab-policies.js +12 -0
- package/web/dash/tabs.js +581 -0
- package/web/dashboard.html +12 -1303
package/web/dashboard.html
CHANGED
|
@@ -8,391 +8,9 @@
|
|
|
8
8
|
rel="icon"
|
|
9
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
10
|
/>
|
|
11
|
-
<link rel="stylesheet" href="/vendor/uPlot.min.css"
|
|
11
|
+
<link rel="stylesheet" href="/vendor/uPlot.min.css" />
|
|
12
|
+
<link rel="stylesheet" href="/dash/styles.css" />
|
|
12
13
|
<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-group { font-size: 10px; color: var(--fg2); text-transform: uppercase; letter-spacing: 0.5px; padding: 4px 4px 0; border-top: 1px dashed var(--border); margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
346
|
-
.kanban-group:first-child { border-top: none; margin-top: 0; }
|
|
347
|
-
.kanban-card.card-child { margin-left: 8px; border-left: 2px solid var(--border); }
|
|
348
|
-
.kanban-card .card-crumb { color: var(--fg3); font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
349
|
-
.kanban-card {
|
|
350
|
-
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
351
|
-
padding: 8px 10px; cursor: default;
|
|
352
|
-
transition: transform 0.2s ease, opacity 0.3s ease, box-shadow 0.2s ease;
|
|
353
|
-
animation: card-enter 0.35s ease-out;
|
|
354
|
-
}
|
|
355
|
-
.kanban-card:hover { border-color: var(--cyan); box-shadow: 0 0 8px rgba(88,166,255,0.15); transform: translateY(-1px); }
|
|
356
|
-
.kanban-card.card-exit { animation: card-exit 0.3s ease-in forwards; }
|
|
357
|
-
.kanban-card .card-id { font-size: 10px; color: var(--cyan); font-weight: 600; }
|
|
358
|
-
.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; }
|
|
359
|
-
.kanban-card .card-meta { display: flex; align-items: center; gap: 6px; font-size: 10px; color: var(--fg3); }
|
|
360
|
-
.kanban-card .card-priority { padding: 0 4px; border-radius: 3px; font-weight: 600; font-size: 10px; }
|
|
361
|
-
.card-priority.p0 { background: rgba(248,81,73,0.2); color: var(--red); }
|
|
362
|
-
.card-priority.p1 { background: rgba(210,153,34,0.2); color: var(--yellow); }
|
|
363
|
-
.card-priority.p2 { background: rgba(88,166,255,0.15); color: var(--cyan); }
|
|
364
|
-
.card-priority.p3, .card-priority.p4 { background: rgba(72,79,88,0.2); color: var(--fg3); }
|
|
365
|
-
.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; }
|
|
366
|
-
@keyframes card-enter { 0% { opacity: 0; transform: translateY(-12px) scale(0.95); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
|
|
367
|
-
@keyframes card-exit { 0% { opacity: 1; transform: translateX(0) scale(1); } 100% { opacity: 0; transform: translateX(30px) scale(0.9); } }
|
|
368
|
-
|
|
369
|
-
/* โโ Toasts โโ */
|
|
370
|
-
.toast-container { position: fixed; top: 16px; right: 16px; z-index: 1000; display: flex; flex-direction: column; gap: 8px; }
|
|
371
|
-
.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; }
|
|
372
|
-
.toast.success { border-color: var(--green); color: var(--green); }
|
|
373
|
-
.toast.error { border-color: var(--red); color: var(--red); }
|
|
374
|
-
@keyframes toast-in { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
|
|
375
|
-
|
|
376
|
-
/* โโ Collapsible โโ */
|
|
377
|
-
.collapsible-header {
|
|
378
|
-
cursor: pointer;
|
|
379
|
-
user-select: none;
|
|
380
|
-
display: flex;
|
|
381
|
-
align-items: center;
|
|
382
|
-
gap: 6px;
|
|
383
|
-
}
|
|
384
|
-
.collapsible-header::before {
|
|
385
|
-
content: '\25B6';
|
|
386
|
-
font-size: 10px;
|
|
387
|
-
color: var(--fg3);
|
|
388
|
-
transition: transform 0.2s;
|
|
389
|
-
}
|
|
390
|
-
.collapsible-header.open::before { transform: rotate(90deg); }
|
|
391
|
-
.collapsible-body { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
|
392
|
-
.collapsible-body.open { max-height: 600px; }
|
|
393
|
-
|
|
394
|
-
.empty { color: var(--fg3); font-style: italic; padding: 8px 0; }
|
|
395
|
-
</style>
|
|
396
14
|
</head>
|
|
397
15
|
<body>
|
|
398
16
|
<div class="toast-container" id="toast-container"></div>
|
|
@@ -408,934 +26,25 @@
|
|
|
408
26
|
<span class="header-ts" id="data-ts"></span>
|
|
409
27
|
<div>
|
|
410
28
|
<span class="status-dot disconnected" id="ws-status" role="status" aria-label="Connection status: disconnected"></span>
|
|
411
|
-
<span class="refresh-info" id="refresh-info">Connecting
|
|
29
|
+
<span class="refresh-info" id="refresh-info">Connectingโฆ</span>
|
|
412
30
|
</div>
|
|
413
31
|
</div>
|
|
414
32
|
</header>
|
|
415
33
|
|
|
416
|
-
|
|
417
|
-
<div class="hero-row">
|
|
418
|
-
<div class="panel">
|
|
419
|
-
<h2>Tasks & Agents</h2>
|
|
420
|
-
<div class="chart-container chart-hero" id="chart-tasks"></div>
|
|
421
|
-
</div>
|
|
422
|
-
<div class="panel">
|
|
423
|
-
<h2>Compression & Memory</h2>
|
|
424
|
-
<div class="chart-container chart-hero" id="chart-compression"></div>
|
|
425
|
-
</div>
|
|
426
|
-
</div>
|
|
427
|
-
|
|
428
|
-
<!-- Core Metrics -->
|
|
429
|
-
<div class="metrics-row">
|
|
430
|
-
<div class="panel">
|
|
431
|
-
<h2>Models & Routing</h2>
|
|
432
|
-
<div id="model-roles"></div>
|
|
433
|
-
<h3>Router</h3>
|
|
434
|
-
<div id="model-router"></div>
|
|
435
|
-
<h3>Cost Optimization</h3>
|
|
436
|
-
<div id="model-cost-opt"></div>
|
|
437
|
-
<h3>Routing Matrix</h3>
|
|
438
|
-
<div id="model-routing-matrix" class="table-wrap"></div>
|
|
439
|
-
<h3>Recent Routing Decisions</h3>
|
|
440
|
-
<div id="model-routes" class="table-wrap"><div class="empty">No routing decisions yet</div></div>
|
|
441
|
-
<h3>Session Usage by Model</h3>
|
|
442
|
-
<div id="model-usage" class="table-wrap"></div>
|
|
443
|
-
<div class="kv" style="margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
|
|
444
|
-
<span class="label" style="font-weight:600">Total Cost</span>
|
|
445
|
-
<span class="value green" id="model-cost">$0.0000</span>
|
|
446
|
-
</div>
|
|
447
|
-
</div>
|
|
34
|
+
<nav class="tabbar" id="tabbar" role="tablist"></nav>
|
|
448
35
|
|
|
449
|
-
|
|
450
|
-
<h2>Memory</h2>
|
|
451
|
-
<div id="memory-panel">
|
|
452
|
-
<div class="kv"><span class="label">L1 Working</span><span class="value" id="mem-l1">-</span></div>
|
|
453
|
-
<div class="kv"><span class="label">L2 Session</span><span class="value" id="mem-l2">-</span></div>
|
|
454
|
-
<div class="kv"><span class="label">L3 Semantic</span><span class="value" id="mem-l3">-</span></div>
|
|
455
|
-
<div class="kv"><span class="label">L4 Knowledge</span><span class="value" id="mem-l4">-</span></div>
|
|
456
|
-
</div>
|
|
457
|
-
<h3>Memory Hit Rate</h3>
|
|
458
|
-
<div id="mem-hitrate-panel">
|
|
459
|
-
<div class="kv"><span class="label">Hits</span><span class="value green" id="mem-hits">0</span></div>
|
|
460
|
-
<div class="kv"><span class="label">Misses</span><span class="value red" id="mem-misses">0</span></div>
|
|
461
|
-
<div class="kv"><span class="label">Rate</span><span class="value" id="mem-hitrate">N/A</span></div>
|
|
462
|
-
<div class="gauge-track"><div class="gauge-fill" id="mem-hitrate-gauge" style="width:0%;background:var(--green)"></div></div>
|
|
463
|
-
</div>
|
|
464
|
-
<div class="chart-container chart-spark" id="chart-hitrate"></div>
|
|
465
|
-
<h3>Context Compression</h3>
|
|
466
|
-
<div id="compression-panel">
|
|
467
|
-
<div class="kv"><span class="label">Raw</span><span class="value" id="comp-raw">0 KB</span></div>
|
|
468
|
-
<div class="kv"><span class="label">Compressed</span><span class="value" id="comp-ctx">0 KB</span></div>
|
|
469
|
-
<div class="kv"><span class="label">Savings</span><span class="value green" id="comp-savings">0%</span></div>
|
|
470
|
-
<div class="kv"><span class="label">Tool Calls</span><span class="value" id="comp-calls">0</span></div>
|
|
471
|
-
</div>
|
|
472
|
-
<h3>Recent Memory Queries</h3>
|
|
473
|
-
<div id="memory-recent-queries"><div class="empty">No recent queries</div></div>
|
|
474
|
-
</div>
|
|
475
|
-
|
|
476
|
-
<div class="panel">
|
|
477
|
-
<h2>Deploy Pipeline</h2>
|
|
478
|
-
<div id="deploy-buckets">
|
|
479
|
-
<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>
|
|
480
|
-
<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>
|
|
481
|
-
<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>
|
|
482
|
-
<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>
|
|
483
|
-
<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>
|
|
484
|
-
<div class="kv" style="margin-top:8px;border-top:1px solid var(--border);padding-top:6px">
|
|
485
|
-
<span class="label">Batches</span><span class="value" id="deploy-batch-count">0</span>
|
|
486
|
-
</div>
|
|
487
|
-
<div class="kv"><span class="label">Saved Ops</span><span class="value green" id="deploy-saved-ops">0</span></div>
|
|
488
|
-
</div>
|
|
489
|
-
<h3>Deploy Trend</h3>
|
|
490
|
-
<div class="chart-container chart-spark" id="chart-deploy"></div>
|
|
491
|
-
<h3>Coordination</h3>
|
|
492
|
-
<div id="coord-panel">
|
|
493
|
-
<div class="kv"><span class="label">Active Agents</span><span class="value" id="coord-agents">0</span></div>
|
|
494
|
-
<div class="kv"><span class="label">Total Agents</span><span class="value" id="coord-total-agents">0</span></div>
|
|
495
|
-
<div class="kv"><span class="label">Completed</span><span class="value green" id="coord-completed-agents">0</span></div>
|
|
496
|
-
<div class="kv"><span class="label">Resource Claims</span><span class="value" id="coord-claims">0</span></div>
|
|
497
|
-
<div class="kv"><span class="label">Pending Deploys</span><span class="value" id="coord-deploys">0</span></div>
|
|
498
|
-
<div class="kv"><span class="label">Active Worktrees</span><span class="value cyan" id="coord-worktrees">0</span></div>
|
|
499
|
-
<div class="kv"><span class="label">Pattern Uses</span><span class="value" id="coord-pattern-hits">0</span></div>
|
|
500
|
-
<div class="kv"><span class="label">Pattern Successes</span><span class="value green" id="coord-pattern-successes">0</span></div>
|
|
501
|
-
</div>
|
|
502
|
-
<h3>Recent Agents</h3>
|
|
503
|
-
<div id="coord-agents-table" class="table-wrap"><div class="empty">No agents registered</div></div>
|
|
504
|
-
</div>
|
|
505
|
-
</div>
|
|
506
|
-
|
|
507
|
-
<!-- Session Telemetry -->
|
|
508
|
-
<div class="full-row" id="session-panel" style="display:none">
|
|
509
|
-
<div class="panel">
|
|
510
|
-
<h2>Session Telemetry <span style="font-size:11px;color:var(--fg3);font-weight:400" id="sess-label">(current)</span></h2>
|
|
511
|
-
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px">
|
|
512
|
-
<div>
|
|
513
|
-
<h3>Overview</h3>
|
|
514
|
-
<div class="kv"><span class="label">Session ID</span><span class="value cyan" id="sess-id">-</span></div>
|
|
515
|
-
<div class="kv"><span class="label">Uptime</span><span class="value" id="sess-uptime">-</span></div>
|
|
516
|
-
<div class="kv"><span class="label">Tokens Used</span><span class="value" id="sess-tokens">0</span></div>
|
|
517
|
-
<div class="kv"><span class="label">Tokens Saved</span><span class="value green" id="sess-saved">0</span></div>
|
|
518
|
-
<div class="kv"><span class="label">Tool Calls</span><span class="value" id="sess-tools">0</span></div>
|
|
519
|
-
<div class="kv"><span class="label">Policy Checks</span><span class="value" id="sess-checks">0</span></div>
|
|
520
|
-
<div class="kv"><span class="label">Policy Blocks</span><span class="value red" id="sess-blocks">0</span></div>
|
|
521
|
-
<div class="kv"><span class="label">Errors</span><span class="value red" id="sess-errors">0</span></div>
|
|
522
|
-
<div id="sess-token-io-detail"></div>
|
|
523
|
-
<div id="sess-progress-wrap" style="display:none">
|
|
524
|
-
<h3>Step Progress</h3>
|
|
525
|
-
<div class="kv"><span class="label" id="sess-step-name">-</span><span class="value" id="sess-step-count">0/0</span></div>
|
|
526
|
-
<div class="progress-track"><div class="progress-fill" id="sess-progress-bar" style="width:0%"></div></div>
|
|
527
|
-
</div>
|
|
528
|
-
</div>
|
|
529
|
-
<div>
|
|
530
|
-
<h3>Cost</h3>
|
|
531
|
-
<div class="kv"><span class="label">Session Cost</span><span class="value" id="sess-cost">$0.0000</span></div>
|
|
532
|
-
<div class="kv"><span class="label">Without UAP</span><span class="value" id="sess-cost-without">$0.0000</span></div>
|
|
533
|
-
<div class="cost-savings" id="sess-savings-box">
|
|
534
|
-
<div class="big-number" id="sess-savings-pct">0%</div>
|
|
535
|
-
<div class="sub">estimated savings with UAP</div>
|
|
536
|
-
</div>
|
|
537
|
-
<h3>Agents</h3>
|
|
538
|
-
<div id="sess-agents"><div class="empty">No active agents</div></div>
|
|
539
|
-
<h3>Per-Agent Breakdown</h3>
|
|
540
|
-
<div id="sess-agent-breakdown" class="table-wrap"><div class="empty">No agent data</div></div>
|
|
541
|
-
</div>
|
|
542
|
-
<div>
|
|
543
|
-
<h3>Skills</h3>
|
|
544
|
-
<div id="sess-skills" class="chip-list"><span class="empty">No skills loaded</span></div>
|
|
545
|
-
<h3>Patterns</h3>
|
|
546
|
-
<div id="sess-patterns" class="chip-list"><span class="empty">No patterns matched</span></div>
|
|
547
|
-
<h3>Deploys</h3>
|
|
548
|
-
<div id="sess-deploys"><div class="empty">No deploys</div></div>
|
|
549
|
-
</div>
|
|
550
|
-
</div>
|
|
551
|
-
</div>
|
|
552
|
-
</div>
|
|
553
|
-
|
|
554
|
-
<!-- Compliance -->
|
|
555
|
-
<div class="split-row">
|
|
556
|
-
<div class="panel">
|
|
557
|
-
<h2>Policies</h2>
|
|
558
|
-
<div id="policy-table" class="table-wrap"></div>
|
|
559
|
-
<h3>Enforcement Stages</h3>
|
|
560
|
-
<div id="policy-stages"></div>
|
|
561
|
-
</div>
|
|
562
|
-
<div class="panel">
|
|
563
|
-
<h2>Compliance & Audit</h2>
|
|
564
|
-
<h3>Block Rate Trend</h3>
|
|
565
|
-
<div class="chart-container chart-spark" id="chart-compliance"></div>
|
|
566
|
-
<h3>Failures by Mechanism</h3>
|
|
567
|
-
<div id="compliance-mechanisms"></div>
|
|
568
|
-
<h3>Recent Failures</h3>
|
|
569
|
-
<div id="compliance-failures" class="table-wrap"><div class="empty">No recent failures</div></div>
|
|
570
|
-
<h3>Audit Trail</h3>
|
|
571
|
-
<div id="audit-trail"></div>
|
|
572
|
-
<h3>Live Events</h3>
|
|
573
|
-
<div class="event-feed" id="live-events"><div class="empty">Connecting to event stream...</div></div>
|
|
574
|
-
</div>
|
|
575
|
-
</div>
|
|
576
|
-
|
|
577
|
-
<!-- Session History -->
|
|
578
|
-
<div class="full-row" id="session-history-panel" style="display:none">
|
|
579
|
-
<div class="panel">
|
|
580
|
-
<h2>Session History</h2>
|
|
581
|
-
<div id="session-history-table" class="table-wrap"><div class="empty">No session history</div></div>
|
|
582
|
-
</div>
|
|
583
|
-
</div>
|
|
584
|
-
|
|
585
|
-
<!-- Token Savings by Influence -->
|
|
586
|
-
<div class="full-row" id="savings-panel">
|
|
587
|
-
<div class="panel">
|
|
588
|
-
<h2>Token Savings by Influence</h2>
|
|
589
|
-
<div id="savings-summary" style="margin-bottom:12px;font-size:14px"></div>
|
|
590
|
-
<div id="savings-table" class="table-wrap"><div class="empty">No savings data</div></div>
|
|
591
|
-
</div>
|
|
592
|
-
</div>
|
|
593
|
-
|
|
594
|
-
<!-- Orchestrations & Hierarchy -->
|
|
595
|
-
<div class="full-row" id="orch-panel">
|
|
596
|
-
<div class="panel">
|
|
597
|
-
<h2>Orchestrations & Hierarchy <span style="font-size:11px;color:var(--fg3);font-weight:400" id="orch-label"></span></h2>
|
|
598
|
-
<div id="orch-tree"><div class="empty">No orchestrations yet</div></div>
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
|
-
|
|
602
|
-
<!-- Task Board -->
|
|
603
|
-
<div class="full-row" id="kanban-panel" style="display:none">
|
|
604
|
-
<div class="panel">
|
|
605
|
-
<h2>Task Board</h2>
|
|
606
|
-
<div class="kanban" id="kanban-board">
|
|
607
|
-
<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>
|
|
608
|
-
<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>
|
|
609
|
-
<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>
|
|
610
|
-
<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>
|
|
611
|
-
<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>
|
|
612
|
-
</div>
|
|
613
|
-
</div>
|
|
614
|
-
</div>
|
|
615
|
-
|
|
616
|
-
<!-- Performance (collapsible) -->
|
|
617
|
-
<div class="full-row">
|
|
618
|
-
<div class="panel">
|
|
619
|
-
<h2 class="collapsible-header" id="perf-toggle" onclick="toggleCollapsible('perf')">Performance</h2>
|
|
620
|
-
<div class="collapsible-body" id="perf-body">
|
|
621
|
-
<div id="perf-panel"><div class="empty">No performance data</div></div>
|
|
622
|
-
</div>
|
|
623
|
-
</div>
|
|
624
|
-
</div>
|
|
36
|
+
<main id="view"></main>
|
|
625
37
|
</div>
|
|
626
38
|
|
|
627
39
|
<script>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// page is opened without the server substitution (mutations then 401).
|
|
634
|
-
const DASHBOARD_TOKEN = '__UAP_DASHBOARD_TOKEN__'.startsWith('__UAP') ? '' : '__UAP_DASHBOARD_TOKEN__';
|
|
635
|
-
const MUTATION_HEADERS = { 'X-Uap-Dashboard-Token': DASHBOARD_TOKEN };
|
|
636
|
-
// Server-injected push cadence (uap dash serve --refresh); the fallback
|
|
637
|
-
// poll below matches it. 2000 when opened without server substitution.
|
|
638
|
-
const REFRESH_MS = Number('__UAP_DASH_REFRESH_MS__') > 0 ? Number('__UAP_DASH_REFRESH_MS__') : 2000;
|
|
639
|
-
let ws = null;
|
|
640
|
-
let reconnectTimer = null;
|
|
641
|
-
let reconnectDelay = 1000;
|
|
642
|
-
const MAX_RECONNECT_DELAY = 30000;
|
|
643
|
-
let chartTasks = null, chartCompression = null, chartDeploy = null, chartHitrate = null, chartCompliance = null;
|
|
644
|
-
const uplotReady = typeof uPlot !== 'undefined';
|
|
645
|
-
|
|
646
|
-
function showToast(message, type = 'info') {
|
|
647
|
-
const c = document.getElementById('toast-container');
|
|
648
|
-
const t = document.createElement('div');
|
|
649
|
-
t.className = 'toast ' + type;
|
|
650
|
-
t.textContent = message;
|
|
651
|
-
c.appendChild(t);
|
|
652
|
-
setTimeout(() => t.remove(), 3000);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function toggleCollapsible(id) {
|
|
656
|
-
document.getElementById(id + '-toggle').classList.toggle('open');
|
|
657
|
-
document.getElementById(id + '-body').classList.toggle('open');
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// โโ WebSocket โโ
|
|
661
|
-
function connect() {
|
|
662
|
-
clearTimeout(reconnectTimer);
|
|
663
|
-
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
664
|
-
ws = new WebSocket(WS_URL);
|
|
665
|
-
ws.onopen = () => {
|
|
666
|
-
reconnectDelay = 1000;
|
|
667
|
-
const dot = document.getElementById('ws-status');
|
|
668
|
-
dot.className = 'status-dot connected';
|
|
669
|
-
dot.setAttribute('aria-label', 'Connection status: live');
|
|
670
|
-
document.getElementById('refresh-info').textContent = 'Live';
|
|
671
|
-
document.title = 'UAP Dashboard - Live';
|
|
672
|
-
};
|
|
673
|
-
ws.onmessage = (e) => {
|
|
674
|
-
try { render(JSON.parse(e.data)); } catch (err) { console.error('Render error:', err); }
|
|
675
|
-
document.getElementById('refresh-info').textContent = 'Live - ' + new Date().toLocaleTimeString();
|
|
676
|
-
};
|
|
677
|
-
ws.onclose = () => {
|
|
678
|
-
document.getElementById('ws-status').className = 'status-dot disconnected';
|
|
679
|
-
document.getElementById('refresh-info').textContent = 'Disconnected - reconnecting...';
|
|
680
|
-
document.title = 'UAP Dashboard - Disconnected';
|
|
681
|
-
reconnectTimer = setTimeout(connect, reconnectDelay);
|
|
682
|
-
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
|
|
683
|
-
};
|
|
684
|
-
ws.onerror = () => ws.close();
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// โโ SSE Event Stream โโ
|
|
688
|
-
let eventSource = null;
|
|
689
|
-
const liveEvents = [];
|
|
690
|
-
function connectSSE() {
|
|
691
|
-
try {
|
|
692
|
-
eventSource = new EventSource(API_URL + '/api/events');
|
|
693
|
-
eventSource.onmessage = (e) => {
|
|
694
|
-
try {
|
|
695
|
-
const ev = JSON.parse(e.data);
|
|
696
|
-
// Dedup by id: the connect-burst and the cross-process poller can
|
|
697
|
-
// briefly overlap. Ignore events we already hold.
|
|
698
|
-
if (ev.id != null && liveEvents.some((x) => x.id === ev.id)) return;
|
|
699
|
-
liveEvents.unshift(ev);
|
|
700
|
-
if (liveEvents.length > 20) liveEvents.pop();
|
|
701
|
-
renderLiveEvents();
|
|
702
|
-
} catch {}
|
|
703
|
-
};
|
|
704
|
-
// Full live snapshot (cross-process) โ used when the WebSocket is down so
|
|
705
|
-
// SSE alone keeps the dashboard live rather than falling to slow polling. (C)
|
|
706
|
-
eventSource.addEventListener('snapshot', (e) => {
|
|
707
|
-
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
708
|
-
try {
|
|
709
|
-
render(JSON.parse(e.data));
|
|
710
|
-
document.getElementById('ws-status').className = 'status-dot connected';
|
|
711
|
-
document.getElementById('refresh-info').textContent = 'Live (SSE) - ' + new Date().toLocaleTimeString();
|
|
712
|
-
} catch {}
|
|
713
|
-
});
|
|
714
|
-
eventSource.onerror = () => { eventSource.close(); setTimeout(connectSSE, 5000); };
|
|
715
|
-
} catch {}
|
|
716
|
-
}
|
|
717
|
-
function renderLiveEvents() {
|
|
718
|
-
const c = document.getElementById('live-events');
|
|
719
|
-
if (!c || !liveEvents.length) return;
|
|
720
|
-
let html = '';
|
|
721
|
-
for (const ev of liveEvents) {
|
|
722
|
-
const cat = ev.category || ev.type || 'system';
|
|
723
|
-
const ts = ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString() : '';
|
|
724
|
-
const title = ev.title || ev.message || '';
|
|
725
|
-
const detail = ev.detail || '';
|
|
726
|
-
const msg = (title && detail) ? `${title} โ ${detail}` : (title || detail || JSON.stringify(ev.data || '').slice(0, 80));
|
|
727
|
-
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>`;
|
|
728
|
-
}
|
|
729
|
-
c.innerHTML = html;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
733
|
-
// โโ CHARTS (uPlot) โโ
|
|
734
|
-
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
735
|
-
|
|
736
|
-
const CC = {
|
|
737
|
-
done:'#3fb950', inProg:'#58a6ff', blocked:'#f85149', open:'#484f58', agents:'#bc8cff',
|
|
738
|
-
raw:'#58a6ff', ctx:'#3fb950', hitRate:'#bc8cff', deploy:'#58a6ff', blockRate:'#f85149',
|
|
40
|
+
// Server injects these at serve time (same-origin only). The token gates
|
|
41
|
+
// every mutation; the refresh cadence matches `uap dash serve --refresh`.
|
|
42
|
+
window.__UAP_BOOT = {
|
|
43
|
+
token: '__UAP_DASHBOARD_TOKEN__'.indexOf('__UAP') === 0 ? '' : '__UAP_DASHBOARD_TOKEN__',
|
|
44
|
+
refreshMs: Number('__UAP_DASH_REFRESH_MS__') > 0 ? Number('__UAP_DASH_REFRESH_MS__') : 2000,
|
|
739
45
|
};
|
|
740
|
-
|
|
741
|
-
function tsUnix(arr) { return arr.map(p => new Date(p.timestamp).getTime() / 1000); }
|
|
742
|
-
|
|
743
|
-
function addTooltip(u, colors, labels, fmtVal) {
|
|
744
|
-
const tt = document.createElement('div');
|
|
745
|
-
tt.className = 'u-tooltip'; tt.style.display = 'none';
|
|
746
|
-
u.root.appendChild(tt);
|
|
747
|
-
u.hooks.setCursor = u.hooks.setCursor || [];
|
|
748
|
-
u.hooks.setCursor.push(() => {
|
|
749
|
-
const { idx, left, top } = u.cursor;
|
|
750
|
-
if (idx == null) { tt.style.display = 'none'; return; }
|
|
751
|
-
const date = new Date(u.data[0][idx] * 1000);
|
|
752
|
-
let rows = `<div class="tt-time">${date.toLocaleTimeString()}</div>`;
|
|
753
|
-
for (let i = 1; i < u.series.length; i++) {
|
|
754
|
-
if (!u.series[i].show) continue;
|
|
755
|
-
const val = u.data[i][idx];
|
|
756
|
-
if (val == null) continue;
|
|
757
|
-
const c = colors[i-1] || '#fff', l = labels[i-1] || '';
|
|
758
|
-
const v = fmtVal ? fmtVal(val, i) : val.toLocaleString();
|
|
759
|
-
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>`;
|
|
760
|
-
}
|
|
761
|
-
tt.innerHTML = rows; tt.style.display = '';
|
|
762
|
-
tt.style.left = (left + 16) + 'px'; tt.style.top = Math.max(0, top - 10) + 'px';
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function mkChart(elId, ts, extractors, opts) {
|
|
767
|
-
if (!uplotReady || !ts || ts.length < 2) return null;
|
|
768
|
-
const el = document.getElementById(elId);
|
|
769
|
-
if (!el) return null;
|
|
770
|
-
el.innerHTML = '';
|
|
771
|
-
const timestamps = tsUnix(ts);
|
|
772
|
-
const data = [timestamps, ...extractors.map(fn => ts.map(fn))];
|
|
773
|
-
const u = new uPlot(opts(el.clientWidth), data, el);
|
|
774
|
-
return u;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function initChartTasks(ts) {
|
|
778
|
-
const colors = [CC.done, CC.inProg, CC.blocked, CC.open, CC.agents];
|
|
779
|
-
const labels = ['Done', 'In Progress', 'Blocked', 'Open', 'Active Agents'];
|
|
780
|
-
const extractors = [
|
|
781
|
-
p => p.tasks?.done || 0, p => p.tasks?.inProgress || 0,
|
|
782
|
-
p => p.tasks?.blocked || 0, p => p.tasks?.open || 0,
|
|
783
|
-
p => p.coordination?.activeAgents || 0,
|
|
784
|
-
];
|
|
785
|
-
const u = mkChart('chart-tasks', ts, extractors, (w) => ({
|
|
786
|
-
width: w, height: 260,
|
|
787
|
-
cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
|
|
788
|
-
scales: { x: { time: true }, y: { min: 0 }, agents: { min: 0 } },
|
|
789
|
-
axes: [
|
|
790
|
-
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' },
|
|
791
|
-
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace', size: 50 },
|
|
792
|
-
{ stroke: CC.agents, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'agents' },
|
|
793
|
-
],
|
|
794
|
-
series: [
|
|
795
|
-
{},
|
|
796
|
-
{ label: 'Done', stroke: CC.done, fill: CC.done+'30', width: 2 },
|
|
797
|
-
{ label: 'In Prog', stroke: CC.inProg, fill: CC.inProg+'30', width: 2 },
|
|
798
|
-
{ label: 'Blocked', stroke: CC.blocked, fill: CC.blocked+'20', width: 2 },
|
|
799
|
-
{ label: 'Open', stroke: CC.open, fill: CC.open+'20', width: 1, dash: [4,2] },
|
|
800
|
-
{ label: 'Agents', stroke: CC.agents, width: 2, dash: [6,3], scale: 'agents' },
|
|
801
|
-
],
|
|
802
|
-
}));
|
|
803
|
-
if (u) addTooltip(u, colors, labels);
|
|
804
|
-
return u;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function initChartCompression(ts) {
|
|
808
|
-
const colors = [CC.raw, CC.ctx, CC.hitRate];
|
|
809
|
-
const labels = ['Raw KB', 'Compressed KB', 'Hit Rate %'];
|
|
810
|
-
const parseHR = p => { const h = p.memoryHitsMisses?.hitRate; return typeof h === 'string' ? parseFloat(h)||0 : h||0; };
|
|
811
|
-
const extractors = [
|
|
812
|
-
p => Math.round((p.compression?.rawBytes||0)/1024),
|
|
813
|
-
p => Math.round((p.compression?.contextBytes||0)/1024),
|
|
814
|
-
parseHR,
|
|
815
|
-
];
|
|
816
|
-
const u = mkChart('chart-compression', ts, extractors, (w) => ({
|
|
817
|
-
width: w, height: 260,
|
|
818
|
-
cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
|
|
819
|
-
scales: { x: { time: true }, y: { min: 0 }, pct: { min: 0, max: 100 } },
|
|
820
|
-
axes: [
|
|
821
|
-
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' },
|
|
822
|
-
{ stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace', size: 50, values: (_,v) => v.map(x => x+' KB') },
|
|
823
|
-
{ stroke: CC.hitRate, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'pct', values: (_,v) => v.map(x => x+'%') },
|
|
824
|
-
],
|
|
825
|
-
series: [
|
|
826
|
-
{},
|
|
827
|
-
{ label: 'Raw', stroke: CC.raw, fill: CC.raw+'20', width: 2 },
|
|
828
|
-
{ label: 'Compressed', stroke: CC.ctx, fill: CC.ctx+'20', width: 2 },
|
|
829
|
-
{ label: 'Hit Rate', stroke: CC.hitRate, width: 2, dash: [6,3], scale: 'pct' },
|
|
830
|
-
],
|
|
831
|
-
}));
|
|
832
|
-
if (u) addTooltip(u, colors, labels, (v,i) => i===3 ? v+'%' : v+' KB');
|
|
833
|
-
return u;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function initSparkline(elId, ts, fn, color, label) {
|
|
837
|
-
if (!uplotReady || !ts || ts.length < 2) return null;
|
|
838
|
-
const el = document.getElementById(elId);
|
|
839
|
-
if (!el) return null;
|
|
840
|
-
el.innerHTML = '';
|
|
841
|
-
const timestamps = tsUnix(ts);
|
|
842
|
-
const values = ts.map(fn);
|
|
843
|
-
const data = [timestamps, values];
|
|
844
|
-
const u = new uPlot({
|
|
845
|
-
width: el.clientWidth, height: 60,
|
|
846
|
-
cursor: { sync: { key: 'uap' }, focus: { prox: 30 }, points: { show: false } },
|
|
847
|
-
legend: { show: false },
|
|
848
|
-
scales: { x: { time: true }, y: { min: 0 } },
|
|
849
|
-
axes: [{ show: false }, { show: false }],
|
|
850
|
-
series: [{}, { stroke: color, fill: color+'25', width: 1.5 }],
|
|
851
|
-
}, data, el);
|
|
852
|
-
// Sparkline tooltip
|
|
853
|
-
const tt = document.createElement('div');
|
|
854
|
-
tt.className = 'u-tooltip'; tt.style.display = 'none';
|
|
855
|
-
u.root.appendChild(tt);
|
|
856
|
-
u.hooks.setCursor = u.hooks.setCursor || [];
|
|
857
|
-
u.hooks.setCursor.push(() => {
|
|
858
|
-
const { idx } = u.cursor;
|
|
859
|
-
if (idx == null) { tt.style.display = 'none'; return; }
|
|
860
|
-
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>`;
|
|
861
|
-
tt.style.display = ''; tt.style.left = (u.cursor.left+12)+'px'; tt.style.top = '0px';
|
|
862
|
-
});
|
|
863
|
-
return u;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
let chartsInit = false;
|
|
867
|
-
// Seed each uPlot legend to the LATEST data point so the values read as
|
|
868
|
-
// live numbers instead of '--' until the user hovers. Fail-safe per chart.
|
|
869
|
-
function seedLegends() {
|
|
870
|
-
[chartTasks, chartCompression, chartDeploy, chartHitrate, chartCompliance].forEach(u => {
|
|
871
|
-
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 */ }
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
function updateAllCharts(ts) {
|
|
875
|
-
if (!uplotReady || !ts || ts.length < 2) return;
|
|
876
|
-
const parseHR = p => { const h=p.memoryHitsMisses?.hitRate; return typeof h==='string'?parseFloat(h)||0:h||0; };
|
|
877
|
-
const parseBR = p => { const b=p.compliance?.blockRate; return typeof b==='string'?parseFloat(b)||0:b||0; };
|
|
878
|
-
if (!chartsInit) {
|
|
879
|
-
chartTasks = initChartTasks(ts);
|
|
880
|
-
chartCompression = initChartCompression(ts);
|
|
881
|
-
chartDeploy = initSparkline('chart-deploy', ts, p=>(p.deployBuckets?.queued||0)+(p.deployBuckets?.executing||0)+(p.deployBuckets?.batched||0), CC.deploy, 'Active Deploys');
|
|
882
|
-
chartHitrate = initSparkline('chart-hitrate', ts, parseHR, CC.hitRate, 'Hit Rate %');
|
|
883
|
-
chartCompliance = initSparkline('chart-compliance', ts, parseBR, CC.blockRate, 'Block Rate %');
|
|
884
|
-
chartsInit = true;
|
|
885
|
-
seedLegends();
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
// Update data
|
|
889
|
-
const timestamps = tsUnix(ts);
|
|
890
|
-
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)]);
|
|
891
|
-
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)]);
|
|
892
|
-
if (chartDeploy) chartDeploy.setData([timestamps, ts.map(p=>(p.deployBuckets?.queued||0)+(p.deployBuckets?.executing||0)+(p.deployBuckets?.batched||0))]);
|
|
893
|
-
if (chartHitrate) chartHitrate.setData([timestamps, ts.map(parseHR)]);
|
|
894
|
-
if (chartCompliance) chartCompliance.setData([timestamps, ts.map(parseBR)]);
|
|
895
|
-
seedLegends();
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
899
|
-
// โโ MAIN RENDER โโ
|
|
900
|
-
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
901
|
-
|
|
902
|
-
function render(data) {
|
|
903
|
-
if (!data) return;
|
|
904
|
-
const _errors = [];
|
|
905
|
-
function safe(label, fn) { try { fn(); } catch(e) { _errors.push(label+': '+(e.message||e)); console.error('Render ['+label+']:', e); } }
|
|
906
|
-
|
|
907
|
-
const sys = data.system || {};
|
|
908
|
-
const tasks = data.tasks || {};
|
|
909
|
-
const coord = data.coordination || {};
|
|
910
|
-
const models = data.models || { roles:{}, sessionUsage:[], totalCost:0, strategy:'unknown' };
|
|
911
|
-
const mem = data.memory || {};
|
|
912
|
-
const l1 = mem.l1 || { entries:0, sizeKB:0 };
|
|
913
|
-
const l2 = mem.l2 || { entries:0 };
|
|
914
|
-
const l3 = mem.l3 || { status:'Unknown', uptime:'' };
|
|
915
|
-
const l4 = mem.l4 || { entities:0, relationships:0 };
|
|
916
|
-
const comp = mem.compression || { rawBytes:0, contextBytes:0, savingsPercent:'0%', totalCalls:0 };
|
|
917
|
-
const hm = mem.hitsMisses || { hits:0, misses:0, hitRate:'N/A' };
|
|
918
|
-
const policies = data.policies || [];
|
|
919
|
-
const policyFiles = data.policyFiles || [];
|
|
920
|
-
const audit = data.auditTrail || [];
|
|
921
|
-
const timeSeries = data.timeSeries || [];
|
|
922
|
-
const compliance = data.compliance || {};
|
|
923
|
-
const session = data.session || null;
|
|
924
|
-
|
|
925
|
-
safe('timestamp', () => { if (data.timestamp) setText('data-ts', new Date(data.timestamp).toLocaleTimeString()); });
|
|
926
|
-
|
|
927
|
-
safe('system', () => {
|
|
928
|
-
setText('sys-version', sys.version || '?');
|
|
929
|
-
setText('sys-branch', sys.branch || '?');
|
|
930
|
-
const el = document.getElementById('sys-dirty');
|
|
931
|
-
if (el) { const d = sys.dirty||0; el.textContent = d > 0 ? d+' files' : 'clean'; el.className = 'val '+(d > 0 ? 'yellow' : 'green'); }
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
safe('charts', () => updateAllCharts(timeSeries));
|
|
935
|
-
|
|
936
|
-
safe('coordination', () => {
|
|
937
|
-
setText('coord-agents', coord.activeAgents||0);
|
|
938
|
-
setText('coord-total-agents', coord.totalAgents||0);
|
|
939
|
-
setText('coord-completed-agents', coord.completedAgents||0);
|
|
940
|
-
setText('coord-claims', coord.activeClaims||0);
|
|
941
|
-
setText('coord-deploys', coord.pendingDeploys||0);
|
|
942
|
-
setText('coord-worktrees', coord.activeWorktrees||0);
|
|
943
|
-
setText('coord-pattern-hits', coord.patternHits||0);
|
|
944
|
-
setText('coord-pattern-successes', coord.patternSuccesses||0);
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
safe('agents-table', () => {
|
|
948
|
-
const agents = coord.agents || [];
|
|
949
|
-
if (agents.length > 0) {
|
|
950
|
-
let h = '<table><tr><th>ID</th><th>Name</th><th>Status</th><th>Started</th></tr>';
|
|
951
|
-
for (const a of agents) {
|
|
952
|
-
const cls = a.status==='active'?'active':a.status==='completed'?'on':'idle';
|
|
953
|
-
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>`;
|
|
954
|
-
}
|
|
955
|
-
document.getElementById('coord-agents-table').innerHTML = h + '</table>';
|
|
956
|
-
} else { document.getElementById('coord-agents-table').innerHTML = '<div class="empty">No agents registered</div>'; }
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
safe('deploy', () => {
|
|
960
|
-
const d = data.deployBuckets || {};
|
|
961
|
-
const total = d.totalActions || (d.done||0)+(d.failed||0)+(d.queued||0)+(d.batched||0)+(d.executing||0) || 1;
|
|
962
|
-
const max = Math.max(total, 1);
|
|
963
|
-
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; };
|
|
964
|
-
setBar('deploy-queued',d.queued||0); setBar('deploy-batched',d.batched||0);
|
|
965
|
-
setBar('deploy-executing',d.executing||0); setBar('deploy-done',d.done||0); setBar('deploy-failed',d.failed||0);
|
|
966
|
-
setText('deploy-batch-count',d.batchCount||0); setText('deploy-saved-ops',d.savedOps||0);
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
safe('models', () => {
|
|
970
|
-
// Roles
|
|
971
|
-
let rh = '';
|
|
972
|
-
for (const [role, model] of Object.entries(models.roles||{})) {
|
|
973
|
-
const c = role==='planner'?'green':role==='executor'?'cyan':role==='reviewer'?'purple':'';
|
|
974
|
-
rh += `<div class="kv"><span class="label">${esc(capitalize(role))}</span><span class="value ${c}">${model?esc(modelName(String(model))):'โ'}</span></div>`;
|
|
975
|
-
}
|
|
976
|
-
document.getElementById('model-roles').innerHTML = rh;
|
|
977
|
-
|
|
978
|
-
// Router
|
|
979
|
-
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>`;
|
|
980
|
-
|
|
981
|
-
// Cost Optimization
|
|
982
|
-
const co = models.costOptimization || {};
|
|
983
|
-
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.toFixed(0)+'%':'N/A'}</span></div><div class="kv"><span class="label">Max Degradation</span><span class="value">${co.maxPerformanceDegradation?co.maxPerformanceDegradation.toFixed(0)+'%':'N/A'}</span></div>`;
|
|
984
|
-
|
|
985
|
-
// Routing Matrix
|
|
986
|
-
const mx = Object.entries(models.routingMatrix||{});
|
|
987
|
-
if (mx.length > 0) {
|
|
988
|
-
let h = '<table><tr><th>Task Type</th><th>Planner</th><th>Executor</th></tr>';
|
|
989
|
-
for (const [tt, m] of mx) {
|
|
990
|
-
const isObj = m && typeof m === 'object';
|
|
991
|
-
const planner = isObj ? (m.planner||'-') : '-';
|
|
992
|
-
const executor = isObj ? (m.executor||'-') : String(m);
|
|
993
|
-
h += `<tr><td>${esc(tt)}</td><td style="color:var(--green)">${esc(modelName(planner))}</td><td style="color:var(--cyan)">${esc(modelName(executor))}</td></tr>`;
|
|
994
|
-
}
|
|
995
|
-
document.getElementById('model-routing-matrix').innerHTML = h+'</table>';
|
|
996
|
-
} else { document.getElementById('model-routing-matrix').innerHTML = '<div class="empty">No routing matrix</div>'; }
|
|
997
|
-
|
|
998
|
-
// Routing Decisions
|
|
999
|
-
const rd = models.recentRoutingDecisions || [];
|
|
1000
|
-
if (rd.length > 0) {
|
|
1001
|
-
let h = '<table><tr><th>Time</th><th>Model</th><th>Task</th><th>Tokens</th><th>Cost</th><th>Result</th></tr>';
|
|
1002
|
-
for (const r of rd.slice(0,10)) {
|
|
1003
|
-
const t = r.timestamp?r.timestamp.slice(11,19):'-';
|
|
1004
|
-
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>`;
|
|
1005
|
-
}
|
|
1006
|
-
document.getElementById('model-routes').innerHTML = h+'</table>';
|
|
1007
|
-
} else { document.getElementById('model-routes').innerHTML = '<div class="empty">No routing decisions yet</div>'; }
|
|
1008
|
-
|
|
1009
|
-
// Usage table
|
|
1010
|
-
const usage = models.sessionUsage || [];
|
|
1011
|
-
if (usage.length > 0) {
|
|
1012
|
-
let h = '<table><tr><th>Model</th><th>Tasks</th><th>In</th><th>Out</th><th>Cost</th><th>Success</th></tr>';
|
|
1013
|
-
for (const u of usage) {
|
|
1014
|
-
const rate = typeof u.successRate==='number'?(u.successRate*100).toFixed(0):'0';
|
|
1015
|
-
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>`;
|
|
1016
|
-
}
|
|
1017
|
-
document.getElementById('model-usage').innerHTML = h+'</table>';
|
|
1018
|
-
} else { document.getElementById('model-usage').innerHTML = '<div class="empty">No usage recorded</div>'; }
|
|
1019
|
-
setText('model-cost', '$'+(typeof models.totalCost==='number'?models.totalCost.toFixed(4):'0.0000'));
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
safe('memory', () => {
|
|
1023
|
-
setText('mem-l1', l1.entries+' entries ('+l1.sizeKB+' KB)');
|
|
1024
|
-
setText('mem-l2', l2.entries+' entries');
|
|
1025
|
-
const l3El = document.getElementById('mem-l3');
|
|
1026
|
-
if (l3El) { l3El.textContent = l3.status==='Running'?'Qdrant '+(l3.uptime||''):l3.status||'Stopped'; l3El.className = 'value '+(l3.status==='Running'?'green':'yellow'); }
|
|
1027
|
-
setText('mem-l4', l4.entities+' entities, '+l4.relationships+' rels');
|
|
1028
|
-
setText('mem-hits', hm.hits||0);
|
|
1029
|
-
setText('mem-misses', hm.misses||0);
|
|
1030
|
-
const hrStr = String(hm.hitRate||'N/A');
|
|
1031
|
-
const hrVal = parseFloat(hrStr)||0;
|
|
1032
|
-
setText('mem-hitrate', hrStr.includes('%')?hrStr:hrStr==='N/A'?hrStr:hrStr+'%');
|
|
1033
|
-
const g = document.getElementById('mem-hitrate-gauge');
|
|
1034
|
-
if (g) { g.style.width = Math.min(hrVal,100)+'%'; g.style.background = hrVal>80?'var(--green)':hrVal>50?'var(--yellow)':'var(--red)'; }
|
|
1035
|
-
const hrEl = document.getElementById('mem-hitrate');
|
|
1036
|
-
if (hrEl) hrEl.className = 'value '+(hrVal>80?'green':hrVal>50?'yellow':'red');
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
safe('compression', () => {
|
|
1040
|
-
setText('comp-raw', Math.round((comp.rawBytes||0)/1024)+' KB');
|
|
1041
|
-
setText('comp-ctx', Math.round((comp.contextBytes||0)/1024)+' KB');
|
|
1042
|
-
setText('comp-savings', comp.savingsPercent||'0%');
|
|
1043
|
-
setText('comp-calls', comp.totalCalls||0);
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
safe('memory-queries', () => {
|
|
1047
|
-
const rq = mem.recentQueries || [];
|
|
1048
|
-
if (rq.length > 0) {
|
|
1049
|
-
let h = '<table><tr><th>Type</th><th>Query</th><th>Time</th></tr>';
|
|
1050
|
-
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>`;
|
|
1051
|
-
document.getElementById('memory-recent-queries').innerHTML = h+'</table>';
|
|
1052
|
-
} else { document.getElementById('memory-recent-queries').innerHTML = '<div class="empty">No recent queries</div>'; }
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
safe('session', () => {
|
|
1056
|
-
const sp = document.getElementById('session-panel');
|
|
1057
|
-
if (!session) { sp.style.display = 'none'; return; }
|
|
1058
|
-
sp.style.display = '';
|
|
1059
|
-
setText('sess-id', (session.sessionId||'').slice(0,12));
|
|
1060
|
-
setText('sess-uptime', session.uptime||'-');
|
|
1061
|
-
setText('sess-tokens', fmt(session.tokensUsed||0));
|
|
1062
|
-
setText('sess-saved', fmt(session.tokensSaved||0));
|
|
1063
|
-
setText('sess-tools', session.toolCalls||0);
|
|
1064
|
-
setText('sess-checks', session.policyChecks||0);
|
|
1065
|
-
setText('sess-blocks', session.policyBlocks||0);
|
|
1066
|
-
setText('sess-errors', session.errors||0);
|
|
1067
|
-
const td = document.getElementById('sess-token-io-detail');
|
|
1068
|
-
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>` : '';
|
|
1069
|
-
setText('sess-cost', '$'+(typeof session.totalCostUsd==='number'?session.totalCostUsd.toFixed(4):'0.0000'));
|
|
1070
|
-
setText('sess-cost-without', '$'+(typeof session.estimatedCostWithoutUap==='number'?session.estimatedCostWithoutUap.toFixed(4):'0.0000'));
|
|
1071
|
-
setText('sess-savings-pct', typeof session.costSavingsPercent==='number'?session.costSavingsPercent.toFixed(0)+'%':'0%');
|
|
1072
|
-
const pw = document.getElementById('sess-progress-wrap');
|
|
1073
|
-
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)+'%'; }
|
|
1074
|
-
else { pw.style.display='none'; }
|
|
1075
|
-
|
|
1076
|
-
const agents = session.agents||[];
|
|
1077
|
-
if (agents.length > 0) {
|
|
1078
|
-
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>`; }
|
|
1079
|
-
document.getElementById('sess-agents').innerHTML = ah;
|
|
1080
|
-
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>';
|
|
1081
|
-
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>`; }
|
|
1082
|
-
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);
|
|
1083
|
-
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>`;
|
|
1084
|
-
document.getElementById('sess-agent-breakdown').innerHTML = bh;
|
|
1085
|
-
} 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>'; }
|
|
1086
|
-
|
|
1087
|
-
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>';
|
|
1088
|
-
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>';
|
|
1089
|
-
const batch = session.deployBatchSummary;
|
|
1090
|
-
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>';
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
safe('session-history', () => {
|
|
1094
|
-
const sessions = data.sessions||[];
|
|
1095
|
-
const hp = document.getElementById('session-history-panel');
|
|
1096
|
-
if (!sessions.length) { hp.style.display='none'; return; }
|
|
1097
|
-
hp.style.display='';
|
|
1098
|
-
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>';
|
|
1099
|
-
for (const s of sessions) {
|
|
1100
|
-
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>':'';
|
|
1101
|
-
const tI=s.tokensIn||0,tO=s.tokensOut||0;
|
|
1102
|
-
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>`;
|
|
1103
|
-
}
|
|
1104
|
-
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);
|
|
1105
|
-
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>`;
|
|
1106
|
-
document.getElementById('session-history-table').innerHTML = h;
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
// Unified Policies
|
|
1110
|
-
safe('policies', () => {
|
|
1111
|
-
const pm = new Map();
|
|
1112
|
-
for (const p of policies) pm.set(p.name||p.id, {...p, source:'db'});
|
|
1113
|
-
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'}); }
|
|
1114
|
-
const all = [...pm.values()];
|
|
1115
|
-
if (all.length > 0) {
|
|
1116
|
-
let h='<table><tr><th>Name</th><th>Category</th><th>Level</th><th>Stage</th><th>Status</th><th>Actions</th></tr>';
|
|
1117
|
-
for (const p of all) {
|
|
1118
|
-
const isDb = p.source==='db';
|
|
1119
|
-
const status = isDb ? `<span class="badge ${p.isActive?'on':'off'}">${p.isActive?'ON':'OFF'}</span>` : '<span class="badge file-only">FILE</span>';
|
|
1120
|
-
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>';
|
|
1121
|
-
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>`;
|
|
1122
|
-
}
|
|
1123
|
-
document.getElementById('policy-table').innerHTML = h+'</table>';
|
|
1124
|
-
const stages = {'pre-exec':0,'post-exec':0,review:0,always:0};
|
|
1125
|
-
for (const p of all) if (p.enforcementStage && stages[p.enforcementStage]!=null) stages[p.enforcementStage]++;
|
|
1126
|
-
const maxS = Math.max(...Object.values(stages),1);
|
|
1127
|
-
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>`;
|
|
1128
|
-
document.getElementById('policy-stages').innerHTML = sh;
|
|
1129
|
-
} else { document.getElementById('policy-table').innerHTML='<div class="empty">No policies</div>'; document.getElementById('policy-stages').innerHTML=''; }
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
safe('compliance', () => {
|
|
1133
|
-
const fbm = compliance.failuresByMechanism || {};
|
|
1134
|
-
const me = Object.entries(fbm);
|
|
1135
|
-
if (me.length > 0) {
|
|
1136
|
-
const max = Math.max(...me.map(([,v])=>v),1);
|
|
1137
|
-
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>`;
|
|
1138
|
-
document.getElementById('compliance-mechanisms').innerHTML = h;
|
|
1139
|
-
} else { document.getElementById('compliance-mechanisms').innerHTML='<div class="empty">No failure mechanisms</div>'; }
|
|
1140
|
-
|
|
1141
|
-
const rf = compliance.recentFailures || [];
|
|
1142
|
-
if (rf.length > 0) {
|
|
1143
|
-
let h='<table><tr><th>Time</th><th>Policy</th><th>Op</th><th>Mechanism</th><th>Reason</th></tr>';
|
|
1144
|
-
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>`;
|
|
1145
|
-
document.getElementById('compliance-failures').innerHTML = h+'</table>';
|
|
1146
|
-
} else { document.getElementById('compliance-failures').innerHTML='<div class="empty">No recent failures</div>'; }
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
safe('audit', () => {
|
|
1150
|
-
if (audit.length > 0) {
|
|
1151
|
-
let h=''; for (const e of audit.slice(0,15)) {
|
|
1152
|
-
const cls=e.allowed?'pass':'block', ts=typeof e.executedAt==='string'&&e.executedAt.length>=19?e.executedAt.slice(11,19):e.executedAt||'-';
|
|
1153
|
-
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>`;
|
|
1154
|
-
}
|
|
1155
|
-
document.getElementById('audit-trail').innerHTML = h;
|
|
1156
|
-
} else { document.getElementById('audit-trail').innerHTML='<div class="empty">No audit entries</div>'; }
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
safe('kanban', () => renderKanban(tasks));
|
|
1160
|
-
safe('savings', () => renderSavings(data.savingsByInfluence));
|
|
1161
|
-
safe('orchestration', () => renderOrchestration(data.orchestrationTree));
|
|
1162
|
-
|
|
1163
|
-
safe('performance', () => {
|
|
1164
|
-
const hp = (data.performance||{}).hotPaths || [];
|
|
1165
|
-
const pp = document.getElementById('perf-panel');
|
|
1166
|
-
if (!pp) return;
|
|
1167
|
-
if (hp.length > 0) {
|
|
1168
|
-
let h='<table><tr><th>Operation</th><th>Avg (ms)</th><th>P95 (ms)</th><th>Calls</th></tr>';
|
|
1169
|
-
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>`;
|
|
1170
|
-
pp.innerHTML = h+'</table>';
|
|
1171
|
-
} else { pp.innerHTML='<div class="empty">No performance data</div>'; }
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
if (_errors.length > 0) {
|
|
1175
|
-
let eb = document.getElementById('render-errors');
|
|
1176
|
-
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); }
|
|
1177
|
-
eb.innerHTML='<strong>Render errors:</strong> '+_errors.map(e=>esc(e)).join(' | '); eb.style.display='';
|
|
1178
|
-
} else { const eb=document.getElementById('render-errors'); if(eb) eb.style.display='none'; }
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// โโ API Actions โโ
|
|
1182
|
-
async function togglePolicy(id) { try { const r=await fetch(`${API_URL}/api/policy/${id}/toggle`,{method:'POST',headers:MUTATION_HEADERS}); 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'); } }
|
|
1183
|
-
async function setStage(id,stage) { try { const r=await fetch(`${API_URL}/api/policy/${id}/stage`,{method:'POST',headers:{'Content-Type':'application/json',...MUTATION_HEADERS},body:JSON.stringify({stage})}); if(!r.ok)throw new Error(); showToast(`Stage: ${stage}`,'success'); } catch { showToast('Failed to set stage','error'); } }
|
|
1184
|
-
async function setLevel(id,level) { try { const r=await fetch(`${API_URL}/api/policy/${id}/level`,{method:'POST',headers:{'Content-Type':'application/json',...MUTATION_HEADERS},body:JSON.stringify({level})}); if(!r.ok)throw new Error(); showToast(`Level: ${level}`,'success'); } catch { showToast('Failed to set level','error'); } }
|
|
1185
|
-
|
|
1186
|
-
// โโ Kanban โโ
|
|
1187
|
-
const TYPE_ICONS = {task:'\u25C6',bug:'\uD83D\uDC1B',feature:'\u2728',epic:'\uD83C\uDFAF',chore:'\uD83D\uDD27',story:'\uD83D\uDCD6'};
|
|
1188
|
-
const PRIO_LABELS = ['P0','P1','P2','P3','P4'];
|
|
1189
|
-
let prevCardMap = new Map();
|
|
1190
|
-
function renderSavings(sv) {
|
|
1191
|
-
sv = sv || { influences: [], totalTokensSaved: 0, totalCostSavedUsd: 0 };
|
|
1192
|
-
const badge = (q) => {
|
|
1193
|
-
const c = q === 'measured' ? '#3fb950' : q === 'estimated' ? '#d29922' : '#484f58';
|
|
1194
|
-
return `<span style="color:${c};font-size:10px;text-transform:uppercase;border:1px solid ${c};border-radius:3px;padding:1px 5px">${q}</span>`;
|
|
1195
|
-
};
|
|
1196
|
-
setHTML('savings-summary',
|
|
1197
|
-
`<strong style="color:#58a6ff;font-size:18px">${fmt(sv.totalTokensSaved)}</strong> tokens ยท ` +
|
|
1198
|
-
`<strong style="color:#3fb950;font-size:18px">$${(sv.totalCostSavedUsd||0).toFixed(2)}</strong> saved across all UAP influences`);
|
|
1199
|
-
if (!sv.influences.length) { setHTML('savings-table', '<div class="empty">No savings data</div>'); return; }
|
|
1200
|
-
// Unmeasured influences (e.g. routing with no UAP-driven calls yet) render an
|
|
1201
|
-
// explicit dimmed "โ" rather than a real-looking $0, so idle != broken.
|
|
1202
|
-
const idleCell = '<span style="color:var(--fg3)" title="not active / no data recorded yet">\u2014</span>';
|
|
1203
|
-
const rows = sv.influences.map(i => {
|
|
1204
|
-
const idle = i.quality === 'unmeasured';
|
|
1205
|
-
const tok = idle ? idleCell : fmt(i.tokensSaved);
|
|
1206
|
-
const cost = idle ? idleCell : `<span style="color:#3fb950">$${(i.costSavedUsd||0).toFixed(4)}</span>`;
|
|
1207
|
-
return `<tr${idle ? ' style="opacity:.55"' : ''}><td>${esc(i.influence)}</td>` +
|
|
1208
|
-
`<td style="text-align:right">${tok}</td>` +
|
|
1209
|
-
`<td style="text-align:right">${cost}</td>` +
|
|
1210
|
-
`<td>${badge(i.quality)}</td><td style="color:var(--fg3);font-size:11px">${esc(i.detail||'')}</td></tr>`;
|
|
1211
|
-
}).join('');
|
|
1212
|
-
setHTML('savings-table',
|
|
1213
|
-
`<table><thead><tr><th>Influence</th><th style="text-align:right">Tokens saved</th>` +
|
|
1214
|
-
`<th style="text-align:right">Cost saved</th><th>Quality</th><th>Detail</th></tr></thead><tbody>${rows}</tbody></table>`);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function orchNode(n, depth) {
|
|
1218
|
-
const pad = depth * 16;
|
|
1219
|
-
const sc = { done:'#3fb950', in_progress:'#58a6ff', blocked:'#f85149', failed:'#f85149', open:'#8b949e', pending:'#8b949e' }[n.status] || '#8b949e';
|
|
1220
|
-
const icon = n.type === 'epic' ? '๐ฏ' : n.type === 'feature' ? 'โจ' : n.type === 'phase' ? 'โถ' : 'โข';
|
|
1221
|
-
const agents = (n.agents && n.agents.length) ? ` <span style="color:#a371f7;font-size:10px">[${n.agents.join(', ')}]</span>` : '';
|
|
1222
|
-
let html = `<div style="padding:3px 0 3px ${pad}px;border-left:1px solid var(--border)">` +
|
|
1223
|
-
`<span style="color:${sc}">โ</span> ${icon} <span>${esc((n.title||n.id).slice(0,70))}</span>` +
|
|
1224
|
-
` <span style="color:var(--fg3);font-size:10px">${n.status}</span>${agents}</div>`;
|
|
1225
|
-
for (const c of (n.children||[])) html += orchNode(c, depth + 1);
|
|
1226
|
-
return html;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
function renderOrchestration(ot) {
|
|
1230
|
-
ot = ot || { missions: [], ledger: null, agents: [], hasHierarchy: false };
|
|
1231
|
-
setText('orch-label', `${ot.missions.length} root(s) ยท ${ot.agents.length} agent(s)`);
|
|
1232
|
-
let html = '';
|
|
1233
|
-
if (ot.ledger) {
|
|
1234
|
-
const L = ot.ledger;
|
|
1235
|
-
html += `<div style="margin-bottom:14px;padding:10px;background:var(--bg2);border-radius:6px">` +
|
|
1236
|
-
`<div style="font-weight:600;margin-bottom:6px">Active build: ${L.mission.slice(0,80)} ` +
|
|
1237
|
-
`<span style="color:#58a6ff">${L.done}/${L.total} (${L.pct}%)</span></div>`;
|
|
1238
|
-
html += L.items.map(it => {
|
|
1239
|
-
const sc = { done:'#3fb950', in_progress:'#58a6ff', failed:'#f85149', pending:'#8b949e' }[it.status] || '#8b949e';
|
|
1240
|
-
const dep = it.deps && it.deps.length ? ` <span style="color:var(--fg3);font-size:10px">โ ${it.deps.join(',')}</span>` : '';
|
|
1241
|
-
return `<div style="padding:2px 0"><span style="color:${sc}">โ</span> ${it.kind==='epic'?'๐ฏ':'โข'} ${esc(it.title.slice(0,70))} <span style="color:var(--fg3);font-size:10px">${it.status}</span>${dep}</div>`;
|
|
1242
|
-
}).join('');
|
|
1243
|
-
html += `</div>`;
|
|
1244
|
-
}
|
|
1245
|
-
const withKids = ot.missions.filter(m => (m.children||[]).length > 0);
|
|
1246
|
-
const shown = (withKids.length ? withKids : ot.missions).slice(0, 20);
|
|
1247
|
-
if (!ot.ledger && shown.length === 0) { setHTML('orch-tree', '<div class="empty">No orchestrations yet โ run an epic/orchestrated build</div>'); return; }
|
|
1248
|
-
html += shown.map(m => orchNode(m, 0)).join('');
|
|
1249
|
-
setHTML('orch-tree', html);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function renderKanban(tasks) {
|
|
1253
|
-
const items=tasks.items||[], cols={open:[],in_progress:[],blocked:[],done:[],wont_do:[]}, ncm=new Map();
|
|
1254
|
-
// A "family" is a group with real hierarchy anywhere on the board \u2014
|
|
1255
|
-
// those get headers + indenting; standalone tasks keep the flat look.
|
|
1256
|
-
const groupCounts=new Map(), families=new Set();
|
|
1257
|
-
for (const i of items) {
|
|
1258
|
-
if(cols[i.status]) cols[i.status].push(i); ncm.set(i.id,i.status);
|
|
1259
|
-
const g=i.groupId||i.id; groupCounts.set(g,(groupCounts.get(g)||0)+1);
|
|
1260
|
-
if((i.depth||0)>0) families.add(g);
|
|
1261
|
-
}
|
|
1262
|
-
for (const [g,n] of groupCounts) if(n>1) families.add(g);
|
|
1263
|
-
setText('kb-open-count',cols.open.length); setText('kb-progress-count',cols.in_progress.length);
|
|
1264
|
-
setText('kb-blocked-count',cols.blocked.length); setText('kb-done-count',cols.done.length); setText('kb-wontdo-count',cols.wont_do.length);
|
|
1265
|
-
renderCol('kb-open',cols.open,ncm,families); renderCol('kb-progress',cols.in_progress,ncm,families); renderCol('kb-blocked',cols.blocked,ncm,families); renderCol('kb-done',cols.done,ncm,families); renderCol('kb-wontdo',cols.wont_do,ncm,families);
|
|
1266
|
-
const p=document.getElementById('kanban-panel'); if(p) p.style.display=items.length>0?'':'none';
|
|
1267
|
-
prevCardMap=ncm;
|
|
1268
|
-
}
|
|
1269
|
-
function renderCol(cid,items,ncm,families) {
|
|
1270
|
-
const c=document.getElementById(cid); if(!c) return;
|
|
1271
|
-
if(!items.length) { c.innerHTML='<div class="kanban-empty">No tasks</div>'; return; }
|
|
1272
|
-
// Families first (clustered per group: root, then children by depth),
|
|
1273
|
-
// then ungrouped singles \u2014 priority breaks ties everywhere.
|
|
1274
|
-
const fam=families||new Set();
|
|
1275
|
-
const sorted=[...items].sort((a,b)=>{
|
|
1276
|
-
const ga=fam.has(a.groupId||a.id)?0:1, gb=fam.has(b.groupId||b.id)?0:1;
|
|
1277
|
-
if(ga!==gb) return ga-gb;
|
|
1278
|
-
if((a.groupId||a.id)!==(b.groupId||b.id))
|
|
1279
|
-
// Tiebreak on groupId: distinct groups with identical titles
|
|
1280
|
-
// (auto-created deliver missions) must not interleave.
|
|
1281
|
-
return String(a.groupTitle||a.title||'').localeCompare(String(b.groupTitle||b.title||''))
|
|
1282
|
-
|| String(a.groupId||a.id).localeCompare(String(b.groupId||b.id));
|
|
1283
|
-
if((a.depth||0)!==(b.depth||0)) return (a.depth||0)-(b.depth||0);
|
|
1284
|
-
return (a.priority!=null?a.priority:2)-(b.priority!=null?b.priority:2);
|
|
1285
|
-
});
|
|
1286
|
-
const ids=new Set(items.map(i=>i.id));
|
|
1287
|
-
c.querySelectorAll('.kanban-card').forEach(el=>{
|
|
1288
|
-
if(ids.has(el.dataset.id)||el.classList.contains('card-exit')) return;
|
|
1289
|
-
// Clear the inline animation:none that reconciliation sets on live
|
|
1290
|
-
// cards โ it outranks the .card-exit rule, and a never-starting
|
|
1291
|
-
// animation means animationend never fires and the card never leaves.
|
|
1292
|
-
el.style.animation='';
|
|
1293
|
-
el.classList.add('card-exit');
|
|
1294
|
-
el.addEventListener('animationend',()=>el.remove(),{once:true});
|
|
1295
|
-
setTimeout(()=>el.remove(),600); // fallback if the animation is skipped
|
|
1296
|
-
});
|
|
1297
|
-
c.querySelectorAll('.kanban-group,.kanban-empty').forEach(el=>el.remove());
|
|
1298
|
-
let lastGroup=null;
|
|
1299
|
-
for (const item of sorted) {
|
|
1300
|
-
const g=item.groupId||item.id;
|
|
1301
|
-
if(fam.has(g)&&g!==lastGroup){
|
|
1302
|
-
const h=document.createElement('div');h.className='kanban-group';h.textContent='\u25B8 '+(item.groupTitle||g);h.title=item.groupTitle||g;c.appendChild(h);
|
|
1303
|
-
}
|
|
1304
|
-
lastGroup=g;
|
|
1305
|
-
let card=c.querySelector('.kanban-card[data-id="'+item.id+'"]');
|
|
1306
|
-
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='';});}}else{card.style.animation='none';}
|
|
1307
|
-
c.appendChild(card); // appendChild moves existing nodes into group order
|
|
1308
|
-
card.classList.toggle('card-child',(item.depth||0)>0);
|
|
1309
|
-
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):'';
|
|
1310
|
-
// Grandchildren carry a breadcrumb to their direct parent \u2014 the group
|
|
1311
|
-
// header only names the root.
|
|
1312
|
-
const crumb=(item.depth||0)>1&&item.parentId?'<div class="card-crumb">\u21B3 '+esc(item.parentTitle||item.parentId)+'</div>':'';
|
|
1313
|
-
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>'+crumb+'<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>';
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// โโ Helpers โโ
|
|
1318
|
-
function setText(id,val){const e=document.getElementById(id);if(e)e.textContent=String(val);}
|
|
1319
|
-
function setHTML(id,html){const e=document.getElementById(id);if(e)e.innerHTML=html;}
|
|
1320
|
-
function fmt(n){if(typeof n!=='number'||isNaN(n))return'0';return Math.round(n).toLocaleString();}
|
|
1321
|
-
function capitalize(s){return s?s.charAt(0).toUpperCase()+s.slice(1):'';}
|
|
1322
|
-
const MODEL_NAMES={'fable-5':'Fable 5','opus-4.8':'Claude Opus 4.8','opus-4.6':'Claude Opus 4.6','sonnet-5':'Claude Sonnet 5','sonnet-4.6':'Claude Sonnet 4.6','haiku-4.5':'Claude Haiku 4.5','qwen36-a3b':'Qwen 3.6 35B A3B','qwen35-a3b':'Qwen 3.5 35B A3B','qwen35':'Qwen 3.5 35B A3B','gpt-5.4':'GPT 5.4','gpt-5.3-codex':'GPT 5.3 Codex'};
|
|
1323
|
-
function modelName(id){return MODEL_NAMES[id]||id||'unknown';}
|
|
1324
|
-
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML;}
|
|
1325
|
-
|
|
1326
|
-
// โโ Init โโ
|
|
1327
|
-
connect();
|
|
1328
|
-
connectSSE();
|
|
1329
|
-
|
|
1330
|
-
// Fallback poll
|
|
1331
|
-
setInterval(async()=>{
|
|
1332
|
-
if(ws&&ws.readyState===WebSocket.OPEN)return;
|
|
1333
|
-
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{}
|
|
1334
|
-
}, REFRESH_MS);
|
|
1335
|
-
|
|
1336
|
-
// Resize: re-init charts
|
|
1337
|
-
let resizeT;
|
|
1338
|
-
window.addEventListener('resize',()=>{clearTimeout(resizeT);resizeT=setTimeout(()=>{chartsInit=false;chartTasks=chartCompression=chartDeploy=chartHitrate=chartCompliance=null;},300);});
|
|
1339
46
|
</script>
|
|
47
|
+
<script src="/dash/core.js"></script>
|
|
48
|
+
<script src="/dash/tabs.js"></script>
|
|
1340
49
|
</body>
|
|
1341
50
|
</html>
|