P2-04: schedule editor UI
Closes the schedule foundations slice — operator can now drive the
plumbing P2-01..03 landed without touching the JSON API.
* New routes:
- GET /hosts/{id}/schedules (list)
- GET /hosts/{id}/schedules/new (create form)
- POST /hosts/{id}/schedules/new (create)
- GET /hosts/{id}/schedules/{sid}/edit (edit form)
- POST /hosts/{id}/schedules/{sid}/edit (update)
- POST /hosts/{id}/schedules/{sid}/delete (delete, confirm-then-redirect)
* List view (web/templates/pages/schedules_list.html):
status, cron, paths, retention summary, tags, edit/delete buttons.
Header shows "version N · agent in sync" or "agent at vM" when the
push hasn't been ack'd yet — backed by host_schedule_version +
applied_schedule_version. Empty-state CTA points at /schedules/new.
* Create/edit form (web/templates/pages/schedule_edit.html, shared):
cron expression with five quick-pick presets (daily 3am / every 6h
/ @hourly / weekly Sun / monthly 1st), paths textarea (one per
line), excludes textarea, tags (comma-separated), retention as six
numeric fields (mirrors restic's --keep-* flags one-for-one),
bandwidth caps, enabled toggle. Side panel explains the
reconciliation flow so the operator knows what saving actually
does. Validation errors re-render with operator's input intact.
* internal/server/http/ui_schedules.go owns the handlers; reuses
the same validateSchedule + pushScheduleSetAsync used by the JSON
API path. Each save audit-logs schedule.created / schedule.updated
/ schedule.deleted (matching the JSON API actions).
* store.RetentionPolicy gains a Summary() method ("last=7, d=14,
w=4" or "—"). Used by the list view's table cell so templates
don't have to do any conditional retention rendering.
* Two new template helpers: list (string varargs → []string, used
for the cron preset row) and joinComma (sibling to joinDot for
the rare list that wants commas). RetentionPolicy.Summary covers
the schedule-list case but the helpers are general.
* host_detail.html secondary tabs row converted from inert <div>s
into <a> links. Snapshots active by default; Schedules now points
at the new page. Jobs/Repo/Settings remain inert until their
P2 owners ship.
Hooks UI deferred to P2-15 (lands with the hook execution path).
Single-kind UI (backup only) by design — other kinds get a UI when
their job dispatch lands in P2-05..08.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,7 @@ Sizes: **S** = under a day, **M** = 1–3 days, **L** = 3–7 days.
|
||||
- [x] **P2-01** (M) Schedule schema + CRUD API. `schedules` table was already laid down in 0001; this slice adds `store.Schedule`/`RetentionPolicy`/`ScheduleOptions` types, `CreateSchedule` / `GetSchedule` / `ListSchedulesByHost` / `UpdateSchedule` / `DeleteSchedule` / `GetHostScheduleVersion` / `SetHostAppliedScheduleVersion` (mutations bump `host_schedule_version` atomically in-tx), and REST endpoints `GET|POST /api/hosts/{id}/schedules` + `PUT|DELETE /api/hosts/{id}/schedules/{sid}`. Validation: cron-expr parses via `robfig/cron/v3` (same parser the agent will use, so anything that validates here will fire there); kind ∈ {backup, forget, prune, check} (init/unlock are operator-only); backup schedules require ≥1 path; hooks rejected on non-backup kinds (spec §14.3). Mutations audit-logged. Server + store tests cover the happy path, validation, and version bumps.
|
||||
- [x] **P2-02** (L) Server-pushed schedule reconciliation. `pushScheduleSet*` helpers (on-hello + async post-CRUD flavours), wiring in `onAgentHello` (always pushes, even when the host has no repo creds yet), `pushScheduleSetAsync` called from Create/Update/Delete handlers (no-op when the host is offline; on-hello catches up). `MsgScheduleAck` handled in the WS dispatcher: `OnScheduleAck` callback persists `applied_schedule_version`. Agent-side `schedule.set` handler ships in P2-03; the server side now has parity tests.
|
||||
- [x] **P2-03** (M) Agent local scheduler. New `internal/agent/scheduler` package wraps `robfig/cron/v3` — `Apply(ScheduleSetPayload, Sender)` stops the prior cron (waits for in-flight entries), rebuilds from scratch (skipping disabled entries + skipping bad cron exprs with a warn log), starts, and emits `schedule.ack`. On a tick the entry sends a new `schedule.fire` envelope to the server with `{schedule_id, scheduled_at}`. The server's `OnScheduleFire` callback (`dispatchScheduledJob`) looks up the schedule, builds args from kind, persists a `jobs` row with `actor_kind=schedule` + `scheduled_id`, and ships `command.run` back on the same conn — agent runs the job through the existing dispatcher. Tx is swapped on every Apply so reconnect is handled naturally (cron entries that fire against a dropped tx log + skip the tick). `CreateJob` now writes `scheduled_id`; this column was in the schema since 0001 but never populated. Tests: scheduler unit tests cover ack-on-apply, cron tick → fire envelope, disabled-entries silent, replace-prior-state stops the old cron. Server-side end-to-end test covers fire → command.run with the right job_id/kind/args, plus jobs row with actor_kind=schedule + scheduled_id linking back. **Deferred:** persistence of next-fire times across agent restarts (a missed fire window during downtime simply fires once on reconnect — desirable behaviour).
|
||||
- [ ] **P2-04** (M) Schedule editor UI (paths, excludes, tags, cron, retention)
|
||||
- [x] **P2-04** (M) Schedule editor UI. New "Schedules" sub-tab on host detail (header + run-now panel preserved across the snapshot/schedule pages). List view shows status, cron, paths, retention summary (`store.RetentionPolicy.Summary()` renders "last=7, d=14, w=4"), tags, and edit/delete buttons. The header carries a "version N · agent in sync / agent at vM" indicator backed by `host_schedule_version` + `applied_schedule_version`. Create/edit form covers cron expr (with quick-pick presets), paths textarea, excludes textarea, tags (comma-separated), retention (six numeric inputs mirroring restic's `--keep-*` flags), bandwidth caps, enabled toggle. Form validation re-renders with the operator's typed input still in place. Each save fires `pushScheduleSetAsync` so an online agent re-arms within a few seconds. Hooks UI deferred to P2-15 (lands when the hook execution path does).
|
||||
- [ ] **P2-05** (M) `forget` command with retention policy (keep-last/daily/weekly/monthly/yearly)
|
||||
- [ ] **P2-06** (M) `prune` command (admin-only, uses non-append-only credential)
|
||||
- [ ] **P2-07** (S) `check` command (random subset + `--read-data-subset`)
|
||||
|
||||
Reference in New Issue
Block a user