@miller-tech/uap 1.134.0 → 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/cli/deliver.js +11 -2
- package/dist/cli/deliver.js.map +1 -1
- 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/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 +27 -0
- 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
|
@@ -1350,6 +1350,7 @@ class SessionMonitor:
|
|
|
1350
1350
|
deferral_streak: int = 0 # consecutive no-tool turns deferring the work (Fix A)
|
|
1351
1351
|
deferral_break_fires: int = 0 # monotonic count of forced deferral-breaks (Fix A)
|
|
1352
1352
|
tool_starvation_streak: int = 0 # Consecutive forced turns with no tool_calls produced
|
|
1353
|
+
last_request_msg_count: int = 0 # Message count of the previous request (compaction-boundary detection)
|
|
1353
1354
|
malformed_tool_streak: int = 0 # consecutive malformed pseudo tool payloads
|
|
1354
1355
|
invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
|
|
1355
1356
|
required_tool_miss_streak: int = 0 # required tool turns with no tool call
|
|
@@ -9815,6 +9816,32 @@ async def messages(request: Request):
|
|
|
9815
9816
|
last_text,
|
|
9816
9817
|
)
|
|
9817
9818
|
|
|
9819
|
+
# --- Compaction-boundary detection: a client-side auto-compact collapses
|
|
9820
|
+
# the conversation (observed live 2026-07-10: msgs 61 -> 3) into a fresh
|
|
9821
|
+
# epoch whose last assistant message is the text-only SUMMARY. Anti-spin
|
|
9822
|
+
# counters accumulated before the boundary (consecutive_forced_count,
|
|
9823
|
+
# starvation/no-write streaks, tool-state machine) then misfire on the
|
|
9824
|
+
# very first post-compact turn — the starvation breaker stripped tools
|
|
9825
|
+
# from a brand-new epoch because the summary "looked like" a text-only
|
|
9826
|
+
# stall. A halving-plus collapse of the message count is the boundary
|
|
9827
|
+
# signal; reset the per-conversation spin state so the new epoch starts
|
|
9828
|
+
# clean. (Token accounting is NOT reset — record_request below re-measures
|
|
9829
|
+
# the compacted size naturally.)
|
|
9830
|
+
prev_msg_count = getattr(monitor, "last_request_msg_count", 0)
|
|
9831
|
+
if prev_msg_count >= 8 and n_messages <= prev_msg_count // 2:
|
|
9832
|
+
monitor.reset_tool_turn_state(reason="compaction_boundary")
|
|
9833
|
+
monitor.consecutive_forced_count = 0
|
|
9834
|
+
monitor.no_progress_streak = 0
|
|
9835
|
+
monitor.tool_starvation_streak = 0
|
|
9836
|
+
monitor.consecutive_no_write_turns = 0
|
|
9837
|
+
logger.info(
|
|
9838
|
+
"COMPACTION BOUNDARY: message count collapsed %d -> %d; reset "
|
|
9839
|
+
"tool-turn/anti-spin state for the fresh epoch",
|
|
9840
|
+
prev_msg_count,
|
|
9841
|
+
n_messages,
|
|
9842
|
+
)
|
|
9843
|
+
monitor.last_request_msg_count = n_messages
|
|
9844
|
+
|
|
9818
9845
|
# --- Option F: Estimate tokens and record in session monitor ---
|
|
9819
9846
|
estimated_tokens = estimate_total_tokens(body)
|
|
9820
9847
|
monitor.record_request(estimated_tokens)
|
package/web/dash/core.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/* UAP Dashboard core — app shell, data channels, tab router, shared UI helpers.
|
|
2
|
+
* Loaded before tabs.js. tabs.js registers each tab then calls UAP.start(). */
|
|
3
|
+
(function () {
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
var BOOT = window.__UAP_BOOT || { token: '', refreshMs: 2000 };
|
|
7
|
+
var WS_URL = 'ws://' + location.host;
|
|
8
|
+
var API_URL = location.origin;
|
|
9
|
+
var MUTATION_HEADERS = { 'X-Uap-Dashboard-Token': BOOT.token || '' };
|
|
10
|
+
var REFRESH_MS = Number(BOOT.refreshMs) > 0 ? Number(BOOT.refreshMs) : 2000;
|
|
11
|
+
|
|
12
|
+
// ── Canonical tab order (bar is built from this; tabs.js supplies renderers) ──
|
|
13
|
+
var TAB_DEFS = [
|
|
14
|
+
{ id: 'overview', label: 'Overview' },
|
|
15
|
+
{ id: 'tasks', label: 'Tasks & Epics' },
|
|
16
|
+
{ id: 'agents', label: 'Agents & Sessions' },
|
|
17
|
+
{ id: 'orchestration', label: 'Orchestration' },
|
|
18
|
+
{ id: 'deliver', label: 'Deliver' },
|
|
19
|
+
{ id: 'policies', label: 'Policies' },
|
|
20
|
+
{ id: 'models', label: 'Models' },
|
|
21
|
+
{ id: 'memory', label: 'Memory' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────── helpers ───────────────────────────
|
|
25
|
+
function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
|
|
26
|
+
function fmtNum(n) { if (typeof n !== 'number' || isNaN(n)) { n = Number(n) || 0; } return Math.round(n).toLocaleString(); }
|
|
27
|
+
function fmtBytes(b) { b = b || 0; if (b < 1024) return b + ' B'; if (b < 1048576) return Math.round(b / 1024) + ' KB'; return (b / 1048576).toFixed(1) + ' MB'; }
|
|
28
|
+
function fmtKB(b) { return Math.round((b || 0) / 1024) + ' KB'; }
|
|
29
|
+
function fmtUsd(n) { return '$' + (typeof n === 'number' ? n.toFixed(4) : '0.0000'); }
|
|
30
|
+
function fmtDur(ms) { ms = ms || 0; var s = Math.floor(ms / 1000); if (s < 60) return s + 's'; var m = Math.floor(s / 60); if (m < 60) return m + 'm'; var h = Math.floor(m / 60); return h + 'h ' + (m % 60) + 'm'; }
|
|
31
|
+
function timeAgo(iso) {
|
|
32
|
+
if (!iso) return '-';
|
|
33
|
+
var t = new Date(iso).getTime(); if (isNaN(t)) return '-';
|
|
34
|
+
var d = Date.now() - t; if (d < 0) d = 0;
|
|
35
|
+
var s = Math.floor(d / 1000); if (s < 60) return s + 's ago';
|
|
36
|
+
var m = Math.floor(s / 60); if (m < 60) return m + 'm ago';
|
|
37
|
+
var h = Math.floor(m / 60); if (h < 24) return h + 'h ago';
|
|
38
|
+
return Math.floor(h / 24) + 'd ago';
|
|
39
|
+
}
|
|
40
|
+
function shortTime(iso) { if (!iso) return '-'; var d = new Date(iso); return isNaN(d.getTime()) ? String(iso).slice(11, 19) : d.toLocaleTimeString(); }
|
|
41
|
+
function capitalize(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
|
|
42
|
+
var 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' };
|
|
43
|
+
function modelName(id) { return MODEL_NAMES[id] || id || 'unknown'; }
|
|
44
|
+
|
|
45
|
+
// Hyperscript DOM builder (textContent-safe; pass {html} only for pre-escaped strings).
|
|
46
|
+
function el(tag, attrs) {
|
|
47
|
+
var e = document.createElement(tag), i;
|
|
48
|
+
if (attrs) {
|
|
49
|
+
for (var k in attrs) {
|
|
50
|
+
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
51
|
+
var v = attrs[k];
|
|
52
|
+
if (v == null) continue;
|
|
53
|
+
if (k === 'class') e.className = v;
|
|
54
|
+
else if (k === 'text') e.textContent = v;
|
|
55
|
+
else if (k === 'html') e.innerHTML = v;
|
|
56
|
+
else if (k === 'style' && typeof v === 'object') { for (var sk in v) e.style[sk] = v[sk]; }
|
|
57
|
+
else if (k === 'dataset' && typeof v === 'object') { for (var dk in v) e.dataset[dk] = v[dk]; }
|
|
58
|
+
else if (k.slice(0, 2) === 'on' && typeof v === 'function') e.addEventListener(k.slice(2).toLowerCase(), v);
|
|
59
|
+
else e.setAttribute(k, v);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (i = 2; i < arguments.length; i++) {
|
|
63
|
+
var kids = arguments[i];
|
|
64
|
+
if (!Array.isArray(kids)) kids = [kids];
|
|
65
|
+
for (var j = 0; j < kids.length; j++) {
|
|
66
|
+
var kid = kids[j];
|
|
67
|
+
if (kid == null || kid === false) continue;
|
|
68
|
+
if (kid && kid.nodeType) e.appendChild(kid);
|
|
69
|
+
else e.appendChild(document.createTextNode(String(kid)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return e;
|
|
73
|
+
}
|
|
74
|
+
function clear(node) { while (node && node.firstChild) node.removeChild(node.firstChild); return node; }
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────── toast ───────────────────────────
|
|
77
|
+
function toast(msg, kind) {
|
|
78
|
+
var c = document.getElementById('toast-container');
|
|
79
|
+
if (!c) return;
|
|
80
|
+
var t = el('div', { class: 'toast ' + (kind || 'ok'), text: msg });
|
|
81
|
+
c.appendChild(t);
|
|
82
|
+
setTimeout(function () { t.remove(); }, 3200);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─────────────────────────── modal (confirm + form) ───────────────────────────
|
|
86
|
+
function modalShell(build) {
|
|
87
|
+
return new Promise(function (resolve) {
|
|
88
|
+
var backdrop = el('div', { class: 'modal-backdrop' });
|
|
89
|
+
function close(val) { backdrop.remove(); document.removeEventListener('keydown', onKey); resolve(val); }
|
|
90
|
+
function onKey(e) { if (e.key === 'Escape') close(null); }
|
|
91
|
+
document.addEventListener('keydown', onKey);
|
|
92
|
+
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(null); });
|
|
93
|
+
backdrop.appendChild(build(close));
|
|
94
|
+
document.body.appendChild(backdrop);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function confirmDialog(message, opts) {
|
|
98
|
+
opts = opts || {};
|
|
99
|
+
return modalShell(function (close) {
|
|
100
|
+
return el('div', { class: 'modal' },
|
|
101
|
+
el('h3', { text: opts.title || 'Confirm' }),
|
|
102
|
+
el('div', { class: 'modal-msg', text: message }),
|
|
103
|
+
el('div', { class: 'modal-actions' },
|
|
104
|
+
el('button', { class: 'btn', onclick: function () { close(false); } }, 'Cancel'),
|
|
105
|
+
el('button', { class: 'btn ' + (opts.danger ? 'btn-danger' : 'btn-primary'), onclick: function () { close(true); } }, opts.okLabel || (opts.danger ? 'Delete' : 'Confirm'))
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// fields: [{name,label,type:'text|textarea|select|number',options?,value?,placeholder?}]
|
|
111
|
+
function formDialog(title, fields, opts) {
|
|
112
|
+
opts = opts || {};
|
|
113
|
+
return modalShell(function (close) {
|
|
114
|
+
var inputs = {};
|
|
115
|
+
var fieldNodes = fields.map(function (f) {
|
|
116
|
+
var input;
|
|
117
|
+
if (f.type === 'textarea') input = el('textarea', { placeholder: f.placeholder || '' });
|
|
118
|
+
else if (f.type === 'select') { input = el('select'); (f.options || []).forEach(function (o) { var v = typeof o === 'string' ? o : o.value; var lb = typeof o === 'string' ? o : o.label; input.appendChild(el('option', { value: v }, lb)); }); }
|
|
119
|
+
else input = el('input', { type: f.type || 'text', placeholder: f.placeholder || '' });
|
|
120
|
+
if (f.value != null) input.value = f.value;
|
|
121
|
+
inputs[f.name] = input;
|
|
122
|
+
return el('label', { class: 'field' }, f.label || f.name, input);
|
|
123
|
+
});
|
|
124
|
+
return el('div', { class: 'modal' },
|
|
125
|
+
el('h3', { text: title }),
|
|
126
|
+
el('div', { class: 'modal-fields' }, fieldNodes),
|
|
127
|
+
el('div', { class: 'modal-actions' },
|
|
128
|
+
el('button', { class: 'btn', onclick: function () { close(null); } }, 'Cancel'),
|
|
129
|
+
el('button', { class: 'btn btn-primary', onclick: function () { var out = {}; for (var n in inputs) out[n] = inputs[n].value; close(out); } }, opts.okLabel || 'Submit')
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─────────────────────────── drawer ───────────────────────────
|
|
136
|
+
var drawerEl = null, drawerBackdrop = null;
|
|
137
|
+
function ensureDrawer() {
|
|
138
|
+
if (drawerEl) return;
|
|
139
|
+
drawerBackdrop = el('div', { class: 'drawer-backdrop', onclick: closeDrawer });
|
|
140
|
+
drawerEl = el('div', { class: 'drawer', role: 'dialog', 'aria-modal': 'true' },
|
|
141
|
+
el('div', { class: 'drawer-head' }, el('h3', { id: 'drawer-title' }), el('button', { class: 'drawer-close', title: 'Close', onclick: closeDrawer }, '×')),
|
|
142
|
+
el('div', { class: 'drawer-body', id: 'drawer-body' })
|
|
143
|
+
);
|
|
144
|
+
document.body.appendChild(drawerBackdrop);
|
|
145
|
+
document.body.appendChild(drawerEl);
|
|
146
|
+
}
|
|
147
|
+
function drawer(title, contentNode) {
|
|
148
|
+
ensureDrawer();
|
|
149
|
+
drawerEl.querySelector('#drawer-title').textContent = title;
|
|
150
|
+
var body = drawerEl.querySelector('#drawer-body');
|
|
151
|
+
clear(body);
|
|
152
|
+
if (typeof contentNode === 'string') body.innerHTML = contentNode; else if (contentNode) body.appendChild(contentNode);
|
|
153
|
+
drawerEl.classList.add('open');
|
|
154
|
+
drawerBackdrop.classList.add('open');
|
|
155
|
+
}
|
|
156
|
+
function closeDrawer() { if (drawerEl) { drawerEl.classList.remove('open'); drawerBackdrop.classList.remove('open'); } }
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────── collapsible ───────────────────────────
|
|
159
|
+
function collapsible(title, bodyNode, opts) {
|
|
160
|
+
opts = opts || {};
|
|
161
|
+
var open = !!opts.open;
|
|
162
|
+
var header = el('div', { class: 'collapsible-header' + (open ? ' open' : '') }, title);
|
|
163
|
+
var inner = el('div', { class: 'cb-inner' });
|
|
164
|
+
if (typeof bodyNode === 'string') inner.innerHTML = bodyNode; else if (bodyNode) inner.appendChild(bodyNode);
|
|
165
|
+
var body = el('div', { class: 'collapsible-body' + (open ? ' open' : '') }, inner);
|
|
166
|
+
header.addEventListener('click', function () { header.classList.toggle('open'); body.classList.toggle('open'); });
|
|
167
|
+
return el('div', { class: 'collapsible' }, header, body);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─────────────────────────── api ───────────────────────────
|
|
171
|
+
function api(path, body) {
|
|
172
|
+
var opts = { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, MUTATION_HEADERS) };
|
|
173
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
174
|
+
return fetch(API_URL + path, opts).then(function (r) {
|
|
175
|
+
return r.json().then(function (j) {
|
|
176
|
+
if (!r.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
|
|
177
|
+
return j;
|
|
178
|
+
}, function () { if (!r.ok) throw new Error('HTTP ' + r.status); return {}; });
|
|
179
|
+
}).catch(function (e) { toast(e.message || 'Request failed', 'err'); throw e; });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─────────────────────────── charts (uPlot) ───────────────────────────
|
|
183
|
+
var uplotReady = typeof uPlot !== 'undefined';
|
|
184
|
+
var CC = { done: '#3fb950', inProg: '#58a6ff', blocked: '#f85149', open: '#484f58', agents: '#bc8cff', raw: '#58a6ff', ctx: '#3fb950', hitRate: '#bc8cff', deploy: '#58a6ff', blockRate: '#f85149' };
|
|
185
|
+
var chartReg = {}; // id -> { u, host }
|
|
186
|
+
function tsUnix(arr) { return arr.map(function (p) { return new Date(p.timestamp).getTime() / 1000; }); }
|
|
187
|
+
function addTooltip(u, colors, labels, fmtVal) {
|
|
188
|
+
var tt = el('div', { class: 'u-tooltip' }); tt.style.display = 'none'; u.root.appendChild(tt);
|
|
189
|
+
u.hooks.setCursor = u.hooks.setCursor || [];
|
|
190
|
+
u.hooks.setCursor.push(function () {
|
|
191
|
+
var idx = u.cursor.idx, left = u.cursor.left, top = u.cursor.top;
|
|
192
|
+
if (idx == null) { tt.style.display = 'none'; return; }
|
|
193
|
+
var date = new Date(u.data[0][idx] * 1000);
|
|
194
|
+
var rows = '<div class="tt-time">' + date.toLocaleTimeString() + '</div>';
|
|
195
|
+
for (var i = 1; i < u.series.length; i++) {
|
|
196
|
+
if (!u.series[i].show) continue;
|
|
197
|
+
var val = u.data[i][idx]; if (val == null) continue;
|
|
198
|
+
var c = colors[i - 1] || '#fff', l = labels[i - 1] || '';
|
|
199
|
+
var v = fmtVal ? fmtVal(val, i) : val.toLocaleString();
|
|
200
|
+
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>';
|
|
201
|
+
}
|
|
202
|
+
tt.innerHTML = rows; tt.style.display = ''; tt.style.left = (left + 16) + 'px'; tt.style.top = Math.max(0, top - 10) + 'px';
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
var AXIS = { stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' };
|
|
206
|
+
function heroOpts(kind, w) {
|
|
207
|
+
if (kind === 'tasks') return {
|
|
208
|
+
width: w, height: 260, cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
|
|
209
|
+
scales: { x: { time: true }, y: { min: 0 }, agents: { min: 0 } },
|
|
210
|
+
axes: [AXIS, Object.assign({}, AXIS, { size: 50 }), { stroke: CC.agents, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'agents' }],
|
|
211
|
+
series: [{}, { label: 'Done', stroke: CC.done, fill: CC.done + '30', width: 2 }, { label: 'In Prog', stroke: CC.inProg, fill: CC.inProg + '30', width: 2 }, { label: 'Blocked', stroke: CC.blocked, fill: CC.blocked + '20', width: 2 }, { label: 'Open', stroke: CC.open, fill: CC.open + '20', width: 1, dash: [4, 2] }, { label: 'Agents', stroke: CC.agents, width: 2, dash: [6, 3], scale: 'agents' }],
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
width: w, height: 260, cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
|
|
215
|
+
scales: { x: { time: true }, y: { min: 0 }, pct: { min: 0, max: 100 } },
|
|
216
|
+
axes: [AXIS, Object.assign({}, AXIS, { size: 50, values: function (_, v) { return v.map(function (x) { return x + ' KB'; }); } }), { stroke: CC.hitRate, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'pct', values: function (_, v) { return v.map(function (x) { return x + '%'; }); } }],
|
|
217
|
+
series: [{}, { label: 'Raw', stroke: CC.raw, fill: CC.raw + '20', width: 2 }, { label: 'Compressed', stroke: CC.ctx, fill: CC.ctx + '20', width: 2 }, { label: 'Hit Rate', stroke: CC.hitRate, width: 2, dash: [6, 3], scale: 'pct' }],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
var parseHR = function (p) { var h = p.memoryHitsMisses && p.memoryHitsMisses.hitRate; return typeof h === 'string' ? parseFloat(h) || 0 : h || 0; };
|
|
221
|
+
var parseBR = function (p) { var b = p.compliance && p.compliance.blockRate; return typeof b === 'string' ? parseFloat(b) || 0 : b || 0; };
|
|
222
|
+
function heroData(kind, ts) {
|
|
223
|
+
var x = tsUnix(ts);
|
|
224
|
+
if (kind === 'tasks') return [x, ts.map(function (p) { return (p.tasks && p.tasks.done) || 0; }), ts.map(function (p) { return (p.tasks && p.tasks.inProgress) || 0; }), ts.map(function (p) { return (p.tasks && p.tasks.blocked) || 0; }), ts.map(function (p) { return (p.tasks && p.tasks.open) || 0; }), ts.map(function (p) { return (p.coordination && p.coordination.activeAgents) || 0; })];
|
|
225
|
+
return [x, ts.map(function (p) { return Math.round(((p.compression && p.compression.rawBytes) || 0) / 1024); }), ts.map(function (p) { return Math.round(((p.compression && p.compression.contextBytes) || 0) / 1024); }), ts.map(parseHR)];
|
|
226
|
+
}
|
|
227
|
+
function syncHero(id, kind, ts) {
|
|
228
|
+
if (!uplotReady || !ts || ts.length < 2) return;
|
|
229
|
+
var host = document.getElementById(id); if (!host) return;
|
|
230
|
+
var reg = chartReg[id], data = heroData(kind, ts);
|
|
231
|
+
if (!reg || reg.host !== host || !document.body.contains(reg.u.root)) {
|
|
232
|
+
if (reg && reg.u) { try { reg.u.destroy(); } catch (e) {} }
|
|
233
|
+
clear(host);
|
|
234
|
+
var u = new uPlot(heroOpts(kind, host.clientWidth || 600), data, host);
|
|
235
|
+
addTooltip(u, kind === 'tasks' ? [CC.done, CC.inProg, CC.blocked, CC.open, CC.agents] : [CC.raw, CC.ctx, CC.hitRate], kind === 'tasks' ? ['Done', 'In Progress', 'Blocked', 'Open', 'Active Agents'] : ['Raw KB', 'Compressed KB', 'Hit Rate %'], kind === 'tasks' ? null : function (v, i) { return i === 3 ? v + '%' : v + ' KB'; });
|
|
236
|
+
chartReg[id] = { u: u, host: host };
|
|
237
|
+
} else { reg.u.setData(data); }
|
|
238
|
+
seedLegend(chartReg[id].u);
|
|
239
|
+
}
|
|
240
|
+
function syncSpark(id, ts, fn, color, label) {
|
|
241
|
+
if (!uplotReady || !ts || ts.length < 2) return;
|
|
242
|
+
var host = document.getElementById(id); if (!host) return;
|
|
243
|
+
var reg = chartReg[id], data = [tsUnix(ts), ts.map(fn)];
|
|
244
|
+
if (!reg || reg.host !== host || !document.body.contains(reg.u.root)) {
|
|
245
|
+
if (reg && reg.u) { try { reg.u.destroy(); } catch (e) {} }
|
|
246
|
+
clear(host);
|
|
247
|
+
var u = new uPlot({ width: host.clientWidth || 300, height: 60, cursor: { sync: { key: 'uap' }, focus: { prox: 30 }, points: { show: false } }, legend: { show: false }, scales: { x: { time: true }, y: { min: 0 } }, axes: [{ show: false }, { show: false }], series: [{}, { stroke: color, fill: color + '25', width: 1.5 }] }, data, host);
|
|
248
|
+
chartReg[id] = { u: u, host: host, label: label };
|
|
249
|
+
} else { reg.u.setData(data); }
|
|
250
|
+
seedLegend(chartReg[id].u);
|
|
251
|
+
}
|
|
252
|
+
function seedLegend(u) { try { if (u && u.data && u.data[0] && u.data[0].length) u.setLegend({ idx: u.data[0].length - 1 }); } catch (e) {} }
|
|
253
|
+
function resetCharts() { for (var id in chartReg) { try { chartReg[id].u.destroy(); } catch (e) {} } chartReg = {}; }
|
|
254
|
+
var charts = { syncHero: syncHero, syncSpark: syncSpark, parseHR: parseHR, parseBR: parseBR, CC: CC, ready: uplotReady };
|
|
255
|
+
|
|
256
|
+
// ─────────────────────────── tab registry + router ───────────────────────────
|
|
257
|
+
var tabs = {}; // id -> { label, render, update }
|
|
258
|
+
var activeId = null, activeRoot = null, enteredId = null;
|
|
259
|
+
function registerTab(id, def) { tabs[id] = def; }
|
|
260
|
+
function buildTabBar() {
|
|
261
|
+
var bar = document.getElementById('tabbar'); if (!bar) return;
|
|
262
|
+
clear(bar);
|
|
263
|
+
TAB_DEFS.forEach(function (d) {
|
|
264
|
+
var badge = el('span', { class: 'tab-badge', id: 'tabbadge-' + d.id, style: { display: 'none' } });
|
|
265
|
+
var btn = el('button', { class: 'tab', id: 'tab-' + d.id, onclick: function () { goto(d.id); } }, d.label, badge);
|
|
266
|
+
bar.appendChild(btn);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function setActiveTabButton(id) {
|
|
270
|
+
TAB_DEFS.forEach(function (d) { var b = document.getElementById('tab-' + d.id); if (b) b.classList.toggle('active', d.id === id); });
|
|
271
|
+
}
|
|
272
|
+
function currentHashTab() { var h = (location.hash || '').replace(/^#/, ''); return tabs[h] ? h : 'overview'; }
|
|
273
|
+
function goto(id) { if (location.hash.replace(/^#/, '') === id) { onRoute(); } else { location.hash = id; } }
|
|
274
|
+
function onRoute() {
|
|
275
|
+
var id = currentHashTab();
|
|
276
|
+
activeId = id; activeRoot = document.getElementById('view');
|
|
277
|
+
setActiveTabButton(id);
|
|
278
|
+
if (!activeRoot) return;
|
|
279
|
+
clear(activeRoot);
|
|
280
|
+
enteredId = id;
|
|
281
|
+
var def = tabs[id];
|
|
282
|
+
if (!def) { activeRoot.appendChild(el('div', { class: 'empty' }, 'Loading…')); return; }
|
|
283
|
+
try { def.render(activeRoot, state || {}); } catch (e) { activeRoot.appendChild(el('div', { class: 'empty', text: 'Render error: ' + (e.message || e) })); console.error(e); }
|
|
284
|
+
}
|
|
285
|
+
function refreshActive() {
|
|
286
|
+
if (!activeId || !activeRoot) return;
|
|
287
|
+
var def = tabs[activeId]; if (!def) return;
|
|
288
|
+
try {
|
|
289
|
+
if (def.update && enteredId === activeId) def.update(activeRoot, state || {});
|
|
290
|
+
else { clear(activeRoot); enteredId = activeId; def.render(activeRoot, state || {}); }
|
|
291
|
+
} catch (e) { console.error('tab update', e); }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─────────────────────────── snapshot + header ───────────────────────────
|
|
295
|
+
var state = null;
|
|
296
|
+
var snapSubs = [];
|
|
297
|
+
function onSnapshot(fn) { snapSubs.push(fn); }
|
|
298
|
+
function badge(id, count, alert) {
|
|
299
|
+
var b = document.getElementById('tabbadge-' + id); if (!b) return;
|
|
300
|
+
if (!count) { b.style.display = 'none'; return; }
|
|
301
|
+
b.style.display = ''; b.textContent = count; b.classList.toggle('alert', !!alert);
|
|
302
|
+
}
|
|
303
|
+
function updateHeader(d) {
|
|
304
|
+
var sys = d.system || {};
|
|
305
|
+
setText('sys-version', sys.version || '?');
|
|
306
|
+
setText('sys-branch', sys.branch || '?');
|
|
307
|
+
var el2 = document.getElementById('sys-dirty');
|
|
308
|
+
if (el2) { var dd = sys.dirty || 0; el2.textContent = dd > 0 ? dd + ' files' : 'clean'; el2.className = 'val ' + (dd > 0 ? 'yellow' : 'green'); }
|
|
309
|
+
if (d.timestamp) setText('data-ts', new Date(d.timestamp).toLocaleTimeString());
|
|
310
|
+
}
|
|
311
|
+
function updateBadges(d) {
|
|
312
|
+
var coord = d.coordination || {}, tasks = d.tasks || {}, runs = d.deliverRuns || [];
|
|
313
|
+
badge('agents', coord.activeAgents || 0);
|
|
314
|
+
badge('tasks', (tasks.inProgress || 0) + (tasks.blocked || 0));
|
|
315
|
+
badge('deliver', runs.filter(function (r) { return r.status === 'running'; }).length);
|
|
316
|
+
var blocks = (d.compliance && d.compliance.totalBlocks) || 0;
|
|
317
|
+
badge('policies', blocks, blocks > 0);
|
|
318
|
+
}
|
|
319
|
+
// A missing/incompatible better-sqlite3 binding makes every DB-backed panel
|
|
320
|
+
// silently empty. Surface it as a fixed banner instead of a dead-looking UI.
|
|
321
|
+
function renderHealth(hh) {
|
|
322
|
+
var existing = document.getElementById('db-health-banner');
|
|
323
|
+
if (!hh || hh.ok) { if (existing) existing.remove(); return; }
|
|
324
|
+
if (!existing) {
|
|
325
|
+
existing = el('div', { id: 'db-health-banner', role: 'alert', style: { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '2000', background: 'var(--red)', color: '#fff', padding: '8px 16px', fontSize: '12px', textAlign: 'center' } });
|
|
326
|
+
document.body.appendChild(existing);
|
|
327
|
+
document.body.style.paddingTop = '34px';
|
|
328
|
+
}
|
|
329
|
+
existing.textContent = '\u26A0 Dashboard database unavailable — panels read empty. ' + (hh.remediation || hh.error || '');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function applySnapshot(d) {
|
|
333
|
+
if (!d) return;
|
|
334
|
+
state = d; UAP.state = d;
|
|
335
|
+
try { updateHeader(d); } catch (e) {}
|
|
336
|
+
try { updateBadges(d); } catch (e) {}
|
|
337
|
+
try { renderHealth(d.health); } catch (e) {}
|
|
338
|
+
refreshActive();
|
|
339
|
+
for (var i = 0; i < snapSubs.length; i++) { try { snapSubs[i](d); } catch (e) {} }
|
|
340
|
+
}
|
|
341
|
+
function setText(id, val) { var e = document.getElementById(id); if (e) e.textContent = String(val); }
|
|
342
|
+
|
|
343
|
+
// ─────────────────────────── data channels ───────────────────────────
|
|
344
|
+
var ws = null, reconnectTimer = null, reconnectDelay = 1000, MAX_DELAY = 30000;
|
|
345
|
+
function setConn(cls, txt, title) {
|
|
346
|
+
var dot = document.getElementById('ws-status'); if (dot) { dot.className = 'status-dot ' + cls; dot.setAttribute('aria-label', 'Connection: ' + txt); }
|
|
347
|
+
var info = document.getElementById('refresh-info'); if (info) info.textContent = txt;
|
|
348
|
+
if (title) document.title = 'UAP Dashboard - ' + title;
|
|
349
|
+
}
|
|
350
|
+
function connectWS() {
|
|
351
|
+
clearTimeout(reconnectTimer);
|
|
352
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
353
|
+
ws = new WebSocket(WS_URL);
|
|
354
|
+
ws.onopen = function () { reconnectDelay = 1000; setConn('connected', 'Live', 'Live'); };
|
|
355
|
+
ws.onmessage = function (e) { try { applySnapshot(JSON.parse(e.data)); setConn('connected', 'Live - ' + new Date().toLocaleTimeString(), 'Live'); } catch (err) { console.error(err); } };
|
|
356
|
+
ws.onclose = function () { setConn('disconnected', 'Disconnected - reconnecting…', 'Disconnected'); reconnectTimer = setTimeout(connectWS, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_DELAY); };
|
|
357
|
+
ws.onerror = function () { ws.close(); };
|
|
358
|
+
}
|
|
359
|
+
var eventSource = null, liveEvents = [];
|
|
360
|
+
function connectSSE() {
|
|
361
|
+
try {
|
|
362
|
+
eventSource = new EventSource(API_URL + '/api/events');
|
|
363
|
+
eventSource.onmessage = function (e) {
|
|
364
|
+
try { var ev = JSON.parse(e.data); if (ev.id != null && liveEvents.some(function (x) { return x.id === ev.id; })) return; liveEvents.unshift(ev); if (liveEvents.length > 40) liveEvents.pop(); for (var i = 0; i < evSubs.length; i++) { try { evSubs[i](liveEvents); } catch (_) {} } } catch (_) {}
|
|
365
|
+
};
|
|
366
|
+
eventSource.addEventListener('snapshot', function (e) { if (ws && ws.readyState === WebSocket.OPEN) return; try { applySnapshot(JSON.parse(e.data)); setConn('connected', 'Live (SSE) - ' + new Date().toLocaleTimeString(), 'Live'); } catch (_) {} });
|
|
367
|
+
eventSource.onerror = function () { eventSource.close(); setTimeout(connectSSE, 5000); };
|
|
368
|
+
} catch (_) {}
|
|
369
|
+
}
|
|
370
|
+
var evSubs = [];
|
|
371
|
+
function onEvents(fn) { evSubs.push(fn); }
|
|
372
|
+
function startPoll() {
|
|
373
|
+
setInterval(function () {
|
|
374
|
+
if (ws && ws.readyState === WebSocket.OPEN) return;
|
|
375
|
+
fetch(API_URL + '/api/dashboard').then(function (r) { return r.ok ? r.json() : null; }).then(function (d) { if (d) { applySnapshot(d); setConn('polling', 'Polling - ' + new Date().toLocaleTimeString(), 'Polling'); } }).catch(function () {});
|
|
376
|
+
}, REFRESH_MS);
|
|
377
|
+
}
|
|
378
|
+
var resizeT;
|
|
379
|
+
window.addEventListener('resize', function () { clearTimeout(resizeT); resizeT = setTimeout(function () { resetCharts(); refreshActive(); }, 300); });
|
|
380
|
+
window.addEventListener('hashchange', onRoute);
|
|
381
|
+
|
|
382
|
+
function start() {
|
|
383
|
+
buildTabBar();
|
|
384
|
+
onRoute();
|
|
385
|
+
// Kick an immediate fetch so the first paint doesn't wait for the WS handshake.
|
|
386
|
+
fetch(API_URL + '/api/dashboard').then(function (r) { return r.ok ? r.json() : null; }).then(function (d) { if (d && !state) applySnapshot(d); }).catch(function () {});
|
|
387
|
+
connectWS();
|
|
388
|
+
connectSSE();
|
|
389
|
+
startPoll();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
window.UAP = {
|
|
393
|
+
get state() { return state; }, set state(v) { state = v; },
|
|
394
|
+
esc: esc, fmtNum: fmtNum, fmtBytes: fmtBytes, fmtKB: fmtKB, fmtUsd: fmtUsd, fmtDur: fmtDur, timeAgo: timeAgo, shortTime: shortTime, capitalize: capitalize, modelName: modelName,
|
|
395
|
+
el: el, clear: clear, toast: toast, confirm: confirmDialog, form: formDialog, drawer: drawer, closeDrawer: closeDrawer, collapsible: collapsible,
|
|
396
|
+
api: api, registerTab: registerTab, goto: goto, onSnapshot: onSnapshot, onEvents: onEvents, get liveEvents() { return liveEvents; }, charts: charts, start: start, API_URL: API_URL,
|
|
397
|
+
};
|
|
398
|
+
})();
|