P3 sweep fixes: snap-row CSS, tree expand, --no-ownership drop, target path

Bug fixes from the Playwright sweep against the live smoke server:

1. Snapshot-picker layout. The .snap-row class was used in the wireframe
   but never landed in web/styles/input.css; rows rendered as vertical
   blocks instead of a 6-column grid. Added the token (mirrors host-row
   shape with restore-specific column widths).

2. Tree expansion. hx-target='closest .tree-row + .tree-children' isn't
   a valid HTMX selector — modifiers don't chain. Replaced HTMX-driven
   expansion with a small window.__rmTreeToggle helper that uses plain
   fetch + .tree-pair wrapper structure for trivial sibling lookup.
   Caches loaded state per node.

3. --no-ownership flag dropped. Restic 0.17 introduced --no-ownership;
   0.16 rejects it ('unknown flag') before doing any work. Since the
   agent runs as root in the systemd unit, restored files keep their
   original uid/gid either way and the parent dir is root-owned, so
   the 'cp without sudo' rationale doesn't hold. Drop the flag entirely.

4. Default target dir moved to /var/lib/restic-manager/restore. The
   systemd unit pins ReadWritePaths to /etc/restic-manager +
   /var/lib/restic-manager (with ProtectSystem=strict making the rest
   of /var read-only); writes to /var/restic-restore failed with
   'read-only file system'.

5. Confirm summary HTML escaping. defaultTarget JS literal evaluates
   to a string with literal angle brackets; insertion into innerHTML
   must escape them. Added an inline HTML-escape pass.

tasks.md ticked for the Restore sub-phase with a sweep summary
covering the live end-to-end test.
This commit is contained in:
2026-05-04 15:57:42 +01:00
parent 1111124573
commit e22b41d452
9 changed files with 133 additions and 56 deletions
+7 -4
View File
@@ -384,11 +384,14 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques
}
// defaultRestoreTargetRoot is the parent of the per-job restore
// directory. Chosen on a per-host basis would be nicer but the agent
// is the one that actually creates it, and /var/restic-restore is
// fine for Linux hosts (the agent's systemd unit runs as root).
// directory. The agent's systemd unit pins ReadWritePaths to
// /etc/restic-manager + /var/lib/restic-manager (with ProtectSystem=
// strict making the rest of /var read-only); restore writes have to
// land inside one of those, so we keep them under
// /var/lib/restic-manager/restore where the agent is already allowed
// to write. The /restore subdir is created by the agent on demand.
func defaultRestoreTargetRoot() string {
return "/var/restic-restore"
return "/var/lib/restic-manager/restore"
}
// defaultRestoreTargetDir surfaces the placeholder path shown on the
+2 -2
View File
@@ -302,8 +302,8 @@ func TestRestorePostHappyPathDispatches(t *testing.T) {
if cp.Restore.InPlace {
t.Fatal("expected new-directory mode (in_place=false)")
}
if !strings.HasPrefix(cp.Restore.TargetDir, "/var/restic-restore/") {
t.Fatalf("target_dir: got %q, want prefix /var/restic-restore/", cp.Restore.TargetDir)
if !strings.HasPrefix(cp.Restore.TargetDir, "/var/lib/restic-manager/restore/") {
t.Fatalf("target_dir: got %q, want prefix /var/lib/restic-manager/restore/", cp.Restore.TargetDir)
}
if len(cp.Restore.Paths) != 2 {
t.Fatalf("paths: got %d, want 2", len(cp.Restore.Paths))