@lifeaitools/clauth 2.1.1 → 2.10.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.
Files changed (98) hide show
  1. package/.clauth-skill/references/keys-guide.md +270 -270
  2. package/cli/api.js +238 -238
  3. package/cli/commands/install.js +413 -396
  4. package/cli/commands/login.test.js +49 -1
  5. package/cli/commands/ops-install.js +11 -10
  6. package/cli/commands/ops.test.js +270 -0
  7. package/cli/commands/scrub.test.js +70 -0
  8. package/cli/commands/serve.js +2613 -11219
  9. package/cli/commands/uninstall.js +164 -164
  10. package/cli/dashboard/dashboard.js +592 -0
  11. package/cli/dashboard/global.css +65 -0
  12. package/cli/dashboard/panel-element.js +79 -0
  13. package/cli/dashboard/panels/build-status-panel.css +12 -0
  14. package/cli/dashboard/panels/build-status-panel.js +98 -0
  15. package/cli/dashboard/panels/panel-registry.js +58 -0
  16. package/cli/dashboard/panels/services-panel.css +95 -0
  17. package/cli/dashboard/panels/services-panel.js +872 -0
  18. package/cli/dashboard/panels/surfaces-panel.css +59 -0
  19. package/cli/dashboard/panels/surfaces-panel.js +474 -0
  20. package/cli/dashboard/panels/toolbar-panel.css +42 -0
  21. package/cli/dashboard/panels/toolbar-panel.js +374 -0
  22. package/cli/dashboard/panels/tunnel-panel.css +61 -0
  23. package/cli/dashboard/panels/tunnel-panel.js +688 -0
  24. package/cli/dashboard/panels/webdav-panel.css +38 -0
  25. package/cli/dashboard/panels/webdav-panel.js +166 -0
  26. package/cli/fingerprint.test.js +95 -0
  27. package/cli/http/components/callagent-terminal-component.js +310 -0
  28. package/cli/http/components/dashboard-component.js +123 -0
  29. package/cli/http/components/inbox-component.js +41 -0
  30. package/cli/http/components/mcp-transport-component.js +275 -0
  31. package/cli/http/components/oauth-component.js +207 -0
  32. package/cli/http/components/service-crud-component.js +288 -0
  33. package/cli/http/components/supervisor-component.js +104 -0
  34. package/cli/http/components/system-component.js +184 -0
  35. package/cli/http/components/tunnel-component.js +508 -0
  36. package/cli/http/components/vault-auth-component.js +264 -0
  37. package/cli/http/components/watchdog-component.js +28 -0
  38. package/cli/http/request-utils.js +21 -0
  39. package/cli/http/servers/ops-server.js +335 -0
  40. package/cli/index.js +5 -91
  41. package/cli/lib/fs-git.test.js +467 -0
  42. package/cli/mcp/providers/call-agent-provider.js +30 -0
  43. package/cli/mcp/providers/clauth-core-provider.js +461 -0
  44. package/cli/mcp/providers/ops-provider.js +42 -0
  45. package/cli/mcp/providers/terminal-provider.js +81 -0
  46. package/cli/mcp/tool-registry.js +48 -0
  47. package/cli/mcp/tool-schemas.js +822 -0
  48. package/cli/mcp/tool-schemas.test.js +49 -0
  49. package/cli/ops/pm2-preflight.js +54 -0
  50. package/cli/recovery.js +6 -0
  51. package/cli/recovery.test.js +179 -0
  52. package/cli/services/agent-pool-instance.js +38 -0
  53. package/cli/services/claude-binary.js +29 -0
  54. package/cli/services/delegation-lane-instance.js +35 -0
  55. package/cli/services/delegation-lane-instance.test.js +37 -0
  56. package/cli/services/file-io.js +38 -0
  57. package/cli/services/file-io.test.js +10 -0
  58. package/cli/services/fs-mount.js +66 -0
  59. package/cli/services/fs-mount.test.js +14 -0
  60. package/cli/services/fs-upload-sessions.js +20 -0
  61. package/cli/services/fs-upload-sessions.test.js +14 -0
  62. package/cli/services/git-exec.js +37 -0
  63. package/cli/services/git-exec.test.js +41 -0
  64. package/cli/services/git-import-guard.js +28 -0
  65. package/cli/services/git-import-guard.test.js +18 -0
  66. package/cli/services/service-status.js +191 -0
  67. package/cli/services/service-status.test.js +39 -0
  68. package/cli/services/terminal-dispatch.js +135 -0
  69. package/cli/services/terminal-dispatch.test.js +9 -0
  70. package/cli/services/terminal-registry.js +39 -0
  71. package/cli/services/terminal-registry.test.js +31 -0
  72. package/cli/services/terminal-sessions.js +100 -0
  73. package/cli/services/terminal-sessions.test.js +26 -0
  74. package/cli/services/terminal-window.js +106 -0
  75. package/cli/services/vault-read.js +14 -0
  76. package/cli/services/vault-read.test.js +35 -0
  77. package/cli/supervisor-registry.js +116 -0
  78. package/cli/supervisor-registry.test.js +165 -0
  79. package/cli/supervisor-ui.test.js +230 -30
  80. package/cli/webdav-config.js +84 -0
  81. package/cli/webdav-config.test.js +79 -0
  82. package/cli/webdav-obscure.js +23 -0
  83. package/cli/webdav-obscure.test.js +41 -0
  84. package/cli/webdav-rclone-discovery.js +44 -0
  85. package/cli/webdav-service.js +30 -118
  86. package/cli/webdav-service.test.js +107 -0
  87. package/install.ps1 +102 -102
  88. package/install.sh +49 -49
  89. package/package.json +2 -2
  90. package/scripts/bootstrap.cjs +121 -121
  91. package/supabase/functions/auth-vault/index.ts +350 -350
  92. package/supabase/migrations/001_clauth_schema.sql +94 -94
  93. package/supabase/migrations/002_vault_helpers.sql +90 -90
  94. package/supabase/migrations/20260317_lockout.sql +26 -26
  95. package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
  96. package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
  97. package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
  98. package/cli/commands/codevelop.js +0 -1190
