@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.
Files changed (35) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cli/deliver.js +11 -2
  3. package/dist/cli/deliver.js.map +1 -1
  4. package/dist/dashboard/controls.d.ts +66 -0
  5. package/dist/dashboard/controls.d.ts.map +1 -0
  6. package/dist/dashboard/controls.js +188 -0
  7. package/dist/dashboard/controls.js.map +1 -0
  8. package/dist/dashboard/data-service.d.ts +20 -0
  9. package/dist/dashboard/data-service.d.ts.map +1 -1
  10. package/dist/dashboard/data-service.js +47 -0
  11. package/dist/dashboard/data-service.js.map +1 -1
  12. package/dist/dashboard/server.d.ts.map +1 -1
  13. package/dist/dashboard/server.js +81 -5
  14. package/dist/dashboard/server.js.map +1 -1
  15. package/dist/delivery/convergence-loop.d.ts +5 -0
  16. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  17. package/dist/delivery/convergence-loop.js +11 -0
  18. package/dist/delivery/convergence-loop.js.map +1 -1
  19. package/dist/delivery/run-state.d.ts +12 -0
  20. package/dist/delivery/run-state.d.ts.map +1 -1
  21. package/dist/delivery/run-state.js +58 -1
  22. package/dist/delivery/run-state.js.map +1 -1
  23. package/docs/DASHBOARD_UPLIFT_SPEC.md +171 -0
  24. package/package.json +1 -1
  25. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  26. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  27. package/tools/agents/scripts/anthropic_proxy.py +27 -0
  28. package/web/dash/core.js +398 -0
  29. package/web/dash/styles.css +307 -0
  30. package/web/dash/tab-deliver.js +12 -0
  31. package/web/dash/tab-memory.js +12 -0
  32. package/web/dash/tab-models.js +12 -0
  33. package/web/dash/tab-policies.js +12 -0
  34. package/web/dash/tabs.js +581 -0
  35. 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)
@@ -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
+ })();