P2R-01: REST + WS rewire against the slim shape
Schedules CRUD now takes {cron, enabled, source_group_ids[]} with cron
parsed via robfig/cron/v3 and group membership scoped to the host.
New source-groups CRUD lives at /api/hosts/{id}/source-groups; delete
refuses with 409 if any schedule still references the group, returning
the schedule list so the UI can prompt 'remove from these schedules
first.' Repo-maintenance GET/PUT manages forget/prune/check cadences
on host_repo_maintenance — no version bump, the server-side ticker
(P2R-06) drives execution.
Per-source-group Run-now (POST /hosts/{id}/source-groups/{gid}/run)
resolves the group's includes/excludes/retention/tag and dispatches a
backup command.run with the new structured CommandRunPayload fields
(Includes/Excludes/Tag). Old per-host /hosts/{id}/run-backup and
/hosts/{id}/init-repo return 410 Gone with a redirect message.
schedule_push.go is rebuilt: buildScheduleSetPayload assembles the
slim wire shape, pushScheduleSetOnConn ships it during the on-hello
window, pushScheduleSetAsync fires after every CRUD mutation, and
dispatchScheduledJob handles agent schedule.fire by iterating the
schedule's source groups and dispatching one backup per group with
actor_kind=schedule and scheduled_id pointing at the schedule.
Auto-init at first WS connect: when the host has repo creds bound and
no init job in its history, server dispatches restic init. Restic's
'config file already exists' soft-success means re-runs against an
existing repo no-op; we don't auto-retry on failure (operator triggers
re-init manually via the danger zone in P2R-09).
api.Schedule drops Kind/Paths/Excludes/Tags/RetentionPolicy/Manual etc.
in favour of {id, cron, enabled, source_groups: [...]}. The agent
scheduler stops checking sch.Manual; cmd/agent's backup dispatch reads
Includes/Excludes/Tag instead of Args.
Tests cover the new HTTP surface end-to-end: source-groups CRUD with
in-use refusal, schedule validation (bad cron / missing groups /
foreign group), repo-maintenance auto-seed and validation, the 410
route, and buildScheduleSetPayload's wire-shape correctness. Full
suite passes; smoke env exercises auto-init dispatch on hello,
async push after schedule create, and per-source-group Run-now
landing the right paths/excludes/tag at the agent.
This commit is contained in:
@@ -105,15 +105,43 @@ func (s *Server) routes(r chi.Router) {
|
||||
r.Get("/hosts/{id}/repo-credentials", s.handleGetHostCredentials)
|
||||
r.Put("/hosts/{id}/repo-credentials", s.handleSetHostCredentials)
|
||||
|
||||
// Per-host schedule CRUD. Mutations bump host_schedule_version;
|
||||
// the agent sync path (P2-02) picks up the new version on the
|
||||
// next reconciliation tick.
|
||||
// Per-host schedule CRUD. Mutations bump host_schedule_version
|
||||
// and async-push to a connected agent (see schedule_push.go).
|
||||
r.Get("/hosts/{id}/schedules", s.handleListSchedules)
|
||||
r.Post("/hosts/{id}/schedules", s.handleCreateSchedule)
|
||||
r.Put("/hosts/{id}/schedules/{sid}", s.handleUpdateSchedule)
|
||||
r.Delete("/hosts/{id}/schedules/{sid}", s.handleDeleteSchedule)
|
||||
|
||||
// Source-group CRUD. A group is "what gets backed up" — paths,
|
||||
// excludes, retention, retry. Group name doubles as the
|
||||
// snapshot tag (restic --tag <name>).
|
||||
r.Get("/hosts/{id}/source-groups", s.handleListSourceGroups)
|
||||
r.Post("/hosts/{id}/source-groups", s.handleCreateSourceGroup)
|
||||
r.Get("/hosts/{id}/source-groups/{gid}", s.handleGetSourceGroup)
|
||||
r.Put("/hosts/{id}/source-groups/{gid}", s.handleUpdateSourceGroup)
|
||||
r.Delete("/hosts/{id}/source-groups/{gid}", s.handleDeleteSourceGroup)
|
||||
|
||||
// Repo maintenance cadences (forget / prune / check). Driven
|
||||
// by the server-side ticker (P2R-06), not the agent's cron.
|
||||
r.Get("/hosts/{id}/repo-maintenance", s.handleGetRepoMaintenance)
|
||||
r.Put("/hosts/{id}/repo-maintenance", s.handleUpdateRepoMaintenance)
|
||||
|
||||
// Per-source-group Run-now (JSON variant). HTMX action is
|
||||
// mounted at the equivalent path outside /api below — both
|
||||
// resolve to the same handler, which sniffs HX-Request.
|
||||
r.Post("/hosts/{id}/source-groups/{gid}/run", s.handleRunSourceGroup)
|
||||
})
|
||||
|
||||
// Per-source-group Run-now (HTMX form action). Available even
|
||||
// when the server is started without UI templates so REST callers
|
||||
// against the non-/api path also work.
|
||||
r.Post("/hosts/{id}/source-groups/{gid}/run", s.handleRunSourceGroup)
|
||||
// Retired routes — see ui_handlers.go for the messages. Mounted
|
||||
// outside the UI gate so cached browser tabs get a clear 410
|
||||
// even if the server runs without templates.
|
||||
r.Post("/hosts/{id}/run-backup", s.handleUIRunBackupGone)
|
||||
r.Post("/hosts/{id}/init-repo", s.handleUIInitRepoGone)
|
||||
|
||||
// Agent ↔ server WebSocket. Bearer-authenticated inside the handler.
|
||||
if s.deps.Hub != nil {
|
||||
r.Mount("/ws/agent", ws.AgentHandler(ws.HandlerDeps{
|
||||
@@ -143,11 +171,9 @@ func (s *Server) routes(r chi.Router) {
|
||||
r.Get("/login", s.handleUILoginGet)
|
||||
r.Post("/login", s.handleUILoginPost)
|
||||
r.Post("/logout", s.handleUILogoutPost)
|
||||
// HTMX action endpoint for "Run now" buttons on the dashboard.
|
||||
r.Post("/hosts/{id}/run-backup", s.handleUIRunBackup)
|
||||
// HTMX action endpoint for the red "Initialise repo" button
|
||||
// shown in the run-now panel until the repo is confirmed init'd.
|
||||
r.Post("/hosts/{id}/init-repo", s.handleUIInitRepo)
|
||||
// Per-host Run-now and manual Init-repo are mounted at the
|
||||
// outer router (so they reply 410 even without UI). Per-
|
||||
// source-group Run-now lives there too — same reason.
|
||||
// Add host flow.
|
||||
r.Get("/hosts/new", s.handleUIAddHostGet)
|
||||
r.Post("/hosts/new", s.handleUIAddHostPost)
|
||||
|
||||
Reference in New Issue
Block a user