@@ -0,0 +1,59 @@
1
+ /* cli/dashboard/panels/surfaces-panel.css
2
+ The Local Software Factory Supervisor -- surfaces grid + operations/log
3
+ (data-panel="surfaces-log"). Owned entirely by surfaces-panel.js.
4
+ Depends on the tokens in ../global.css, loaded before this file. */
5
+ .supervisor-panel{margin:14px 16px;border:1px solid var(--panel-border);background:linear-gradient(135deg,rgba(15,23,42,.96),rgba(10,15,26,.98));border-radius:12px;padding:14px}
6
+ .supervisor-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px}
7
+ .supervisor-title{font-size:.95rem;font-weight:700;color:var(--text-bright)}
8
+ .supervisor-sub{font-size:.76rem;color:var(--grey);margin-top:3px;line-height:1.35}
9
+ .supervisor-actions{display:flex;gap:8px;flex-wrap:wrap}
10
+ .supervisor-action{background:var(--bg-dark);color:var(--grey-light);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:.75rem;cursor:pointer}
11
+ .supervisor-action:hover:not(:disabled){background:var(--bg-card);border-color:var(--grey-darker)}
12
+ /* Visible but inert until a surface is selected — the operator can always
13
+ SEE which commands exist, which is the whole point of showing them. */
14
+ .supervisor-action:disabled{opacity:.42;cursor:not-allowed}
15
+ /* The selected row has to be unmistakable, otherwise "which surface am I
16
+ about to restart?" is a guess. */
17
+ .supervisor-row.selected{border-color:var(--cyan);background:rgba(56,189,248,.10)}
18
+ .supervisor-action:hover{border-color:var(--cyan);color:#e0f2fe}
19
+ /* stretch: both cards get the height of the taller one, so the log card is
20
+ as tall as the surfaces card instead of shrink-wrapping its text. */
21
+ .supervisor-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;align-items:stretch}
22
+ .supervisor-card{border:1px solid var(--bg-card);background:rgba(2,6,23,.52);border-radius:10px;padding:10px;min-width:0;display:flex;flex-direction:column}
23
+ /* Right-hand column: log on top, WebDAV beneath it. WebDAV never needed the
24
+ full page width, and taking it out of the full-width flow is what gives
25
+ Surfaces the whole left column to grow into. */
26
+ .supervisor-col{display:flex;flex-direction:column;gap:10px;min-width:0}
27
+ .supervisor-col > .supervisor-card:first-child{flex:1 1 auto;min-height:0}
28
+ .supervisor-card h4{margin:0 0 8px;color:var(--grey-light);font-size:.75rem;letter-spacing:.08em;text-transform:uppercase}
29
+ .supervisor-kpi{font-size:1.35rem;font-weight:700;color:var(--text-white)}
30
+ .supervisor-meta{font-size:.72rem;color:var(--grey);font-family:'Courier New',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
31
+ .supervisor-pill{display:inline-flex;align-items:center;border-radius:999px;padding:2px 7px;margin:2px 4px 2px 0;font-size:.68rem;border:1px solid var(--border);color:var(--grey-light);background:var(--bg-dark)}
32
+ .supervisor-pill.ok{border-color:rgba(74,222,128,.35);color:var(--green-light);background:rgba(34,197,94,.08)}
33
+ .supervisor-pill.warn{border-color:rgba(250,204,21,.35);color:var(--gold-light);background:rgba(250,204,21,.08)}
34
+ .supervisor-pill.bad{border-color:rgba(248,113,113,.35);color:var(--pink);background:rgba(248,113,113,.08)}
35
+ /* Card GRID, not a stack. flex-direction:column gave one full-width row per
36
+ surface, so ten surfaces meant ten rows and constant scrolling to see the
37
+ fleet. auto-fill + minmax packs as many small cards per row as the panel
38
+ width allows and reflows on resize, with no breakpoint to maintain. */
39
+ .supervisor-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(178px,1fr));gap:7px;max-height:520px;overflow:auto;align-content:start}
40
+ .supervisor-row{border:1px solid var(--bg-card);border-radius:8px;padding:8px 9px;background:rgba(15,23,42,.45);cursor:pointer;display:flex;flex-direction:column;gap:4px}
41
+ .supervisor-row:hover{border-color:var(--border)}
42
+ .supervisor-row.selected{border-color:var(--cyan);background:rgba(56,189,248,.08)}
43
+ .supervisor-row.readonly{cursor:default}
44
+ /* Narrow cards cannot hold name + two pills + a button on one line, so the
45
+ header wraps instead of squashing the name to an ellipsis. */
46
+ .supervisor-row-top{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
47
+ .supervisor-name{font-size:.82rem;color:var(--text-bright);font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
48
+ .supervisor-row-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}
49
+ .supervisor-cmdbar{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:8px;padding:8px;border:1px solid var(--bg-card);border-radius:8px;background:rgba(2,6,23,.4);min-height:36px}
50
+ .supervisor-cmdbar-empty{font-size:.74rem;color:var(--grey-dim)}
51
+ .supervisor-cmdbar-name{font-size:.78rem;color:var(--text-bright);font-weight:600;margin-right:4px}
52
+ /* flex:1 + min-height:0 makes the log fill whatever height the card has
53
+ instead of stopping at its content. min-height:0 is load-bearing: without
54
+ it a flex child refuses to shrink below content size and the box overflows
55
+ the card rather than scrolling inside it. */
56
+ .supervisor-log{flex:1 1 auto;min-height:220px;font-family:'Courier New',monospace;font-size:.7rem;color:var(--grey);white-space:pre-wrap;word-break:break-word;overflow:auto;background:rgba(2,6,23,.55);border:1px solid var(--bg-card);border-radius:6px;padding:7px}
57
+ .supervisor-log-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
58
+ .supervisor-feedback{margin-top:8px;padding:7px 9px;border-radius:6px;font-family:'Courier New',monospace;font-size:.72rem;color:var(--green-light);background:rgba(34,197,94,.08);border:1px solid rgba(74,222,128,.22);display:none}
59
+ .supervisor-feedback.bad{color:var(--pink);background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25)}
@@ -0,0 +1,474 @@
1
+ // cli/dashboard/panels/surfaces-panel.js
2
+ // The Surfaces/Supervisor panel, as a real Web Component: <clauth-surfaces-panel>.
3
+ // Owns its own Shadow DOM for plugin discovery, surface cards, cmdbar
4
+ // actions (start/stop/restart/reconcile/test/promote/rollback), health
5
+ // polling, and the log tail -- same conversion shape as tunnel-panel.js (the
6
+ // reference implementation): a class extending window.ClauthPanelElement,
7
+ // its own template()/mount(), instance state instead of module-level `let`,
8
+ // and mount() -- not connectedCallback() -- doing the real fetch/poll work.
9
+ // connectedCallback() (panel-element.js) fires during HTML parsing, before
10
+ // dashboard.js (and its BASE/writeHeaders/htmlEscape globals) has loaded;
11
+ // calling real logic there throws a live "X is not defined" a vm-based test
12
+ // harness would never catch. mount() is instead called explicitly by
13
+ // ClauthPanelRegistry.mountAll(), after every script has loaded.
14
+ //
15
+ // Scope note: this panel owns the SURFACES GRID + OPERATIONS/LOG only. The
16
+ // WebDAV Mounts card that used to sit physically nested inside the same
17
+ // served <section> is a SEPARATE concern already converted to its own
18
+ // component (<clauth-webdav-panel>, see webdav-panel.js/webdav-panel.css) --
19
+ // it is not reproduced here. The served HTML swap (serve.js) needs to place
20
+ // <clauth-webdav-panel> as a sibling of this tag, not inside it.
21
+ //
22
+ // htmlEscape()/writeHeaders() (shared with every panel) stay in dashboard.js
23
+ // -- referenced here, not duplicated.
24
+ //
25
+ // Loaded as a plain classic <script> (not type="module") BEFORE dashboard.js
26
+ // -- same pattern as every other panel component.
27
+
28
+ // ── Pure helpers/constants (no `this`, no shadow-DOM access needed) ────────
29
+ const SUPERVISOR_SURFACE_ACTIONS = ["start", "stop", "restart", "reconcile", "test", "promote", "rollback"];
30
+
31
+ const CLAUTH_SELF_PSEUDO_SURFACE = {
32
+ __pseudo: true,
33
+ id: "clauth:mcp-sse",
34
+ plugin_id: "clauth",
35
+ name: "clauth (self)",
36
+ lifecycle_owner: "external",
37
+ destination: "self",
38
+ state: "self_managed",
39
+ port: null,
40
+ health: null,
41
+ };
42
+
43
+ // "2m ago" / "just now" — an absolute ISO timestamp on a card answers the
44
+ // wrong question; what matters is how stale the reading is.
45
+ function healthAge(iso) {
46
+ const then = Date.parse(iso || "");
47
+ if (!then) return "";
48
+ const secs = Math.max(0, Math.round((Date.now() - then) / 1000));
49
+ if (secs < 10) return "just now";
50
+ if (secs < 90) return secs + "s ago";
51
+ if (secs < 5400) return Math.round(secs / 60) + "m ago";
52
+ return Math.round(secs / 3600) + "h ago";
53
+ }
54
+
55
+ function supervisorBadge(text, kind) {
56
+ return '<span class="supervisor-pill ' + (kind || '') + '">' + htmlEscape(text) + '</span>';
57
+ }
58
+
59
+ async function supervisorJson(path, options) {
60
+ const response = await fetch(BASE + path, { cache: "no-store", ...(options || {}) });
61
+ const data = await response.json().catch(() => ({}));
62
+ if (!response.ok || data.error) throw new Error(data.error || ("HTTP " + response.status));
63
+ return data;
64
+ }
65
+
66
+ // An MCP transport endpoint is not a page. Opening /mcp, /sse or /message in a
67
+ // browser tab gets you a raw event stream — literally "event: endpoint / data:
68
+ // https://host/message?sessionId=..." — which is what the Open button did.
69
+ // Strip the transport segment and open the service root, which is the only
70
+ // thing a browser can render.
71
+ const MCP_TRANSPORT_PATHS = ["/mcp", "/sse", "/message", "/messages", "/stream"];
72
+ function browsableUrl(raw) {
73
+ if (!raw) return null;
74
+ try {
75
+ const parsed = new URL(raw);
76
+ const trimmed = parsed.pathname.replace(/\/+$/, "").toLowerCase();
77
+ if (MCP_TRANSPORT_PATHS.includes(trimmed)) {
78
+ parsed.pathname = "/";
79
+ parsed.search = "";
80
+ }
81
+ return parsed.toString();
82
+ } catch (e) {
83
+ return raw;
84
+ }
85
+ }
86
+
87
+ // Derives an "Open UI" target for a surface without storing one on disk —
88
+ // a real localhost port wins, else the first externally-routable URL.
89
+ // Always the service ROOT: a surface's health path (/health, /api/version) is
90
+ // a probe target, not a page, and several surfaces 404 at it.
91
+ function openUiUrlForSurface(surface) {
92
+ if (surface.port && surface.port !== "auto") return "http://127.0.0.1:" + surface.port + "/";
93
+ const external = (surface.routes || []).find(r => r.kind === "external" && r.url);
94
+ return external ? browsableUrl(external.url) : null;
95
+ }
96
+
97
+ class ClauthSurfacesPanel extends window.ClauthPanelElement {
98
+ static tagName = "clauth-surfaces-panel";
99
+ static cssHref = "/dashboard/panels/surfaces-panel.css";
100
+
101
+ template() {
102
+ return `
103
+ <div class="supervisor-head">
104
+ <div>
105
+ <div class="supervisor-title">Local Software Factory Supervisor</div>
106
+ <div class="supervisor-sub">clauth owns local LIFEAI plugin discovery, candidate tests, clauth-owned surface operations, receipts, and supervisor event logs. Dev Center should call this surface instead of PM2 directly.</div>
107
+ </div>
108
+ <div class="supervisor-actions">
109
+ <button class="supervisor-action" id="supervisor-refresh-btn" data-action="refreshSurfaceHealth" title="Re-probe every surface's health endpoint now">Refresh</button>
110
+ <button class="supervisor-action" data-action="rescanSupervisorPlugins">Rescan plugins</button>
111
+ </div>
112
+ </div>
113
+ <div id="supervisor-status" class="supervisor-sub">Loading supervisor state…</div>
114
+ <div id="supervisor-feedback" class="supervisor-feedback" role="status"></div>
115
+ <div class="supervisor-grid" style="margin-top:10px">
116
+ <div class="supervisor-card">
117
+ <h4>Surfaces</h4>
118
+ <div class="supervisor-kpi" id="supervisor-surface-count">—</div>
119
+ <div class="supervisor-meta" id="supervisor-surface-meta">destination + owner matrix</div>
120
+ <div class="supervisor-cmdbar" id="supervisor-cmdbar"><span class="supervisor-cmdbar-empty">Select a surface below to act on it.</span></div>
121
+ <div class="supervisor-list" id="supervisor-surfaces" style="margin-top:8px"></div>
122
+ </div>
123
+ <div class="supervisor-col">
124
+ <div class="supervisor-card">
125
+ <div class="supervisor-log-head">
126
+ <h4 style="margin:0 0 8px">Operations + log</h4>
127
+ <button class="supervisor-action" style="margin-left:auto" data-action="openFullLog" title="Open the full clauth daemon log in a new tab">Open log</button>
128
+ </div>
129
+ <div class="supervisor-log-head">
130
+ <div class="supervisor-kpi" id="supervisor-operation-count">—</div>
131
+ <div class="supervisor-meta">operations recorded</div>
132
+ </div>
133
+ <div class="supervisor-meta" id="supervisor-log-path">events.jsonl</div>
134
+ <div class="supervisor-log" id="supervisor-events" style="margin-top:8px">No events loaded.</div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ `;
139
+ }
140
+
141
+ mount() {
142
+ this.selectedSupervisorSurface = null; // { id, name } | null
143
+ this.lastSupervisorSurfaceRows = [];
144
+ this.logTailStarted = false;
145
+ this.lastHealthProbeAt = 0;
146
+ this.cockpitLoadedOnce = false;
147
+ this.loadSupervisorCockpit();
148
+ this.startSupervisorLogTail();
149
+ }
150
+
151
+ startSupervisorLogTail() {
152
+ if (this.logTailStarted) return; // mount() can in principle run more than once
153
+ this.logTailStarted = true;
154
+ // 3s was far too aggressive for a panel nobody watches continuously, and it
155
+ // was self-defeating: each tick issues GET /health + /v1/surfaces + the log
156
+ // read, and the request logger writes a line for each — so the dashboard was
157
+ // the dominant author of the very log it renders. 15s keeps the panel live
158
+ // without the panel being the workload.
159
+ setInterval(() => this.loadSupervisorCockpit(), 15000);
160
+ // Re-probe health when the operator comes BACK to the window. A dashboard
161
+ // left open for an hour is showing an hour-old answer to "is it up?", and
162
+ // the moment you look at it is exactly when you want that answer to be now.
163
+ // Cheap because it is read-only (no restarts) and debounced.
164
+ window.addEventListener("focus", () => { this.refreshSurfaceHealth({ quiet: true }); });
165
+ }
166
+
167
+ // Read-only sweep: POST /v1/surfaces/health probes every surface and records
168
+ // the result, then the cockpit reload paints it. Never starts or restarts
169
+ // anything, so it is safe to fire on a focus event.
170
+ async refreshSurfaceHealth(options) {
171
+ const quiet = Boolean(options && options.quiet);
172
+ // A focus event fires on every alt-tab; without this a quick tab dance would
173
+ // queue a probe per switch against the same endpoints.
174
+ if (quiet && Date.now() - this.lastHealthProbeAt < 5000) return;
175
+ this.lastHealthProbeAt = Date.now();
176
+ const btn = this.$("#supervisor-refresh-btn");
177
+ if (btn) { btn.disabled = true; btn.textContent = "Probing…"; }
178
+ try {
179
+ const result = await supervisorJson("/v1/surfaces/health", { method: "POST" });
180
+ const probed = result.probed || [];
181
+ const down = probed.filter(p => p.health === "down");
182
+ if (!quiet || down.length) {
183
+ this.supervisorFeedback(
184
+ down.length
185
+ ? down.length + " of " + probed.length + " surfaces not answering: " + down.map(p => p.surface_id).join(", ")
186
+ : "All " + probed.filter(p => p.health !== "no_probe").length + " probed surfaces healthy.",
187
+ down.length > 0);
188
+ }
189
+ } catch (err) {
190
+ if (!quiet) this.supervisorFeedback("Health probe failed: " + (err.message || err), true);
191
+ } finally {
192
+ if (btn) { btn.disabled = false; btn.textContent = "Refresh"; }
193
+ }
194
+ await this.loadSupervisorCockpit();
195
+ }
196
+
197
+ supervisorFeedback(text, bad) {
198
+ const el = this.$("#supervisor-feedback");
199
+ if (!el) return;
200
+ el.textContent = text || "";
201
+ el.className = "supervisor-feedback" + (bad ? " bad" : "");
202
+ el.style.display = text ? "block" : "none";
203
+ }
204
+
205
+ async loadSupervisorCockpit() {
206
+ const status = this.$("#supervisor-status");
207
+ // Only announce loading on the FIRST paint. On a background poll this
208
+ // overwrote the healthy/vault/version badges with "Loading supervisor state…"
209
+ // every tick, so the panel visibly flashed on a fixed cadence forever.
210
+ if (status && !this.cockpitLoadedOnce) status.textContent = "Loading supervisor state…";
211
+ try {
212
+ const [health, surfaces, logs, daemonLog] = await Promise.all([
213
+ supervisorJson("/health"),
214
+ supervisorJson("/v1/surfaces"),
215
+ supervisorJson("/v1/logs?limit=40"),
216
+ // The Operations card tails the clauth DAEMON log — the actual
217
+ // clauth-serve.log an operator means by "the clauth log" — not the
218
+ // supervisor's events.jsonl, which is a separate plugin-event stream.
219
+ fetch(BASE + "/v1/daemon-log?lines=60").then(r => r.json()).catch(() => null),
220
+ ]);
221
+ const surfaceRows = [CLAUTH_SELF_PSEUDO_SURFACE, ...(surfaces.surfaces || [])];
222
+ this.lastSupervisorSurfaceRows = surfaceRows;
223
+ if (this.selectedSupervisorSurface && !surfaceRows.some(s => (s.plugin_id + ":" + s.id) === this.selectedSupervisorSurface.id)) {
224
+ this.selectedSupervisorSurface = null;
225
+ }
226
+ this.$("#supervisor-surface-count").textContent = String(surfaceRows.length - 1);
227
+ this.$("#supervisor-operation-count").textContent = String((logs.operations || []).length);
228
+ this.$("#supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
229
+ // Name the file actually being shown. It said events.jsonl while rendering
230
+ // the daemon log, which makes the card unverifiable — you cannot go look at
231
+ // the file it claims to be tailing.
232
+ this.$("#supervisor-log-path").textContent =
233
+ (daemonLog && daemonLog.log_path) || logs.log_path || "events.jsonl";
234
+ // Replacing innerHTML resets scrollTop to 0. The surfaces list is taller
235
+ // than its box, so on every poll tick the row you were looking at — or had
236
+ // just selected — scrolled out from under you. That is the "jumps up and
237
+ // down": not the data changing, the scroll position being thrown away on a
238
+ // fixed cadence. Preserve it across the swap.
239
+ const surfacesEl = this.$("#supervisor-surfaces");
240
+ const keepScroll = surfacesEl.scrollTop;
241
+ surfacesEl.innerHTML = surfaceRows.map(s => this.renderSupervisorSurface(s)).join("");
242
+ surfacesEl.scrollTop = keepScroll;
243
+ this.renderSupervisorCmdbar();
244
+ this.cockpitLoadedOnce = true;
245
+ const logEl = this.$("#supervisor-events");
246
+ // Newest-at-bottom, like a real tail -- and only auto-scroll if the
247
+ // reader was already at the bottom, so scrolling up to read history
248
+ // during a poll tick doesn't get yanked back down.
249
+ const wasAtBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 12;
250
+ // Real daemon log when it is reachable; supervisor events only as a
251
+ // fallback so the card is never blank if the tail route fails.
252
+ if (daemonLog && Array.isArray(daemonLog.lines) && daemonLog.lines.length) {
253
+ logEl.textContent = daemonLog.lines.join("\n");
254
+ } else {
255
+ logEl.textContent = (logs.events || []).slice(-40).map(e => {
256
+ const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
257
+ return (e.ts || e.created_at || "") + " " + label;
258
+ }).join("\n") || "No events yet.";
259
+ }
260
+ if (wasAtBottom) logEl.scrollTop = logEl.scrollHeight;
261
+ if (status) status.innerHTML = supervisorBadge("supervisor " + (health.status === "ok" ? "healthy" : (health.status || "unknown")), health.status === "ok" ? "ok" : "warn") + supervisorBadge("vault " + (health.vault_locked ? "locked" : "unlocked"), health.vault_locked ? "warn" : "ok") + supervisorBadge("v" + (health.clauth_version || window.__CLAUTH_VERSION__), "");
262
+ } catch (err) {
263
+ if (status) status.innerHTML = supervisorBadge("supervisor unreachable", "bad") + " " + htmlEscape(err.message || err);
264
+ this.supervisorFeedback("Supervisor unavailable — actions are disabled until the local control plane returns.", true);
265
+ }
266
+ }
267
+
268
+ openFullLog() {
269
+ window.open(BASE + "/v1/daemon-log?format=text&lines=4000", "_blank");
270
+ }
271
+
272
+ // Per-item dynamic args (which card was clicked) come off data-* attributes
273
+ // on the clicked element -- the standard delegated-event pattern (see
274
+ // tunnel-panel.js's wizSelectTunnel).
275
+ selectSupervisorSurface(e, el) {
276
+ const compositeId = el.dataset.surfaceId;
277
+ const name = el.dataset.surfaceName;
278
+ this.selectedSupervisorSurface = this.selectedSupervisorSurface && this.selectedSupervisorSurface.id === compositeId
279
+ ? null // clicking the already-selected card deselects it
280
+ : { id: compositeId, name };
281
+ this.$$(".supervisor-row[data-surface-id]").forEach(row => {
282
+ row.classList.toggle("selected", row.dataset.surfaceId === (this.selectedSupervisorSurface && this.selectedSupervisorSurface.id));
283
+ });
284
+ this.renderSupervisorCmdbar();
285
+ }
286
+
287
+ renderSupervisorCmdbar() {
288
+ const bar = this.$("#supervisor-cmdbar");
289
+ if (!bar) return;
290
+ // The buttons are ALWAYS rendered — disabled until a surface is selected,
291
+ // never hidden. A control you cannot see is a control that does not exist.
292
+ const sel = this.selectedSupervisorSurface;
293
+ const enable = Boolean(sel);
294
+ const label = sel
295
+ ? '<span class="supervisor-cmdbar-name">' + htmlEscape(sel.name) + '</span>'
296
+ : '<span class="supervisor-cmdbar-empty">Select a surface &rarr;</span>';
297
+ bar.innerHTML = label + SUPERVISOR_SURFACE_ACTIONS.map(a =>
298
+ '<button class="supervisor-action" data-supervisor-action="' + a + '"'
299
+ + (enable
300
+ ? ' data-action="runSupervisorSurface" data-surface-id="' + htmlEscape(sel.id) + '" data-surface-action="' + a + '"'
301
+ : ' disabled')
302
+ + '>' + a + '</button>').join("");
303
+ }
304
+
305
+ renderSupervisorSurface(surface) {
306
+ const compositeId = surface.plugin_id + ":" + surface.id;
307
+ const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
308
+ const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
309
+ const openUrl = surface.__pseudo ? null : openUiUrlForSurface(surface);
310
+ const isSelected = this.selectedSupervisorSurface && this.selectedSupervisorSurface.id === compositeId;
311
+ const rowClass = "supervisor-row" + (surface.__pseudo ? " readonly" : "") + (isSelected ? " selected" : "");
312
+ const rowAction = surface.__pseudo ? "" : ' data-action="selectSupervisorSurface" data-surface-id="' + htmlEscape(compositeId) + '" data-surface-name="' + htmlEscape(surface.name || compositeId) + '"';
313
+ // The Open button tells the truth about where it lands. last_open_ok comes
314
+ // from the same read-only sweep that fills the health pill: three surfaces in
315
+ // the live fleet are healthy at /health and 404 at /, because an MCP server
316
+ // has no browsable page. Those get sent to their health endpoint — a real
317
+ // response — and the button says so, instead of opening an error page.
318
+ const openTarget = surface.last_open_ok === false && surface.last_health_url
319
+ ? surface.last_health_url
320
+ : openUrl;
321
+ const openLabel = surface.last_open_ok === false ? "Open health" : "Open UI";
322
+ const openTitle = surface.last_open_ok === false
323
+ ? "This surface has no browsable page (its root answered with an error) — opening its health endpoint instead"
324
+ : "Open this surface";
325
+ const openBtn = surface.__pseudo
326
+ ? '<button class="supervisor-action" data-action="openClauthSelfMcp" title="Open clauth\'s own MCP endpoint">Open</button>'
327
+ : (openTarget ? '<button class="supervisor-action" data-action="openSupervisorSurfaceLink" data-open-target="' + htmlEscape(openTarget) + '" title="' + htmlEscape(openTitle) + '">' + openLabel + '</button>' : "");
328
+ // Health is a RUNTIME fact and gets its own pill — "state" is a manifest
329
+ // fact (registered + enabled + hash unchanged) and stays "current" for a
330
+ // surface that is not listening at all, which is why it cannot double as a
331
+ // status light.
332
+ const healthPill = surface.__pseudo
333
+ ? supervisorBadge("self", "")
334
+ : (surface.last_health_ok === true ? supervisorBadge("up · " + healthAge(surface.last_health_at), "ok")
335
+ : surface.last_health_ok === false ? supervisorBadge("DOWN · " + healthAge(surface.last_health_at), "bad")
336
+ : supervisorBadge("health unknown", "warn"));
337
+ const healthLine = surface.__pseudo
338
+ ? "clauth's own MCP endpoint"
339
+ : (surface.last_health_ok === false && surface.last_health_error
340
+ ? surface.last_health_error + " · " + (surface.health || "no health")
341
+ : (surface.health || surface.health_url || "no health"));
342
+ return '<div class="' + rowClass + '" data-surface-id="' + htmlEscape(compositeId) + '"' + rowAction + '>' +
343
+ '<div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.name || compositeId) + '</span>' +
344
+ healthPill +
345
+ supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) +
346
+ openBtn +
347
+ '</div>' +
348
+ '<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
349
+ '<div class="supervisor-meta">' + htmlEscape(healthLine) + '</div>' +
350
+ '</div>';
351
+ }
352
+
353
+ async rescanSupervisorPlugins() {
354
+ try {
355
+ await supervisorJson("/v1/plugins/rescan", { method: "POST", headers: writeHeaders() });
356
+ this.supervisorFeedback("Plugin rescan completed.", false);
357
+ await this.loadSupervisorCockpit();
358
+ } catch (err) { this.supervisorFeedback("Plugin rescan failed: " + (err.message || err), true); }
359
+ }
360
+
361
+ async runSupervisorSurface(id, action) {
362
+ // The action is SYNCHRONOUS on the daemon: it spawns the declared command and
363
+ // blocks until that command exits or the surface's timeout (30s default)
364
+ // fires, then answers with the receipt. So the click blocks — say so, and
365
+ // disable the bar rather than leaving buttons live and looking ignored.
366
+ const bar = this.$("#supervisor-cmdbar");
367
+ const buttons = bar ? Array.from(bar.querySelectorAll("button")) : [];
368
+ buttons.forEach(b => { b.disabled = true; });
369
+ const pressed = buttons.find(b => b.dataset.supervisorAction === action);
370
+ const pressedLabel = pressed ? pressed.textContent : action;
371
+ if (pressed) pressed.textContent = action + "…";
372
+ this.supervisorFeedback("Running " + action + " on " + id + " — waiting for the command to exit…", false);
373
+ try {
374
+ const receipt = await supervisorJson("/v1/surfaces/" + encodeURIComponent(id) + "/actions", { method: "POST", headers: writeHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ action }) });
375
+ const result = receipt.resulting_state || {};
376
+ // exit 0 from "pm2 start" means pm2 ACCEPTED the command, not that the
377
+ // service is listening — that gap is how a surface reads "operation
378
+ // completed" while nothing answers on its port. For the actions that are
379
+ // supposed to change liveness, go ask the health endpoint.
380
+ let verdict = result.state || "completed";
381
+ if (result.ok !== false && ["start", "restart", "reconcile", "stop"].includes(action)) {
382
+ const sweep = await supervisorJson("/v1/surfaces/health", { method: "POST" }).catch(() => null);
383
+ const mine = sweep && (sweep.probed || []).find(p => p.surface_id === id);
384
+ if (mine && mine.health !== "no_probe") verdict += " · health " + (mine.health === "healthy" ? "up" : "DOWN");
385
+ }
386
+ this.supervisorFeedback(
387
+ action + " " + id + " → " + verdict
388
+ + (result.status !== undefined && result.status !== null ? " (exit " + result.status + ")" : "")
389
+ + " · receipt " + (receipt.operationId || "recorded")
390
+ + (result.ok === false && result.stderr ? " · " + String(result.stderr).split("\n")[0] : ""),
391
+ result.ok === false);
392
+ } catch (err) {
393
+ this.supervisorFeedback("Surface " + action + " failed: " + (err.message || err), true);
394
+ } finally {
395
+ if (pressed) pressed.textContent = pressedLabel;
396
+ buttons.forEach(b => { b.disabled = false; });
397
+ }
398
+ await this.loadSupervisorCockpit();
399
+ }
400
+
401
+ // clauth doesn't manage itself as a plugin, so its own MCP endpoint has no
402
+ // entry in the real surfaces registry — this reads the vault value directly
403
+ // (never hardcoded) rather than storing a synthetic surfaces row.
404
+ async openClauthSelfMcp(e) {
405
+ if (e) e.stopPropagation();
406
+ try {
407
+ const res = await fetch(BASE + "/v/mcp-clauth-endpoint");
408
+ const text = (await res.text()).trim();
409
+ if (!res.ok) {
410
+ this.supervisorFeedback("Could not read clauth's own MCP endpoint: " + text, true);
411
+ return;
412
+ }
413
+ // The vault value is the MCP transport URL (.../mcp). Opening it verbatim
414
+ // showed a raw SSE stream instead of clauth.
415
+ window.open(browsableUrl(text), "_blank");
416
+ } catch (err) { this.supervisorFeedback("Could not read clauth's own MCP endpoint: " + (err.message || err), true); }
417
+ }
418
+
419
+ openSupervisorSurfaceLink(e, el) {
420
+ if (e) e.stopPropagation();
421
+ const target = el.dataset.openTarget;
422
+ if (target) window.open(target, "_blank");
423
+ }
424
+
425
+ get actions() {
426
+ return {
427
+ refreshSurfaceHealth: () => this.refreshSurfaceHealth(),
428
+ // Dispatches through window.rescanSupervisorPlugins()/window.runSupervisorSurface()
429
+ // (the bridges below), NOT this.rescanSupervisorPlugins()/this.runSupervisorSurface()
430
+ // directly -- installWriteAccessGuards() wraps the GLOBAL name with the
431
+ // write-token acquire-and-retry gate. Calling the instance method directly
432
+ // would silently bypass that gate, restoring the exact write-lock defect
433
+ // this mechanism exists to prevent. Same pattern as tunnel-panel.js's
434
+ // wizSubmitCfToken / webdav-panel.js's submitMount+deleteMount bridges.
435
+ rescanSupervisorPlugins: () => window.rescanSupervisorPlugins(),
436
+ runSupervisorSurface: (e, el) => window.runSupervisorSurface(el.dataset.surfaceId, el.dataset.surfaceAction),
437
+ openFullLog: () => this.openFullLog(),
438
+ selectSupervisorSurface: (e, el) => this.selectSupervisorSurface(e, el),
439
+ openClauthSelfMcp: (e) => this.openClauthSelfMcp(e),
440
+ openSupervisorSurfaceLink: (e, el) => this.openSupervisorSurfaceLink(e, el),
441
+ };
442
+ }
443
+ }
444
+ customElements.define(ClauthSurfacesPanel.tagName, ClauthSurfacesPanel);
445
+
446
+ // Write-guard bridges: installWriteAccessGuards() (dashboard.js) wraps every
447
+ // name in WRITE_ACTIONS by reassigning window[name] to a guarded version --
448
+ // it can only do that for a real global function, not a class instance
449
+ // method. These thin globals delegate to the real implementation on the live
450
+ // panel instance; installWriteAccessGuards wraps THESE names, and the
451
+ // component's own action dispatcher calls window.rescanSupervisorPlugins()/
452
+ // window.runSupervisorSurface() (see the `actions` getter above), never
453
+ // this.rescanSupervisorPlugins()/this.runSupervisorSurface() directly, so the
454
+ // guard is never bypassed.
455
+ window.rescanSupervisorPlugins = () => {
456
+ const el = document.querySelector(ClauthSurfacesPanel.tagName);
457
+ return el?.rescanSupervisorPlugins();
458
+ };
459
+ window.runSupervisorSurface = (id, action) => {
460
+ const el = document.querySelector(ClauthSurfacesPanel.tagName);
461
+ return el?.runSupervisorSurface(id, action);
462
+ };
463
+
464
+ // Panel interface registration — see ./panel-registry.js. mount() is called
465
+ // explicitly by ClauthPanelRegistry.mountAll(), after every script has
466
+ // loaded -- connectedCallback() (panel-element.js) only builds the shadow
467
+ // DOM during HTML parsing, before dashboard.js (and its globals) exists.
468
+ window.ClauthPanelRegistry.register({
469
+ id: "surfaces",
470
+ mount: () => {
471
+ const el = document.querySelector(ClauthSurfacesPanel.tagName);
472
+ if (el) el.mount();
473
+ },
474
+ });
@@ -0,0 +1,42 @@
1
+ /* cli/dashboard/panels/toolbar-panel.css
2
+ The menu bar (data-panel="menu-bar") and its two modals: Change Password
3
+ (chpw-*) and the Enroll Machine result box live entirely in toolbar-panel.js,
4
+ so their styling lives here too. Depends on the tokens in ../global.css,
5
+ loaded before this file. */
6
+ .toolbar{display:flex;gap:8px;margin-bottom:1rem;flex-wrap:wrap;align-items:center;border:1px solid var(--panel-border);background:linear-gradient(135deg,rgba(15,23,42,.96),rgba(10,15,26,.98));border-radius:12px;padding:14px}
7
+ .btn-refresh{background:var(--blue);color:var(--white);padding:7px 18px;font-size:.85rem;border-radius:7px;border:none;cursor:pointer;font-weight:500}
8
+ .btn-refresh:hover{background:var(--blue-dark)}
9
+ .btn-lock{background:var(--bg-card);color:var(--red);border:1px solid var(--border);padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500}
10
+ .btn-lock:hover{background:#2d1f1f;border-color:var(--red)}
11
+ .btn-check{background:#0f2d2d;color:var(--emerald);border:1px solid #064e3b;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500;transition:all .15s}
12
+ .btn-check:hover{background:#134e4a;border-color:var(--emerald)}.btn-check:disabled{opacity:.5;cursor:not-allowed}
13
+ .btn-add{background:var(--green-bg);color:var(--green);border:1px solid var(--green-dark);padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500;transition:all .15s}
14
+ .btn-add:hover{background:#1a3d22;border-color:var(--green)}
15
+ .btn-stop{padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500}
16
+ .btn-stop.restart{background:var(--green-bg);border:1px solid var(--green-dark);color:var(--green-light)}
17
+ .btn-stop.stop{background:var(--red-darker);border:1px solid var(--red-dark);color:#fca5a5}
18
+ .chpw-panel{display:none;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
19
+ .chpw-panel h3{font-size:.9rem;font-weight:600;color:var(--text-white);margin-bottom:1rem}
20
+ .chpw-row{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:.75rem}
21
+ .chpw-field{display:flex;flex-direction:column;gap:4px}
22
+ .chpw-field label{font-size:.75rem;color:var(--amber)}
23
+ .chpw-input{background:var(--bg-dark);border:1px solid var(--border);border-radius:6px;color:var(--text-bright);font-family:'Courier New',monospace;font-size:.88rem;padding:7px 12px;outline:none;width:200px;transition:border-color .2s}
24
+ .chpw-input:focus{border-color:var(--blue)}
25
+ .chpw-foot{display:flex;gap:8px;align-items:center}
26
+ .btn-chpw-save{background:var(--border-dark);color:var(--blue-light);padding:7px 18px;font-size:.85rem;border-radius:6px;border:none;cursor:pointer;font-weight:500;transition:background .15s}
27
+ .btn-chpw-save:hover{background:var(--blue-hover-bg)}
28
+ .chpw-msg{font-size:.82rem}
29
+ .chpw-msg.ok{color:var(--green)} .chpw-msg.fail{color:var(--red)}
30
+ /* Enroll Machine panel reuses the "add-*" visual language services-panel.css
31
+ defines for its own Add Service form -- restated here because Shadow DOM
32
+ encapsulation means that stylesheet (loaded into a DIFFERENT shadow root)
33
+ cannot reach into this one. */
34
+ .add-panel{display:none;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
35
+ .add-panel h3{font-size:.9rem;font-weight:600;color:var(--text-white);margin-bottom:1rem}
36
+ .add-row{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:.75rem}
37
+ .add-field{display:flex;flex-direction:column;gap:4px}
38
+ .add-field label{font-size:.75rem;color:var(--amber)}
39
+ .add-input{background:var(--bg-dark);border:1px solid var(--border);border-radius:6px;color:var(--text-bright);font-family:'Courier New',monospace;font-size:.88rem;padding:7px 12px;outline:none;width:200px;transition:border-color .2s}
40
+ .add-input:focus{border-color:var(--blue)}
41
+ .add-msg{font-size:.82rem}
42
+ .add-msg.ok{color:var(--green)} .add-msg.fail{color:var(--red)}