P2 redesign Phase 5 — prune/check/unlock + maintenance ticker + repo stats + pending-runs queue #3
Reference in New Issue
Block a user
Delete Branch "p2r-phase5-maintenance"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Phase 5 of the P2 redesign — six task IDs (P2R-03..P2R-08) close out the server-side maintenance story:
RunPrune/RunCheck/RunUnlock/RunStats), agent runners + dispatcher cases, operator HTTP run-now endpoints (POST /api/hosts/{id}/repo/{prune,check,unlock}+ HTMX twins), Repo-page UI buttons.internal/server/maintenancepackage + side-effectingDispatchMaintenanceglue. 60s tick readshost_repo_maintenance, derives last-fire fromLatestJobByKind, dispatches via the samedispatchJobWithPayloadpath as operator Run-now. Forget reshape: multi-groupForgetGroupspayload (one job → N restic-forget invocations per tick).repo.statsafter every backup/check/prune/unlock; server persists into newhost_repo_statsprojection table; Repo page surfaces total size, last-check status (color-coded), last-prune time, lock-detected banner.pending_runs. Drained on 30s tick + on agent reconnect viaonAgentHello. Exponential backoff capped at 30m. Abandons rows onretry_maxor genuinely-gone schedule/group.Also lands: admin-credentials slot (
host_credentials.kind = 'admin', separate AADhost:<id>:admin), agent secrets-store split with backwards-compatible decode of legacy flat blobs, and a newrunWithPumphelper consolidating Forget/Init/Prune/Check/Unlock pump duplication.Plan: `docs/superpowers/plans/2026-05-03-p2-redesign-phase-5.md`. Tasks ticked in `tasks.md`.
Architecture notes
Test plan
🤖 Generated with Claude Code
aa80a3418etoe1b60f02aeAdds GET/PUT/DELETE /api/hosts/{id}/admin-credentials handlers that mirror the existing repo-credentials endpoints but write to store.CredKindAdmin with AEAD additional-data "host:<id>:admin" (scoped away from the repo slot to prevent cross-binding). PUT immediately pushes a config.update(Slot:"admin") to the agent when it is connected, and the new pushAdminCredsToAgent helper is wired for use by the upcoming prune run-now endpoint (D2) to push on-demand before dispatch.Adds POST /api/hosts/{id}/repo/{prune,check,unlock} (and matching outer routes for HTMX form posts). Prune pushes the admin-cred slot via pushAdminCredsToAgent before dispatch and refuses with admin_creds_required when the slot is not set. Check reads check_subset_pct from host_repo_maintenance (overridable via ?subset=N, clamped 0-100; non-numeric override falls back to DB value silently). Unlock needs no admin creds. All three share the same wantsHTML/HX-Redirect response split as the per-source-group run-now endpoint.- hostRepoPage gains AdminURL/AdminUsername/HasAdminPassword, Online, and StatsView (pre-dereferenced projection of host_repo_stats). - loadHostRepoPage loads the admin slot (tolerating ErrNotFound), hub.Connected, and stats (tolerating ErrNotFound). - renderRepoPage gains an adminErr parameter; all callers updated. - handleUIAdminCredentialsSave / handleUIAdminCredentialsDelete added (form-POST handlers mirroring the repo-creds pattern, with audit). - Routes /hosts/{id}/admin-credentials POST and /delete POST registered. - Template: Admin credentials form after Connection, Run-now HTMX buttons after Maintenance, Repo health stats panel in right rail. - Tests: 9 new tests covering rendering, disabled states, save/delete round-trips, audit rows, and idempotent delete.Wires a 60s server-side ticker to the pure-logic maintenance.Decide introduced in the previous commit. Decisions flow through a new DispatchMaintenance method on *Server, which: - skips offline hosts (no pending_runs queueing — maintenance is not a backup, missed fires shouldn't pile up) - silently skips prune when admin creds aren't bound - pushes admin creds before prune, then dispatches with RequiresAdminCreds=true (same as operator-driven prune) - persists job rows with actor_kind="system" Reshapes the forget wire payload from a single RetentionPolicy to a ForgetGroups list (one tag + per-group keep-* per source group). The agent walks the groups and runs `restic forget --tag <name> --keep-*` once per group. Dead-code removed: CommandRunPayload.RetentionPolicy, the old forget JSON-decode in cmd/agent, and the single-policy form of restic.RunForget.e1b60f02aetod8dd21b5e0