@miller-tech/uap 1.135.2 → 1.136.0

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.
@@ -0,0 +1,267 @@
1
+ /* UAP Dashboard charts — uPlot chart + sparkline builders.
2
+ *
3
+ * The builder functions (addTooltip, mkChart, initChartTasks, initChartCompression,
4
+ * initSparkline, seedLegends, updateAllCharts) are ported 1:1 from the original
5
+ * monolithic web/dashboard.html inline <script>. On top of them sit the
6
+ * registry-based adapters (syncHero / syncSpark) that the tabbed views use to
7
+ * create-or-update charts idempotently on every snapshot.
8
+ *
9
+ * Loaded BEFORE core.js; exposes window.UAPCharts. core.js re-exports it as
10
+ * UAP.charts for the tab modules. */
11
+ (function () {
12
+ 'use strict';
13
+
14
+ var uplotReady = typeof uPlot !== 'undefined';
15
+
16
+ // ── Chart colors (ported 1:1) ──
17
+ const CC = {
18
+ done:'#3fb950', inProg:'#58a6ff', blocked:'#f85149', open:'#484f58', agents:'#bc8cff',
19
+ raw:'#58a6ff', ctx:'#3fb950', hitRate:'#bc8cff', deploy:'#58a6ff', blockRate:'#f85149',
20
+ };
21
+
22
+ function tsUnix(arr) { return arr.map(p => new Date(p.timestamp).getTime() / 1000); }
23
+
24
+ // ── Tooltip (ported 1:1) ──
25
+ function addTooltip(u, colors, labels, fmtVal) {
26
+ const tt = document.createElement('div');
27
+ tt.className = 'u-tooltip'; tt.style.display = 'none';
28
+ u.root.appendChild(tt);
29
+ u.hooks.setCursor = u.hooks.setCursor || [];
30
+ u.hooks.setCursor.push(() => {
31
+ const { idx, left, top } = u.cursor;
32
+ if (idx == null) { tt.style.display = 'none'; return; }
33
+ const date = new Date(u.data[0][idx] * 1000);
34
+ let rows = `<div class="tt-time">${date.toLocaleTimeString()}</div>`;
35
+ for (let i = 1; i < u.series.length; i++) {
36
+ if (!u.series[i].show) continue;
37
+ const val = u.data[i][idx];
38
+ if (val == null) continue;
39
+ const c = colors[i-1] || '#fff', l = labels[i-1] || '';
40
+ const v = fmtVal ? fmtVal(val, i) : val.toLocaleString();
41
+ 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>`;
42
+ }
43
+ tt.innerHTML = rows; tt.style.display = '';
44
+ tt.style.left = (left + 16) + 'px'; tt.style.top = Math.max(0, top - 10) + 'px';
45
+ });
46
+ }
47
+
48
+ // ── Generic chart builder (ported 1:1) ──
49
+ function mkChart(elId, ts, extractors, opts) {
50
+ if (!uplotReady || !ts || ts.length < 2) return null;
51
+ const el = document.getElementById(elId);
52
+ if (!el) return null;
53
+ el.innerHTML = '';
54
+ const timestamps = tsUnix(ts);
55
+ const data = [timestamps, ...extractors.map(fn => ts.map(fn))];
56
+ const u = new uPlot(opts(el.clientWidth), data, el);
57
+ return u;
58
+ }
59
+
60
+ function initChartTasks(ts) {
61
+ const colors = [CC.done, CC.inProg, CC.blocked, CC.open, CC.agents];
62
+ const labels = ['Done', 'In Progress', 'Blocked', 'Open', 'Active Agents'];
63
+ const extractors = [
64
+ p => p.tasks?.done || 0, p => p.tasks?.inProgress || 0,
65
+ p => p.tasks?.blocked || 0, p => p.tasks?.open || 0,
66
+ p => p.coordination?.activeAgents || 0,
67
+ ];
68
+ const u = mkChart('chart-tasks', ts, extractors, (w) => ({
69
+ width: w, height: 260,
70
+ cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
71
+ scales: { x: { time: true }, y: { min: 0 }, agents: { min: 0 } },
72
+ axes: [
73
+ { stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' },
74
+ { stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace', size: 50 },
75
+ { stroke: CC.agents, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'agents' },
76
+ ],
77
+ series: [
78
+ {},
79
+ { label: 'Done', stroke: CC.done, fill: CC.done+'30', width: 2 },
80
+ { label: 'In Prog', stroke: CC.inProg, fill: CC.inProg+'30', width: 2 },
81
+ { label: 'Blocked', stroke: CC.blocked, fill: CC.blocked+'20', width: 2 },
82
+ { label: 'Open', stroke: CC.open, fill: CC.open+'20', width: 1, dash: [4,2] },
83
+ { label: 'Agents', stroke: CC.agents, width: 2, dash: [6,3], scale: 'agents' },
84
+ ],
85
+ }));
86
+ if (u) addTooltip(u, colors, labels);
87
+ return u;
88
+ }
89
+
90
+ function initChartCompression(ts) {
91
+ const colors = [CC.raw, CC.ctx, CC.hitRate];
92
+ const labels = ['Raw KB', 'Compressed KB', 'Hit Rate %'];
93
+ const extractors = [
94
+ p => Math.round((p.compression?.rawBytes||0)/1024),
95
+ p => Math.round((p.compression?.contextBytes||0)/1024),
96
+ parseHR,
97
+ ];
98
+ const u = mkChart('chart-compression', ts, extractors, (w) => ({
99
+ width: w, height: 260,
100
+ cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
101
+ scales: { x: { time: true }, y: { min: 0 }, pct: { min: 0, max: 100 } },
102
+ axes: [
103
+ { stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' },
104
+ { stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace', size: 50, values: (_,v) => v.map(x => x+' KB') },
105
+ { stroke: CC.hitRate, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'pct', values: (_,v) => v.map(x => x+'%') },
106
+ ],
107
+ series: [
108
+ {},
109
+ { label: 'Raw', stroke: CC.raw, fill: CC.raw+'20', width: 2 },
110
+ { label: 'Compressed', stroke: CC.ctx, fill: CC.ctx+'20', width: 2 },
111
+ { label: 'Hit Rate', stroke: CC.hitRate, width: 2, dash: [6,3], scale: 'pct' },
112
+ ],
113
+ }));
114
+ if (u) addTooltip(u, colors, labels, (v,i) => i===3 ? v+'%' : v+' KB');
115
+ return u;
116
+ }
117
+
118
+ // ── Sparkline builder (ported 1:1) ──
119
+ function initSparkline(elId, ts, fn, color, label) {
120
+ if (!uplotReady || !ts || ts.length < 2) return null;
121
+ const el = document.getElementById(elId);
122
+ if (!el) return null;
123
+ el.innerHTML = '';
124
+ const timestamps = tsUnix(ts);
125
+ const values = ts.map(fn);
126
+ const data = [timestamps, values];
127
+ const u = new uPlot({
128
+ width: el.clientWidth, height: 60,
129
+ cursor: { sync: { key: 'uap' }, focus: { prox: 30 }, points: { show: false } },
130
+ legend: { show: false },
131
+ scales: { x: { time: true }, y: { min: 0 } },
132
+ axes: [{ show: false }, { show: false }],
133
+ series: [{}, { stroke: color, fill: color+'25', width: 1.5 }],
134
+ }, data, el);
135
+ // Sparkline tooltip
136
+ const tt = document.createElement('div');
137
+ tt.className = 'u-tooltip'; tt.style.display = 'none';
138
+ u.root.appendChild(tt);
139
+ u.hooks.setCursor = u.hooks.setCursor || [];
140
+ u.hooks.setCursor.push(() => {
141
+ const { idx } = u.cursor;
142
+ if (idx == null) { tt.style.display = 'none'; return; }
143
+ 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>`;
144
+ tt.style.display = ''; tt.style.left = (u.cursor.left+12)+'px'; tt.style.top = '0px';
145
+ });
146
+ return u;
147
+ }
148
+
149
+ // ── Snapshot field parsers (ported 1:1) ──
150
+ const parseHR = p => { const h = p.memoryHitsMisses?.hitRate; return typeof h === 'string' ? parseFloat(h)||0 : h||0; };
151
+ const parseBR = p => { const b = p.compliance?.blockRate; return typeof b === 'string' ? parseFloat(b)||0 : b||0; };
152
+
153
+ // ── Fixed chart set + updateAllCharts (ported 1:1) ──
154
+ let chartTasks = null, chartCompression = null, chartDeploy = null, chartHitrate = null, chartCompliance = null;
155
+ let chartsInit = false;
156
+ // Seed each uPlot legend to the LATEST data point so the values read as
157
+ // live numbers instead of '--' until the user hovers. Fail-safe per chart.
158
+ function seedLegends() {
159
+ [chartTasks, chartCompression, chartDeploy, chartHitrate, chartCompliance].forEach(u => {
160
+ 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 */ }
161
+ });
162
+ }
163
+ function updateAllCharts(ts) {
164
+ if (!uplotReady || !ts || ts.length < 2) return;
165
+ if (!chartsInit) {
166
+ chartTasks = initChartTasks(ts);
167
+ chartCompression = initChartCompression(ts);
168
+ chartDeploy = initSparkline('chart-deploy', ts, p=>(p.deployBuckets?.queued||0)+(p.deployBuckets?.executing||0)+(p.deployBuckets?.batched||0), CC.deploy, 'Active Deploys');
169
+ chartHitrate = initSparkline('chart-hitrate', ts, parseHR, CC.hitRate, 'Hit Rate %');
170
+ chartCompliance = initSparkline('chart-compliance', ts, parseBR, CC.blockRate, 'Block Rate %');
171
+ chartsInit = true;
172
+ seedLegends();
173
+ return;
174
+ }
175
+ // Update data
176
+ const timestamps = tsUnix(ts);
177
+ 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)]);
178
+ 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)]);
179
+ if (chartDeploy) chartDeploy.setData([timestamps, ts.map(p=>(p.deployBuckets?.queued||0)+(p.deployBuckets?.executing||0)+(p.deployBuckets?.batched||0))]);
180
+ if (chartHitrate) chartHitrate.setData([timestamps, ts.map(parseHR)]);
181
+ if (chartCompliance) chartCompliance.setData([timestamps, ts.map(parseBR)]);
182
+ seedLegends();
183
+ }
184
+
185
+ // ─────────────────────────────────────────────────────────────────────────
186
+ // Registry-based adapters for the tabbed views. Tab renders are re-entrant
187
+ // (hosts are recreated on tab switch), so charts are keyed by host id and
188
+ // recreated when the host node changes, else updated in place via setData.
189
+ // ─────────────────────────────────────────────────────────────────────────
190
+ var chartReg = {}; // id -> { u, host }
191
+ var AXIS = { stroke: '#484f58', grid: { stroke: '#21262d' }, ticks: { stroke: '#21262d' }, font: '10px SF Mono,monospace' };
192
+ function clear(node) { while (node && node.firstChild) node.removeChild(node.firstChild); return node; }
193
+
194
+ function heroOpts(kind, w) {
195
+ if (kind === 'tasks') return {
196
+ width: w, height: 260, cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
197
+ scales: { x: { time: true }, y: { min: 0 }, agents: { min: 0 } },
198
+ axes: [AXIS, Object.assign({}, AXIS, { size: 50 }), { stroke: CC.agents, grid: { show: false }, side: 1, font: '10px SF Mono,monospace', size: 50, scale: 'agents' }],
199
+ 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' }],
200
+ };
201
+ return {
202
+ width: w, height: 260, cursor: { sync: { key: 'uap' }, focus: { prox: 30 } },
203
+ scales: { x: { time: true }, y: { min: 0 }, pct: { min: 0, max: 100 } },
204
+ 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 + '%'; }); } }],
205
+ 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' }],
206
+ };
207
+ }
208
+ function heroData(kind, ts) {
209
+ var x = tsUnix(ts);
210
+ 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; })];
211
+ 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)];
212
+ }
213
+ function syncHero(id, kind, ts) {
214
+ if (!uplotReady || !ts || ts.length < 2) return;
215
+ var host = document.getElementById(id); if (!host) return;
216
+ var reg = chartReg[id], data = heroData(kind, ts);
217
+ if (!reg || reg.host !== host || !document.body.contains(reg.u.root)) {
218
+ if (reg && reg.u) { try { reg.u.destroy(); } catch (e) {} }
219
+ clear(host);
220
+ var u = new uPlot(heroOpts(kind, host.clientWidth || 600), data, host);
221
+ 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'; });
222
+ chartReg[id] = { u: u, host: host };
223
+ } else { reg.u.setData(data); }
224
+ seedLegend(chartReg[id].u);
225
+ }
226
+ function syncSpark(id, ts, fn, color, label) {
227
+ if (!uplotReady || !ts || ts.length < 2) return;
228
+ var host = document.getElementById(id); if (!host) return;
229
+ var reg = chartReg[id], data = [tsUnix(ts), ts.map(fn)];
230
+ if (!reg || reg.host !== host || !document.body.contains(reg.u.root)) {
231
+ if (reg && reg.u) { try { reg.u.destroy(); } catch (e) {} }
232
+ clear(host);
233
+ 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);
234
+ chartReg[id] = { u: u, host: host, label: label };
235
+ } else { reg.u.setData(data); }
236
+ seedLegend(chartReg[id].u);
237
+ }
238
+ 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) {} }
239
+ function resetCharts() {
240
+ for (var id in chartReg) { try { chartReg[id].u.destroy(); } catch (e) {} }
241
+ chartReg = {};
242
+ // Also reset the fixed chart set (mirrors the original resize handler).
243
+ chartsInit = false;
244
+ chartTasks = chartCompression = chartDeploy = chartHitrate = chartCompliance = null;
245
+ }
246
+
247
+ // ── Exports ──
248
+ window.UAPCharts = {
249
+ // 1:1 ported builders
250
+ mkChart: mkChart,
251
+ initSparkline: initSparkline,
252
+ addTooltip: addTooltip,
253
+ updateAllCharts: updateAllCharts,
254
+ initChartTasks: initChartTasks,
255
+ initChartCompression: initChartCompression,
256
+ seedLegends: seedLegends,
257
+ tsUnix: tsUnix,
258
+ // registry adapters + shared parsers/colors
259
+ syncHero: syncHero,
260
+ syncSpark: syncSpark,
261
+ resetCharts: resetCharts,
262
+ parseHR: parseHR,
263
+ parseBR: parseBR,
264
+ CC: CC,
265
+ ready: uplotReady,
266
+ };
267
+ })();
package/web/dash/core.js CHANGED
@@ -42,7 +42,8 @@
42
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
43
  function modelName(id) { return MODEL_NAMES[id] || id || 'unknown'; }
