P2R-02 follow-up: clickable rows on Sources/Schedules + cron-preset tooltips
Aligns Sources and Schedules tab rows with the dashboard's row-click
UX: whole-row click navigates to the row's edit page (mirroring
.host-row.clickable). Drops the redundant Edit buttons; Run-now and
Delete remain in .row-action cells that sit above the row-link
overlay via z-index.
Schedule edit form's cron preset chips now carry human-readable
title= tooltips ("Every day at 03:00", "Every Sunday at 03:00", etc).
tasks.md gets a binding row-design rule covering all current and
future list-row templates, and the P2R-02 entry is split into the
six slices already agreed with the operator (slices 1–3 marked
done, 4 next).
This commit is contained in:
@@ -2,6 +2,14 @@
|
||||
|
||||
Project-specific rules for Claude when working in this repo.
|
||||
|
||||
## Run `go vet` before every commit
|
||||
|
||||
CI runs `go vet ./...` and will fail the build on any vet error.
|
||||
Run it locally before staging a commit and fix anything it flags.
|
||||
A common one is `res, _ := http.Do(...); defer res.Body.Close()` —
|
||||
if `err != nil` then `res` may be nil and the deferred close
|
||||
panics. Always check the error before touching `res`.
|
||||
|
||||
## No `Co-Authored-By` trailers on commits
|
||||
|
||||
Don't add `Co-Authored-By: Claude ...` (or any other co-author
|
||||
|
||||
@@ -439,7 +439,10 @@ func TestRunSourceGroupOfflineHost(t *testing.T) {
|
||||
url+"/hosts/"+hostID+"/source-groups/"+gid+"/run", nil)
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, _ := stdhttp.DefaultClient.Do(req)
|
||||
res, err := stdhttp.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != stdhttp.StatusServiceUnavailable {
|
||||
t.Errorf("offline: want 503, got %d", res.StatusCode)
|
||||
@@ -456,7 +459,10 @@ func TestRunSourceGroupUnknownGroup(t *testing.T) {
|
||||
url+"/hosts/"+hostID+"/source-groups/no-such-gid/run", nil)
|
||||
req.AddCookie(cookie)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, _ := stdhttp.DefaultClient.Do(req)
|
||||
res, err := stdhttp.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != stdhttp.StatusNotFound {
|
||||
t.Errorf("unknown group: want 404, got %d", res.StatusCode)
|
||||
|
||||
@@ -142,16 +142,27 @@ Sizes: **S** = under a day, **M** = 1–3 days, **L** = 3–7 days.
|
||||
- **Auto-init at enrolment**: server dispatches `restic init` on first WS connect (was P2-old "Init repo" button — now invisible to the operator). On success: emit a normal job row with `kind=init` so the audit trail still shows it. On `init` returning "config file already exists" (e.g. re-enrolment against an existing repo): treat as soft success per existing restic-wrapper behaviour.
|
||||
- **Tests**: rewrite the deleted `schedules_test.go` and `schedule_push_test.go` against new endpoints; new `source_groups_test.go`, `repo_maintenance_test.go`, `auto_init_test.go`. End-to-end: enrol → server pushes creds → server dispatches init → agent runs it → schedule reconcile fires → operator hits per-source-group Run-now → backup runs → snapshots refresh.
|
||||
|
||||
### P2 redesign — Phase 4 (UI rewire, against v4 wireframes) — TODO
|
||||
### P2 redesign — Phase 4 (UI rewire, against v4 wireframes) — IN PROGRESS
|
||||
|
||||
> **Row-design rule (binding for every list-row template in this app, current and future):**
|
||||
> Whole-row click navigates to the row's primary detail/edit page —
|
||||
> mirror `.host-row.clickable` on the dashboard
|
||||
> (`partials/host_row.html`): an absolute-positioned `.row-link`
|
||||
> overlay with `text-indent: -9999px` covers the row, action buttons
|
||||
> live in `.row-action` cells that sit above via z-index. **Do not
|
||||
> add an explicit "Edit" button** when the row is clickable — it
|
||||
> duplicates the affordance and dilutes the click target. Action
|
||||
> cells are reserved for verbs that aren't "open this row" (Run-now,
|
||||
> Delete, Pause, etc).
|
||||
|
||||
- [ ] **P2R-02** (L) UI templates rebuilt against the new model:
|
||||
- `/hosts/{id}/sources` — list of source groups with per-row meta (includes/excludes count, retention summary via `RetentionPolicy.Summary()`, usage = which schedules reference this group, snapshot count for `tag = group.name`). Run-now / Edit / Delete actions per row.
|
||||
- `/hosts/{id}/sources/{gid}/edit` (and `/sources/new`) — name (= snapshot tag), includes/excludes textareas, retention as a 3×2 keep-* grid, retry-on-offline, inline conflict banner above retention when granularity ↔ cadence mismatch detected (uses `SourceGroup.conflict_dimension` cache).
|
||||
- `/hosts/{id}/schedules` — slim list (status / cron / source-tags / actions) plus new-schedule form (cron with quick-pick chips, source-group multi-select via clickable check pickers, enabled toggle).
|
||||
- `/hosts/{id}/repo` — connection (URL/user/password — pre-filled from `GET /api/hosts/{id}/repo-credentials` redacted view; cert pin), bandwidth caps (host-wide), maintenance rows (forget/prune/check cadence + check subset %), danger-zone re-init.
|
||||
- **Re-enable the four host-detail sub-tabs** (Snapshots is already live; Schedules / Sources / Repo become real links again; Settings stays inert until later). Drop the stop-gap inert-div hack from P2R-00.4.
|
||||
- **Per-source-group Run-now buttons** replace today's per-host `Run backup now` buttons (right-rail + dashboard row + empty-snapshots state). Dashboard row's Run-now becomes either "Run all groups" (if exactly one schedule covers all groups) or "Open →" (multi-group hosts).
|
||||
- Header "version N · agent in sync / agent at vM" indicator preserved (still backed by `host_schedule_version` + `applied_schedule_version`).
|
||||
- **Slice 1 ✅** Sub-tab navigation skeleton — extract header/vitals/sub-tabs into a `host_chrome` partial; Sources / Schedules / Repo become real `<a>` links; placeholder pages share the chrome; version indicator restored. (commit `a535822`)
|
||||
- **Slice 2 ✅** Sources tab — `/hosts/{id}/sources` list with per-row meta + clickable rows + per-group Run-now/Delete; `/sources/new` and `/sources/{gid}/edit` form (name, includes/excludes, 3×2 keep-* grid, retry-on-offline, inline conflict banner from `ConflictDimension` cache); validation re-renders form with input intact; refuses to delete a host's last source group. (commits `0ed9c3d`, `dede74f`)
|
||||
- **Slice 3 ✅** Schedules tab — `/hosts/{id}/schedules` slim list (status / cron / source-tags / actions, clickable rows) plus `/schedules/new` and `/schedules/{sid}/edit` form (cron with five quick-pick chips that have human-readable tooltips, source-group multi-pick as styled check cards, enabled toggle); per-schedule Run-now reuses `dispatchScheduledJob`. (commit `67ca769` + clickable-row follow-up)
|
||||
- **Slice 4 (next)** `/hosts/{id}/repo` — connection (URL/user/password — pre-filled from `GET /api/hosts/{id}/repo-credentials` redacted view; cert pin), bandwidth caps (host-wide; new `PUT /api/hosts/{id}/bandwidth`), maintenance rows (forget/prune/check cadence + check subset %), danger-zone re-init.
|
||||
- **Slice 5** Per-source-group Run-now wiring on dashboard row + empty-snapshots state. Dashboard row's Run-now becomes either "Run all groups" (if exactly one schedule covers all groups) or "Open →" (multi-group hosts).
|
||||
- **Slice 6** Playwright sweep — login → walk new tabs → create source group → create schedule → run-now → confirm dispatch.
|
||||
- Header "version N · agent in sync / agent at vM" indicator preserved across all tabs (backed by `host_schedule_version` + `applied_schedule_version`).
|
||||
- Form validation re-renders with the operator's typed input intact (mirror P2-04's behaviour). Each save fires `pushScheduleSetAsync` so an online agent re-arms within seconds.
|
||||
|
||||
### P2 redesign — Phase 5 (server-side maintenance ticker) — TODO
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -193,6 +193,18 @@
|
||||
column-gap: 18px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
/* Whole-row click → edit page, mirroring .host-row.clickable on the
|
||||
dashboard. Action cells sit above via z-index so their buttons
|
||||
keep working. */
|
||||
.src-row.clickable { position: relative; }
|
||||
.src-row.clickable .row-link {
|
||||
position: absolute; inset: 0; z-index: 0;
|
||||
text-indent: -9999px; overflow: hidden;
|
||||
}
|
||||
.src-row.clickable:hover { background: var(--panel-hi); cursor: pointer; }
|
||||
.src-row.clickable > * { position: relative; z-index: 1; pointer-events: none; }
|
||||
.src-row.clickable > .row-link { pointer-events: auto; }
|
||||
.src-row.clickable > .row-action { pointer-events: auto; }
|
||||
|
||||
/* ---------- schedule rows (Schedules tab) ---------- */
|
||||
.schd-row {
|
||||
@@ -206,6 +218,16 @@
|
||||
font-size: 11px; color: var(--ink-fade);
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
}
|
||||
/* Whole-row click → edit page (matches .host-row.clickable). */
|
||||
.schd-row.clickable { position: relative; }
|
||||
.schd-row.clickable .row-link {
|
||||
position: absolute; inset: 0; z-index: 0;
|
||||
text-indent: -9999px; overflow: hidden;
|
||||
}
|
||||
.schd-row.clickable:hover { background: var(--panel-hi); cursor: pointer; }
|
||||
.schd-row.clickable > * { position: relative; z-index: 1; pointer-events: none; }
|
||||
.schd-row.clickable > .row-link { pointer-events: auto; }
|
||||
.schd-row.clickable > .row-action { pointer-events: auto; }
|
||||
|
||||
/* ---------- cron preset chips ---------- */
|
||||
.preset-chip {
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
<div></div>
|
||||
</div>
|
||||
{{range $i, $sc := $page.Schedules}}
|
||||
<div class="schd-row {{if not (eq $i 0)}}hairline{{end}}">
|
||||
<div class="schd-row clickable {{if not (eq $i 0)}}hairline{{end}}">
|
||||
<a href="/hosts/{{$host.ID}}/schedules/{{$sc.ID}}/edit" class="row-link" aria-label="Edit schedule">edit</a>
|
||||
<div>
|
||||
{{if $sc.Enabled}}
|
||||
<span class="mono text-[11px] text-ok">enabled</span>
|
||||
@@ -51,14 +52,13 @@
|
||||
<span class="tag" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent); {{if not $sc.Enabled}}opacity: 0.6;{{end}}">{{if $name}}{{$name}}{{else}}<span class="text-ink-fade">unknown</span>{{end}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex gap-1.5 justify-end">
|
||||
<div class="flex gap-1.5 justify-end row-action">
|
||||
{{if and $sc.Enabled (eq $host.Status "online")}}
|
||||
<button class="btn btn-primary"
|
||||
hx-post="/hosts/{{$host.ID}}/schedules/{{$sc.ID}}/run"
|
||||
hx-swap="none"
|
||||
hx-disabled-elt="this">Run now</button>
|
||||
{{end}}
|
||||
<a href="/hosts/{{$host.ID}}/schedules/{{$sc.ID}}/edit" class="btn">Edit</a>
|
||||
<form method="post" action="/hosts/{{$host.ID}}/schedules/{{$sc.ID}}/delete" style="display: inline;"
|
||||
onsubmit="return confirm('Delete this schedule? Existing snapshots are not affected.');">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
<div class="panel rounded-[7px] overflow-hidden">
|
||||
{{range $i, $row := $page.Groups}}
|
||||
{{$g := $row.Group}}
|
||||
<div class="src-row {{if not (eq $i 0)}}hairline{{end}}">
|
||||
<div class="src-row clickable {{if not (eq $i 0)}}hairline{{end}}">
|
||||
<a href="/hosts/{{$host.ID}}/sources/{{$g.ID}}/edit" class="row-link" aria-label="Edit {{$g.Name}}">{{$g.Name}}</a>
|
||||
<div>
|
||||
<div class="flex items-center" style="gap: 10px;">
|
||||
<span class="tag mono" style="border-color: color-mix(in oklch, var(--accent), transparent 60%); color: var(--accent);">{{$g.Name}}</span>
|
||||
@@ -52,7 +53,7 @@
|
||||
{{if gt $row.SnapshotCount 0}} · <span class="mono">{{$row.SnapshotCount}}</span> snapshot{{if ne $row.SnapshotCount 1}}s{{end}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end" style="gap: 6px;">
|
||||
<div class="flex justify-end row-action" style="gap: 6px;">
|
||||
{{if and (gt (len $g.Includes) 0) (eq $host.Status "online")}}
|
||||
<button class="btn btn-primary"
|
||||
hx-post="/hosts/{{$host.ID}}/source-groups/{{$g.ID}}/run"
|
||||
@@ -62,7 +63,6 @@
|
||||
<button class="btn" disabled
|
||||
title="{{if eq (len $g.Includes) 0}}add at least one include path before running{{else}}host is offline{{end}}">Run now</button>
|
||||
{{end}}
|
||||
<a href="/hosts/{{$host.ID}}/sources/{{$g.ID}}/edit" class="btn">Edit</a>
|
||||
{{if gt $row.UsedBy 0}}
|
||||
<button class="btn btn-danger" disabled
|
||||
title="remove this group from {{$row.UsedBy}} schedule{{if ne $row.UsedBy 1}}s{{end}} first">Delete</button>
|
||||
|
||||
@@ -26,9 +26,16 @@
|
||||
<label class="field-label" for="cron">Cron expression</label>
|
||||
<input type="text" id="cron" name="cron" class="field mono" value="{{$f.CronExpr}}" required autofocus />
|
||||
<div class="flex flex-wrap gap-1.5 mt-2.5" id="cron-presets">
|
||||
{{range list "0 3 * * *" "@hourly" "0 */6 * * *" "0 3 * * 0" "0 3 1 * *"}}
|
||||
<span class="preset-chip" data-cron="{{.}}">{{.}}</span>
|
||||
{{end}}
|
||||
<span class="preset-chip" data-cron="0 3 * * *"
|
||||
title="Every day at 03:00">0 3 * * *</span>
|
||||
<span class="preset-chip" data-cron="@hourly"
|
||||
title="Every hour, on the hour (00 minutes)">@hourly</span>
|
||||
<span class="preset-chip" data-cron="0 */6 * * *"
|
||||
title="Every 6 hours, on the hour (00:00, 06:00, 12:00, 18:00)">0 */6 * * *</span>
|
||||
<span class="preset-chip" data-cron="0 3 * * 0"
|
||||
title="Every Sunday at 03:00">0 3 * * 0</span>
|
||||
<span class="preset-chip" data-cron="0 3 1 * *"
|
||||
title="The 1st of every month at 03:00">0 3 1 * *</span>
|
||||
</div>
|
||||
<div class="field-help mt-2.5">
|
||||
Standard 5-field cron with descriptors. Server validates with the same parser the agent uses to fire — what saves here is what runs.
|
||||
|
||||
Reference in New Issue
Block a user