@miller-tech/uap 2.21.3 → 2.36.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 (328) hide show
  1. package/README.md +22 -10
  2. package/config/model-profiles/qwen38-coder390.json +46 -0
  3. package/config/model-profiles/qwen38.json +3 -2
  4. package/config/model-registry.json +110 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/benchmarks/paired/types.d.ts +2 -2
  7. package/dist/bin/cli.js +315 -20
  8. package/dist/bin/cli.js.map +1 -1
  9. package/dist/cli/attention.d.ts +6 -0
  10. package/dist/cli/attention.d.ts.map +1 -0
  11. package/dist/cli/attention.js +115 -0
  12. package/dist/cli/attention.js.map +1 -0
  13. package/dist/cli/bench-report.d.ts +82 -0
  14. package/dist/cli/bench-report.d.ts.map +1 -0
  15. package/dist/cli/bench-report.js +237 -0
  16. package/dist/cli/bench-report.js.map +1 -0
  17. package/dist/cli/clients.d.ts +6 -0
  18. package/dist/cli/clients.d.ts.map +1 -0
  19. package/dist/cli/clients.js +142 -0
  20. package/dist/cli/clients.js.map +1 -0
  21. package/dist/cli/dashboard-grants.d.ts +26 -0
  22. package/dist/cli/dashboard-grants.d.ts.map +1 -0
  23. package/dist/cli/dashboard-grants.js +69 -0
  24. package/dist/cli/dashboard-grants.js.map +1 -0
  25. package/dist/cli/dashboard-pace.d.ts +31 -0
  26. package/dist/cli/dashboard-pace.d.ts.map +1 -0
  27. package/dist/cli/dashboard-pace.js +42 -0
  28. package/dist/cli/dashboard-pace.js.map +1 -0
  29. package/dist/cli/dashboard-session.d.ts +25 -0
  30. package/dist/cli/dashboard-session.d.ts.map +1 -0
  31. package/dist/cli/dashboard-session.js +814 -0
  32. package/dist/cli/dashboard-session.js.map +1 -0
  33. package/dist/cli/dashboard-types.d.ts +18 -0
  34. package/dist/cli/dashboard-types.d.ts.map +1 -0
  35. package/dist/cli/dashboard-types.js +9 -0
  36. package/dist/cli/dashboard-types.js.map +1 -0
  37. package/dist/cli/dashboard.d.ts +2 -18
  38. package/dist/cli/dashboard.d.ts.map +1 -1
  39. package/dist/cli/dashboard.js +43 -793
  40. package/dist/cli/dashboard.js.map +1 -1
  41. package/dist/cli/deliver.d.ts +35 -0
  42. package/dist/cli/deliver.d.ts.map +1 -1
  43. package/dist/cli/deliver.js +93 -9
  44. package/dist/cli/deliver.js.map +1 -1
  45. package/dist/cli/diff.d.ts +85 -0
  46. package/dist/cli/diff.d.ts.map +1 -0
  47. package/dist/cli/diff.js +293 -0
  48. package/dist/cli/diff.js.map +1 -0
  49. package/dist/cli/eta.d.ts +35 -0
  50. package/dist/cli/eta.d.ts.map +1 -0
  51. package/dist/cli/eta.js +101 -0
  52. package/dist/cli/eta.js.map +1 -0
  53. package/dist/cli/handsfree.d.ts +7 -0
  54. package/dist/cli/handsfree.d.ts.map +1 -1
  55. package/dist/cli/handsfree.js +27 -0
  56. package/dist/cli/handsfree.js.map +1 -1
  57. package/dist/cli/hooks.d.ts.map +1 -1
  58. package/dist/cli/hooks.js +126 -5
  59. package/dist/cli/hooks.js.map +1 -1
  60. package/dist/cli/loops.d.ts +24 -5
  61. package/dist/cli/loops.d.ts.map +1 -1
  62. package/dist/cli/loops.js +92 -12
  63. package/dist/cli/loops.js.map +1 -1
  64. package/dist/cli/models.d.ts +62 -0
  65. package/dist/cli/models.d.ts.map +1 -0
  66. package/dist/cli/models.js +567 -0
  67. package/dist/cli/models.js.map +1 -0
  68. package/dist/cli/plan.d.ts +9 -0
  69. package/dist/cli/plan.d.ts.map +1 -1
  70. package/dist/cli/plan.js +57 -4
  71. package/dist/cli/plan.js.map +1 -1
  72. package/dist/cli/proxy-relay.d.ts +55 -0
  73. package/dist/cli/proxy-relay.d.ts.map +1 -0
  74. package/dist/cli/proxy-relay.js +116 -0
  75. package/dist/cli/proxy-relay.js.map +1 -0
  76. package/dist/cli/proxy.d.ts +4 -0
  77. package/dist/cli/proxy.d.ts.map +1 -1
  78. package/dist/cli/proxy.js +26 -1
  79. package/dist/cli/proxy.js.map +1 -1
  80. package/dist/cli/sandbox.d.ts +101 -4
  81. package/dist/cli/sandbox.d.ts.map +1 -1
  82. package/dist/cli/sandbox.js +393 -44
  83. package/dist/cli/sandbox.js.map +1 -1
  84. package/dist/cli/self.d.ts +13 -0
  85. package/dist/cli/self.d.ts.map +1 -0
  86. package/dist/cli/self.js +94 -0
  87. package/dist/cli/self.js.map +1 -0
  88. package/dist/cli/systemd-services.d.ts +2 -8
  89. package/dist/cli/systemd-services.d.ts.map +1 -1
  90. package/dist/cli/systemd-services.js +5 -20
  91. package/dist/cli/systemd-services.js.map +1 -1
  92. package/dist/cli/task.js +1 -1
  93. package/dist/cli/task.js.map +1 -1
  94. package/dist/cli/wizard-config.d.ts.map +1 -1
  95. package/dist/cli/wizard-config.js +31 -2
  96. package/dist/cli/wizard-config.js.map +1 -1
  97. package/dist/config/settings-registry.d.ts +1 -1
  98. package/dist/config/settings-registry.d.ts.map +1 -1
  99. package/dist/config/settings-registry.js +108 -0
  100. package/dist/config/settings-registry.js.map +1 -1
  101. package/dist/coordination/capability-router.js +2 -2
  102. package/dist/coordination/capability-router.js.map +1 -1
  103. package/dist/coordination/database.d.ts +13 -0
  104. package/dist/coordination/database.d.ts.map +1 -1
  105. package/dist/coordination/database.js +63 -7
  106. package/dist/coordination/database.js.map +1 -1
  107. package/dist/coordination/eta-inject.d.ts +32 -0
  108. package/dist/coordination/eta-inject.d.ts.map +1 -0
  109. package/dist/coordination/eta-inject.js +64 -0
  110. package/dist/coordination/eta-inject.js.map +1 -0
  111. package/dist/coordination/reactor.d.ts.map +1 -1
  112. package/dist/coordination/reactor.js +14 -0
  113. package/dist/coordination/reactor.js.map +1 -1
  114. package/dist/coordination/service.d.ts +18 -8
  115. package/dist/coordination/service.d.ts.map +1 -1
  116. package/dist/coordination/service.js +38 -28
  117. package/dist/coordination/service.js.map +1 -1
  118. package/dist/dashboard/client-registry.d.ts +163 -0
  119. package/dist/dashboard/client-registry.d.ts.map +1 -0
  120. package/dist/dashboard/client-registry.js +549 -0
  121. package/dist/dashboard/client-registry.js.map +1 -0
  122. package/dist/dashboard/controls.d.ts.map +1 -1
  123. package/dist/dashboard/controls.js +33 -4
  124. package/dist/dashboard/controls.js.map +1 -1
  125. package/dist/dashboard/data-seeder.d.ts +10 -7
  126. package/dist/dashboard/data-seeder.d.ts.map +1 -1
  127. package/dist/dashboard/data-seeder.js +31 -75
  128. package/dist/dashboard/data-seeder.js.map +1 -1
  129. package/dist/dashboard/data-service.d.ts +9 -0
  130. package/dist/dashboard/data-service.d.ts.map +1 -1
  131. package/dist/dashboard/data-service.js +32 -3
  132. package/dist/dashboard/data-service.js.map +1 -1
  133. package/dist/dashboard/placement-routes.d.ts +119 -0
  134. package/dist/dashboard/placement-routes.d.ts.map +1 -0
  135. package/dist/dashboard/placement-routes.js +331 -0
  136. package/dist/dashboard/placement-routes.js.map +1 -0
  137. package/dist/dashboard/server.d.ts.map +1 -1
  138. package/dist/dashboard/server.js +408 -2
  139. package/dist/dashboard/server.js.map +1 -1
  140. package/dist/delivery/attention.d.ts +130 -0
  141. package/dist/delivery/attention.d.ts.map +1 -0
  142. package/dist/delivery/attention.js +341 -0
  143. package/dist/delivery/attention.js.map +1 -0
  144. package/dist/delivery/decompose.d.ts +18 -0
  145. package/dist/delivery/decompose.d.ts.map +1 -1
  146. package/dist/delivery/decompose.js +22 -1
  147. package/dist/delivery/decompose.js.map +1 -1
  148. package/dist/delivery/eta-estimate.d.ts +111 -0
  149. package/dist/delivery/eta-estimate.d.ts.map +1 -0
  150. package/dist/delivery/eta-estimate.js +171 -0
  151. package/dist/delivery/eta-estimate.js.map +1 -0
  152. package/dist/delivery/eta.d.ts +23 -0
  153. package/dist/delivery/eta.d.ts.map +1 -0
  154. package/dist/delivery/eta.js +59 -0
  155. package/dist/delivery/eta.js.map +1 -0
  156. package/dist/delivery/orchestrated-mission.d.ts +10 -0
  157. package/dist/delivery/orchestrated-mission.d.ts.map +1 -1
  158. package/dist/delivery/orchestrated-mission.js +15 -1
  159. package/dist/delivery/orchestrated-mission.js.map +1 -1
  160. package/dist/delivery/plan-check.d.ts +7 -1
  161. package/dist/delivery/plan-check.d.ts.map +1 -1
  162. package/dist/delivery/plan-check.js +23 -2
  163. package/dist/delivery/plan-check.js.map +1 -1
  164. package/dist/delivery/run-state.d.ts +1 -1
  165. package/dist/delivery/run-state.d.ts.map +1 -1
  166. package/dist/delivery/run-state.js +8 -1
  167. package/dist/delivery/run-state.js.map +1 -1
  168. package/dist/delivery/task-orchestrator.d.ts +8 -3
  169. package/dist/delivery/task-orchestrator.d.ts.map +1 -1
  170. package/dist/delivery/task-orchestrator.js +75 -43
  171. package/dist/delivery/task-orchestrator.js.map +1 -1
  172. package/dist/delivery/task-sync.d.ts +21 -0
  173. package/dist/delivery/task-sync.d.ts.map +1 -1
  174. package/dist/delivery/task-sync.js +44 -8
  175. package/dist/delivery/task-sync.js.map +1 -1
  176. package/dist/delivery/task-workspace.d.ts +3 -3
  177. package/dist/delivery/task-workspace.js +3 -3
  178. package/dist/generators/self-card.d.ts +65 -0
  179. package/dist/generators/self-card.d.ts.map +1 -0
  180. package/dist/generators/self-card.js +256 -0
  181. package/dist/generators/self-card.js.map +1 -0
  182. package/dist/inference/probe.js +1 -1
  183. package/dist/inference/probe.js.map +1 -1
  184. package/dist/memory/prepopulate.d.ts.map +1 -1
  185. package/dist/memory/prepopulate.js +2 -0
  186. package/dist/memory/prepopulate.js.map +1 -1
  187. package/dist/models/analytics.d.ts +32 -0
  188. package/dist/models/analytics.d.ts.map +1 -1
  189. package/dist/models/analytics.js +68 -5
  190. package/dist/models/analytics.js.map +1 -1
  191. package/dist/models/plan-readability.d.ts +55 -0
  192. package/dist/models/plan-readability.d.ts.map +1 -0
  193. package/dist/models/plan-readability.js +339 -0
  194. package/dist/models/plan-readability.js.map +1 -0
  195. package/dist/placement/admission.d.ts +59 -0
  196. package/dist/placement/admission.d.ts.map +1 -0
  197. package/dist/placement/admission.js +197 -0
  198. package/dist/placement/admission.js.map +1 -0
  199. package/dist/placement/auto.d.ts +44 -0
  200. package/dist/placement/auto.d.ts.map +1 -0
  201. package/dist/placement/auto.js +212 -0
  202. package/dist/placement/auto.js.map +1 -0
  203. package/dist/placement/enforce.d.ts +128 -0
  204. package/dist/placement/enforce.d.ts.map +1 -0
  205. package/dist/placement/enforce.js +655 -0
  206. package/dist/placement/enforce.js.map +1 -0
  207. package/dist/placement/idle.d.ts +50 -0
  208. package/dist/placement/idle.d.ts.map +1 -0
  209. package/dist/placement/idle.js +213 -0
  210. package/dist/placement/idle.js.map +1 -0
  211. package/dist/placement/ledger.d.ts +323 -0
  212. package/dist/placement/ledger.d.ts.map +1 -0
  213. package/dist/placement/ledger.js +263 -0
  214. package/dist/placement/ledger.js.map +1 -0
  215. package/dist/placement/preview.d.ts +51 -0
  216. package/dist/placement/preview.d.ts.map +1 -0
  217. package/dist/placement/preview.js +61 -0
  218. package/dist/placement/preview.js.map +1 -0
  219. package/dist/placement/probes.d.ts +47 -0
  220. package/dist/placement/probes.d.ts.map +1 -0
  221. package/dist/placement/probes.js +207 -0
  222. package/dist/placement/probes.js.map +1 -0
  223. package/dist/placement/proxy-env.d.ts +21 -0
  224. package/dist/placement/proxy-env.d.ts.map +1 -0
  225. package/dist/placement/proxy-env.js +42 -0
  226. package/dist/placement/proxy-env.js.map +1 -0
  227. package/dist/placement/registry.d.ts +1038 -0
  228. package/dist/placement/registry.d.ts.map +1 -0
  229. package/dist/placement/registry.js +274 -0
  230. package/dist/placement/registry.js.map +1 -0
  231. package/dist/policies/liveness.d.ts +6 -8
  232. package/dist/policies/liveness.d.ts.map +1 -1
  233. package/dist/policies/liveness.js +6 -25
  234. package/dist/policies/liveness.js.map +1 -1
  235. package/dist/policies/sandbox-anchor.d.ts +91 -0
  236. package/dist/policies/sandbox-anchor.d.ts.map +1 -0
  237. package/dist/policies/sandbox-anchor.js +187 -0
  238. package/dist/policies/sandbox-anchor.js.map +1 -0
  239. package/dist/policies/sandbox-grants.d.ts +131 -0
  240. package/dist/policies/sandbox-grants.d.ts.map +1 -0
  241. package/dist/policies/sandbox-grants.js +562 -0
  242. package/dist/policies/sandbox-grants.js.map +1 -0
  243. package/dist/state/manifest.d.ts +4 -2
  244. package/dist/state/manifest.d.ts.map +1 -1
  245. package/dist/state/manifest.js +32 -16
  246. package/dist/state/manifest.js.map +1 -1
  247. package/dist/utils/checkout-root.d.ts +2 -0
  248. package/dist/utils/checkout-root.d.ts.map +1 -0
  249. package/dist/utils/checkout-root.js +43 -0
  250. package/dist/utils/checkout-root.js.map +1 -0
  251. package/dist/utils/resolve-uap-bin.d.ts +5 -0
  252. package/dist/utils/resolve-uap-bin.d.ts.map +1 -0
  253. package/dist/utils/resolve-uap-bin.js +33 -0
  254. package/dist/utils/resolve-uap-bin.js.map +1 -0
  255. package/dist/utils/terminal-safe.d.ts +12 -0
  256. package/dist/utils/terminal-safe.d.ts.map +1 -0
  257. package/dist/utils/terminal-safe.js +53 -0
  258. package/dist/utils/terminal-safe.js.map +1 -0
  259. package/docs/ATTRIBUTION.md +1 -0
  260. package/docs/DASHBOARD_UPLIFT_SPEC.md +8 -8
  261. package/docs/INDEX.md +6 -5
  262. package/docs/architecture/OVERVIEW.md +15 -3
  263. package/docs/architecture/PROTOCOL.md +7 -1
  264. package/docs/architecture/adr/0006-operator-directed-model-placement.md +86 -0
  265. package/docs/architecture/adr/0007-auto-displacement-allowlist-as-standing-consent.md +79 -0
  266. package/docs/architecture/adr/0008-fleet-clients-link-out.md +92 -0
  267. package/docs/architecture/adr/0009-completion-triggered-dispatch-and-per-leaf-model-dials.md +71 -0
  268. package/docs/architecture/adr/0010-attention-queue-and-afk-auto-decider.md +159 -0
  269. package/docs/architecture/adr/0011-sandbox-grants-and-network-egress-boundary.md +219 -0
  270. package/docs/architecture/adr/0012-operator-legibility-surfaces.md +175 -0
  271. package/docs/getting-started/INSTALLATION.md +1 -1
  272. package/docs/guides/AUTOMATIC_FEATURES.md +8 -0
  273. package/docs/guides/LOCAL_MODELS.md +1 -1
  274. package/docs/guides/PROXY.md +2 -1
  275. package/docs/guides/SANDBOX.md +91 -4
  276. package/docs/integrations/MCP_ROUTER.md +1 -1
  277. package/docs/plans/model-performance-proof-options.md +176 -0
  278. package/docs/reference/API.md +1 -1
  279. package/docs/reference/CLI.md +169 -4
  280. package/docs/reference/COMPARISON.md +1 -1
  281. package/docs/reference/CONFIGURATION.md +12 -3
  282. package/docs/reference/CONFIGURATION_REFERENCE.md +269 -14
  283. package/docs/reference/DATABASE_SCHEMA.md +1 -1
  284. package/docs/reference/FEATURES.md +15 -3
  285. package/docs/reference/PATTERNS.md +1 -1
  286. package/docs/reference/PLATFORMS.md +14 -1
  287. package/docs/specs/operator-model-placement.md +1314 -0
  288. package/docs/specs/operator-model-placement.review.md +339 -0
  289. package/package.json +2 -2
  290. package/scripts/maintenance/publish-npm.sh +23 -11
  291. package/scripts/version-bump.sh +6 -51
  292. package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
  293. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
  294. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
  295. package/src/policies/schemas/policies/mandatory-testing-deployment.md +2 -5
  296. package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
  297. package/templates/hooks/session-start.sh +44 -0
  298. package/tools/agents/UAP/cli.py +14 -3
  299. package/tools/agents/UAP/paths.py +50 -0
  300. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
  301. package/tools/agents/scripts/anthropic_proxy.py +1386 -99
  302. package/tools/agents/scripts/toolcall_path_normalizer.py +46 -1
  303. package/tools/agents/tests/test_anthropic_proxy_streaming.py +29 -6
  304. package/tools/agents/tests/test_doubling_break.py +64 -5
  305. package/tools/agents/tests/test_error_loop_break.py +10 -4
  306. package/tools/agents/tests/test_error_loop_ignores_correctives.py +13 -2
  307. package/tools/agents/tests/test_lockstep_escalation.py +280 -9
  308. package/tools/agents/tests/test_path_containment.py +94 -0
  309. package/tools/agents/tests/test_proxy_unix_socket.py +162 -0
  310. package/tools/agents/tests/test_signature_quality.py +349 -0
  311. package/tools/agents/tests/test_uap_compliance.py +11 -1
  312. package/web/dash/core.js +1 -0
  313. package/web/dash/tab-clients.js +196 -0
  314. package/web/dash/tab-models.js +387 -8
  315. package/web/dash/tab-policies.js +87 -4
  316. package/web/dash/tabs.js +77 -73
  317. package/web/dashboard.html +6 -0
  318. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  319. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  320. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  321. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  322. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  323. package/web/dash/tab-agents.js +0 -21
  324. package/web/dash/tab-deliver.js +0 -12
  325. package/web/dash/tab-memory.js +0 -12
  326. package/web/dash/tab-orchestration.js +0 -21
  327. package/web/dash/tab-overview.js +0 -21
  328. package/web/dash/tab-tasks.js +0 -21
