@lifeaitools/clauth 2.0.3 → 2.1.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/cli/commands/serve.js +118 -16
- package/cli/supervisor-registry.js +23 -0
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -907,6 +907,13 @@ export function dashboardHtml(port, whitelist, isStaged = false, initWriteToken
|
|
|
907
907
|
.supervisor-sub{font-size:.76rem;color:#94a3b8;margin-top:3px;line-height:1.35}
|
|
908
908
|
.supervisor-actions{display:flex;gap:8px;flex-wrap:wrap}
|
|
909
909
|
.supervisor-action{background:#0f172a;color:#cbd5e1;border:1px solid #334155;border-radius:6px;padding:6px 10px;font-size:.75rem;cursor:pointer}
|
|
910
|
+
.supervisor-action:hover:not(:disabled){background:#1e293b;border-color:#475569}
|
|
911
|
+
/* Visible but inert until a surface is selected — the operator can always
|
|
912
|
+
SEE which commands exist, which is the whole point of showing them. */
|
|
913
|
+
.supervisor-action:disabled{opacity:.42;cursor:not-allowed}
|
|
914
|
+
/* The selected row has to be unmistakable, otherwise "which surface am I
|
|
915
|
+
about to restart?" is a guess. */
|
|
916
|
+
.supervisor-row.selected{border-color:#38bdf8;background:rgba(56,189,248,.10)}
|
|
910
917
|
.supervisor-action:hover{border-color:#38bdf8;color:#e0f2fe}
|
|
911
918
|
.supervisor-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
|
|
912
919
|
.supervisor-card{border:1px solid #1e293b;background:rgba(2,6,23,.52);border-radius:10px;padding:10px;min-width:0}
|
|
@@ -1588,7 +1595,13 @@ let supervisorLogTailStarted = false;
|
|
|
1588
1595
|
function startSupervisorLogTail() {
|
|
1589
1596
|
if (supervisorLogTailStarted) return; // showMain() can run more than once per page load
|
|
1590
1597
|
supervisorLogTailStarted = true;
|
|
1591
|
-
|
|
1598
|
+
// 3s was far too aggressive for a panel nobody watches continuously, and it
|
|
1599
|
+
// was self-defeating: each tick issues GET /health + /v1/surfaces + the log
|
|
1600
|
+
// read, and the request logger writes a line for each — so the dashboard was
|
|
1601
|
+
// the dominant author of the very log it renders. clauth-serve.log had grown
|
|
1602
|
+
// to 63 MB / 892,272 lines, its tail almost entirely this poll.
|
|
1603
|
+
// 15s keeps the panel live without the panel being the workload.
|
|
1604
|
+
setInterval(loadSupervisorCockpit, 15000);
|
|
1592
1605
|
}
|
|
1593
1606
|
|
|
1594
1607
|
function supervisorBadge(text, kind) {
|
|
@@ -1626,14 +1639,22 @@ const CLAUTH_SELF_PSEUDO_SURFACE = {
|
|
|
1626
1639
|
health: null,
|
|
1627
1640
|
};
|
|
1628
1641
|
|
|
1642
|
+
let supervisorCockpitLoadedOnce = false;
|
|
1629
1643
|
async function loadSupervisorCockpit() {
|
|
1630
1644
|
const status = document.getElementById("supervisor-status");
|
|
1631
|
-
|
|
1645
|
+
// Only announce loading on the FIRST paint. On a background poll this
|
|
1646
|
+
// overwrote the healthy/vault/version badges with "Loading supervisor state…"
|
|
1647
|
+
// every tick, so the panel visibly flashed on a fixed cadence forever.
|
|
1648
|
+
if (status && !supervisorCockpitLoadedOnce) status.textContent = "Loading supervisor state…";
|
|
1632
1649
|
try {
|
|
1633
|
-
const [health, surfaces, logs] = await Promise.all([
|
|
1650
|
+
const [health, surfaces, logs, daemonLog] = await Promise.all([
|
|
1634
1651
|
supervisorJson("/health"),
|
|
1635
1652
|
supervisorJson("/v1/surfaces"),
|
|
1636
1653
|
supervisorJson("/v1/logs?limit=40"),
|
|
1654
|
+
// The Operations card tails the clauth DAEMON log — the actual
|
|
1655
|
+
// clauth-serve.log an operator means by "the clauth log" — not the
|
|
1656
|
+
// supervisor's events.jsonl, which is a separate plugin-event stream.
|
|
1657
|
+
fetch(BASE + "/v1/daemon-log?lines=60").then(r => r.json()).catch(() => null),
|
|
1637
1658
|
]);
|
|
1638
1659
|
const surfaceRows = [CLAUTH_SELF_PSEUDO_SURFACE, ...(surfaces.surfaces || [])];
|
|
1639
1660
|
lastSupervisorSurfaceRows = surfaceRows;
|
|
@@ -1643,18 +1664,37 @@ async function loadSupervisorCockpit() {
|
|
|
1643
1664
|
document.getElementById("supervisor-surface-count").textContent = String(surfaceRows.length - 1);
|
|
1644
1665
|
document.getElementById("supervisor-operation-count").textContent = String((logs.operations || []).length);
|
|
1645
1666
|
document.getElementById("supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
|
|
1646
|
-
|
|
1647
|
-
|
|
1667
|
+
// Name the file actually being shown. It said events.jsonl while rendering
|
|
1668
|
+
// the daemon log, which makes the card unverifiable — you cannot go look at
|
|
1669
|
+
// the file it claims to be tailing.
|
|
1670
|
+
document.getElementById("supervisor-log-path").textContent =
|
|
1671
|
+
(daemonLog && daemonLog.log_path) || logs.log_path || "events.jsonl";
|
|
1672
|
+
// Replacing innerHTML resets scrollTop to 0. The surfaces list is taller
|
|
1673
|
+
// than its box, so on every poll tick the row you were looking at — or had
|
|
1674
|
+
// just selected — scrolled out from under you. That is the "jumps up and
|
|
1675
|
+
// down": not the data changing, the scroll position being thrown away on a
|
|
1676
|
+
// fixed cadence. Preserve it across the swap.
|
|
1677
|
+
const surfacesEl = document.getElementById("supervisor-surfaces");
|
|
1678
|
+
const keepScroll = surfacesEl.scrollTop;
|
|
1679
|
+
surfacesEl.innerHTML = surfaceRows.map(renderSupervisorSurface).join("");
|
|
1680
|
+
surfacesEl.scrollTop = keepScroll;
|
|
1648
1681
|
renderSupervisorCmdbar();
|
|
1682
|
+
supervisorCockpitLoadedOnce = true;
|
|
1649
1683
|
const logEl = document.getElementById("supervisor-events");
|
|
1650
1684
|
// Newest-at-bottom, like a real tail -- and only auto-scroll if the
|
|
1651
1685
|
// reader was already at the bottom, so scrolling up to read history
|
|
1652
1686
|
// during a poll tick doesn't get yanked back down.
|
|
1653
1687
|
const wasAtBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 12;
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1688
|
+
// Real daemon log when it is reachable; supervisor events only as a
|
|
1689
|
+
// fallback so the card is never blank if the tail route fails.
|
|
1690
|
+
if (daemonLog && Array.isArray(daemonLog.lines) && daemonLog.lines.length) {
|
|
1691
|
+
logEl.textContent = daemonLog.lines.join("\\n");
|
|
1692
|
+
} else {
|
|
1693
|
+
logEl.textContent = (logs.events || []).slice(-40).map(e => {
|
|
1694
|
+
const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
|
|
1695
|
+
return (e.ts || e.created_at || "") + " " + label;
|
|
1696
|
+
}).join("\\n") || "No events yet.";
|
|
1697
|
+
}
|
|
1658
1698
|
if (wasAtBottom) logEl.scrollTop = logEl.scrollHeight;
|
|
1659
1699
|
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 || "${VERSION}"), "");
|
|
1660
1700
|
} catch (err) {
|
|
@@ -1684,13 +1724,22 @@ function selectSupervisorSurface(compositeId, name) {
|
|
|
1684
1724
|
function renderSupervisorCmdbar() {
|
|
1685
1725
|
const bar = document.getElementById("supervisor-cmdbar");
|
|
1686
1726
|
if (!bar) return;
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1727
|
+
// The buttons are ALWAYS rendered — disabled until a surface is selected,
|
|
1728
|
+
// never hidden. The previous version emitted only a "Select a surface below"
|
|
1729
|
+
// sentence and materialised the buttons on click, so an operator who had not
|
|
1730
|
+
// clicked yet saw no commands at all and had no way to know the panel had
|
|
1731
|
+
// any. "The commands are missing" was an accurate report of what was on
|
|
1732
|
+
// screen. A control you cannot see is a control that does not exist.
|
|
1733
|
+
const sel = selectedSupervisorSurface;
|
|
1734
|
+
const enable = Boolean(sel);
|
|
1735
|
+
const label = sel
|
|
1736
|
+
? '<span class="supervisor-cmdbar-name">' + htmlEscape(sel.name) + '</span>'
|
|
1737
|
+
: '<span class="supervisor-cmdbar-empty">Select a surface →</span>';
|
|
1738
|
+
bar.innerHTML = label + SUPERVISOR_SURFACE_ACTIONS.map(a =>
|
|
1739
|
+
'<button class="supervisor-action" data-supervisor-action="' + a + '"'
|
|
1740
|
+
+ (enable ? ' onclick="runSupervisorSurface(' + jsArg(sel.id) + ',' + jsArg(a) + ')"' : ' disabled')
|
|
1741
|
+
+ '>' + a + '</button>').join("")
|
|
1742
|
+
+ (enable ? '' : '');
|
|
1694
1743
|
}
|
|
1695
1744
|
|
|
1696
1745
|
function renderSupervisorSurface(surface) {
|
|
@@ -4498,6 +4547,59 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4498
4547
|
return ok(res, { tunnels: listTunnels() });
|
|
4499
4548
|
}
|
|
4500
4549
|
|
|
4550
|
+
// GET /v1/daemon-log — tail of THE clauth daemon log (clauth-serve.log),
|
|
4551
|
+
// which is what an operator means by "the clauth log". /v1/logs below is a
|
|
4552
|
+
// different thing: supervisor plugin events out of events.jsonl. The
|
|
4553
|
+
// dashboard's Operations card rendered that second file, so it showed
|
|
4554
|
+
// plugin_discovered/credential_required chatter and never the daemon's own
|
|
4555
|
+
// activity.
|
|
4556
|
+
//
|
|
4557
|
+
// Reads only the LAST slice of the file, never the whole thing: this log is
|
|
4558
|
+
// routinely tens of megabytes (63 MB / 892,272 lines when this was written),
|
|
4559
|
+
// so a naive readFileSync would stall the daemon on every poll.
|
|
4560
|
+
if (method === "GET" && reqPath === "/v1/daemon-log") {
|
|
4561
|
+
const want = Number(url.searchParams.get("lines") || 60);
|
|
4562
|
+
const lines = Number.isFinite(want) ? Math.max(1, Math.min(want, 500)) : 60;
|
|
4563
|
+
try {
|
|
4564
|
+
const stat = fs.statSync(LOG_FILE);
|
|
4565
|
+
const TAIL_BYTES = 96 * 1024;
|
|
4566
|
+
const start = Math.max(0, stat.size - TAIL_BYTES);
|
|
4567
|
+
const fd = fs.openSync(LOG_FILE, "r");
|
|
4568
|
+
const buf = Buffer.alloc(Math.min(TAIL_BYTES, stat.size));
|
|
4569
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
4570
|
+
fs.closeSync(fd);
|
|
4571
|
+
const text = buf.toString("utf8");
|
|
4572
|
+
// Drop the first line when we started mid-file — it is a partial line.
|
|
4573
|
+
let all = text.split(/\r?\n/).filter(Boolean);
|
|
4574
|
+
if (start > 0) all = all.slice(1);
|
|
4575
|
+
// Strip routine HTTP access lines. The dashboard itself polls /health,
|
|
4576
|
+
// /ping, /builds, /tunnel, /v1/surfaces, /v1/logs and /v1/daemon-log, so
|
|
4577
|
+
// an unfiltered tail is almost entirely the dashboard watching itself —
|
|
4578
|
+
// which is exactly what an operator sees as "it just has bullshit in
|
|
4579
|
+
// it". What belongs here is what the daemon DID: operations, errors,
|
|
4580
|
+
// startup, auth events. Pass `?raw=1` for the unfiltered tail.
|
|
4581
|
+
if (url.searchParams.get("raw") !== "1") {
|
|
4582
|
+
const NOISE = /\s(GET|HEAD)\s+\/(ping|health|builds|tunnel|v1\/(surfaces|logs|daemon-log|plugins)|favicon\.ico)\b/;
|
|
4583
|
+
all = all.filter((l) => !NOISE.test(l));
|
|
4584
|
+
}
|
|
4585
|
+
const rows = all.slice(-lines);
|
|
4586
|
+
return ok(res, {
|
|
4587
|
+
schema: "clauth.daemon.log.v1",
|
|
4588
|
+
log_path: LOG_FILE,
|
|
4589
|
+
size_bytes: stat.size,
|
|
4590
|
+
lines: rows,
|
|
4591
|
+
});
|
|
4592
|
+
} catch (err) {
|
|
4593
|
+
return ok(res, {
|
|
4594
|
+
schema: "clauth.daemon.log.v1",
|
|
4595
|
+
log_path: LOG_FILE,
|
|
4596
|
+
size_bytes: 0,
|
|
4597
|
+
lines: [],
|
|
4598
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4599
|
+
});
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4501
4603
|
if (method === "GET" && reqPath === "/v1/logs") {
|
|
4502
4604
|
const limit = Number(url.searchParams.get("limit") || 100);
|
|
4503
4605
|
const boundedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 100;
|
|
@@ -853,6 +853,29 @@ export function operation(action, target, prior, result, actor = "localhost") {
|
|
|
853
853
|
state.operations = [receipt, ...(state.operations || [])].slice(0, 500);
|
|
854
854
|
saveSupervisorState(state);
|
|
855
855
|
appendJsonl(file("events.jsonl"), { ts: now(), kind: "operation", ...receipt });
|
|
856
|
+
// Mirror one human-readable line into the clauth DAEMON log as well.
|
|
857
|
+
//
|
|
858
|
+
// events.jsonl stays the structured record that /v1/logs and state read.
|
|
859
|
+
// But an operator asking for "the clauth log" means clauth-serve.log, and
|
|
860
|
+
// until now supervisor operations never appeared there — so a surface
|
|
861
|
+
// start/stop/restart was invisible in the log people actually tail, and the
|
|
862
|
+
// dashboard had to render a second, separate stream to show them at all.
|
|
863
|
+
// One log, with the events in it.
|
|
864
|
+
//
|
|
865
|
+
// Best-effort by construction: a logging failure must never fail the
|
|
866
|
+
// operation whose receipt this is.
|
|
867
|
+
try {
|
|
868
|
+
const logFile = process.env.CLAUTH_SERVE_LOG
|
|
869
|
+
|| path.join(os.tmpdir(), "clauth-serve.log");
|
|
870
|
+
if (fs.existsSync(path.dirname(logFile))) {
|
|
871
|
+
const okFlag = result?.ok === false ? "FAIL" : "ok";
|
|
872
|
+
const tgt = target && typeof target === "object"
|
|
873
|
+
? (target.surface_id || target.plugin_id || target.service || JSON.stringify(target))
|
|
874
|
+
: String(target ?? "");
|
|
875
|
+
const stateLabel = result?.state ? ` ${result.state}` : "";
|
|
876
|
+
fs.appendFileSync(logFile, `[${receipt.created_at}] ${action} ${tgt} ${okFlag}${stateLabel} actor=${actor}\n`, "utf8");
|
|
877
|
+
}
|
|
878
|
+
} catch { /* logging must never break the operation it records */ }
|
|
856
879
|
return receipt;
|
|
857
880
|
}
|
|
858
881
|
|