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:
@@ -2,6 +2,8 @@ package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -103,6 +105,31 @@ type RetentionPolicy struct {
|
||||
KeepYearly *int `json:"keep_yearly,omitempty"`
|
||||
}
|
||||
|
||||
// Summary renders a compact human view of the policy for templates
|
||||
// and logs — "last=7, d=14, w=4" or "—" when nothing is set.
|
||||
func (p RetentionPolicy) Summary() string {
|
||||
parts := []string{}
|
||||
for _, kv := range []struct {
|
||||
k string
|
||||
v *int
|
||||
}{
|
||||
{"last", p.KeepLast},
|
||||
{"h", p.KeepHourly},
|
||||
{"d", p.KeepDaily},
|
||||
{"w", p.KeepWeekly},
|
||||
{"m", p.KeepMonthly},
|
||||
{"y", p.KeepYearly},
|
||||
} {
|
||||
if kv.v != nil {
|
||||
parts = append(parts, fmt.Sprintf("%s=%d", kv.k, *kv.v))
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "—"
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// ScheduleOptions covers per-schedule knobs that aren't core to the
|
||||
// command itself — currently bandwidth caps. Stored as JSON so
|
||||
// future fields don't churn the schema.
|
||||
|
||||
Reference in New Issue
Block a user