@miller-tech/uap 1.134.0 → 1.135.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) 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/acceptance-judge.d.ts +1 -1
  16. package/dist/delivery/acceptance-judge.d.ts.map +1 -1
  17. package/dist/delivery/acceptance-judge.js +78 -5
  18. package/dist/delivery/acceptance-judge.js.map +1 -1
  19. package/dist/delivery/convergence-loop.d.ts +5 -0
  20. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  21. package/dist/delivery/convergence-loop.js +11 -0
  22. package/dist/delivery/convergence-loop.js.map +1 -1
  23. package/dist/delivery/run-state.d.ts +12 -0
  24. package/dist/delivery/run-state.d.ts.map +1 -1
  25. package/dist/delivery/run-state.js +58 -1
  26. package/dist/delivery/run-state.js.map +1 -1
  27. package/docs/DASHBOARD_UPLIFT_SPEC.md +171 -0
  28. package/package.json +1 -1
  29. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  30. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  31. package/tools/agents/scripts/anthropic_proxy.py +27 -0
  32. package/web/dash/core.js +398 -0
  33. package/web/dash/styles.css +307 -0
  34. package/web/dash/tab-deliver.js +12 -0
  35. package/web/dash/tab-memory.js +12 -0
  36. package/web/dash/tab-models.js +12 -0
  37. package/web/dash/tab-policies.js +12 -0
  38. package/web/dash/tabs.js +581 -0
  39. package/web/dashboard.html +12 -1303