@@ -0,0 +1,196 @@
1
+ /**
2
+ * tab-clients.js — Clients fleet panel.
3
+ *
4
+ * Loaded AFTER tabs.js (the tab-policies.js precedent) so it owns the
5
+ * 'clients' tab outright — there is no inline stub in tabs.js. Shows one
6
+ * card per discovered client folder (auto-scan of the scan root + manual
7
+ * entries in ~/.uap/clients.json): deliver-run states, task queue depth,
8
+ * git branch/dirty, last activity, and a link-out to that client's OWN
9
+ * dashboard. "Open dashboard" spawns the client's `uap dashboard serve`
10
+ * on demand (registry-only, loopback, token-gated on the spawned instance
11
+ * exactly like this one) — full control for every client, but the host
12
+ * server never mutates another project itself.
13
+ *
14
+ * Fleet reads hit many client DBs, so this tab throttles its own polling
15
+ * (10s) rather than riding the 2s refresh tick, and rebuilds only when the
16
+ * payload actually changes (sig gate — same trick as the Deliver tab).
17
+ *
18
+ * All DOM is built with UAP.el (no innerHTML); the one mutation (spawn)
19
+ * goes through UAP.api (token-gated); reads use fetch on UAP.API_URL.
20
+ */
21
+ (function () {
22
+ 'use strict';
23
+ if (typeof UAP === 'undefined' || !UAP.registerTab) return;
24
+ var U = window.UAP;
25
+ var el = U.el;
26
+
27
+ var REFRESH_MS = 10000;
28
+ var lastFetch = 0;
29
+ var inFlight = false;
30
+ var lastSig = null;
31
+ var rootEl = null;
32
+
33
+ function get(path) {
34
+ return fetch(UAP.API_URL + path).then(function (r) {
35
+ if (!r.ok) throw new Error('HTTP ' + r.status);
36
+ return r.json();
37
+ });
38
+ }
39
+
40
+ // ── local render helpers (same shapes as tabs.js) ──
41
+ function panel(title) {
42
+ return el('div', { class: 'panel' }, el('h2', {}, title));
43
+ }
44
+ function kv(label, value, cls) {
45
+ return el('div', { class: 'kv' }, el('span', { class: 'label', text: label }), el('span', { class: 'value ' + (cls || ''), text: value }));
46
+ }
47
+ function chip(text, cls) {
48
+ return el('span', { class: 'chip ' + (cls || ''), text: text });
49
+ }
50
+ function empty(msg) {
51
+ return el('div', { class: 'empty', text: msg || 'No data' });
52
+ }
53
+ function sig(clients) {
54
+ return JSON.stringify((clients || []).map(function (c) {
55
+ return [c.path, c.name, c.port, c.manual, c.branch, c.dirty, c.dashboardAlive, c.degraded, c.lastActivity,
56
+ c.deliver.running, c.deliver.interrupted, c.deliver.failed, c.deliver.total,
57
+ c.tasks.open, c.tasks.inProgress, c.tasks.blocked];
58
+ }));
59
+ }
60
+
61
+ // ── card ──
62
+ function clientCard(c) {
63
+ var card = panel(c.name || c.path);
64
+
65
+ // Status chips: color carries meaning only (status hues, primary for action).
66
+ var chips = el('div', { class: 'chip-list' });
67
+ if (c.host) chips.appendChild(chip('this dashboard', 'active'));
68
+ else if (c.dashboardAlive) chips.appendChild(chip('dashboard up', 'active'));
69
+ else if (!c.managed) chips.appendChild(chip('unmanaged', 'inactive'));
70
+ if (c.branch && c.branch !== '?') chips.appendChild(chip(c.branch));
71
+ if (c.dirty > 0) chips.appendChild(chip(c.dirty + ' dirty'));
72
+ if (c.manual) chips.appendChild(chip('pinned'));
73
+ if (c.degraded) chips.appendChild(chip('degraded', 'inactive'));
74
+ card.appendChild(chips);
75
+
76
+ if (!c.managed) {
77
+ card.appendChild(el('div', { class: 'section-note', text: 'Git repo without .uap — run `uap init` inside it to make it a managed client.' }));
78
+ return card;
79
+ }
80
+
81
+ // Deliver runs — green running, yellow interrupted, red failed.
82
+ var d = c.deliver || { running: 0, interrupted: 0, failed: 0, total: 0 };
83
+ card.appendChild(kv('Deliver runs',
84
+ (d.running ? d.running + ' running' : '') +
85
+ (d.interrupted ? (d.running ? ' · ' : '') + d.interrupted + ' interrupted' : '') +
86
+ (d.failed ? (d.running || d.interrupted ? ' · ' : '') + d.failed + ' failed' : '') ||
87
+ (d.total ? 'all quiet (' + d.total + ' done)' : 'none'),
88
+ d.running ? 'green' : d.failed ? 'red' : d.interrupted ? 'yellow' : ''));
89
+
90
+ // Job queue depth.
91
+ var t = c.tasks || { open: 0, inProgress: 0, blocked: 0, total: 0 };
92
+ card.appendChild(kv('Queue',
93
+ (t.inProgress || 0) + ' in progress · ' + (t.open || 0) + ' open · ' + (t.blocked || 0) + ' blocked',
94
+ t.blocked ? 'yellow' : ''));
95
+
96
+ card.appendChild(kv('Last activity', U.timeAgo(c.lastActivity)));
97
+ if (c.error) card.appendChild(el('div', { class: 'section-note', text: c.error }));
98
+
99
+ // Action: link out. Alive → straight to the client's dashboard; down →
100
+ // spawn on demand first. The host entry links back to this dashboard.
101
+ // Alive non-host cards also get a Stop (the teardown twin of Launch —
102
+ // without it the tab would be an orphan factory).
103
+ var actions = el('div', { class: 'toolbar', style: { marginBottom: '0' } });
104
+ var url = c.host ? U.API_URL : 'http://127.0.0.1:' + c.port + '/';
105
+ var btn = el('button', {
106
+ class: 'btn btn-primary',
107
+ onclick: function () { openClient(c, url); },
108
+ }, c.dashboardAlive || c.host ? 'Open dashboard' : 'Launch dashboard');
109
+ actions.appendChild(btn);
110
+ if (c.managed && !c.host && c.dashboardAlive) {
111
+ actions.appendChild(el('button', {
112
+ class: 'btn btn-danger',
113
+ onclick: function () { stopClient(c); },
114
+ }, 'Stop'));
115
+ }
116
+ card.appendChild(actions);
117
+ return card;
118
+ }
119
+
120
+ function stopClient(c) {
121
+ U.confirm('Stop the dashboard for ' + (c.name || 'this client') + '? Terminates that dashboard server only — deliver runs in that project keep running.', { danger: true, okLabel: 'Stop' }).then(function (ok) {
122
+ if (!ok) return;
123
+ U.api('/api/clients/stop', { path: c.path }).then(function (res) {
124
+ if (res && res.stopped) U.toast('Dashboard stopped', 'ok');
125
+ else if (res && !res.wasAlive) U.toast('No live dashboard for that client', 'ok');
126
+ else U.toast((res && res.reason) || 'Could not stop', 'err');
127
+ refresh();
128
+ });
129
+ });
130
+ }
131
+
132
+ function openClient(c, url) {
133
+ if (c.dashboardAlive || c.host) { window.open(url, '_blank'); return; }
134
+ // Open the placeholder synchronously (inside the user-activation window):
135
+ // the spawn wait runs seconds, and window.open from a promise callback
136
+ // would be popup-blocked.
137
+ var win = window.open('', '_blank');
138
+ U.api('/api/clients/serve', { path: c.path }).then(function (res) {
139
+ if (res && res.alive) {
140
+ if (win) win.location = 'http://127.0.0.1:' + res.port + '/';
141
+ refresh();
142
+ } else {
143
+ if (win) win.close();
144
+ U.toast('Dashboard is starting — refresh the tab in a few seconds and open it again', 'err');
145
+ }
146
+ }).catch(function () { if (win) win.close(); /* U.api already toasted the failure */ });
147
+ }
148
+
149
+ function build(clients) {
150
+ if (!rootEl) return;
151
+ U.clear(rootEl);
152
+ rootEl.appendChild(el('div', { class: 'section-note', style: { marginBottom: '12px' },
153
+ text: 'Client folders: auto-scan of the parent directory + manual entries in ~/.uap/clients.json (uap clients add/remove). Opening a client starts ITS OWN dashboard — full control stays on that instance, loopback-only and token-gated.' }));
154
+ if (!clients || !clients.length) {
155
+ rootEl.appendChild(empty('No client folders discovered'));
156
+ return;
157
+ }
158
+ var grid = el('div', { class: 'card-grid' });
159
+ clients.forEach(function (c) { grid.appendChild(clientCard(c)); });
160
+ rootEl.appendChild(grid);
161
+ }
162
+
163
+ function refresh() {
164
+ if (inFlight) return; // a hung fetch must not wedge the tab permanently
165
+ lastFetch = Date.now();
166
+ inFlight = true;
167
+ get('/api/clients').then(function (d) {
168
+ inFlight = false;
169
+ var clients = (d && d.clients) || [];
170
+ var s = sig(clients);
171
+ if (s === lastSig) return;
172
+ lastSig = s;
173
+ build(clients);
174
+ }).catch(function (e) {
175
+ inFlight = false;
176
+ if (rootEl) {
177
+ U.clear(rootEl);
178
+ rootEl.appendChild(el('div', { class: 'empty', text: 'Fleet unavailable: ' + (e.message || e) }));
179
+ }
180
+ });
181
+ }
182
+
183
+ U.registerTab('clients', {
184
+ label: 'Clients',
185
+ render: function (root) {
186
+ rootEl = root;
187
+ lastSig = null;
188
+ U.clear(root);
189
+ root.appendChild(el('div', { class: 'empty', text: 'Loading client fleet…' }));
190
+ refresh();
191
+ },
192
+ update: function () {
193
+ if (Date.now() - lastFetch >= REFRESH_MS) refresh();
194
+ },
195
+ });
196
+ })();
@@ -1,12 +1,391 @@
1
1
  /**
2
- * tab-models.js — Models tab stub
3
- * Registers with UAP.registerTab and renders a "coming soon" message
4
- * (textContent only, per the module contract — no innerHTML).
2
+ * tab-models.js — Models & Placement panel.
3
+ *
4
+ * Loaded AFTER tabs.js (the tab-policies.js precedent) so this full renderer
5
+ * overrides the inline routing-only Models stub. Placement state comes from
6
+ * /api/placement/state, which syncs the ledger (probes + unit reconcile) —
7
+ * heavier than the 2s dash refresh, so this tab throttles its own polling
8
+ * (10s) rather than riding the refresh tick.
9
+ *
10
+ * Full management surface (phase 3/4): pending requests resolve/dismiss (the
11
+ * approval signature — expected_victims — rides along so enforcement only
12
+ * evicts what the operator saw), residents unload, the phase-4 auto policy
13
+ * (enable/disable + the displacement allowlist behind the same explicit
14
+ * consent gate the CLI's --yes demands), and the ranked preview.
15
+ *
16
+ * All DOM is built with UAP.el (no innerHTML); mutations go through UAP.api
17
+ * (token-gated); reads use fetch on UAP.API_URL.
5
18
  */
