From 24876813968a1c4f81930d48dd6af0157d1e8200 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sat, 25 Jul 2026 07:37:02 +0200 Subject: [PATCH] =?UTF-8?q?style:=20gofmt=20normalization=20=E2=80=94=20no?= =?UTF-8?q?=20logic=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gofmt -w across the controller tree (46 files) so gofmt -l is empty — disarms the formatting landmine where a targeted edit + accidental gofmt -w swept ~46 unrelated files. Pure formatting: whitespace + gofmt's optional-semicolon removal in reflowed inline closures. One doc comment reworded ('' -> 'the empty string') to avoid gofmt's Go-1.19 doc-comment typographic substitition ('' -> curly quote) muddying its meaning. No build/vet/test behavior change. --- .../internal/agentapi/netstorage_test.go | 4 +-- .../internal/appbackup/captureset_test.go | 6 ++-- controller/internal/appbackup/dbdump.go | 3 +- controller/internal/appexport/crypto.go | 4 +-- controller/internal/appexport/export.go | 4 +-- .../appexport/export_additive_test.go | 6 ++-- controller/internal/appexport/fabplan_test.go | 2 +- .../internal/appexport/roundtrip_test.go | 26 +++++++-------- controller/internal/backup/recovery_unit.go | 9 +++--- controller/internal/backup/tier2.go | 10 +++--- controller/internal/config/config.go | 2 +- .../internal/integrations/integrations.go | 4 +-- controller/internal/integrations/lifecycle.go | 14 ++++++-- controller/internal/integrations/manager.go | 14 ++++++-- .../integrations/onlyoffice_nextcloud.go | 2 +- controller/internal/report/builder.go | 1 - controller/internal/report/config_refresh.go | 2 +- .../internal/report/escrow_confirm_test.go | 6 ++-- controller/internal/report/types.go | 32 +++++++++---------- controller/internal/selftest/selftest.go | 2 +- controller/internal/selfupdate/state.go | 6 ++-- controller/internal/selfupdate/updater.go | 7 ++-- controller/internal/settings/smb.go | 10 +++--- controller/internal/setup/setup.go | 4 +-- .../internal/stacks/lifecycle_orphan_test.go | 6 ++-- .../stacks/userdata_belt_gate_test.go | 4 +-- controller/internal/sync/sync.go | 8 ++--- controller/internal/system/info.go | 8 ++--- controller/internal/system/mounts_linux.go | 2 +- controller/internal/system/mounts_other.go | 2 +- .../internal/web/app_export_domain_test.go | 4 +-- .../internal/web/backup_page_state_test.go | 22 ++++++------- controller/internal/web/backups_split_test.go | 24 +++++++------- controller/internal/web/escrow_handlers.go | 2 +- .../internal/web/export_download_test.go | 14 ++++---- controller/internal/web/fab_export_test.go | 28 ++++++++-------- .../internal/web/filebrowser_gate_test.go | 10 +++--- controller/internal/web/funcmap.go | 2 +- controller/internal/web/intermediary.go | 18 +++++------ controller/internal/web/intermediary_test.go | 14 ++++---- controller/internal/web/lifecycle_test.go | 14 ++++---- controller/internal/web/mobile_nav_test.go | 10 +++--- .../internal/web/netstorage_gate_test.go | 5 ++- .../internal/web/storage_handlers_test.go | 11 ++++--- controller/internal/web/storage_init_job.go | 8 ++--- .../web/storage_network_template_test.go | 18 +++++------ 46 files changed, 221 insertions(+), 193 deletions(-) diff --git a/controller/internal/agentapi/netstorage_test.go b/controller/internal/agentapi/netstorage_test.go index 0f25b3e..ee83a06 100644 --- a/controller/internal/agentapi/netstorage_test.go +++ b/controller/internal/agentapi/netstorage_test.go @@ -9,8 +9,8 @@ import ( ) func netStub(t *testing.T) (*httptest.Server, string, *struct { - addBody AddNetStorageRequest - removed string + addBody AddNetStorageRequest + removed string }) { captured := &struct { addBody AddNetStorageRequest diff --git a/controller/internal/appbackup/captureset_test.go b/controller/internal/appbackup/captureset_test.go index 3ad5ecf..52361ca 100644 --- a/controller/internal/appbackup/captureset_test.go +++ b/controller/internal/appbackup/captureset_test.go @@ -108,10 +108,10 @@ func TestComputeCaptureSet_ExcludedInvisible(t *testing.T) { func TestComputeCaptureSet_StructuralGuards(t *testing.T) { binds := []ClassifiedBind{ - {ComposeBind: ComposeBind{Root: RootHDD, RelPath: "../evil"}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d1 traversal - {ComposeBind: ComposeBind{Root: RootHDD, RelPath: ""}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d2 bare hdd root + {ComposeBind: ComposeBind{Root: RootHDD, RelPath: "../evil"}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d1 traversal + {ComposeBind: ComposeBind{Root: RootHDD, RelPath: ""}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d2 bare hdd root {ComposeBind: ComposeBind{Root: RootHDD, RelPath: "backups/primary/x"}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d3 reserved zone - {ComposeBind: ComposeBind{Root: RootUserdata, RelPath: ""}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d4 bare userdata — ALLOWED + {ComposeBind: ComposeBind{Root: RootUserdata, RelPath: ""}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d4 bare userdata — ALLOWED } cs := ComputeCaptureSet(binds, true, TierOffsite, drv) diff --git a/controller/internal/appbackup/dbdump.go b/controller/internal/appbackup/dbdump.go index 81a7607..a22d88d 100644 --- a/controller/internal/appbackup/dbdump.go +++ b/controller/internal/appbackup/dbdump.go @@ -764,7 +764,8 @@ func getMariaDBPassword(ctx context.Context, containerID string) string { // - else known[containerName] → containerName (the container name IS the stack — don't strip, e.g. my-cache). // - else longest known prefix → handles _postgres / -1 / compose-suffixed names. // - else → candidate (fall back to today's suffix-strip; preserves behaviour when -// the stack list is empty/unavailable, so nothing regresses). +// the stack list is empty/unavailable, so nothing regresses). +// // A nil/empty `known` map = the legacy fast path (pure suffix-strip). func deriveStackName(containerName string, known map[string]bool) string { candidate := suffixStripStackName(containerName) diff --git a/controller/internal/appexport/crypto.go b/controller/internal/appexport/crypto.go index 59f46bd..efd4158 100644 --- a/controller/internal/appexport/crypto.go +++ b/controller/internal/appexport/crypto.go @@ -15,8 +15,8 @@ import ( ) const ( - magicHeader = "FABE" // Felhom App Bundle Encrypted - scryptN = 1 << 15 // 32768 + magicHeader = "FABE" // Felhom App Bundle Encrypted + scryptN = 1 << 15 // 32768 scryptR = 8 scryptP = 1 saltSize = 32 diff --git a/controller/internal/appexport/export.go b/controller/internal/appexport/export.go index f4ea8f3..165779b 100644 --- a/controller/internal/appexport/export.go +++ b/controller/internal/appexport/export.go @@ -468,8 +468,8 @@ func (e *Exporter) GetDebugInfo() map[string]interface{} { defer e.mu.Unlock() info := map[string]interface{}{ - "debug_enabled": e.debug, - "version": e.version, + "debug_enabled": e.debug, + "version": e.version, "has_active_job": e.activeJob != nil, } diff --git a/controller/internal/appexport/export_additive_test.go b/controller/internal/appexport/export_additive_test.go index a63a619..ea506ee 100644 --- a/controller/internal/appexport/export_additive_test.go +++ b/controller/internal/appexport/export_additive_test.go @@ -189,7 +189,7 @@ func TestExport_HDDMountBasenameCollisionFailsLoud(t *testing.T) { prov := &hddProvider{ rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true}, - mounts: []string{a, b}, hddPath: hdd, + mounts: []string{a, b}, hddPath: hdd, } drive := t.TempDir() e := NewExporter(prov, log.New(io.Discard, "", 0), "test") @@ -229,7 +229,7 @@ func TestFabRoundTrip_UserdataPlacement(t *testing.T) { prov := &hddProvider{ rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true}, - mounts: []string{ud}, hddPath: hdd, + mounts: []string{ud}, hddPath: hdd, } drive := t.TempDir() e := NewExporter(prov, lg, "test") @@ -249,7 +249,7 @@ func TestFabRoundTrip_UserdataPlacement(t *testing.T) { prov2 := &hddProvider{ rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: false}, - hddPath: hdd, + hddPath: hdd, } e2 := NewExporter(prov2, lg, "test") if err := e2.StartImport(ImportRequest{FABPath: fabPath}); err != nil { diff --git a/controller/internal/appexport/fabplan_test.go b/controller/internal/appexport/fabplan_test.go index 6d249e4..403581f 100644 --- a/controller/internal/appexport/fabplan_test.go +++ b/controller/internal/appexport/fabplan_test.go @@ -22,7 +22,7 @@ type fabProv struct { mounts []string } -func (p *fabProv) GetStackHDDPath(string) string { return p.hddPath } +func (p *fabProv) GetStackHDDPath(string) string { return p.hddPath } func (p *fabProv) GetStackHDDMounts(string) []string { return p.mounts } func (p *fabProv) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) { return p.binds, p.has diff --git a/controller/internal/appexport/roundtrip_test.go b/controller/internal/appexport/roundtrip_test.go index 0b49864..54c8c7d 100644 --- a/controller/internal/appexport/roundtrip_test.go +++ b/controller/internal/appexport/roundtrip_test.go @@ -35,22 +35,22 @@ func (p *rtProvider) GetStackDir(string) (string, bool) { return p.stackDir, tru func (p *rtProvider) GetStackComposePath(string) (string, bool) { return filepath.Join(p.stackDir, "docker-compose.yml"), true } -func (p *rtProvider) GetStackHDDMounts(string) []string { return nil } -func (p *rtProvider) GetStackHDDPath(string) string { return "" } +func (p *rtProvider) GetStackHDDMounts(string) []string { return nil } +func (p *rtProvider) GetStackHDDPath(string) string { return "" } func (p *rtProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) { return nil, false } -func (p *rtProvider) IsStackRunning(string) bool { return p.running } -func (p *rtProvider) StopStack(string) error { p.stopped++; return nil } -func (p *rtProvider) StartStack(string) error { p.started = true; return nil } -func (p *rtProvider) GetStackDisplayName(n string) string { return "RT " + n } -func (p *rtProvider) GetStackNeedsHDD(string) bool { return false } -func (p *rtProvider) GetDockerVolumes(string) []string { return p.volumes } -func (p *rtProvider) IsStackDeployed(string) bool { return p.deployed } -func (p *rtProvider) GetDecryptedEnv(string) map[string]string { return nil } -func (p *rtProvider) GetStacksBaseDir() string { return p.stacksDir } -func (p *rtProvider) RefreshStacks() error { return nil } -func (p *rtProvider) RemoveStackVolumes(string) error { p.removed++; return nil } +func (p *rtProvider) IsStackRunning(string) bool { return p.running } +func (p *rtProvider) StopStack(string) error { p.stopped++; return nil } +func (p *rtProvider) StartStack(string) error { p.started = true; return nil } +func (p *rtProvider) GetStackDisplayName(n string) string { return "RT " + n } +func (p *rtProvider) GetStackNeedsHDD(string) bool { return false } +func (p *rtProvider) GetDockerVolumes(string) []string { return p.volumes } +func (p *rtProvider) IsStackDeployed(string) bool { return p.deployed } +func (p *rtProvider) GetDecryptedEnv(string) map[string]string { return nil } +func (p *rtProvider) GetStacksBaseDir() string { return p.stacksDir } +func (p *rtProvider) RefreshStacks() error { return nil } +func (p *rtProvider) RemoveStackVolumes(string) error { p.removed++; return nil } func (p *rtProvider) SaveEncryptedAppConfig(stackDir string, env map[string]string) error { p.savedEnv = env return nil diff --git a/controller/internal/backup/recovery_unit.go b/controller/internal/backup/recovery_unit.go index 3544089..761714e 100644 --- a/controller/internal/backup/recovery_unit.go +++ b/controller/internal/backup/recovery_unit.go @@ -18,10 +18,11 @@ import ( // RecoveryManifest describes an app's self-contained, SECRET-FREE recovery unit (Phase 2). // // The unit on a drive is `/backups/primary//` and contains: -// compose/ docker-compose.yml + .felhom.yml + a SECRET-STRIPPED app.yaml -// db-dumps/ app-consistent DB dump(s) (written by the dump flow) -// volume-dumps/ named-volume tars (written by the dump flow) -// manifest.json this file +// +// compose/ docker-compose.yml + .felhom.yml + a SECRET-STRIPPED app.yaml +// db-dumps/ app-consistent DB dump(s) (written by the dump flow) +// volume-dumps/ named-volume tars (written by the dump flow) +// manifest.json this file // // The unit holds NO secret values, NO data-encrypting keys, and NOT the Docker image — only the // pinned image tag(s) (re-pulled on restore) and the NAMES of the secret/data-key env vars. The diff --git a/controller/internal/backup/tier2.go b/controller/internal/backup/tier2.go index 05d3f3e..3b2e11a 100644 --- a/controller/internal/backup/tier2.go +++ b/controller/internal/backup/tier2.go @@ -187,10 +187,12 @@ func (m *Manager) tier2FitsSystemDrive(sys string, unitSizeBytes int64) bool { } // Tier-2 v2 layout (Task 3b, architecture §8): backups/secondary// holds -// .felhom-tier2-layout (marker file, content "2" — written LAST, after all legs + reconcile) -// recovery-unit/ (the unit leg, layout-identical to v1) -// hdd// (per-bind HDD legs, relpath-mirroring) -// userdata// (per-bind USERDATA legs) +// +// .felhom-tier2-layout (marker file, content "2" — written LAST, after all legs + reconcile) +// recovery-unit/ (the unit leg, layout-identical to v1) +// hdd// (per-bind HDD legs, relpath-mirroring) +// userdata// (per-bind USERDATA legs) +// // Relpath-mirroring represents N>1 dirs + nested binds natively (the v1 flat-appdata N>1 refusal is // lifted structurally) and makes restore position-derivable (dest relpath → live path under the app's // current HDD_PATH). The whole tree is DERIVED from live data — migration is delete-and-rebuild. diff --git a/controller/internal/config/config.go b/controller/internal/config/config.go index 4549f6a..e52d59e 100644 --- a/controller/internal/config/config.go +++ b/controller/internal/config/config.go @@ -17,7 +17,7 @@ import ( // is fetched once via the hub consume endpoint (never in config). Mirrors hub offsite.Descriptor. type OffsiteConfig struct { Enabled bool `yaml:"enabled"` - Type string `yaml:"type"` // "shared" | "dedicated" + Type string `yaml:"type"` // "shared" | "dedicated" Host string `yaml:"host"` User string `yaml:"user"` Port int `yaml:"port"` // 23 diff --git a/controller/internal/integrations/integrations.go b/controller/internal/integrations/integrations.go index e03632a..9ff52c7 100644 --- a/controller/internal/integrations/integrations.go +++ b/controller/internal/integrations/integrations.go @@ -44,13 +44,13 @@ type Handler interface { // StatusInfo is returned by the API for UI display. type StatusInfo struct { - Key string `json:"key"` // "onlyoffice:filebrowser" + Key string `json:"key"` // "onlyoffice:filebrowser" Provider string `json:"provider"` Target string `json:"target"` Label string `json:"label"` Description string `json:"description"` Enabled bool `json:"enabled"` - Status string `json:"status"` // "active", "error", "disabled", "provider_stopped", "target_unavailable" + Status string `json:"status"` // "active", "error", "disabled", "provider_stopped", "target_unavailable" LastError string `json:"last_error,omitempty"` TargetDeployed bool `json:"target_deployed"` TargetRunning bool `json:"target_running"` diff --git a/controller/internal/integrations/lifecycle.go b/controller/internal/integrations/lifecycle.go index af0e254..164e9c4 100644 --- a/controller/internal/integrations/lifecycle.go +++ b/controller/internal/integrations/lifecycle.go @@ -101,7 +101,12 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) { provStack, pOk := m.stacks.GetStack(provider) if !pOk || !provStack.Deployed || !isStackUp(provStack.State) { if m.isDebug() { - m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — provider %s not up (found=%v deployed=%v state=%v)", key, provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { if pOk { return provStack.State }; return "" }()) + m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — provider %s not up (found=%v deployed=%v state=%v)", key, provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { + if pOk { + return provStack.State + } + return "" + }()) } continue } @@ -109,7 +114,12 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) { tgtStack, tOk := m.stacks.GetStack(target) if !tOk || !tgtStack.Deployed || !isStackUp(tgtStack.State) { if m.isDebug() { - m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — target %s not up (found=%v deployed=%v state=%v)", key, target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { if tOk { return tgtStack.State }; return "" }()) + m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — target %s not up (found=%v deployed=%v state=%v)", key, target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { + if tOk { + return tgtStack.State + } + return "" + }()) } continue } diff --git a/controller/internal/integrations/manager.go b/controller/internal/integrations/manager.go index a492434..7dfea3f 100644 --- a/controller/internal/integrations/manager.go +++ b/controller/internal/integrations/manager.go @@ -100,7 +100,12 @@ func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bo // Validate: provider must be deployed and running provStack, pOk := m.stacks.GetStack(provider) if m.isDebug() { - m.logger.Printf("[DEBUG] [integrations] Toggle: provider %s found=%v deployed=%v state=%v", provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { if pOk { return provStack.State }; return "" }()) + m.logger.Printf("[DEBUG] [integrations] Toggle: provider %s found=%v deployed=%v state=%v", provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { + if pOk { + return provStack.State + } + return "" + }()) } if !pOk || !provStack.Deployed { return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nincs telepítve", provider) @@ -113,7 +118,12 @@ func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bo if target != "filebrowser" { tgtStack, tOk := m.stacks.GetStack(target) if m.isDebug() { - m.logger.Printf("[DEBUG] [integrations] Toggle: target %s found=%v deployed=%v state=%v", target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { if tOk { return tgtStack.State }; return "" }()) + m.logger.Printf("[DEBUG] [integrations] Toggle: target %s found=%v deployed=%v state=%v", target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { + if tOk { + return tgtStack.State + } + return "" + }()) } if !tOk || !tgtStack.Deployed { return state, fmt.Errorf("a célalkalmazás (%s) nincs telepítve", target) diff --git a/controller/internal/integrations/onlyoffice_nextcloud.go b/controller/internal/integrations/onlyoffice_nextcloud.go index df65681..ac7ce5a 100644 --- a/controller/internal/integrations/onlyoffice_nextcloud.go +++ b/controller/internal/integrations/onlyoffice_nextcloud.go @@ -32,7 +32,7 @@ func (h *OnlyOfficeNextcloudHandler) Apply(ac *ApplyContext) error { // Install and configure OnlyOffice app in Nextcloud commands := []struct { - args []string + args []string tolerate string // substring in output to tolerate as success }{ { diff --git a/controller/internal/report/builder.go b/controller/internal/report/builder.go index 6aed1bc..536e856 100644 --- a/controller/internal/report/builder.go +++ b/controller/internal/report/builder.go @@ -321,4 +321,3 @@ func buildStacksReport(stackMgr *stacks.Manager) StacksReport { return sr } - diff --git a/controller/internal/report/config_refresh.go b/controller/internal/report/config_refresh.go index 11d9e78..8c86929 100644 --- a/controller/internal/report/config_refresh.go +++ b/controller/internal/report/config_refresh.go @@ -12,7 +12,7 @@ import "log" // - Applied reads the persisted last-applied config_version (0 = none recorded yet). // - Record persists a newly-applied config_version. // - Refresh re-pulls controller.yaml from the hub and writes it (re-merging local_api). It must -// NOT touch settings.json. A hub-unreachable / write failure returns an error. +// NOT touch settings.json. A hub-unreachable / write failure returns an error. // - Restart triggers the graceful self-restart (process exit → Docker restart → fresh config). type ConfigRefresher struct { Applied func() int diff --git a/controller/internal/report/escrow_confirm_test.go b/controller/internal/report/escrow_confirm_test.go index 6ea6719..544c1e3 100644 --- a/controller/internal/report/escrow_confirm_test.go +++ b/controller/internal/report/escrow_confirm_test.go @@ -86,9 +86,9 @@ func TestEscrowConfirm_StaleBlobStaysPending(t *testing.T) { // Scenario C — no status / hash-less blob / no local password file → stay pending SILENTLY. func TestEscrowConfirm_SilentPendingCases(t *testing.T) { h := newConfirmer(t) - h.c.Reconcile(nil) // no escrow row on the hub - h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: true}) // hash NULL (legacy blob) - h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: false, ResticPwSHA256: hubHash}) // hash without identity blob — fail-closed + h.c.Reconcile(nil) // no escrow row on the hub + h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: true}) // hash NULL (legacy blob) + h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: false, ResticPwSHA256: hubHash}) // hash without identity blob — fail-closed h.localOK = false h.c.Reconcile(matchStatus(hubHash)) // no local password file if h.flips != 0 || h.logbuf.Len() != 0 { diff --git a/controller/internal/report/types.go b/controller/internal/report/types.go index 9c318ed..c5445d8 100644 --- a/controller/internal/report/types.go +++ b/controller/internal/report/types.go @@ -9,22 +9,22 @@ import ( // Report is the JSON payload pushed to the central hub. type Report struct { - Version int `json:"version"` - CustomerID string `json:"customer_id"` - CustomerName string `json:"customer_name"` - ControllerVersion string `json:"controller_version"` - ControllerURL string `json:"controller_url,omitempty"` - ConfigHash string `json:"config_hash,omitempty"` - Timestamp time.Time `json:"timestamp"` - ReportingDisabled bool `json:"reporting_disabled,omitempty"` - System SystemReport `json:"system"` - Storage []StorageReport `json:"storage"` - Containers ContainerReport `json:"containers"` - Backup BackupReport `json:"backup"` - Health HealthReport `json:"health"` - Stacks StacksReport `json:"stacks"` - AppTelemetry []AppTelemetry `json:"app_telemetry,omitempty"` - GeoRestriction *GeoRestrictionReport `json:"geo_restriction,omitempty"` + Version int `json:"version"` + CustomerID string `json:"customer_id"` + CustomerName string `json:"customer_name"` + ControllerVersion string `json:"controller_version"` + ControllerURL string `json:"controller_url,omitempty"` + ConfigHash string `json:"config_hash,omitempty"` + Timestamp time.Time `json:"timestamp"` + ReportingDisabled bool `json:"reporting_disabled,omitempty"` + System SystemReport `json:"system"` + Storage []StorageReport `json:"storage"` + Containers ContainerReport `json:"containers"` + Backup BackupReport `json:"backup"` + Health HealthReport `json:"health"` + Stacks StacksReport `json:"stacks"` + AppTelemetry []AppTelemetry `json:"app_telemetry,omitempty"` + GeoRestriction *GeoRestrictionReport `json:"geo_restriction,omitempty"` // DR recipe — the controller (customer + apps) half of the secret-free reconstruction recipe // (SPIKE-dr-recipe-2026-06-16). The hub assembles it with the agent's storage/guest/PBS half. diff --git a/controller/internal/selftest/selftest.go b/controller/internal/selftest/selftest.go index 9713932..e52a097 100644 --- a/controller/internal/selftest/selftest.go +++ b/controller/internal/selftest/selftest.go @@ -19,7 +19,7 @@ import ( // CheckResult represents the outcome of a single self-test check. type CheckResult struct { Name string `json:"name"` - Status string `json:"status"` // "pass", "warn", "fail" + Status string `json:"status"` // "pass", "warn", "fail" Message string `json:"message"` } diff --git a/controller/internal/selfupdate/state.go b/controller/internal/selfupdate/state.go index a2c41ce..fff69c9 100644 --- a/controller/internal/selfupdate/state.go +++ b/controller/internal/selfupdate/state.go @@ -12,13 +12,13 @@ const stateFileName = "update-state.json" // UpdateState tracks the last update attempt. Persisted to disk as audit log. type UpdateState struct { - Status string `json:"status"` // "pending", "success", "failed" + Status string `json:"status"` // "pending", "success", "failed" PreviousVersion string `json:"previous_version"` PreviousImage string `json:"previous_image"` TargetVersion string `json:"target_version"` TargetImage string `json:"target_image"` - InitiatedAt string `json:"initiated_at"` // RFC3339 - InitiatedBy string `json:"initiated_by"` // "manual" or "auto" + InitiatedAt string `json:"initiated_at"` // RFC3339 + InitiatedBy string `json:"initiated_by"` // "manual" or "auto" CompletedAt string `json:"completed_at,omitempty"` Error string `json:"error,omitempty"` } diff --git a/controller/internal/selfupdate/updater.go b/controller/internal/selfupdate/updater.go index bca99bb..a92343c 100644 --- a/controller/internal/selfupdate/updater.go +++ b/controller/internal/selfupdate/updater.go @@ -454,9 +454,9 @@ type DryRunResult struct { CurrentVersion string `json:"current_version"` LatestVersion string `json:"latest_version"` UpdateAvailable bool `json:"update_available"` - AgentReachable bool `json:"agent_reachable"` // the host agent (which performs the swap) is wired - PullCapable bool `json:"pull_capable"` // in-guest pull path available (full creds OR anonymous; false = half-configured creds) - TargetImage string `json:"target_image"` // what we would pull + swap to + AgentReachable bool `json:"agent_reachable"` // the host agent (which performs the swap) is wired + PullCapable bool `json:"pull_capable"` // in-guest pull path available (full creds OR anonymous; false = half-configured creds) + TargetImage string `json:"target_image"` // what we would pull + swap to BackupRunning bool `json:"backup_running"` Error string `json:"error,omitempty"` } @@ -842,4 +842,3 @@ var runCommandStdin = func(stdin, name string, args ...string) (string, error) { err := cmd.Run() return out.String(), err } - diff --git a/controller/internal/settings/smb.go b/controller/internal/settings/smb.go index 6306a9c..62bb2ff 100644 --- a/controller/internal/settings/smb.go +++ b/controller/internal/settings/smb.go @@ -25,11 +25,11 @@ type SMBSettings struct { // SMBShare is one exported folder. Path is an absolute host path under a registered storage root // (validated by the web layer against the storage registry + deny-list before it ever reaches here). type SMBShare struct { - Name string `json:"name"` // share name (NetBIOS-safe, ≤15); the [section] in smb.conf and the \\SERVER\ path - Path string `json:"path"` // absolute host path + Name string `json:"name"` // share name (NetBIOS-safe, ≤15); the [section] in smb.conf and the \\SERVER\ path + Path string `json:"path"` // absolute host path ReadOnly bool `json:"read_only,omitempty"` // smb.conf `read only = yes` + a :ro compose bind - Offsite bool `json:"offsite"` // [R4] true (default) → backup class mandatory; false → optional (tier-2 only) - CreatedAt string `json:"created_at"` // RFC3339 + Offsite bool `json:"offsite"` // [R4] true (default) → backup class mandatory; false → optional (tier-2 only) + CreatedAt string `json:"created_at"` // RFC3339 } // nbNameRe matches a NetBIOS-safe name: 1–15 chars, letters/digits/hyphen/underscore, not starting @@ -63,7 +63,7 @@ func ValidateSMBShareName(name string) error { if len(name) > 15 { return fmt.Errorf("a megosztás neve legfeljebb 15 karakter lehet") } - if strings.ContainsAny(name, `/\.` ) { + if strings.ContainsAny(name, `/\.`) { return fmt.Errorf("a megosztás neve nem tartalmazhat perjelet vagy pontot") } // RESERVED NAMESPACE (R-7b). The backup engines key the shares source by the pseudo-stack „_shares" diff --git a/controller/internal/setup/setup.go b/controller/internal/setup/setup.go index f13b704..7f3f0c0 100644 --- a/controller/internal/setup/setup.go +++ b/controller/internal/setup/setup.go @@ -43,8 +43,8 @@ type SetupState struct { // SelectedBackup tracks which backup the user chose. type SelectedBackup struct { - Source string `json:"source"` // "local" or "hub" - DrivePath string `json:"drive_path"` // for local + Source string `json:"source"` // "local" or "hub" + DrivePath string `json:"drive_path"` // for local CustomerID string `json:"customer_id"` Timestamp string `json:"timestamp"` } diff --git a/controller/internal/stacks/lifecycle_orphan_test.go b/controller/internal/stacks/lifecycle_orphan_test.go index 33627b5..a4f990a 100644 --- a/controller/internal/stacks/lifecycle_orphan_test.go +++ b/controller/internal/stacks/lifecycle_orphan_test.go @@ -41,9 +41,9 @@ func TestCatalogTemplateSlugs_IgnoresLifecycle(t *testing.T) { t.Fatal(err) } } - mk("bookstack", "") // available - mk("plant-it", "abandoned") // withdrawn, but STILL IN THE CATALOG TREE - mk("someapp", "hidden") // withdrawn, ditto + mk("bookstack", "") // available + mk("plant-it", "abandoned") // withdrawn, but STILL IN THE CATALOG TREE + mk("someapp", "hidden") // withdrawn, ditto m := &Manager{ cfg: &config.Config{Paths: config.PathsConfig{DataDir: dataDir}}, diff --git a/controller/internal/stacks/userdata_belt_gate_test.go b/controller/internal/stacks/userdata_belt_gate_test.go index 06abf47..e3ebb50 100644 --- a/controller/internal/stacks/userdata_belt_gate_test.go +++ b/controller/internal/stacks/userdata_belt_gate_test.go @@ -10,8 +10,8 @@ import ( // Creating ${USERDATA_PATH}/... then would land app data on the rootfs, shadowed when the drive returns. // Companion red-proof: removing the gate in ensureUserdataMounts makes the dir get created → this fails. func TestEnsureUserdataMounts_SkipsAbsentExternalDrive(t *testing.T) { - m := newMigManager(t, "") // sysDataPath = /mnt/sys_drive - m.isMountPoint = func(string) bool { return false } // external drive is NOT mounted + m := newMigManager(t, "") // sysDataPath = /mnt/sys_drive + m.isMountPoint = func(string) bool { return false } // external drive is NOT mounted stackDir := t.TempDir() if err := os.WriteFile(filepath.Join(stackDir, "docker-compose.yml"), []byte(beltCompose), 0o644); err != nil { t.Fatal(err) diff --git a/controller/internal/sync/sync.go b/controller/internal/sync/sync.go index ace9db0..2120e98 100644 --- a/controller/internal/sync/sync.go +++ b/controller/internal/sync/sync.go @@ -51,10 +51,10 @@ type SyncStatus struct { // SyncResult holds the result of a single sync operation. type SyncResult struct { - OK bool `json:"ok"` - NewApps []string `json:"new_apps,omitempty"` - Updated []string `json:"updated,omitempty"` - Message string `json:"message"` + OK bool `json:"ok"` + NewApps []string `json:"new_apps,omitempty"` + Updated []string `json:"updated,omitempty"` + Message string `json:"message"` } // New creates a new Syncer. rescanFn is called after a successful sync to trigger ScanStacks(). diff --git a/controller/internal/system/info.go b/controller/internal/system/info.go index 5fe28e4..bc55eba 100644 --- a/controller/internal/system/info.go +++ b/controller/internal/system/info.go @@ -16,10 +16,10 @@ func debugf(format string, args ...any) { // SystemInfo holds system resource usage information. type SystemInfo struct { - TotalMemMB uint64 `json:"total_mem_mb"` - UsedMemMB uint64 `json:"used_mem_mb"` - AvailMemMB uint64 `json:"avail_mem_mb"` - MemPercent float64 `json:"mem_percent"` + TotalMemMB uint64 `json:"total_mem_mb"` + UsedMemMB uint64 `json:"used_mem_mb"` + AvailMemMB uint64 `json:"avail_mem_mb"` + MemPercent float64 `json:"mem_percent"` DiskTotalGB float64 `json:"disk_total_gb"` DiskUsedGB float64 `json:"disk_used_gb"` diff --git a/controller/internal/system/mounts_linux.go b/controller/internal/system/mounts_linux.go index 875da34..18bcac5 100644 --- a/controller/internal/system/mounts_linux.go +++ b/controller/internal/system/mounts_linux.go @@ -276,7 +276,7 @@ func diskModel(device string) string { type ProbeStatus int const ( - ProbeConnected ProbeStatus = iota + ProbeConnected ProbeStatus = iota ProbeDisconnected ProbeTimeout ) diff --git a/controller/internal/system/mounts_other.go b/controller/internal/system/mounts_other.go index 64d2ed7..a1d2a94 100644 --- a/controller/internal/system/mounts_other.go +++ b/controller/internal/system/mounts_other.go @@ -88,7 +88,7 @@ func CheckBackupDestination(path string) DestinationHealth { type ProbeStatus int const ( - ProbeConnected ProbeStatus = iota + ProbeConnected ProbeStatus = iota ProbeDisconnected ProbeTimeout ) diff --git a/controller/internal/web/app_export_domain_test.go b/controller/internal/web/app_export_domain_test.go index 1c9a524..e00a5a2 100644 --- a/controller/internal/web/app_export_domain_test.go +++ b/controller/internal/web/app_export_domain_test.go @@ -59,8 +59,8 @@ func TestAppExportDomainUsesCustomerDomainNotCSRFToken(t *testing.T) { } } -// The empty-subdomain branch still yields '' — an app without a subdomain must not get a link to -// a bare domain (the truthiness guard is what produces that, and the fix must not disturb it). +// The empty-subdomain branch still yields the empty string — an app without a subdomain must not get +// a link to a bare domain (the truthiness guard is what produces that, and the fix must not disturb it). func TestAppExportDomainEmptyWithoutSubdomain(t *testing.T) { line := exportScriptLine(t, renderExport(t, "")) diff --git a/controller/internal/web/backup_page_state_test.go b/controller/internal/web/backup_page_state_test.go index 768a018..610a4cf 100644 --- a/controller/internal/web/backup_page_state_test.go +++ b/controller/internal/web/backup_page_state_test.go @@ -236,11 +236,11 @@ func TestBackupsTemplate_Tier3Live(t *testing.T) { html := renderBackupPage(t, "backups_apps", data) for _, want := range []string{ - "Sikeres", // active badge (LastStatus ok) + "Sikeres", // active badge (LastStatus ok) "restic → u629488-sub1.your-storagebox.de", // the real off-box host - "Utolsó:", // a relative last-run time is shown - "Kikapcsolva", // radarr off row - "Bekapcsolás", // radarr enable link + "Utolsó:", // a relative last-run time is shown + "Kikapcsolva", // radarr off row + "Bekapcsolás", // radarr enable link `href="/backups/remote#offbox-section"`, // the cross-page anchor jump target (IA split) } { if !strings.Contains(html, want) { @@ -257,13 +257,13 @@ func TestBackupsTemplate_Tier3Live(t *testing.T) { } } for _, banned := range []string{ - "hamarosan", // the dead placeholder copy - "Tier2DriveGroups", // dead template field - "ResticPassword", // dead template field - "restic-pw", // dead element id - "toggleTier", // dead JS fn - "details-tier", // dead Részletek markup - "Részletek", // the removed card title + "hamarosan", // the dead placeholder copy + "Tier2DriveGroups", // dead template field + "ResticPassword", // dead template field + "restic-pw", // dead element id + "toggleTier", // dead JS fn + "details-tier", // dead Részletek markup + "Részletek", // the removed card title "2026-07-11T02:00:00Z", // raw RFC3339 must never leak (timeAgoStr wraps it) } { if strings.Contains(html, banned) { diff --git a/controller/internal/web/backups_split_test.go b/controller/internal/web/backups_split_test.go index d0fb907..22330de 100644 --- a/controller/internal/web/backups_split_test.go +++ b/controller/internal/web/backups_split_test.go @@ -43,18 +43,18 @@ func TestBackupsSplit_SectionsOnExactlyOnePage(t *testing.T) { // marker → the ONE page it belongs to markers := map[string]string{ - "Tárhely áttekintés": "backups", // Section 0 - "Rendszermentés (teljes mentés)": "backups", // whole-guest - ">Adatmentés<": "backups", // stat cards (neutral branch) - `id="offbox-section"`: "backups_remote", // the offbox anchor target - "Távoli mentési cél beállítása": "backups_remote", // manual-target form - "Mely alkalmazások mentődnek": "backups_remote", // toggle list - "

Ütemezés

": "backups_apps", // schedule - "

Adatbázisok

": "backups_apps", // databases - "Alkalmazások mentési állapota": "backups_apps", // per-app rows - `id="restore-app"`: "backups_restore", // restore panel - "Visszaállítás a távoli tárolóból": "backups_restore", // the offsite restore list - `href="/backups/restore/app?name=`: "backups_restore", // R-48: the ONE entry per app + "Tárhely áttekintés": "backups", // Section 0 + "Rendszermentés (teljes mentés)": "backups", // whole-guest + ">Adatmentés<": "backups", // stat cards (neutral branch) + `id="offbox-section"`: "backups_remote", // the offbox anchor target + "Távoli mentési cél beállítása": "backups_remote", // manual-target form + "Mely alkalmazások mentődnek": "backups_remote", // toggle list + "

Ütemezés

": "backups_apps", // schedule + "

Adatbázisok

": "backups_apps", // databases + "Alkalmazások mentési állapota": "backups_apps", // per-app rows + `id="restore-app"`: "backups_restore", // restore panel + "Visszaállítás a távoli tárolóból": "backups_restore", // the offsite restore list + `href="/backups/restore/app?name=`: "backups_restore", // R-48: the ONE entry per app } for marker, home := range markers { for page, html := range pages { diff --git a/controller/internal/web/escrow_handlers.go b/controller/internal/web/escrow_handlers.go index 585d1fc..2200147 100644 --- a/controller/internal/web/escrow_handlers.go +++ b/controller/internal/web/escrow_handlers.go @@ -152,7 +152,7 @@ func (s *Server) escrowPreflightAPIHandler(w http.ResponseWriter, r *http.Reques agentOK := escrowAgentSupported(agent.AgentVersion()) escrowJSON(w, http.StatusOK, map[string]any{ "ok": pf.OK && agentOK, "items": pf.Items, - "agent_supported": agentOK, + "agent_supported": agentOK, "offbox_configured": s.backupMgr != nil && s.backupMgr.OffboxConfigured(), "escrow_state": escrowState, "stale": s.escrowStale(), diff --git a/controller/internal/web/export_download_test.go b/controller/internal/web/export_download_test.go index ad29407..576f02b 100644 --- a/controller/internal/web/export_download_test.go +++ b/controller/internal/web/export_download_test.go @@ -50,13 +50,13 @@ func TestFabDownload_TraversalGuardAndCleanup(t *testing.T) { } refused := []string{ - "..%2Fdecoy.fab", // ../decoy.fab - "..%5Cdecoy.fab", // ..\decoy.fab - "%2Fetc%2Fpasswd", // absolute path - "sub%2Fx.fab", // separator - "..", // bare traversal - ".hidden.fab", // not the exporter's naming (leading dot) - "x.txt", // not a .fab + "..%2Fdecoy.fab", // ../decoy.fab + "..%5Cdecoy.fab", // ..\decoy.fab + "%2Fetc%2Fpasswd", // absolute path + "sub%2Fx.fab", // separator + "..", // bare traversal + ".hidden.fab", // not the exporter's naming (leading dot) + "x.txt", // not a .fab } for _, f := range refused { if rr := fetchDownload(s, f); rr.Code != http.StatusBadRequest { diff --git a/controller/internal/web/fab_export_test.go b/controller/internal/web/fab_export_test.go index d8eda50..9657693 100644 --- a/controller/internal/web/fab_export_test.go +++ b/controller/internal/web/fab_export_test.go @@ -29,23 +29,25 @@ func (p *fabWebProvider) GetStackDir(string) (string, bool) { return p.stackDir, func (p *fabWebProvider) GetStackComposePath(string) (string, bool) { return filepath.Join(p.stackDir, "docker-compose.yml"), true } -func (p *fabWebProvider) GetStackHDDMounts(string) []string { return []string{appbackup.UserdataDir(p.hddPath)} } -func (p *fabWebProvider) GetStackHDDPath(string) string { return p.hddPath } +func (p *fabWebProvider) GetStackHDDMounts(string) []string { + return []string{appbackup.UserdataDir(p.hddPath)} +} +func (p *fabWebProvider) GetStackHDDPath(string) string { return p.hddPath } func (p *fabWebProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) { return p.binds, true } -func (p *fabWebProvider) IsStackRunning(string) bool { return false } -func (p *fabWebProvider) StopStack(string) error { return nil } -func (p *fabWebProvider) StartStack(string) error { return nil } -func (p *fabWebProvider) GetStackDisplayName(n string) string { return n } -func (p *fabWebProvider) GetStackNeedsHDD(string) bool { return true } -func (p *fabWebProvider) GetDockerVolumes(string) []string { return nil } -func (p *fabWebProvider) IsStackDeployed(string) bool { return true } -func (p *fabWebProvider) GetDecryptedEnv(string) map[string]string { return nil } -func (p *fabWebProvider) GetStacksBaseDir() string { return p.stacksDir } +func (p *fabWebProvider) IsStackRunning(string) bool { return false } +func (p *fabWebProvider) StopStack(string) error { return nil } +func (p *fabWebProvider) StartStack(string) error { return nil } +func (p *fabWebProvider) GetStackDisplayName(n string) string { return n } +func (p *fabWebProvider) GetStackNeedsHDD(string) bool { return true } +func (p *fabWebProvider) GetDockerVolumes(string) []string { return nil } +func (p *fabWebProvider) IsStackDeployed(string) bool { return true } +func (p *fabWebProvider) GetDecryptedEnv(string) map[string]string { return nil } +func (p *fabWebProvider) GetStacksBaseDir() string { return p.stacksDir } func (p *fabWebProvider) SaveEncryptedAppConfig(string, map[string]string) error { return nil } -func (p *fabWebProvider) RefreshStacks() error { return nil } -func (p *fabWebProvider) RemoveStackVolumes(string) error { return nil } +func (p *fabWebProvider) RefreshStacks() error { return nil } +func (p *fabWebProvider) RemoveStackVolumes(string) error { return nil } // waitExportDone polls the exporter until the active job finishes. func waitExportDone(t *testing.T, e *appexport.Exporter) { diff --git a/controller/internal/web/filebrowser_gate_test.go b/controller/internal/web/filebrowser_gate_test.go index aabda27..aa01035 100644 --- a/controller/internal/web/filebrowser_gate_test.go +++ b/controller/internal/web/filebrowser_gate_test.go @@ -13,8 +13,8 @@ func TestSkipFileBrowserPath(t *testing.T) { path string want bool }{ - {StableParentDir + "/flash", false}, // external + mounted → keep - {StableParentDir + "/usb", true}, // external + NOT mounted → skip + {StableParentDir + "/flash", false}, // external + mounted → keep + {StableParentDir + "/usb", true}, // external + NOT mounted → skip {"/mnt/sys_drive/felhom-data", false}, // system path (not under StableParentDir) → never skip } for _, c := range cases { @@ -34,9 +34,9 @@ func TestFbNeedsRecreate(t *testing.T) { compose := []byte("services:\n filebrowser:\n image: x\n") cases := []struct { - name string - oldCfg, newCfg, oldCmp, newCmp []byte - want bool + name string + oldCfg, newCfg, oldCmp, newCmp []byte + want bool }{ {"unchanged → no recreate", cfg, cfg, compose, compose, false}, {"config differs → recreate", cfg, []byte("sources:\n - /srv/hdd\n"), compose, compose, true}, diff --git a/controller/internal/web/funcmap.go b/controller/internal/web/funcmap.go index d6719d8..ba8ceb5 100644 --- a/controller/internal/web/funcmap.go +++ b/controller/internal/web/funcmap.go @@ -174,7 +174,7 @@ func (s *Server) templateFuncMap() template.FuncMap { }, // isOperational returns true for any state where the stack has containers // and is not stopped/exited — used by templates for showing action buttons - "isOperational": isOperationalState, + "isOperational": isOperationalState, "routeUnpublished": routeUnpublished, "logoURL": func(slug string) string { return s.cfg.AppLogoURL(slug) diff --git a/controller/internal/web/intermediary.go b/controller/internal/web/intermediary.go index b45040c..7d2f832 100644 --- a/controller/internal/web/intermediary.go +++ b/controller/internal/web/intermediary.go @@ -344,15 +344,15 @@ func (s *Server) driveGateLoop() { // retried → the app is stuck Exited forever even after the bind lands. The agent reports a `guest_boot_id` // that changes on every guest boot but is stable across a controller-only restart; the controller // persists the last-seen value. When it changes (the controller restarts WITH the guest), this: -// 1. gathers the deployed drive-backed apps' stable drive paths, -// 2. GATES on the REAL live in-guest bind — `pollLiveBinds`/`driveBindLive` wait (bounded) until each -// drive's stable path is an actual mountpoint in the controller's own /mnt (rslave) view, which is -// exactly when docker can recreate the app. (The old code sampled the agent's BoundUnderParent -// ONCE during fast startup, raced the ~18s rebind, recreated nothing, and persisted the boot-id — -// burning its one-shot. That is the bug this fixes.) -// 3. recreates every deployed drive-backed app whose bind is now live (`shouldRecreateOnBoot` is -// state-independent, so a stuck-Exited create-time-failure app is included), then persists the -// new boot-id. +// 1. gathers the deployed drive-backed apps' stable drive paths, +// 2. GATES on the REAL live in-guest bind — `pollLiveBinds`/`driveBindLive` wait (bounded) until each +// drive's stable path is an actual mountpoint in the controller's own /mnt (rslave) view, which is +// exactly when docker can recreate the app. (The old code sampled the agent's BoundUnderParent +// ONCE during fast startup, raced the ~18s rebind, recreated nothing, and persisted the boot-id — +// burning its one-shot. That is the bug this fixes.) +// 3. recreates every deployed drive-backed app whose bind is now live (`shouldRecreateOnBoot` is +// state-independent, so a stuck-Exited create-time-failure app is included), then persists the +// new boot-id. // // Apps on a drive that never goes live within the window are left to the normal gate (stop→return→ // restart). Single-flight (runs once, before the periodic gate, in driveGateLoop). Best-effort. diff --git a/controller/internal/web/intermediary_test.go b/controller/internal/web/intermediary_test.go index 32378d5..edf437c 100644 --- a/controller/internal/web/intermediary_test.go +++ b/controller/internal/web/intermediary_test.go @@ -204,7 +204,7 @@ func TestRecreateDriveBackedApps_SyncsFileBrowserAfterRecreate(t *testing.T) { flash := "/mnt/felhom-drives/felhom-flash" present := map[string]bool{flash: true} stacks := []bootStack{ - {name: "romm", deployed: true, hdd: flash, hasContainers: true}, // drive-backed, live → recreate + {name: "romm", deployed: true, hdd: flash, hasContainers: true}, // drive-backed, live → recreate {name: "actualbudget", deployed: true, hdd: "/mnt/sys_drive/felhom-data", hasContainers: true}, // SSD → not recreated {name: "stranded", deployed: true, hdd: "/mnt/felhom-drives/felhom-usb", hasContainers: true}, // drive-backed, bind NOT live → skipped } @@ -247,12 +247,12 @@ func TestStablePathForName(t *testing.T) { // gate. Both fail here. func TestPlanDriveGates(t *testing.T) { paths := []settings.StoragePath{ - {Path: "/mnt/felhom-drives/usb"}, // present + connected → no action - {Path: "/mnt/felhom-drives/flash"}, // ABSENT + connected → STOP - {Path: "/mnt/felhom-drives/back", Disconnected: true}, // present + disconnected → RETURN - {Path: "/mnt/felhom-drives/gone", Disconnected: true}, // ABSENT + disconnected → no action (steady) - {Path: "/mnt/felhom-drives/dead", Decommissioned: true}, // decommissioned → never touched - {Path: "/mnt/sys_drive/felhom-data"}, // INTERNAL SSD (absent from agent) → never gated + {Path: "/mnt/felhom-drives/usb"}, // present + connected → no action + {Path: "/mnt/felhom-drives/flash"}, // ABSENT + connected → STOP + {Path: "/mnt/felhom-drives/back", Disconnected: true}, // present + disconnected → RETURN + {Path: "/mnt/felhom-drives/gone", Disconnected: true}, // ABSENT + disconnected → no action (steady) + {Path: "/mnt/felhom-drives/dead", Decommissioned: true}, // decommissioned → never touched + {Path: "/mnt/sys_drive/felhom-data"}, // INTERNAL SSD (absent from agent) → never gated } disks := []agentapi.DiskInfo{ // present = BoundUnderParent (the usable-in-guest signal), not merely State==attached. diff --git a/controller/internal/web/lifecycle_test.go b/controller/internal/web/lifecycle_test.go index 4ad1093..4567890 100644 --- a/controller/internal/web/lifecycle_test.go +++ b/controller/internal/web/lifecycle_test.go @@ -24,13 +24,13 @@ func lcStack(name, lifecycle string, deployed bool) stacks.Stack { // withdrawing an app dangerous in the first place. func TestVisibleCatalogStacks(t *testing.T) { in := []stacks.Stack{ - lcStack("bookstack", "", false), // available, not deployed → OFFERED - lcStack("immich", "", true), // available, deployed → shown - lcStack("plant-it", "abandoned", false), // withdrawn, not deployed → HIDDEN - lcStack("plant-it-run", "abandoned", true),// withdrawn, DEPLOYED → shown - lcStack("oldapp", "hidden", false), // withdrawn, not deployed → HIDDEN - lcStack("oldapp-run", "hidden", true), // withdrawn, DEPLOYED → shown - lcStack("typoapp", "bogus", false), // unknown → available → OFFERED (fail-open) + lcStack("bookstack", "", false), // available, not deployed → OFFERED + lcStack("immich", "", true), // available, deployed → shown + lcStack("plant-it", "abandoned", false), // withdrawn, not deployed → HIDDEN + lcStack("plant-it-run", "abandoned", true), // withdrawn, DEPLOYED → shown + lcStack("oldapp", "hidden", false), // withdrawn, not deployed → HIDDEN + lcStack("oldapp-run", "hidden", true), // withdrawn, DEPLOYED → shown + lcStack("typoapp", "bogus", false), // unknown → available → OFFERED (fail-open) } got := map[string]bool{} for _, st := range visibleCatalogStacks(in) { diff --git a/controller/internal/web/mobile_nav_test.go b/controller/internal/web/mobile_nav_test.go index c05ccb0..2ecdf28 100644 --- a/controller/internal/web/mobile_nav_test.go +++ b/controller/internal/web/mobile_nav_test.go @@ -65,11 +65,11 @@ func TestMobileCSS_StripRemovedDrawerPresent(t *testing.T) { // The off-canvas drawer + no-JS fallback are present in the mobile block. for _, want := range []string{ - ".js .sidebar", // JS path: sidebar becomes an off-canvas drawer + ".js .sidebar", // JS path: sidebar becomes an off-canvas drawer "translateX(-100%)", // parked off-canvas until opened - ".no-js .sidebar", // no-JS path: sidebar renders static inline - "body.nav-open", // scroll lock while the drawer is open - ".mobile-topbar", // the sticky top bar is styled inside the mobile block + ".no-js .sidebar", // no-JS path: sidebar renders static inline + "body.nav-open", // scroll lock while the drawer is open + ".mobile-topbar", // the sticky top bar is styled inside the mobile block } { if !strings.Contains(block, want) { t.Errorf("768px block missing %q — the drawer/fallback rules are incomplete", want) @@ -82,7 +82,7 @@ func TestMobileCSS_StripRemovedDrawerPresent(t *testing.T) { t.Errorf("stylesheet missing base rule for %q", want) } } - if !strings.Contains(css, ".mobile-topbar { display: none") && + if !strings.Contains(css, ".mobile-topbar { display: none") && !strings.Contains(css, ".mobile-topbar{display:none") { t.Error("no desktop-default `.mobile-topbar { display:none }` — the top bar would leak onto desktop") } diff --git a/controller/internal/web/netstorage_gate_test.go b/controller/internal/web/netstorage_gate_test.go index d433cd5..6cc0156 100644 --- a/controller/internal/web/netstorage_gate_test.go +++ b/controller/internal/web/netstorage_gate_test.go @@ -49,7 +49,10 @@ func TestNetAddGate_OldAgent_RefusedUpFront(t *testing.T) { verifyErr: &agentapi.StatusError{Path: "/netstorage/verify-status", Code: http.StatusNotFound}, } s.netAgentFn = func() (netAgent, error) { return agent, nil } - s.netProbeFn = func(context.Context, string) probeOutcome { t.Error("probe must never run on a gated add"); return probeOutcome{} } + s.netProbeFn = func(context.Context, string) probeOutcome { + t.Error("probe must never run on a gated add") + return probeOutcome{} + } w := postNetAdd(t, s, "media") if w.Code != http.StatusPreconditionFailed { diff --git a/controller/internal/web/storage_handlers_test.go b/controller/internal/web/storage_handlers_test.go index 080015f..c7b47c6 100644 --- a/controller/internal/web/storage_handlers_test.go +++ b/controller/internal/web/storage_handlers_test.go @@ -221,11 +221,12 @@ func TestRunStorageInit_Success(t *testing.T) { // F6 (VALIDATION-n100) — initialize must end in a USABLE (mounted+registered) drive even when the // client disconnects mid-format. Two halves: -// RED-PROOF: runStorageInit on a CANCELLED context (the disconnect) aborts at the mount step → -// the device is formatted but NOT registered (the N100-observed state). This is exactly what -// the pre-fix handler did (it ran the chain on r.Context()). -// FIX: startStorageInit runs the chain on a DETACHED context → it registers regardless of the -// client, and leaves EXACTLY ONE registry entry (marker-last, Scenario B). +// +// RED-PROOF: runStorageInit on a CANCELLED context (the disconnect) aborts at the mount step → +// the device is formatted but NOT registered (the N100-observed state). This is exactly what +// the pre-fix handler did (it ran the chain on r.Context()). +// FIX: startStorageInit runs the chain on a DETACHED context → it registers regardless of the +// client, and leaves EXACTLY ONE registry entry (marker-last, Scenario B). func TestStorageInit_DetachedSurvivesClientDisconnect(t *testing.T) { newMock := func() *mockAgent { return &mockAgent{ diff --git a/controller/internal/web/storage_init_job.go b/controller/internal/web/storage_init_job.go index 225cd3d..d3267ce 100644 --- a/controller/internal/web/storage_init_job.go +++ b/controller/internal/web/storage_init_job.go @@ -29,9 +29,9 @@ type storageInitJob struct { Where string `json:"where,omitempty"` // the registered stable path (done only) Phase string `json:"phase"` // formatting | mounting | registering | done | failed | needs_confirmation | refused Error string `json:"error,omitempty"` - Reason string `json:"reason,omitempty"` // refusal/confirm reason (Hungarian, from the agent) - DurableID string `json:"durable_id,omitempty"` // needs_confirmation: the durable id to confirm against - Opsign string `json:"opsign,omitempty"` // refused: the operator opsign command + Reason string `json:"reason,omitempty"` // refusal/confirm reason (Hungarian, from the agent) + DurableID string `json:"durable_id,omitempty"` // needs_confirmation: the durable id to confirm against + Opsign string `json:"opsign,omitempty"` // refused: the operator opsign command StartedAt time.Time `json:"started_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -54,7 +54,7 @@ const storageInitDeadline = 65 * time.Minute // storageInitParams carries the validated init request into the detached job. type storageInitParams struct { device, fstype, where, label, durableID string - setDefault, confirmed bool + setDefault, confirmed bool } // storageInitState is the single-flight slot (netAddState shape: acquire/release/set/snapshot, diff --git a/controller/internal/web/storage_network_template_test.go b/controller/internal/web/storage_network_template_test.go index d3a8558..78966f3 100644 --- a/controller/internal/web/storage_network_template_test.go +++ b/controller/internal/web/storage_network_template_test.go @@ -28,13 +28,13 @@ func TestStorageNetworkTemplate_CanonicalClasses(t *testing.T) { html := buf.String() for _, want := range []string{ - `class="form-control"`, // the canonical input class - `class="form-group"`, // the canonical field wrapper - "SMB (Synology, QNAP", // protocol-honest ordering: SMB listed first - "NFS (TrueNAS, Linux szerver)", // no bare "ajánlott" claim - "Árva", // the orphan badge renders - "minden felhasználó leképezése", // the Route-A guidance block - "ns-hostid", // the live computed host-id span + `class="form-control"`, // the canonical input class + `class="form-group"`, // the canonical field wrapper + "SMB (Synology, QNAP", // protocol-honest ordering: SMB listed first + "NFS (TrueNAS, Linux szerver)", // no bare "ajánlott" claim + "Árva", // the orphan badge renders + "minden felhasználó leképezése", // the Route-A guidance block + "ns-hostid", // the live computed host-id span "/api/storage/netstorage/add/status", // the poll-driven progress wiring } { if !strings.Contains(html, want) { @@ -42,8 +42,8 @@ func TestStorageNetworkTemplate_CanonicalClasses(t *testing.T) { } } for _, banned := range []string{ - "form-row", // nonexistent class — the old unstyled-look root cause - "form-input", // nonexistent class + "form-row", // nonexistent class — the old unstyled-look root cause + "form-input", // nonexistent class `