Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
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]>
78 lines
2.5 KiB
TypeScript
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>
|
|
);
|
|
}
|