44
44
 
45
- // Hyperscript DOM builder (textContent-safe; pass {html} only for pre-escaped strings).
45
+ // Hyperscript DOM builder — XSS-safe by construction: strings only ever land
46
+ // in textContent / text nodes, never innerHTML.
46
47
  function el(tag, attrs) {
47
48
  var e = document.createElement(tag), i;
48
49
  if (attrs) {
@@ -52,7 +53,6 @@
52
53
  if (v == null) continue;
53
54
  if (k === 'class') e.className = v;
54
55
  else if (k === 'text') e.textContent = v;
55
- else if (k === 'html') e.innerHTML = v;
56
56
  else if (k === 'style' && typeof v === 'object') { for (var sk in v) e.style[sk] = v[sk]; }
57
57
  else if (k === 'dataset' && typeof v === 'object') { for (var dk in v) e.dataset[dk] = v[dk]; }
58
58
  else if (k.slice(0, 2) === 'on' && typeof v === 'function') e.addEventListener(k.slice(2).toLowerCase(), v);
@@ -149,7 +149,7 @@
149
149
  drawerEl.querySelector('#drawer-title').textContent = title;
150
150
  var body = drawerEl.querySelector('#drawer-body');
151
151
  clear(body);
152
- if (typeof contentNode === 'string') body.innerHTML = contentNode; else if (contentNode) body.appendChild(contentNode);
152
+ if (typeof contentNode === 'string') body.textContent = contentNode; else if (contentNode) body.appendChild(contentNode);
153
153
  drawerEl.classList.add('open');
154
154
  drawerBackdrop.classList.add('open');
155
155
  }
@@ -161,7 +161,7 @@
161
161
  var open = !!opts.open;
162
162
  var header = el('div', { class: 'collapsible-header' + (open ? ' open' : '') }, title);
163
163
  var inner = el('div', { class: 'cb-inner' });
164
- if (typeof bodyNode === 'string') inner.innerHTML = bodyNode; else if (bodyNode) inner.appendChild(bodyNode);
164
+ if (typeof bodyNode === 'string') inner.textContent = bodyNode; else if (bodyNode) inner.appendChild(bodyNode);
165
165
  var body = el('div', { class: 'collapsible-body' + (open ? ' open' : '') }, inner);
166
166
  header.addEventListener('click', function () { header.classList.toggle('open'); body.classList.toggle('open'); });
167
167
  return el('div', { class: 'collapsible' }, header, body);
@@ -179,79 +179,19 @@
179
179
  }).catch(function (e) { toast(e.message || 'Request failed', 'err'); throw e; });
