3a9d744360
The offsite restore list rendered up to five inline forms per app row. Two of them —
„Helyreállítás az élő adatok közé (csak a hiányzó fájlok)" and „Teljes visszaállítás
(fájlok + adatbázis)" — were sibling buttons whose difference is whether the customer's
data comes back at all. That mis-selection CAUSED the round-2 incident: an operator who
had read the source pressed the missing-only button and /backup/offbox/reconstitute was
never hit (DIAG-immich-restore-round2-2026-07-19, finding 1).
The rule this establishes: two adjacent controls whose difference is "your data comes
back" vs "your data cannot come back" must never be distinguishable only by layout.
Each row now carries ONE „Visszaállítás…" entry linking to a per-app wizard at
GET /backups/restore/app?name=<app>: three intent CARDS with consequence sentences, a
visible phase strip, danger styling plus the R-43 double-confirm carried over verbatim on
the destructive one, and the pair-honesty panel bound to real OffsiteScratchPair facts.
deriveWizardStep is pure over (op running, size-gate flash, scratch ready); the step is
never accepted from the request, and a running op outranks a stale ?full_prep= so no
commit button survives into a restore. While ANY op runs every mutation form is suppressed
server-side instead of being offered and refused.
No new mutation endpoint: every card posts to the pre-existing /backup/offbox/* handler
with the same field names and gates, and the page works with JavaScript disabled.
internal/{backup,appbackup,selfupdate} untouched. R-45's job registry stays its own item.
Fixes a latent bug found on the way: offboxRedirectTo hardcoded "?" when appending its
flash, which against the wizard's ?name=<app> target would have buried the flash inside
the name value.
No new agent coupling — MinAgent stays 0.90.0.
139 lines
6.0 KiB
Go
139 lines
6.0 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
|
)
|
|
|
|
// v0.126.0 Part A — the shared app_list_row partial (app_row.html) is the ONE row source on
|
|
// the list surfaces. Per-surface render tests: the partial's marker classes are present, the
|
|
// old hand-rolled structures (storage-path-item rows / stack-card rows) are gone, and the
|
|
// caller-provided action blocks (behavior) are untouched.
|
|
// COMPANION red-proof (recorded in REPORT): paste one pre-0.126.0 storage-path-item row block
|
|
// back into backups_remote.html → scripts/app_row_dedup_gate.py exits 1.
|
|
|
|
func appRowSplitData() map[string]interface{} {
|
|
d := splitTestData()
|
|
d["OffboxApps"] = []OffboxAppRow{
|
|
{Name: "calibre-web", DisplayName: "Calibre-Web", Slug: "calibre-web", Enabled: true},
|
|
{Name: "radarr", DisplayName: "Radarr", Slug: "radarr", Enabled: false},
|
|
}
|
|
return d
|
|
}
|
|
|
|
func TestAppRow_RemoteToggleList(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_remote", appRowSplitData())
|
|
if !strings.Contains(html, `class="app-row"`) || !strings.Contains(html, `app-row-name`) {
|
|
t.Error("remote toggle list must render through the app_list_row partial")
|
|
}
|
|
if strings.Contains(html, "storage-path-item") {
|
|
t.Error("old storage-path-item row structure survives on the remote page")
|
|
}
|
|
// Behavior unchanged: the toggle form + both label variants render as before.
|
|
if !strings.Contains(html, `action="/backup/offbox/toggle"`) {
|
|
t.Error("toggle form missing")
|
|
}
|
|
if !strings.Contains(html, "Távoli mentés kikapcsolása") || !strings.Contains(html, "Távoli mentés bekapcsolása") {
|
|
t.Error("toggle button labels changed")
|
|
}
|
|
}
|
|
|
|
func TestAppRow_RestoreLists(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_restore", appRowSplitData())
|
|
if !strings.Contains(html, `class="app-row"`) {
|
|
t.Error("restore-page lists must render through the app_list_row partial")
|
|
}
|
|
if strings.Contains(html, "storage-path-item") {
|
|
t.Error("old storage-path-item row structure survives on the restore page")
|
|
}
|
|
// v0.154.0 (R-48): the restore row is now a single entry that LINKS to the per-app wizard —
|
|
// the mutation forms moved there. The row itself must still be the one, and only, restore
|
|
// control for the app.
|
|
if !strings.Contains(html, `href="/backups/restore/app?name=calibre-web"`) {
|
|
t.Error("per-app restore wizard entry missing")
|
|
}
|
|
if strings.Contains(html, `action="/backup/offbox/restore"`) ||
|
|
strings.Contains(html, `action="/backup/offbox/place"`) ||
|
|
strings.Contains(html, `action="/backup/offbox/reconstitute"`) {
|
|
t.Error("offsite restore mutation forms must not render on the list page (R-48)")
|
|
}
|
|
if !strings.Contains(html, `fab-dl-btn`) || !strings.Contains(html, "Letöltés (.fab)") {
|
|
t.Error(".fab download action missing")
|
|
}
|
|
// The disabled app (radarr) must NOT appear in the restore-to-verify list (Enabled filter),
|
|
// but MUST appear in the .fab list — the partial must not have changed the filters.
|
|
if strings.Count(html, `value="radarr"`) != 0 {
|
|
t.Error("restore-to-verify must list only offbox-enabled apps")
|
|
}
|
|
if !strings.Contains(html, `data-stack="radarr"`) {
|
|
t.Error(".fab list must list all apps")
|
|
}
|
|
}
|
|
|
|
func TestAppRow_Dashboard(t *testing.T) {
|
|
data := map[string]interface{}{
|
|
"Page": "dashboard", "Title": "Vezérlőpult",
|
|
"Stacks": []stacks.Stack{
|
|
{Name: "radarr", Deployed: true, State: stacks.StateRunning,
|
|
Meta: stacks.Metadata{Slug: "radarr", DisplayName: "Radarr", Description: "Filmgyűjtemény kezelése"}},
|
|
},
|
|
"MissingStorage": map[string]string{},
|
|
"NetworkWarnings": map[string]string{},
|
|
"NetworkStubs": map[string]string{},
|
|
"Subdomains": map[string]string{"radarr": "radarr"},
|
|
"RunningCount": 1, "StoppedCount": 0, "TotalCount": 1,
|
|
"SystemInfo": system.SystemInfo{},
|
|
"BackupEnabled": false,
|
|
"Domain": "demo-felhom.eu",
|
|
}
|
|
html := renderBackupPage(t, "dashboard", data)
|
|
if !strings.Contains(html, `class="app-row stack-state-run"`) {
|
|
t.Error("dashboard rows must render through app_list_row with the state RowClass")
|
|
}
|
|
if !strings.Contains(html, `data-href="/apps/radarr"`) {
|
|
t.Error("dashboard row lost its data-href")
|
|
}
|
|
if !strings.Contains(html, "Filmgyűjtemény kezelése") {
|
|
t.Error("dashboard row lost the description secondary line")
|
|
}
|
|
if strings.Contains(html, "stack-card") || strings.Contains(html, "stack-info") {
|
|
t.Error("old stack-card row structure survives on the dashboard")
|
|
}
|
|
}
|
|
|
|
// The Alkalmazások collapsed header is ALIGNED to the row grammar (icon + name left,
|
|
// status + chevron right) while keeping its own expander structure.
|
|
func TestAppRow_AppsHeaderAligned(t *testing.T) {
|
|
d := appRowSplitData()
|
|
d["AppBackupRows"] = []AppBackupRow{
|
|
{StackName: "calibre-web", DisplayName: "Calibre-Web", Slug: "calibre-web",
|
|
Status: "green", Tier3State: "active"},
|
|
}
|
|
d["Backup"] = &backup.FullBackupStatus{
|
|
AppDataInfo: []backup.AppBackupInfo{{StackName: "calibre-web", DisplayName: "Calibre-Web"}},
|
|
}
|
|
d["Offbox"] = &settings.OffboxTarget{Enabled: true, Host: "nas.local", LastStatus: "ok", EscrowState: "escrowed"}
|
|
html := renderBackupPage(t, "backups_apps", d)
|
|
|
|
hdr := html[strings.Index(html, `class="app-backup-row-header"`):]
|
|
hdr = hdr[:strings.Index(hdr, "app-backup-row-detail")]
|
|
iIcon := strings.Index(hdr, "app-row-icon")
|
|
iName := strings.Index(hdr, "app-backup-row-name")
|
|
iDot := strings.Index(hdr, "status-dot")
|
|
iChevron := strings.Index(hdr, "expand-icon")
|
|
if iIcon < 0 || iName < 0 || iDot < 0 || iChevron < 0 {
|
|
t.Fatalf("aligned header pieces missing (icon=%d name=%d dot=%d chevron=%d)", iIcon, iName, iDot, iChevron)
|
|
}
|
|
if !(iIcon < iName && iName < iDot && iDot < iChevron) {
|
|
t.Error("header grammar must be icon, name, ..., status, chevron (left→right)")
|
|
}
|
|
if !strings.Contains(hdr, `onclick="toggleBackupDetail(this)"`) {
|
|
t.Error("expander behavior must be untouched")
|
|
}
|
|
}
|