@@ -0,0 +1,171 @@
1
+ # UAP Dashboard Uplift — Architecture & UI Spec
2
+
3
+ Goal: replace the monolithic `web/dashboard.html` with a modular, tabbed dashboard that shows
4
+ aggregate high-level info AND per-agent/session drill-down, covers every UAP feature, supports
5
+ drill in/out + expand/collapse, and adds full-lifecycle management (start/stop/delete/etc) for
6
+ tasks, epics, orchestration, deliver runs, and agents.
7
+
8
+ ## Design system — UAP Console (MANDATORY, design gate enforced)
9
+ Use ONLY these tokens. No off-token hex, no off-scale spacing.
10
+ ```
11
+ --bg:#0d1117; --surface:#161b22; --surface-raised:#21262d; --border:#30363d;
12
+ --text:#c9d1d9; --text-muted:#8b949e; --text-subtle:#484f58; --primary:#58a6ff;
13
+ ```
14
+ Existing accent tokens already in the page (keep, they are on-system):
15
+ `--cyan --green --yellow --red --purple --orange`.
16
+ Fonts: 'SF Mono','Fira Code','Cascadia Code',monospace for data; system sans for prose.
17
+ Spacing scale: 4,8,12,16,24,32 px only. Radius: var(--radius) (6px). Dark theme only.
18
+
19
+ ## File layout (each file is one deliver mission — no shared-file clobbering)
20
+ ```
21
+ web/dashboard.html thin shell: <head> loads styles.css + vendored uPlot; body has
22
+ header, tab bar, one <main id="view"> mount, loads core.js (module).
23
+ web/dash/core.js data layer + app shell: WS/SSE/poll channels, global `state` (last
24
+ snapshot), tab router (hash-based #overview,#tasks,...), renderActiveTab(),
25
+ confirm-dialog, toast, esc(), fmt helpers, MUTATION token+headers,
26
+ api() POST helper, expand/collapse helpers, drill navigation (goAgent(id)).
27
+ web/dash/charts.js uPlot chart + sparkline builders (ported 1:1 from current behaviour).
28
+ web/dash/styles.css all CSS (ported + new tab/drawer/table/control styles), tokens only.
29
+ web/dash/tab-overview.js aggregate KPIs + hero charts + live summaries with drill links.
30
+ web/dash/tab-tasks.js Tasks & Epics: kanban + table + create/edit/status/assignee/delete + epic ledger.
31
+ web/dash/tab-agents.js Agents & Sessions: agent grid -> per-agent drawer; session history -> per-session drawer.
32
+ web/dash/tab-orchestration.js mission->epic->task tree + agents + orchestrate on/off/auto + ledger controls.
33
+ web/dash/tab-deliver.js deliver run registry list + launch form + cancel/resume + per-run telemetry.
34
+ web/dash/tab-policies.js policies table (existing controls) + compliance + audit + live events.
35
+ web/dash/tab-models.js models/routing config + session usage + routing decisions.
36
+ web/dash/tab-memory.js L1-L4 + compression + hit/miss gauge + recent queries + savings-by-influence.
37
+ ```
38
+ Server change: `server.ts` static handler currently serves `/vendor/`; extend it to ALSO serve
39
+ `/dash/` (same path-traversal guard, content types add `.css`, `.js`).
40
+
41
+ ## Module contract (how tabs plug in)
42
+ `core.js` owns everything shared and exposes a global `UAP` object on `window`:
43
+ - `UAP.state` — the latest `/api/dashboard` snapshot (or null).
44
+ - `UAP.registerTab(id, {label, render(root, state), onEnter?, onLeave?})` — each tab-*.js calls this at load.
45
+ - `UAP.el(tag, attrs, ...children)` — tiny hyperscript DOM builder (avoids innerHTML XSS; sets textContent).
46
+ - `UAP.esc(s)`, `UAP.fmtNum(n)`, `UAP.fmtBytes(n)`, `UAP.fmtUsd(n)`, `UAP.fmtDur(ms)`, `UAP.timeAgo(iso)`.
47
+ - `UAP.api(path, body?)` — POST JSON with the mutation token header; returns parsed JSON or throws; shows toast on error.
48
+ - `UAP.confirm(message, {danger}) : Promise<bool>` — modal confirm; ALL destructive controls await it.
49
+ - `UAP.toast(msg, kind)` — kind: ok|warn|err.
50
+ - `UAP.drawer(title, contentNode)` / `UAP.closeDrawer()` — right-side slide-in panel for drill-down detail.
51
+ - `UAP.goto(hash)` — switch tabs programmatically (e.g. click agent row on Overview -> #agents + open drawer).
52
+ - `UAP.collapsible(title, bodyNode, {open}) : Node` — reusable expand/collapse section.
53
+ - `UAP.onSnapshot(fn)` — subscribe to every new snapshot (tabs use this to live-update if active).
54
+ - `UAP.liveEvents` — capped array of SSE activity events (for policies tab live feed).
55
+
56
+ core.js loads all tab-*.js as `<script type=module>` (or dynamic import) AFTER defining UAP, then
57
+ renders the tab from `location.hash` (default #overview). Tab bar buttons set the hash.
58
+ On each snapshot: if the active tab is live, re-render it; always update header + tab badges.
59
+
60
+ Data channels: reuse the current three (WS primary at ws://host, SSE /api/events for the activity
61
+ feed + snapshot fallback, poll /api/dashboard every REFRESH_MS when WS closed). Reconnect w/ backoff.
62
+ The `__UAP_DASHBOARD_TOKEN__` and `__UAP_DASH_REFRESH_MS__` placeholders are substituted server-side
63
+ in dashboard.html (already handled by server.ts) — read them from a small inline bootstrap script in
64
+ the shell that sets window.__UAP_BOOT = {token, refreshMs} before core.js loads.
65
+
66
+ ## Tabs — content & interactions
67
+
68
+ ### Overview (aggregate landing)
69
+ - KPI row (stat tiles): Tasks done/total + % ; Active agents ; Active deliver runs ; Orchestration
70
+ progress % ; Tokens saved ; Cost saved $ ; Memory entries ; Policy block-rate. Each tile clickable ->
71
+ goto its tab.
72
+ - Hero charts (uPlot): Tasks & Agents over time; Compression & Memory over time (from timeSeries).
73
+ - Live summaries (each a compact table with a "drill" affordance): Active Agents (click row ->
74
+ #agents drawer), Active Deliver Runs (click -> #deliver), Orchestration ledger mini-progress
75
+ (click -> #orchestration), Recent activity (last N events).
76
+
77
+ ### Tasks & Epics
78
+ - Toolbar: "+ New Task" (opens form: title, type[task|epic|bug|...], priority, assignee), filter by
79
+ status, expand/collapse-all groups.
80
+ - Kanban (5 cols open/in_progress/blocked/done/wont_do) grouped into families by groupId (port
81
+ current logic incl. depth indent + breadcrumb). Each card: click -> drawer with detail + controls:
82
+ change status (select), set assignee, claim, close(done), DELETE (danger-confirm). Epic-typed
83
+ cards also show ledger children + "advance/fail item" + "reset ledger" (danger).
84
+ - Endpoints: POST /api/tasks (create), POST /api/tasks/:id/update {status?,assignee?,priority?,title?},
85
+ POST /api/tasks/:id/close, POST /api/tasks/:id/delete, POST /api/tasks/:id/claim {agentId}.
86
+ Epics/ledger: POST /api/ledger/item/:id {status:done|failed|pending}, POST /api/ledger/reset,
87
+ POST /api/ledger/init {mission, items:[...]} (optional).
88
+
89
+ ### Agents & Sessions
90
+ - Agents grid: card per coordination.agents / session.agents (id,name,type,status badge, task, model,
91
+ tokens, cost). Click -> right drawer per-agent detail: full token IO (in/out), model, duration,
92
+ taskCount, skills chips (from coordination.skillsPerAgent[id]), patterns (patternsPerAgent[id]),
93
+ routing decisions if linkable. Control: Deregister agent (danger-confirm) -> POST /api/agents/:id/deregister.
94
+ Toolbar: "Clean stale agents" -> POST /api/agents/clean.
95
+ - Sessions history table (sessions[]): per row tokens/cost/agents/tasks/model/status; click -> drawer
96
+ with the session's detail (for the CURRENT/live session use `state.session` rich object incl agents,
97
+ modelBreakdown, skills, patterns, deploys, step progress bar).
98
+
99
+ ### Orchestration
100
+ - Header: orchestrator toggle (on/off/auto) -> POST /api/orchestrator {state:on|off|auto} (writes
101
+ .uap.json deliver.orchestrate). Show current from state.models or a new field.
102
+ - Active build ledger block (orchestrationTree.ledger): mission, pct, done/total, items w/ deps +
103
+ per-item advance/fail controls (reuse ledger endpoints).
104
+ - Mission tree (orchestrationTree.missions recursive): expand/collapse nodes, show assigned agents,
105
+ status badges. Node controls where it maps to a task: jump to task drawer.
106
+
107
+ ### Deliver
108
+ - Run registry list (NEW data: state.deliverRuns from listRuns()): runId, status
109
+ (running|delivered|failed|interrupted), mission/instruction, phase, taskId, updatedAt.
110
+ - Controls per run: Cancel (running only, danger-confirm) -> POST /api/deliver/:runId/cancel
111
+ (writes stop-file; server also attempts PID kill). Resume (interrupted) -> POST /api/deliver/:runId/resume.
112
+ - Launch form: instruction textarea + options (model preset, max-turns) -> POST /api/deliver/launch
113
+ {instruction, model?, maxTurns?} spawns `uap deliver` detached; returns runId. Show a warning that
114
+ this spawns a host process.
115
+ - Live telemetry: for a running run, show its RunCoordinator status from the agent registry (the
116
+ agent whose task text carries "turn N: X% of gates").
117
+
118
+ ### Policies & Compliance (port existing + keep controls)
119
+ - Policies table: name/category/level/stage/status + toggle button + stage select + level select
120
+ (existing endpoints /api/policy/:id/{toggle,stage,level}).
121
+ - Enforcement stages bar chart. Compliance: block-rate sparkline, failuresByMechanism bars,
122
+ recentFailures table, auditTrail list, Live Events SSE feed.
123
+
124
+ ### Models
125
+ - Roles (planner/executor/reviewer/fallback), strategy, availableModels, enabled, costOptimization,
126
+ routingMatrix (handle BOTH shapes: string tier->model AND {planner,executor}), routingRules.
127
+ - sessionUsage table (model/tasks/in/out/cost/success%), recentRoutingDecisions table, totalCost.
128
+
129
+ ### Memory
130
+ - L1/L2/L3(Qdrant)/L4 cards, compression block, hit/miss gauge + hit-rate sparkline, recentQueries
131
+ table, savingsByInfluence (summary + per-influence table with quality badges).
132
+
133
+ ## Backend — new files & edits
134
+ ```
135
+ src/dashboard/controls/tasks.ts handleTaskCreate/Update/Close/Delete/Claim (import TaskService)
136
+ src/dashboard/controls/epics.ts handleLedgerItem/Reset/Init (import completion-ledger)
137
+ src/dashboard/controls/orchestrator.ts handleOrchestratorToggle (import modifyUapConfig)
138
+ src/dashboard/controls/deliver.ts handleDeliverLaunch/Cancel/Resume + listDeliverRuns
139
+ src/dashboard/controls/agents.ts handleAgentDeregister/CleanStale (import CoordinationService)
140
+ ```
141
+ - `run-state.ts`: add `listRuns(projectRoot): DeliverRunState[]` (readdir deliverRunsDir + loadRunState each).
142
+ - `convergence-loop.ts`: add a cooperative STOP-FILE check per turn (next to the existing guidanceFile
143
+ poll). Stop-file path = `<projectRoot>/.uap/deliver-runs/<runId>/STOP`. If present, break the loop,
144
+ set run status 'interrupted', return. (Cancel endpoint writes this file; also SIGTERM the PID if known.)
145
+ - `data-service.ts`: add `deliverRuns` (from listRuns) and `ledger` (loadLedger) to DashboardData +
146
+ the interface. Add `getAgentDetail(cwd, agentId)` returning the per-agent detail (from session.agents
147
+ or coordination) for drawer use (optional — drawer can also filter client-side from snapshot).
148
+ - `server.ts`: extend static handler to `/dash/`; add the POST routes above, ALL behind
149
+ `mutationAuthorized(req)` (return denyMutation on fail); parse body via readBody/parseJsonBody;
150
+ validate inputs; return JSON. Destructive ops must be idempotent-safe and never throw unhandled.
151
+
152
+ ## Security & safety
153
+ - Every new mutation route: `if (!mutationAuthorized(req)) return denyMutation(res);` FIRST.
154
+ - Deliver launch spawns a subprocess — bind only when host is localhost is NOT enforced, but the token
155
+ gate is the control. Log launches. Detach the child (unref) so the dash server isn't its parent lifetime.
156
+ - Cancel: write stop-file (cooperative) THEN best-effort SIGTERM the recorded PID if the run-state has one.
157
+ - Confirm dialog on client for every delete/cancel/reset/deregister.
158
+
159
+ ## Tests (vitest, test/) — at least:
160
+ - run-state listRuns returns runs sorted newest-first; empty when dir absent.
161
+ - convergence stop-file: a loop with a pre-written STOP file exits with status interrupted after <=1 turn.
162
+ - server control routes: task create/update/delete happy-path + 401 without token; ledger item; agent
163
+ deregister; orchestrator toggle writes config. (Use ephemeral port, temp cwd.)
164
+ - data-service: getDashboardData includes deliverRuns + ledger keys.
165
+
166
+ ## Verification
167
+ - npm run build clean; tsc --noEmit clean; vitest green; design gate green (tokens only).
168
+ - Browser smoke: start `uap dash serve`, load each tab, exercise one mutation per domain, confirm
169
+ drill-in drawer opens and closes, expand/collapse works, live update ticks.
170
+ ```
171
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.134.0",
3
+ "version": "1.135.2",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
+ })();