180
180
  }
181
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 };
182
+ // ─────────────────────────── charts ───────────────────────────
183
+ // uPlot chart + sparkline builders live in charts.js (loaded before core.js):
184
+ // mkChart / initSparkline / addTooltip / updateAllCharts ported 1:1 from the
185
+ // original dashboard, plus the syncHero/syncSpark registry adapters the tabs
186
+ // use. Re-exported here as UAP.charts so tab modules have a single entry point.
187
+ var charts = window.UAPCharts || {
188
+ ready: false, CC: {},
189
+ syncHero: function () {}, syncSpark: function () {}, resetCharts: function () {},
190
+ mkChart: function () { return null; }, initSparkline: function () { return null; },
191
+ addTooltip: function () {}, updateAllCharts: function () {},
192
+ parseHR: function () { return 0; }, parseBR: function () { return 0; },
193
+ };
194
+ function resetCharts() { charts.resetCharts(); }
255
195
 
256
196
  // ─────────────────────────── tab registry + router ───────────────────────────
257
197
  var tabs = {}; // id -> { label, render, update }
@@ -269,6 +269,9 @@ label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px;
269
269
  .toast.ok { border-color: var(--green); color: var(--green); }
270
270
  .toast.err { border-color: var(--red); color: var(--red); }
271
271
  .toast.warn { border-color: var(--yellow); color: var(--yellow); }
