@lifeaitools/clauth 2.1.1 → 2.10.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.
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/cli/api.js +238 -238
- package/cli/commands/install.js +413 -396
- package/cli/commands/login.test.js +49 -1
- package/cli/commands/ops-install.js +11 -10
- package/cli/commands/ops.test.js +270 -0
- package/cli/commands/scrub.test.js +70 -0
- package/cli/commands/serve.js +2618 -11238
- package/cli/commands/uninstall.js +164 -164
- package/cli/dashboard/dashboard.js +592 -0
- package/cli/dashboard/global.css +65 -0
- package/cli/dashboard/panel-element.js +79 -0
- package/cli/dashboard/panels/build-status-panel.css +12 -0
- package/cli/dashboard/panels/build-status-panel.js +86 -0
- package/cli/dashboard/panels/panel-registry.js +58 -0
- package/cli/dashboard/panels/services-panel.css +95 -0
- package/cli/dashboard/panels/services-panel.js +872 -0
- package/cli/dashboard/panels/surfaces-panel.css +59 -0
- package/cli/dashboard/panels/surfaces-panel.js +474 -0
- package/cli/dashboard/panels/toolbar-panel.css +42 -0
- package/cli/dashboard/panels/toolbar-panel.js +374 -0
- package/cli/dashboard/panels/tunnel-panel.css +61 -0
- package/cli/dashboard/panels/tunnel-panel.js +688 -0
- package/cli/dashboard/panels/webdav-panel.css +38 -0
- package/cli/dashboard/panels/webdav-panel.js +166 -0
- package/cli/fingerprint.test.js +95 -0
- package/cli/http/components/callagent-terminal-component.js +310 -0
- package/cli/http/components/dashboard-component.js +123 -0
- package/cli/http/components/inbox-component.js +41 -0
- package/cli/http/components/mcp-transport-component.js +275 -0
- package/cli/http/components/oauth-component.js +207 -0
- package/cli/http/components/service-crud-component.js +288 -0
- package/cli/http/components/supervisor-component.js +104 -0
- package/cli/http/components/system-component.js +184 -0
- package/cli/http/components/tunnel-component.js +508 -0
- package/cli/http/components/vault-auth-component.js +264 -0
- package/cli/http/components/watchdog-component.js +28 -0
- package/cli/http/request-utils.js +21 -0
- package/cli/http/servers/ops-server.js +335 -0
- package/cli/index.js +5 -91
- package/cli/lib/fs-git.test.js +467 -0
- package/cli/mcp/providers/call-agent-provider.js +30 -0
- package/cli/mcp/providers/clauth-core-provider.js +461 -0
- package/cli/mcp/providers/ops-provider.js +42 -0
- package/cli/mcp/providers/terminal-provider.js +81 -0
- package/cli/mcp/tool-registry.js +48 -0
- package/cli/mcp/tool-schemas.js +822 -0
- package/cli/mcp/tool-schemas.test.js +49 -0
- package/cli/ops/pm2-preflight.js +54 -0
- package/cli/recovery.js +6 -0
- package/cli/recovery.test.js +179 -0
- package/cli/services/agent-pool-instance.js +38 -0
- package/cli/services/claude-binary.js +29 -0
- package/cli/services/delegation-lane-instance.js +35 -0
- package/cli/services/delegation-lane-instance.test.js +37 -0
- package/cli/services/file-io.js +38 -0
- package/cli/services/file-io.test.js +10 -0
- package/cli/services/fs-mount.js +66 -0
- package/cli/services/fs-mount.test.js +14 -0
- package/cli/services/fs-upload-sessions.js +20 -0
- package/cli/services/fs-upload-sessions.test.js +14 -0
- package/cli/services/git-exec.js +37 -0
- package/cli/services/git-exec.test.js +41 -0
- package/cli/services/git-import-guard.js +28 -0
- package/cli/services/git-import-guard.test.js +18 -0
- package/cli/services/service-status.js +191 -0
- package/cli/services/service-status.test.js +39 -0
- package/cli/services/terminal-dispatch.js +135 -0
- package/cli/services/terminal-dispatch.test.js +9 -0
- package/cli/services/terminal-registry.js +39 -0
- package/cli/services/terminal-registry.test.js +31 -0
- package/cli/services/terminal-sessions.js +100 -0
- package/cli/services/terminal-sessions.test.js +26 -0
- package/cli/services/terminal-window.js +106 -0
- package/cli/services/vault-read.js +14 -0
- package/cli/services/vault-read.test.js +35 -0
- package/cli/supervisor-registry.js +116 -0
- package/cli/supervisor-registry.test.js +165 -0
- package/cli/supervisor-ui.test.js +230 -30
- package/cli/webdav-config.js +84 -0
- package/cli/webdav-config.test.js +79 -0
- package/cli/webdav-obscure.js +23 -0
- package/cli/webdav-obscure.test.js +41 -0
- package/cli/webdav-rclone-discovery.js +44 -0
- package/cli/webdav-service.js +30 -118
- package/cli/webdav-service.test.js +107 -0
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +2 -2
- package/scripts/bootstrap.cjs +121 -121
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
- package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
- package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
- package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
- 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 →</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)}
|