Vite base: '/clawstor/' caused 404s when the SPA was served at /v2/
because asset paths like /clawstor/assets/... didn't match the actual
serve path /v2/assets/...
- vite.config.ts: switch to base: './' (relative paths work from any
mount point — /v2/, /clawstor/, or bare /)
- serve.rs: mount the SPA at both /v2/ and /clawstor/; add 308 redirect
from /v2 and /clawstor (no trailing slash) to their canonical forms
so browsers don't misinterpret the last segment as a filename and
break relative ./assets/ resolution
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Pivot from per-node dashboard to single-pane-of-glass. The
aggregator (typically the operator's laptop) holds a fleet-CA
leaf cert + the peer list; each dashboard request fans out to
every peer over the existing QUIC/mTLS cluster port and issues
the new DashboardStatus RPC. Peers don't need to run any HTTP
server of their own.
Backend:
* New RPC method DashboardStatus = 0x1c
* Server handler reads BlobStore / TagStore / RefStore /
SnapshotStore / RefTracking counts + on-disk bytes + rustc
release. Cheap: 5 filesystem walks per request.
* Client wrapper call_dashboard_status
* serve_v2 rewritten as aggregator: V2State holds a QuicClient +
peer list from `[[cluster.peers]]`. Endpoints:
GET /api/v2/fleet fan-out to every peer, parallel
GET /api/v2/node/:name/status one peer, on-demand
Failed peers surface as { online: false, error: "..." } cards
instead of dropping.
serve.rs graceful degrade: v2 aggregator routes only mount when
[cluster.tls] is set. Static SPA still serves at /v2/* even
without an aggregator config so operators see the SPA's built-in
"config missing" error.
Deployment model (this session):
* Aggregator runs on quantum (Mac) with a signed leaf.
* Fleet daemons run cluster-only — no HTTP dashboard anywhere
on tank/architect/morpheus. The clawstor-dashboard.service
systemd units on the fleet are being retired.
Frontend rework to consume /api/v2/fleet ships in the next PR.
React 19 + Vite + Tailwind + wouter (tiny router, no external
state library). Consumes the /api/v2/* endpoints shipped in PR 1.
Serves under /v2/* so the legacy dashboard at / stays live.
Pages:
* CommandCenter (/) — fleet strip + this-node stat tiles
* NodeDetail (/nodes/:name) — per-node deep dive
* StorageBrowser (/storage/{blobs,tags,refs,snapshots}) — tables
with prefix filter
* RefTrackingPage (/refs/tracking) — grouped by repo
Backend changes:
* claw-store serve grows --v2-static-dir <path>
* build_app split into build_app_with_v2 for the extra static
mount
* /v2/* falls through to index.html so wouter client routing works
New systemd unit: clawstor-dashboard.service. Points at both
static dirs; installs on any node.
dashboard/ (legacy) untouched. dashboard-v2/ built to
target/dashboard-v2/dist for deploy.
Deploy sequence per node:
1. cp target/release/claw-store ~/clawstor-deploy/
2. rsync dashboard-v2/dist/ ~/clawstor-deploy/dashboard-v2/
3. cp deploy/systemd/clawstor-dashboard.service ~/.config/systemd/user/
4. systemctl --user daemon-reload && enable --now clawstor-dashboard.service
Cross-node fan-out for /api/v2/node/:name/status is PR 3.
Action POSTs (scrub/gc/snapshot/pin) are PR 4.
Three fixes bundled — all defensive around the architect↔tank dev flow:
1. drain_sync_queue no longer drops jobs after 48 attempts. The
previous cutoff (~4h at 5-min drain intervals) silently lost every
pending sync whenever the peer was down longer than that — the
exact scenario the user hit last week when architect was offline.
Now jobs are retried indefinitely; past SYNC_STUCK_ATTEMPTS (12,
~1h) they escalate from WARN to ERROR and appear in /api/status
under sync_queue_stuck so the operator sees them.
2. pull_project is now branch-aware. Old behavior was 'git pull
--ff-only' on whatever branch happened to be checked out. If the
peer was on main but the sender committed on develop, the peer
never picked up the new branch's ref. New behavior:
- git fetch --all --prune --tags first (gets every branch)
- fast-forward the checked-out branch if it has an upstream
- fast-forward every OTHER branch that has an upstream via
update-ref, without switching HEAD — so main can advance while
the operator is off hacking on feature/x.
Diverged branches are left alone (loud error, not silent merge).
Detached HEAD is skipped after fetch.
3. NodeStatus gains sync_queue_depth + sync_queue_stuck fields, wired
into both handle_status (GET /api/status) and the SSE stream. The
dashboard now has ground truth for 'is anything backing up?'
Tests updated: the old test_sync_queue_drops_after_max_attempts is
replaced by test_sync_queue_never_drops_stuck_jobs (invariant: never
drops) and test_sync_queue_stuck_count_threshold (invariant: counts
jobs past the escalation threshold).
Replace the ends_with(".git") name check with a file_type().is_symlink()
check at both the org and repo levels of build_projects_list.
This handles three classes of symlink that all produced duplicate entries:
1. Gitea's repo.git → repo aliases (previously caught by name check)
2. Within-org shortcuts: quantumclaw/bbq → quantum-bbq
3. Cross-org aliases: clawverse/clawhdf5 → ../quantumclaw/clawhdf5
The name check only caught class 1; the symlink check catches all three.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Gitea creates a repo.git → repo symlink alongside every real checkout
in /slab/projects. The directory walker resolved both, found .git inside
each (since the symlink points at the real dir), and emitted a duplicate
entry for every project — e.g. quantumclaw/clawhdf5 and
quantumclaw/clawhdf5.git both appeared in the dashboard.
Add ends_with(".git") guard at both the org and repo levels of
build_projects_list so symlinked aliases are always skipped.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Extract build_app() from run_server() so tests can construct the
router without binding a port (tower::ServiceExt::oneshot pattern)
- Add 10 new tests in serve::tests: validate_project_name (valid +
invalid slugs), daemon_uptime when file absent, GET /api/status,
GET /api/projects, POST /api/activate with invalid name, auth
middleware (no header → 401, wrong token → 401, correct token →
pass-through, GET bypasses auth entirely)
- Add tower + http-body-util to dev-dependencies
- Add project README covering architecture, install (Makefile),
config options, SSH setup, snapshot schedule, CLI reference,
HTTP API table, pinning, and troubleshooting guide
- Bump version to 0.3.0
Test suite: 39 tests, 0 failures
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The HTTP `/api/projects` handler and the SSE `/api/events` stream each
built their own ProjectInfo list — independent inline code, ~50 lines
of mostly identical duplicate. But the two diverged on ONE field:
/api/projects: is_active = active.is_some() || has_hot_cargo_config(...)
/api/events: is_active = active.is_some()
The dashboard hits BOTH every few seconds (a 10-second setInterval
polling /api/projects + a 5-second SSE pushing events). For any
project that was hot-wired via `.cargo/config.toml` but not in the
manifest, the two endpoints disagreed about its `is_active` bit.
The dashboard's last-write-won, and the row visibly flickered as
the active flag toggled. The 'X active' count in the header bounced
along with it.
Extracted `build_projects_list(cfg, &manifest)` and call it from both
sides. Same answer in both code paths. Verified post-deploy:
architect: HTTP=59 active, SSE=59 active (identical)
tank: HTTP=57 active, SSE=57 active (identical)
This also makes the 'total' number stable. The 354 in the dashboard
header is correct semantics — discoverable git repos on disk — but
the label is misleading. Cosmetic cleanup for a follow-up:
- "Projects — N discovered · M active · K shown" reads truer than
"N shown · M active · K total"
- The status card's `active_project_count` (manifest.projects.len)
is a third meaning of 'active' that doesn't match either dashboard
number; consider renaming to `manifest_size` to disambiguate.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds the serve subcommand: an axum 0.7 + tower-http server on
:7700 backing the React dashboard. Routes:
GET /api/status /api/projects /api/snapshots
/api/sync-queue /api/hot /api/events (SSE 5s tick)
POST /api/activate /api/deactivate /api/sync
/api/gc /api/snapshot
Mutating endpoints shell out to /usr/local/bin/claw-store so
all CLI logic stays single-sourced. claw-store/static/ holds
the prebuilt dashboard Vite bundle for --static-dir.
cargo_init.rs gets a minor wiring tweak so the API can read
back the managed cargo config marker.
Co-Authored-By: Claude Opus 4.7 <[email protected]>