272
+ /* Legacy aliases from the original dashboard (kept for 1:1 parity). */
273
+ .toast.success { border-color: var(--green); color: var(--green); }
274
+ .toast.error { border-color: var(--red); color: var(--red); }
272
275
  @keyframes toast-in { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
273
276
 
274
277
  /* ── Collapsible ── */
@@ -305,3 +308,19 @@ label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px;
305
308
  .section-note { color: var(--fg3); font-size: 11px; margin-bottom: 8px; }
306
309
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
307
310
  @media (max-width: 800px) { .grid-2 { grid-template-columns: 1fr; } }
311
+
312
+ /* ── Tab stubs (placeholder views for not-yet-built tabs) ── */
313
+ .coming-soon, .tab-coming-soon { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 300px; color: var(--fg2); text-align: center; }
314
+ .coming-soon .icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
315
+ .coming-soon h2 { color: var(--cyan); margin-bottom: 8px; }
316
+ .coming-soon p { font-size: 12px; }
317
+
318
+ /* ── Spinner ── */
319
+ .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--cyan); border-radius: 50%; animation: spin 0.6s linear infinite; }
320
+ @keyframes spin { to { transform: rotate(360deg); } }
321
+
322
+ /* ── Scrollbar ── */
323
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
324
+ ::-webkit-scrollbar-track { background: var(--bg); }
325
+ ::-webkit-scrollbar-thumb { background: var(--bg3); border-radius: 3px; }
326
+ ::-webkit-scrollbar-thumb:hover { background: var(--fg3); }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * tab-agents.js — Stub tab: Agents
3
+ * Registers with UAP.registerTab and renders a 'coming soon' message.
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ function render() {
9
+ var el = document.createElement('div');
10
+ el.className = 'coming-soon';
11
+ el.innerHTML =
12
+ '<div class="icon">🤖</div>' +
13
+ '<h2>Agents</h2>' +
14
+ '<p>Coming soon</p>';
15
+ return el;
16
+ }
17
+
18
+ if (typeof UAP !== 'undefined' && typeof UAP.registerTab === 'function') {
19
+ UAP.registerTab('agents', { render: render });
20
+ }
21
+ })();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * tab-orchestration.js — Stub tab: Orchestration
3
+ * Registers with UAP.registerTab and renders a 'coming soon' message.
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ function render() {
9
+ var el = document.createElement('div');
10
+ el.className = 'coming-soon';
11
+ el.innerHTML =
12
+ '<div class="icon">🔄</div>' +
13
+ '<h2>Orchestration</h2>' +
14
+ '<p>Coming soon</p>';
15
+ return el;
16
+ }
17
+
18
+ if (typeof UAP !== 'undefined' && typeof UAP.registerTab === 'function') {
19
+ UAP.registerTab('orchestration', { render: render });
20
+ }
21
+ })();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * tab-overview.js — Stub tab: Overview
3
+ * Registers with UAP.registerTab and renders a 'coming soon' message.
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ function render() {
9
+ var el = document.createElement('div');
10
+ el.className = 'coming-soon';
11
+ el.innerHTML =
12
+ '<div class="icon">📊</div>' +
13
+ '<h2>Overview</h2>' +
14
+ '<p>Coming soon</p>';
15
+ return el;
16
+ }
17
+
18
+ if (typeof UAP !== 'undefined' && typeof UAP.registerTab === 'function') {
19
+ UAP.registerTab('overview', { render: render });
20
+ }
21
+ })();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * tab-tasks.js — Stub tab: Tasks
3
+ * Registers with UAP.registerTab and renders a 'coming soon' message.
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ function render() {
9
+ var el = document.createElement('div');
10
+ el.className = 'coming-soon';
11
+ el.innerHTML =
12
+ '<div class="icon">📋</div>' +
13
+ '<h2>Tasks</h2>' +
14
+ '<p>Coming soon</p>';
15
+ return el;
16
+ }
17
+
18
+ if (typeof UAP !== 'undefined' && typeof UAP.registerTab === 'function') {
19
+ UAP.registerTab('tasks', { render: render });
20
+ }
21
+ })();
@@ -44,6 +44,7 @@
44
44
  refreshMs: Number('__UAP_DASH_REFRESH_MS__') > 0 ? Number('__UAP_DASH_REFRESH_MS__') : 2000,
45
45
  };
46
46
  </script>
47
+ <script src="/dash/charts.js"></script>
47
48
  <script src="/dash/core.js"></script>
48
49
  <script src="/dash/tabs.js"></script>
49
50
  </body>