Files
clawstor/dashboard-v2/src/pages/NodeDetail.tsx
T
osobhandClaude Sonnet 5 4ea1cbed2e
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
Add shutdown-prep button to dashboard-v2 NodeDetail
Wires safe-shutdown-prep.sh into the dashboard so an operator can
prep a node for hardware maintenance from a browser instead of SSH.

New RPC methods (0x20/0x21):
- ShutdownPrepCheck runs `--dry-run` to completion and returns the
  full report. Never stops anything, safe to call repeatedly.
- ShutdownPrepExecute starts the real run detached (`systemd-run
  --user --scope --collect`), placing it in a cgroup outside
  claw-store.service's own -- the script's own step 6 stops that
  service, i.e. the process that would otherwise be running it, so
  it has to survive its own parent dying. Returns immediately with
  a "started" message; full output lands in
  /var/lib/claw-store/shutdown-prep.log for whoever's at the machine
  once it's gone dark, since there's no way to stream a live result
  past the point the daemon stops itself.
- Execute double-checks confirm_node_name against the peer's own
  configured name server-side, on top of the aggregator's own path
  match -- defense in depth for a highly consequential action.

Aggregator endpoints (admin-token gated, AuthedCaller::require_admin):
  POST /api/v2/node/:name/shutdown-prep/check
  POST /api/v2/node/:name/shutdown-prep/execute

Frontend: ShutdownPrepPanel on NodeDetail. Check button always
enabled; the real "stop services" button only unlocks after a ready
check, and additionally requires typing the exact node name to
confirm before it's clickable.

Also fixes a script bug found while testing this against the live
daemon process (not caught in manual interactive-shell testing): the
zpool-detection line parsed raw `mount` output positionally, which
returned the wrong field under the daemon's process context for
reasons that didn't reproduce interactively. Switched to
`df --output=source`, which is stable across both.

Verified end-to-end against tank, architect, and morpheus, including
cross-node targeting (tank's dashboard successfully triggered a
check on morpheus over the fleet RPC layer).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-31 14:46:57 -07:00

78 lines
2.5 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Link } from 'wouter';
import { api, NodeStatusV2, fmtBytes } from '../lib/api';
import { StatTile } from '../components/StatTile';
import { ShutdownPrepPanel } from '../components/ShutdownPrepPanel';
interface Props {
name: string;
}
export function NodeDetail({ name }: Props) {
const [status, setStatus] = useState<NodeStatusV2 | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
api
.nodeStatus(name)
.then((n) => {
setStatus(n);
setErr(null);
})
.catch((e) => setErr(String(e)));
}, [name]);
return (
<div className="space-y-6">
<div>
<Link href="/">
<a className="text-sm text-slate-500 hover:text-slate-300">
← fleet
</a>
</Link>
<h1 className="text-2xl font-semibold text-slate-100 mt-2">
{name}
</h1>
</div>
{err && (
<div className="rounded border border-amber-800 bg-amber-950/40 p-3 text-amber-300 text-sm">
{err}
<div className="mt-2 text-xs text-slate-400">
Cross-node lookup lands in a follow-on PR. Until then this
page shows detail only when you're already viewing the
dashboard hosted by {name}. Try opening{' '}
<span className="font-mono">http://{name}:7700/v2/#/nodes/{name}</span>{' '}
directly.
</div>
</div>
)}
{status && (
<>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
<StatTile label="blobs" value={status.blob_count.toLocaleString()} color="ok" />
<StatTile label="tags" value={status.tag_count} color="ok" />
<StatTile label="refs" value={status.ref_count} color="ok" />
<StatTile label="snapshots" value={status.snapshot_count} color="ok" />
<StatTile label="ref-tracking" value={status.ref_tracking_count} color="ok" />
<StatTile
label="store size"
value={fmtBytes(status.blob_store_bytes)}
color="ok"
/>
</div>
<div className="rounded border border-slate-800 bg-slate-900/40 p-4 text-sm">
<div className="text-slate-500 uppercase text-xs tracking-wider">
blob store root
</div>
<div className="font-mono mt-1">{status.blob_store_root ?? '—'}</div>
</div>
<ShutdownPrepPanel name={name} />
</>
)}
</div>
);
}