19
+ (function () {
20
+ 'use strict';
21
+ if (typeof UAP === 'undefined' || !UAP.registerTab) return;
22
+ var el = UAP.el;
6
23
 
7
- UAP.registerTab('models', {
8
- label: 'Models',
9
- render(root, state) {
10
- root.textContent = 'Models — coming soon';
24
+ function get(path) {
25
+ return fetch(UAP.API_URL + path).then(function (r) {
26
+ if (!r.ok) throw new Error('HTTP ' + r.status);
27
+ return r.json();
28
+ });
11
29
  }
12
- });
30
+
31
+ // ── local render helpers (same shapes as tabs.js) ──
32
+ function panel(title) {
33
+ return el('div', { class: 'panel' }, el('h2', {}, title));
34
+ }
35
+ function kv(label, value, cls) {
36
+ return el('div', { class: 'kv' }, el('span', { class: 'label', text: label }), el('span', { class: 'value ' + (cls || ''), text: value }));
37
+ }
38
+ function empty(msg) {
39
+ return el('div', { class: 'empty', text: msg || 'No data' });
40
+ }
41
+ function badge(text, cls) {
42
+ return el('span', { class: 'badge ' + (cls || 'idle'), text: text });
43
+ }
44
+ function tableNode(headers, items, rowFn) {
45
+ var thead = el('tr', {});
46
+ headers.forEach(function (hd) { thead.appendChild(el('th', { text: hd })); });
47
+ var t = el('table', {}, thead);
48
+ items.forEach(function (it) {
49
+ var tr = el('tr', {});
50
+ rowFn(it).forEach(function (c) { tr.appendChild(el('td', {}, c)); });
51
+ t.appendChild(tr);
52
+ });
53
+ return el('div', { class: 'table-wrap' }, t);
54
+ }
55
+ function mib(n) {
56
+ if (typeof n !== 'number' || !isFinite(n)) return '—';
57
+ return Math.round(n).toLocaleString() + ' MiB';
58
+ }
59
+ function btn(text, onclick, cls) {
60
+ return el('button', { class: 'btn ' + (cls || ''), text: text, onclick: onclick });
61
+ }
62
+
63
+ var STATE_TONE = { hot: 'active', warming: 'running', draining: 'failed', paused: 'idle' };
64
+ var REASON_TONE = {
65
+ not_resident: 'idle', unknown_model: 'failed', no_measured_config: 'failed',
66
+ auto_loading: 'running', auto_failed: 'failed', no_controller: 'failed',
67
+ unroutable_target: 'failed', invalid_request: 'failed',
68
+ };
69
+
70
+ // ── throttled placement-state fetch (10s; sync is probe-heavy) ──
71
+ var cache = { at: 0, data: null, inflight: null };
72
+ function placementState(force) {
73
+ if (!force && cache.data && Date.now() - cache.at < 10_000) return Promise.resolve(cache.data);
74
+ if (cache.inflight) return cache.inflight;
75
+ cache.inflight = get('/api/placement/state').then(function (d) {
76
+ cache.at = Date.now();
77
+ cache.data = d;
78
+ cache.inflight = null;
79
+ return d;
80
+ }).catch(function (e) {
81
+ cache.inflight = null;
82
+ throw e;
83
+ });
84
+ return cache.inflight;
85
+ }
86
+
87
+ /** Render an enforce result (steps + ok) — the operator's evidence. */
88
+ function resultBlock(res) {
89
+ var box = el('div', { class: 'empty' });
90
+ box.appendChild(badge(res.ok ? 'ok' : 'FAILED', res.ok ? 'active' : 'failed'));
91
+ if (res.error) box.appendChild(el('div', { text: res.error, class: 'value failed' }));
92
+ (res.steps || []).forEach(function (s) {
93
+ box.appendChild(el('div', { text: (s.ok ? 'ok ' : 'FAIL ') + s.name + (s.detail ? ' — ' + s.detail : '') }));
94
+ });
95
+ return box;
96
+ }
97
+
98
+ function renderPlacement(p, d) {
99
+ var devs = d.devices || {};
100
+ Object.keys(devs).forEach(function (k) {
101
+ var v = devs[k] || {};
102
+ var free = v.free_mib !== undefined ? mib(v.free_mib) : 'unprobed';
103
+ var total = v.total_mib !== undefined ? mib(v.total_mib) : 'total unmeasured';
104
+ var reserve = v.reserved_mib !== undefined ? mib(v.reserved_mib) : 'reserve unmeasured';
105
+ p.appendChild(kv(k, v.kind + ' — ' + total + ', ' + free + ' free, ' + reserve + ' reserved'));
106
+ });
107
+
108
+ p.appendChild(el('h3', {}, 'Residents'));
109
+ var residents = d.residents || [];
110
+ if (!residents.length) p.appendChild(empty('No resident model — nothing is loaded'));
111
+ else
112
+ p.appendChild(
113
+ tableNode(['Model / Config', 'State', 'Device', 'GPU', 'Host RSS', 'Unit', ''], residents, function (r) {
114
+ return [
115
+ el('span', { class: 'mono-sm', text: r.model + '/' + r.config }),
116
+ badge(r.state, STATE_TONE[r.state] || 'idle'),
117
+ r.device,
118
+ el('span', { class: 'value ' + (r.gpu_mib ? 'green' : 'yellow'), text: r.gpu_mib ? mib(r.gpu_mib) : 'unknown' }),
119
+ r.host_rss_mib ? mib(r.host_rss_mib) : '—',
120
+ r.unit || '—',
121
+ btn('Unload', function () {
122
+ UAP.confirm('Unload ' + r.model + '/' + r.config + ' from ' + r.device + '? In-flight work on it is aborted (drain first happens automatically).').then(function (yes) {
123
+ if (!yes) return;
124
+ UAP.api('/api/placement/unload', { model: r.model }).then(function (res) {
125
+ UAP.drawer('Unload ' + r.model, resultBlock(res));
126
+ placementState(true);
127
+ });
128
+ });
129
+ }),
130
+ ];
131
+ }),
132
+ );
133
+
134
+ p.appendChild(el('h3', {}, 'Pending requests'));
135
+ var pending = d.pending || [];
136
+ if (!pending.length) {
137
+ p.appendChild(empty('No requests awaiting the operator'));
138
+ } else {
139
+ // Cards, not a table: each row needs its own action area (option list
140
+ // fetched on demand, the apply approval signature, dismiss).
141
+ pending.forEach(function (q) { p.appendChild(pendingCard(q)); });
142
+ }
143
+
144
+ if ((d.registry_errors || []).length) {
145
+ d.registry_errors.forEach(function (e) {
146
+ p.appendChild(el('div', { class: 'empty', text: 'registry error: ' + e }));
147
+ });
148
+ }
149
+ }
150
+
151
+ function pendingCard(q) {
152
+ var tone = REASON_TONE[q.reason] || 'idle';
153
+ var isAuto = q.reason === 'auto_loading';
154
+ // Remaining TTL, not timeAgo: expires_at is in the FUTURE for a live
155
+ // pending request, and timeAgo clamps future stamps to "0s ago" — which
156
+ // read as "already expired" for the entry's whole lifetime.
157
+ var ttlMs = q.expires_at ? Date.parse(q.expires_at) - Date.now() : NaN;
158
+ var ttl = isNaN(ttlMs)
159
+ ? ''
160
+ : ttlMs <= 0
161
+ ? 'expired'
162
+ : 'expires in ' + UAP.fmtDur(ttlMs);
163
+ var head = el('div', { class: 'collapsible-header open' },
164
+ el('span', { class: 'mono-sm', text: q.id }),
165
+ ' ',
166
+ el('span', { text: q.requested_model }),
167
+ ' client ' + (q.client || '?') + ' ' + ttl,
168
+ ' ',
169
+ badge(q.reason || 'parked', tone));
170
+ var inner = el('div', { class: 'cb-inner' });
171
+ var body = el('div', { class: 'collapsible-body open' }, inner);
172
+ head.addEventListener('click', function () { head.classList.toggle('open'); body.classList.toggle('open'); });
173
+
174
+ if (isAuto) {
175
+ inner.appendChild(el('div', { class: 'muted', text: 'auto load in flight — retrying clients forward when it completes; a manual apply races the background run' }));
176
+ }
177
+
178
+ var optionsBox = el('div', {});
179
+ inner.appendChild(optionsBox);
180
+ var loadOptions = function () {
181
+ UAP.clear(optionsBox);
182
+ optionsBox.appendChild(el('div', { class: 'muted', text: 'Computing options…' }));
183
+ get('/api/placement/preview?model=' + encodeURIComponent(q.requested_model)).then(function (r) {
184
+ UAP.clear(optionsBox);
185
+ optionsBox.appendChild(kv('options', r.reason + (r.matched === false ? ' — not in registry' : ''), r.reason === 'ok' ? 'green' : 'yellow'));
186
+ (r.notes || []).forEach(function (n) { optionsBox.appendChild(el('div', { class: 'empty', text: n })); });
187
+ (r.options || []).forEach(function (o, i) {
188
+ var victims = (o.victims || []).map(function (v) { return v.model + '/' + v.config; }).join(', ');
189
+ var row = el('div', { style: { margin: '4px 0' } },
190
+ badge('#' + (i + 1) + ' ' + o.kind, o.kind === 'reuse' ? 'active' : o.kind === 'load_alongside' ? 'running' : 'failed'),
191
+ ' ',
192
+ el('span', { class: 'mono-sm', text: o.model + '/' + o.config + ' on ' + o.device }),
193
+ ' ',
194
+ el('span', { text: o.cost_mib === null || o.cost_mib === undefined ? '' : mib(o.cost_mib) }),
195
+ ' ',
196
+ victims ? el('span', { class: 'value failed', text: 'evicts ' + victims }) : el('span', { text: '' }),
197
+ ' ',
198
+ btn('Apply', function () {
199
+ var apply = function () {
200
+ UAP.api('/api/placement/resolve', {
201
+ placement_id: q.id,
202
+ option: i + 1,
203
+ expected_victims: o.victims || [],
204
+ }).then(function (res) {
205
+ UAP.drawer('Apply ' + q.id, resultBlock(res));
206
+ placementState(true);
207
+ });
208
+ };
209
+ if (o.kind === 'displace') {
210
+ UAP.confirm('Apply #' + (i + 1) + ' — unload ' + (victims || 'the victim set') + ' and load ' + o.model + '/' + o.config + '?').then(function (yes) { if (yes) apply(); });
211
+ } else apply();
212
+ }, 'btn-primary'));
213
+ optionsBox.appendChild(row);
214
+ });
215
+ if (!(r.options || []).length) optionsBox.appendChild(empty('No viable option — the registry/budget refuses this request'));
216
+ }).catch(function (e) {
217
+ UAP.clear(optionsBox);
218
+ optionsBox.appendChild(el('div', { class: 'empty', text: 'Preview failed: ' + e.message }));
219
+ });
220
+ };
221
+ loadOptions();
222
+
223
+ inner.appendChild(el('div', { style: { marginTop: '8px' } },
224
+ btn('Dismiss', function () {
225
+ UAP.confirm('Dismiss parked request ' + q.id + ' for ' + q.requested_model + '?').then(function (yes) {
226
+ if (!yes) return;
227
+ UAP.api('/api/placement/dismiss', { placement_id: q.id }).then(function () {
228
+ UAP.toast('Dismissed ' + q.id, 'ok');
229
+ placementState(true);
230
+ });
231
+ });
232
+ })));
233
+
234
+ return el('div', { class: 'panel' }, head, body);
235
+ }
236
+
237
+ function renderRegistry(p, d) {
238
+ var models = d.models || [];
239
+ if (!models.length) {
240
+ p.appendChild(empty('Registry empty — no models declared'));
241
+ return;
242
+ }
243
+ models.forEach(function (m) {
244
+ p.appendChild(el('h3', {}, m.display + ' (' + m.model + ')'));
245
+ if (m.engine) p.appendChild(kv('engine', m.engine));
246
+ if (m.unit) p.appendChild(kv('unit', m.unit));
247
+ if (m.service) p.appendChild(kv('service', m.service));
248
+ (m.advertises || []).forEach(function (id) { p.appendChild(kv('advertises', id)); });
249
+ p.appendChild(
250
+ tableNode(['Config', 'Footprint', 'KV geometry', 'Measured', 'Source'], m.configs || [], function (c) {
251
+ return [
252
+ el('span', { class: 'mono-sm', text: c.config }),
253
+ c.measured
254
+ ? el('span', { class: 'value green', text: mib(c.resident_gpu_mib) + ' GPU, ' + mib(c.host_rss_mib) + ' RSS' })
255
+ : el('span', { class: 'value yellow', text: 'unmeasured — fails closed' }),
256
+ c.measured ? (c.kv_kind || '?') + ' ' + (c.kv_resident_cells || '?') + '/' + (c.context_pool_cells || '?') + ' cells' : '—',
257
+ c.measured ? (c.measured_at || '').slice(0, 10) : 'never',
258
+ c.measured ? c.source || '—' : '—',
259
+ ];
260
+ }),
261
+ );
262
+ });
263
+ }
264
+
265
+ // ── phase-4 auto policy (§4.4.1): the UI twin of `uap models auto` ──
266
+ function renderAuto(p, d) {
267
+ var auto = d.auto || { enabled: false, allow_displace: [] };
268
+ var models = (d.models || []).map(function (m) { return m.model; });
269
+ p.appendChild(kv('auto-load', auto.enabled ? 'ENABLED — non-displacing options load automatically' : 'disabled — every park waits for a manual apply', auto.enabled ? 'green' : 'yellow'));
270
+ p.appendChild(el('div', { style: { margin: '8px 0' } },
271
+ btn(auto.enabled ? 'Disable' : 'Enable', function () {
272
+ UAP.api('/api/placement/auto', auto.enabled ? { disable: true } : { enable: true }).then(function (next) {
273
+ UAP.toast('auto-load ' + (next.enabled ? 'enabled' : 'disabled'), 'ok');
274
+ placementState(true);
275
+ });
276
+ }, auto.enabled ? '' : 'btn-primary')));
277
+
278
+ p.appendChild(el('h3', {}, 'Displacement allowlist (standing consent)'));
279
+ var allow = auto.allow_displace || [];
280
+ if (!allow.length) p.appendChild(empty('Empty — displacement always parks for the operator'));
281
+ allow.forEach(function (m) {
282
+ p.appendChild(el('div', { style: { margin: '2px 0' } },
283
+ el('span', { class: 'mono-sm', text: m }),
284
+ ' ',
285
+ btn('Remove', function () {
286
+ UAP.api('/api/placement/auto', { disallow_displace: [m] }).then(function () {
287
+ UAP.toast('removed ' + m + ' from the allowlist', 'ok');
288
+ placementState(true);
289
+ });
290
+ })));
291
+ });
292
+ var unlisted = models.filter(function (m) { return allow.indexOf(m) === -1; });
293
+ if (unlisted.length) {
294
+ var sel = el('select', { style: { marginRight: '8px' } });
295
+ unlisted.forEach(function (m) { sel.appendChild(el('option', { value: m, text: m })); });
296
+ p.appendChild(el('div', { style: { margin: '8px 0' } }, sel,
297
+ btn('Allow displacement…', function () {
298
+ var m = sel.value;
299
+ if (!m) return;
300
+ UAP.confirm('Allow auto-DISPLACEMENT for ' + m + '? A parked request for it will UNLOAD the current resident(s) — the minimal set that makes room, re-evaluated at load time — and load ' + m + ' WITHOUT an operator prompt between them.').then(function (yes) {
301
+ if (!yes) return;
302
+ UAP.api('/api/placement/auto', { allow_displace: [m], yes: true }).then(function () {
303
+ UAP.toast(m + ' may auto-displace', 'ok');
304
+ placementState(true);
305
+ });
306
+ });
307
+ })));
308
+ }
309
+ }
310
+
311
+ function renderPreview(p, d) {
312
+ p.appendChild(el('div', { class: 'muted', text: 'Ranked placement options for a requested model — the same computation the proxy gate uses (reuse > alongside > smallest displace).' }));
313
+ // Dropdown from the registry (keys + advertised ids), not free text: the
314
+ // operator should not have to remember wire ids.
315
+ var choices = [];
316
+ (d.models || []).forEach(function (m) {
317
+ choices.push(m.model);
318
+ (m.advertises || []).forEach(function (id) { choices.push(id); });
319
+ });
320
+ var input = el('input', { type: 'text', placeholder: 'model id, e.g. ' + (choices[0] || 'Qwen3.8-27B'), style: { width: '320px', marginRight: '8px' } });
321
+ var out = el('div', {});
322
+ var run = function () {
323
+ var model = (input.value || '').trim();
324
+ if (!model) return;
325
+ UAP.clear(out);
326
+ out.appendChild(el('div', { class: 'muted', text: 'Computing…' }));
327
+ get('/api/placement/preview?model=' + encodeURIComponent(model))
328
+ .then(function (r) {
329
+ UAP.clear(out);
330
+ out.appendChild(kv('requested', r.requested + ' — ' + r.reason, r.reason === 'ok' ? 'green' : 'yellow'));
331
+ (r.notes || []).forEach(function (n) { out.appendChild(el('div', { class: 'empty', text: n })); });
332
+ var opts = r.options || [];
333
+ if (!opts.length) return;
334
+ out.appendChild(
335
+ tableNode(['#', 'Kind', 'Config', 'Device', 'Cost', 'Victims'], opts, function (o, i) {
336
+ var victims = (o.victims || []).map(function (v) { return v.model + '/' + v.config; }).join(', ');
337
+ return [
338
+ String(i + 1),
339
+ badge(o.kind, o.kind === 'reuse' ? 'active' : o.kind === 'load_alongside' ? 'running' : 'failed'),
340
+ el('span', { class: 'mono-sm', text: o.model + '/' + o.config }),
341
+ o.device,
342
+ o.cost_mib === null || o.cost_mib === undefined ? '—' : mib(o.cost_mib),
343
+ victims || '—',
344
+ ];
345
+ }),
346
+ );
347
+ })
348
+ .catch(function (e) {
349
+ UAP.clear(out);
350
+ out.appendChild(el('div', { class: 'empty', text: 'Preview failed: ' + e.message }));
351
+ });
352
+ };
353
+ input.addEventListener('keydown', function (e) { if (e.key === 'Enter') run(); });
354
+ if (choices.length) {
355
+ var sel = el('select', { style: { marginRight: '8px' }, onchange: function () { input.value = sel.value; } });
356
+ choices.forEach(function (c) { sel.appendChild(el('option', { value: c, text: c })); });
357
+ p.appendChild(el('div', { style: { marginBottom: '8px' } }, sel, input, el('button', { class: 'btn', text: 'Preview', onclick: run })));
358
+ } else {
359
+ p.appendChild(el('div', { style: { marginBottom: '8px' } }, input, el('button', { class: 'btn', text: 'Preview', onclick: run })));
360
+ }
361
+ p.appendChild(out);
362
+ }
363
+
364
+ UAP.registerTab('models', {
365
+ label: 'Models',
366
+ render: function (root) {
367
+ UAP.clear(root);
368
+ var place = panel('Model Placement');
369
+ var reg = panel('Model Registry');
370
+ var prev = panel('Placement Preview');
371
+ var auto = panel('Auto-load Policy');
372
+ root.appendChild(place);
373
+ root.appendChild(reg);
374
+ root.appendChild(prev);
375
+ root.appendChild(auto);
376
+ placementState()
377
+ .then(function (d) {
378
+ UAP.clear(place);
379
+ UAP.clear(reg);
380
+ UAP.clear(auto);
381
+ renderPlacement(place, d);
382
+ renderRegistry(reg, d);
383
+ renderAuto(auto, d);
384
+ renderPreview(prev, d);
385
+ })
386
+ .catch(function (e) {
387
+ place.appendChild(empty('Placement state unavailable: ' + e.message));
388
+ });
389
+ },
390
+ });
391
+ })();