@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.
@@ -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
- setInterval(loadSupervisorCockpit, 3000);
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
- if (status) status.textContent = "Loading supervisor state…";
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
- document.getElementById("supervisor-log-path").textContent = logs.log_path || "events.jsonl";
1647
- document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.map(renderSupervisorSurface).join("");
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
- logEl.textContent = (logs.events || []).slice(-40).map(e => {
1655
- const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
1656
- return (e.ts || e.created_at || "") + " " + label;
1657
- }).join("\\n") || "No events yet.";
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
- if (!selectedSupervisorSurface) {
1688
- bar.innerHTML = '<span class="supervisor-cmdbar-empty">Select a surface below to act on it.</span>';
1689
- return;
1690
- }
1691
- const { id, name } = selectedSupervisorSurface;
1692
- bar.innerHTML = '<span class="supervisor-cmdbar-name">' + htmlEscape(name) + '</span>' +
1693
- SUPERVISOR_SURFACE_ACTIONS.map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(id) + ',' + jsArg(a) + ')">' + a + '</button>').join("");
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 &rarr;</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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {