c6237d4004
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>
97 lines
4.3 KiB
HTML
97 lines
4.3 KiB
HTML
{{define "title"}}{{.Title}}{{end}}
|
|
|
|
{{define "content"}}
|
|
{{$page := .Page}}
|
|
{{$host := $page.Host}}
|
|
<div class="max-w-[1280px] mx-auto px-8 pt-7 pb-14">
|
|
|
|
<div class="crumbs">
|
|
<a href="/">Dashboard</a><span class="sep">/</span>
|
|
<a href="/hosts/{{$host.ID}}">{{$host.Name}}</a><span class="sep">/</span>
|
|
<span class="text-ink-mid">schedules</span>
|
|
</div>
|
|
|
|
{{/* ---------- header ---------- */}}
|
|
<div class="flex items-start justify-between mt-3.5">
|
|
<div>
|
|
<div class="flex items-center gap-3">
|
|
{{if eq $host.Status "online"}}
|
|
<span class="dot dot-online"></span>
|
|
{{else}}
|
|
<span class="dot dot-offline"></span>
|
|
{{end}}
|
|
<h1 class="text-[22px] font-medium tracking-[-0.01em]">
|
|
schedules <span class="text-ink-fade">·</span>
|
|
<span class="mono text-ink font-medium">{{$host.Name}}</span>
|
|
</h1>
|
|
<span class="mono text-[11px] text-ink-mute">version {{$page.Version}}{{if and (gt $page.Version 0) (ne $page.Version $page.AppliedVersion)}} <span class="text-warn">· agent at v{{$page.AppliedVersion}}</span>{{else if gt $page.Version 0}} <span class="text-ok">· agent in sync</span>{{end}}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<a href="/hosts/{{$host.ID}}/schedules/new" class="btn btn-primary">New schedule</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{/* ---------- secondary tabs ---------- */}}
|
|
<div class="flex items-end mt-7">
|
|
<a class="sub-tab" href="/hosts/{{$host.ID}}">Snapshots <span class="mono text-ink-fade text-[11px] ml-1">{{comma $host.SnapshotCount}}</span></a>
|
|
<a class="sub-tab active" href="/hosts/{{$host.ID}}/schedules">Schedules <span class="mono text-ink-fade text-[11px] ml-1">{{len $page.Schedules}}</span></a>
|
|
<div class="sub-tab">Jobs</div>
|
|
<div class="sub-tab">Repo</div>
|
|
<div class="sub-tab">Settings</div>
|
|
</div>
|
|
|
|
{{/* ---------- schedule rows ---------- */}}
|
|
<div class="panel rounded-[7px] mt-6 overflow-hidden">
|
|
{{if eq (len $page.Schedules) 0}}
|
|
<div class="empty-state" style="border: none; background: var(--panel);">
|
|
<h3 class="text-base font-medium tracking-[-0.005em]">No schedules yet.</h3>
|
|
<p class="text-pretty text-ink-mute text-[13px] mt-2 mx-auto max-w-[480px] leading-[1.65]">
|
|
Add one and the agent will start running backups on whatever cron expression you give it.
|
|
Until then, run-now is the only way to trigger a backup.
|
|
</p>
|
|
<div class="mt-5">
|
|
<a href="/hosts/{{$host.ID}}/schedules/new" class="btn btn-primary">New schedule</a>
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
<div class="hairline grid items-baseline px-4 py-2.5 text-[11px] text-ink-fade uppercase tracking-[0.08em]"
|
|
style="grid-template-columns: 0.5fr 1fr 2fr 1.5fr 0.5fr 0.7fr; column-gap: 18px;">
|
|
<div>Status</div>
|
|
<div>Cron</div>
|
|
<div>Paths</div>
|
|
<div>Retention</div>
|
|
<div>Tags</div>
|
|
<div></div>
|
|
</div>
|
|
{{range $page.Schedules}}
|
|
<div class="grid items-center px-4 py-3 text-[13px] hairline"
|
|
style="grid-template-columns: 0.5fr 1fr 2fr 1.5fr 0.5fr 0.7fr; column-gap: 18px;">
|
|
<div>
|
|
{{if .Enabled}}
|
|
<span class="mono text-[11px] text-ok">enabled</span>
|
|
{{else}}
|
|
<span class="mono text-[11px] text-ink-fade">disabled</span>
|
|
{{end}}
|
|
</div>
|
|
<div class="mono text-ink">{{.CronExpr}}</div>
|
|
<div class="mono text-ink-mid text-[12px] truncate" title="{{joinDot .Paths}}">{{joinDot .Paths}}</div>
|
|
<div class="mono text-[12px] text-ink-mid">{{.RetentionPolicy.Summary}}</div>
|
|
<div class="flex gap-1.5 flex-wrap">
|
|
{{- range .Tags -}}<span class="tag">{{.}}</span>{{- end -}}
|
|
</div>
|
|
<div class="text-right flex gap-1.5 justify-end">
|
|
<a href="/hosts/{{$host.ID}}/schedules/{{.ID}}/edit" class="btn">Edit</a>
|
|
<form method="post" action="/hosts/{{$host.ID}}/schedules/{{.ID}}/delete" style="display: inline;"
|
|
onsubmit="return confirm('Delete schedule {{.CronExpr}}? Existing snapshots are not affected.');">
|
|
<button type="submit" class="btn btn-danger">Delete</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
</div>
|
|
|
|
</div>
|
|
{{end}}
|