- 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]>
claw-store/static/ is now in .gitignore. The bundle is installed via
`make install-dashboard` which runs `npm run build` and copies to
/usr/share/claw-store/static — it should not live in the repo.
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]>
The two biggest pain points coming out of the architecture review:
1. The manifest at /var/lib/claw-store/projects.toml was the only
piece of writable state but had no locking, no atomic writes,
and three concurrent writers (daemon poll tick, every CLI verb,
and the dashboard shelling out via /api/activate). Two writers
interleaving silently dropped one of them; a crash mid-write
left a corrupt half-written TOML that the next reader parsed
as an empty manifest.
2. Reboot survival: the dashboard had no systemd unit and was a
stray hand-launched process. Architect lost its dashboard on
todays reboot.
This commit lands:
- Manifest::update(path, FnOnce(&mut Manifest)) — locked-atomic
load-mutate-save in one transaction. Uses libc::flock(LOCK_EX) on
a sidecar .lock file (so the data file can be replaced by rename
without invalidating the lock) and tempfile + persist for the
rename. Concurrent writers serialise; readers see the previous
state or the new state, never a torn write. Manifest::load uses
LOCK_SH so it never races a mid-rename.
- Project.pinned: bool with #[serde(default)] so legacy manifests
parse cleanly. hot::gc_stale_targets and hot::gc_by_space both
skip pinned projects, with a WARN log when every remaining
project is pinned but were still over budget — operator intent
beats space pressure.
- claw-store pin <project> / unpin <project> CLI verbs.
status command surfaces pin marker (📌).
activate preserves an existing rows pinned flag so re-activating
doesnt silently unpin.
- claw-store-serve.service systemd unit. Type=simple, Restart=
on-failure, RestartSec=15, ProtectSystem=strict + ReadWritePaths
=/var/lib/claw-store, ProtectHome, NoNewPrivileges, PrivateTmp.
- daemon poll tick reloads the manifest from disk at the start of
each cycle (so CLI activations between ticks are visible) and
routes its GC write through Manifest::update (so it cant race a
concurrent CLI pin).
- libc + tempfile move from dev-deps into runtime deps.
- empty-manifest fallthrough on load (treat "" as default) so a
half-written tempfile crashed pre-rename doesnt hard-fail the
daemon next boot.
- 25 tests passing incl. new ones: legacy-toml-parses, update-
serializes-two-sequential-writers, pinned-survives-stale-gc,
pinned-survives-space-gc-even-when-lru.
Version bumped 0.1.0 → 0.2.0.
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]>
Skip non-git directories inside org folders so flat layout dirs like
claw-store/target or claw-store/.cargo don't appear as fake repos.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- activate <org/repo>: wire hot tier, write .cargo/config.toml, register
- deactivate <org/repo>: auto-sync to peer, evict hot tier, unregister
- list: show all warm repos with activation status and last-active time
- sync <org/repo>: git push origin then SSH peer to pull
- pull <org/repo>: git pull from origin, stamp last_sync in manifest
- SyncQueue: file-based retry queue (/var/lib/claw-store/sync-queue.toml)
- daemon: drains sync queue on every 5-min poll tick
- config: [peer] host/user for cross-node notification
- manifest: Project.name is now org/repo, adds last_sync field
- hot tier paths are org/repo-namespaced to avoid collisions
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>