style: gofmt normalization — no logic changes

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.
This commit is contained in:
2026-07-25 07:37:02 +02:00
parent 0a582ea07b
commit 2487681396
46 changed files with 221 additions and 193 deletions
@@ -9,8 +9,8 @@ import (
) )
func netStub(t *testing.T) (*httptest.Server, string, *struct { func netStub(t *testing.T) (*httptest.Server, string, *struct {
addBody AddNetStorageRequest addBody AddNetStorageRequest
removed string removed string
}) { }) {
captured := &struct { captured := &struct {
addBody AddNetStorageRequest addBody AddNetStorageRequest
@@ -108,10 +108,10 @@ func TestComputeCaptureSet_ExcludedInvisible(t *testing.T) {
func TestComputeCaptureSet_StructuralGuards(t *testing.T) { func TestComputeCaptureSet_StructuralGuards(t *testing.T) {
binds := []ClassifiedBind{ binds := []ClassifiedBind{
{ComposeBind: ComposeBind{Root: RootHDD, RelPath: "../evil"}, Class: ClassMandatory, Origin: OriginDefaultWritable}, // d1 traversal {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: ""}, 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: 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) cs := ComputeCaptureSet(binds, true, TierOffsite, drv)
+2 -1
View File
@@ -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 known[containerName] → containerName (the container name IS the stack — don't strip, e.g. my-cache).
// - else longest known prefix → handles <stack>_postgres / <stack>-1 / compose-suffixed names. // - else longest known prefix → handles <stack>_postgres / <stack>-1 / compose-suffixed names.
// - else → candidate (fall back to today's suffix-strip; preserves behaviour when // - 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). // A nil/empty `known` map = the legacy fast path (pure suffix-strip).
func deriveStackName(containerName string, known map[string]bool) string { func deriveStackName(containerName string, known map[string]bool) string {
candidate := suffixStripStackName(containerName) candidate := suffixStripStackName(containerName)
+2 -2
View File
@@ -15,8 +15,8 @@ import (
) )
const ( const (
magicHeader = "FABE" // Felhom App Bundle Encrypted magicHeader = "FABE" // Felhom App Bundle Encrypted
scryptN = 1 << 15 // 32768 scryptN = 1 << 15 // 32768
scryptR = 8 scryptR = 8
scryptP = 1 scryptP = 1
saltSize = 32 saltSize = 32
+2 -2
View File
@@ -468,8 +468,8 @@ func (e *Exporter) GetDebugInfo() map[string]interface{} {
defer e.mu.Unlock() defer e.mu.Unlock()
info := map[string]interface{}{ info := map[string]interface{}{
"debug_enabled": e.debug, "debug_enabled": e.debug,
"version": e.version, "version": e.version,
"has_active_job": e.activeJob != nil, "has_active_job": e.activeJob != nil,
} }
@@ -189,7 +189,7 @@ func TestExport_HDDMountBasenameCollisionFailsLoud(t *testing.T) {
prov := &hddProvider{ prov := &hddProvider{
rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true}, rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true},
mounts: []string{a, b}, hddPath: hdd, mounts: []string{a, b}, hddPath: hdd,
} }
drive := t.TempDir() drive := t.TempDir()
e := NewExporter(prov, log.New(io.Discard, "", 0), "test") e := NewExporter(prov, log.New(io.Discard, "", 0), "test")
@@ -229,7 +229,7 @@ func TestFabRoundTrip_UserdataPlacement(t *testing.T) {
prov := &hddProvider{ prov := &hddProvider{
rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true}, rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: true},
mounts: []string{ud}, hddPath: hdd, mounts: []string{ud}, hddPath: hdd,
} }
drive := t.TempDir() drive := t.TempDir()
e := NewExporter(prov, lg, "test") e := NewExporter(prov, lg, "test")
@@ -249,7 +249,7 @@ func TestFabRoundTrip_UserdataPlacement(t *testing.T) {
prov2 := &hddProvider{ prov2 := &hddProvider{
rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: false}, rtProvider: &rtProvider{stackDir: srcStack, stacksDir: t.TempDir(), deployed: false},
hddPath: hdd, hddPath: hdd,
} }
e2 := NewExporter(prov2, lg, "test") e2 := NewExporter(prov2, lg, "test")
if err := e2.StartImport(ImportRequest{FABPath: fabPath}); err != nil { if err := e2.StartImport(ImportRequest{FABPath: fabPath}); err != nil {
@@ -22,7 +22,7 @@ type fabProv struct {
mounts []string 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) GetStackHDDMounts(string) []string { return p.mounts }
func (p *fabProv) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) { func (p *fabProv) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) {
return p.binds, p.has return p.binds, p.has
+13 -13
View File
@@ -35,22 +35,22 @@ func (p *rtProvider) GetStackDir(string) (string, bool) { return p.stackDir, tru
func (p *rtProvider) GetStackComposePath(string) (string, bool) { func (p *rtProvider) GetStackComposePath(string) (string, bool) {
return filepath.Join(p.stackDir, "docker-compose.yml"), true return filepath.Join(p.stackDir, "docker-compose.yml"), true
} }
func (p *rtProvider) GetStackHDDMounts(string) []string { return nil } func (p *rtProvider) GetStackHDDMounts(string) []string { return nil }
func (p *rtProvider) GetStackHDDPath(string) string { return "" } func (p *rtProvider) GetStackHDDPath(string) string { return "" }
func (p *rtProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) { func (p *rtProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) {
return nil, false return nil, false
} }
func (p *rtProvider) IsStackRunning(string) bool { return p.running } func (p *rtProvider) IsStackRunning(string) bool { return p.running }
func (p *rtProvider) StopStack(string) error { p.stopped++; return nil } func (p *rtProvider) StopStack(string) error { p.stopped++; return nil }
func (p *rtProvider) StartStack(string) error { p.started = true; 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) GetStackDisplayName(n string) string { return "RT " + n }
func (p *rtProvider) GetStackNeedsHDD(string) bool { return false } func (p *rtProvider) GetStackNeedsHDD(string) bool { return false }
func (p *rtProvider) GetDockerVolumes(string) []string { return p.volumes } func (p *rtProvider) GetDockerVolumes(string) []string { return p.volumes }
func (p *rtProvider) IsStackDeployed(string) bool { return p.deployed } func (p *rtProvider) IsStackDeployed(string) bool { return p.deployed }
func (p *rtProvider) GetDecryptedEnv(string) map[string]string { return nil } func (p *rtProvider) GetDecryptedEnv(string) map[string]string { return nil }
func (p *rtProvider) GetStacksBaseDir() string { return p.stacksDir } func (p *rtProvider) GetStacksBaseDir() string { return p.stacksDir }
func (p *rtProvider) RefreshStacks() error { return nil } func (p *rtProvider) RefreshStacks() error { return nil }
func (p *rtProvider) RemoveStackVolumes(string) error { p.removed++; return nil } func (p *rtProvider) RemoveStackVolumes(string) error { p.removed++; return nil }
func (p *rtProvider) SaveEncryptedAppConfig(stackDir string, env map[string]string) error { func (p *rtProvider) SaveEncryptedAppConfig(stackDir string, env map[string]string) error {
p.savedEnv = env p.savedEnv = env
return nil return nil
+5 -4
View File
@@ -18,10 +18,11 @@ import (
// RecoveryManifest describes an app's self-contained, SECRET-FREE recovery unit (Phase 2). // RecoveryManifest describes an app's self-contained, SECRET-FREE recovery unit (Phase 2).
// //
// The unit on a drive is `<nsRoot>/backups/primary/<app>/` and contains: // The unit on a drive is `<nsRoot>/backups/primary/<app>/` 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) // compose/ docker-compose.yml + .felhom.yml + a SECRET-STRIPPED app.yaml
// volume-dumps/ named-volume tars (written by the dump flow) // db-dumps/ app-consistent DB dump(s) (written by the dump flow)
// manifest.json this file // 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 // 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 // pinned image tag(s) (re-pulled on restore) and the NAMES of the secret/data-key env vars. The
+6 -4
View File
@@ -187,10 +187,12 @@ func (m *Manager) tier2FitsSystemDrive(sys string, unitSizeBytes int64) bool {
} }
// Tier-2 v2 layout (Task 3b, architecture §8): backups/secondary/<stack>/ holds // Tier-2 v2 layout (Task 3b, architecture §8): backups/secondary/<stack>/ holds
// .felhom-tier2-layout (marker file, content "2" — written LAST, after all legs + reconcile) //
// recovery-unit/ (the unit leg, layout-identical to v1) // .felhom-tier2-layout (marker file, content "2" — written LAST, after all legs + reconcile)
// hdd/<relpath>/ (per-bind HDD legs, relpath-mirroring) // recovery-unit/ (the unit leg, layout-identical to v1)
// userdata/<relpath>/ (per-bind USERDATA legs) // hdd/<relpath>/ (per-bind HDD legs, relpath-mirroring)
// userdata/<relpath>/ (per-bind USERDATA legs)
//
// Relpath-mirroring represents N>1 dirs + nested binds natively (the v1 flat-appdata N>1 refusal is // 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 // 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. // current HDD_PATH). The whole tree is DERIVED from live data — migration is delete-and-rebuild.
+1 -1
View File
@@ -17,7 +17,7 @@ import (
// is fetched once via the hub consume endpoint (never in config). Mirrors hub offsite.Descriptor. // is fetched once via the hub consume endpoint (never in config). Mirrors hub offsite.Descriptor.
type OffsiteConfig struct { type OffsiteConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
Type string `yaml:"type"` // "shared" | "dedicated" Type string `yaml:"type"` // "shared" | "dedicated"
Host string `yaml:"host"` Host string `yaml:"host"`
User string `yaml:"user"` User string `yaml:"user"`
Port int `yaml:"port"` // 23 Port int `yaml:"port"` // 23
@@ -44,13 +44,13 @@ type Handler interface {
// StatusInfo is returned by the API for UI display. // StatusInfo is returned by the API for UI display.
type StatusInfo struct { type StatusInfo struct {
Key string `json:"key"` // "onlyoffice:filebrowser" Key string `json:"key"` // "onlyoffice:filebrowser"
Provider string `json:"provider"` Provider string `json:"provider"`
Target string `json:"target"` Target string `json:"target"`
Label string `json:"label"` Label string `json:"label"`
Description string `json:"description"` Description string `json:"description"`
Enabled bool `json:"enabled"` 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"` LastError string `json:"last_error,omitempty"`
TargetDeployed bool `json:"target_deployed"` TargetDeployed bool `json:"target_deployed"`
TargetRunning bool `json:"target_running"` TargetRunning bool `json:"target_running"`
+12 -2
View File
@@ -101,7 +101,12 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) {
provStack, pOk := m.stacks.GetStack(provider) provStack, pOk := m.stacks.GetStack(provider)
if !pOk || !provStack.Deployed || !isStackUp(provStack.State) { if !pOk || !provStack.Deployed || !isStackUp(provStack.State) {
if m.isDebug() { 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 continue
} }
@@ -109,7 +114,12 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) {
tgtStack, tOk := m.stacks.GetStack(target) tgtStack, tOk := m.stacks.GetStack(target)
if !tOk || !tgtStack.Deployed || !isStackUp(tgtStack.State) { if !tOk || !tgtStack.Deployed || !isStackUp(tgtStack.State) {
if m.isDebug() { 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 continue
} }
+12 -2
View File
@@ -100,7 +100,12 @@ func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bo
// Validate: provider must be deployed and running // Validate: provider must be deployed and running
provStack, pOk := m.stacks.GetStack(provider) provStack, pOk := m.stacks.GetStack(provider)
if m.isDebug() { 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 { if !pOk || !provStack.Deployed {
return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nincs telepítve", provider) 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" { if target != "filebrowser" {
tgtStack, tOk := m.stacks.GetStack(target) tgtStack, tOk := m.stacks.GetStack(target)
if m.isDebug() { 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 { if !tOk || !tgtStack.Deployed {
return state, fmt.Errorf("a célalkalmazás (%s) nincs telepítve", target) return state, fmt.Errorf("a célalkalmazás (%s) nincs telepítve", target)
@@ -32,7 +32,7 @@ func (h *OnlyOfficeNextcloudHandler) Apply(ac *ApplyContext) error {
// Install and configure OnlyOffice app in Nextcloud // Install and configure OnlyOffice app in Nextcloud
commands := []struct { commands := []struct {
args []string args []string
tolerate string // substring in output to tolerate as success tolerate string // substring in output to tolerate as success
}{ }{
{ {
-1
View File
@@ -321,4 +321,3 @@ func buildStacksReport(stackMgr *stacks.Manager) StacksReport {
return sr return sr
} }
+1 -1
View File
@@ -12,7 +12,7 @@ import "log"
// - Applied reads the persisted last-applied config_version (0 = none recorded yet). // - Applied reads the persisted last-applied config_version (0 = none recorded yet).
// - Record persists a newly-applied config_version. // - Record persists a newly-applied config_version.
// - Refresh re-pulls controller.yaml from the hub and writes it (re-merging local_api). It must // - 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). // - Restart triggers the graceful self-restart (process exit → Docker restart → fresh config).
type ConfigRefresher struct { type ConfigRefresher struct {
Applied func() int Applied func() int
@@ -86,9 +86,9 @@ func TestEscrowConfirm_StaleBlobStaysPending(t *testing.T) {
// Scenario C — no status / hash-less blob / no local password file → stay pending SILENTLY. // Scenario C — no status / hash-less blob / no local password file → stay pending SILENTLY.
func TestEscrowConfirm_SilentPendingCases(t *testing.T) { func TestEscrowConfirm_SilentPendingCases(t *testing.T) {
h := newConfirmer(t) h := newConfirmer(t)
h.c.Reconcile(nil) // no escrow row on the hub 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: true}) // hash NULL (legacy blob)
h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: false, ResticPwSHA256: hubHash}) // hash without identity blob — fail-closed h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: false, ResticPwSHA256: hubHash}) // hash without identity blob — fail-closed
h.localOK = false h.localOK = false
h.c.Reconcile(matchStatus(hubHash)) // no local password file h.c.Reconcile(matchStatus(hubHash)) // no local password file
if h.flips != 0 || h.logbuf.Len() != 0 { if h.flips != 0 || h.logbuf.Len() != 0 {
+16 -16
View File
@@ -9,22 +9,22 @@ import (
// Report is the JSON payload pushed to the central hub. // Report is the JSON payload pushed to the central hub.
type Report struct { type Report struct {
Version int `json:"version"` Version int `json:"version"`
CustomerID string `json:"customer_id"` CustomerID string `json:"customer_id"`
CustomerName string `json:"customer_name"` CustomerName string `json:"customer_name"`
ControllerVersion string `json:"controller_version"` ControllerVersion string `json:"controller_version"`
ControllerURL string `json:"controller_url,omitempty"` ControllerURL string `json:"controller_url,omitempty"`
ConfigHash string `json:"config_hash,omitempty"` ConfigHash string `json:"config_hash,omitempty"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
ReportingDisabled bool `json:"reporting_disabled,omitempty"` ReportingDisabled bool `json:"reporting_disabled,omitempty"`
System SystemReport `json:"system"` System SystemReport `json:"system"`
Storage []StorageReport `json:"storage"` Storage []StorageReport `json:"storage"`
Containers ContainerReport `json:"containers"` Containers ContainerReport `json:"containers"`
Backup BackupReport `json:"backup"` Backup BackupReport `json:"backup"`
Health HealthReport `json:"health"` Health HealthReport `json:"health"`
Stacks StacksReport `json:"stacks"` Stacks StacksReport `json:"stacks"`
AppTelemetry []AppTelemetry `json:"app_telemetry,omitempty"` AppTelemetry []AppTelemetry `json:"app_telemetry,omitempty"`
GeoRestriction *GeoRestrictionReport `json:"geo_restriction,omitempty"` GeoRestriction *GeoRestrictionReport `json:"geo_restriction,omitempty"`
// DR recipe — the controller (customer + apps) half of the secret-free reconstruction recipe // 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. // (SPIKE-dr-recipe-2026-06-16). The hub assembles it with the agent's storage/guest/PBS half.
+1 -1
View File
@@ -19,7 +19,7 @@ import (
// CheckResult represents the outcome of a single self-test check. // CheckResult represents the outcome of a single self-test check.
type CheckResult struct { type CheckResult struct {
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` // "pass", "warn", "fail" Status string `json:"status"` // "pass", "warn", "fail"
Message string `json:"message"` Message string `json:"message"`
} }
+3 -3
View File
@@ -12,13 +12,13 @@ const stateFileName = "update-state.json"
// UpdateState tracks the last update attempt. Persisted to disk as audit log. // UpdateState tracks the last update attempt. Persisted to disk as audit log.
type UpdateState struct { type UpdateState struct {
Status string `json:"status"` // "pending", "success", "failed" Status string `json:"status"` // "pending", "success", "failed"
PreviousVersion string `json:"previous_version"` PreviousVersion string `json:"previous_version"`
PreviousImage string `json:"previous_image"` PreviousImage string `json:"previous_image"`
TargetVersion string `json:"target_version"` TargetVersion string `json:"target_version"`
TargetImage string `json:"target_image"` TargetImage string `json:"target_image"`
InitiatedAt string `json:"initiated_at"` // RFC3339 InitiatedAt string `json:"initiated_at"` // RFC3339
InitiatedBy string `json:"initiated_by"` // "manual" or "auto" InitiatedBy string `json:"initiated_by"` // "manual" or "auto"
CompletedAt string `json:"completed_at,omitempty"` CompletedAt string `json:"completed_at,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
+3 -4
View File
@@ -454,9 +454,9 @@ type DryRunResult struct {
CurrentVersion string `json:"current_version"` CurrentVersion string `json:"current_version"`
LatestVersion string `json:"latest_version"` LatestVersion string `json:"latest_version"`
UpdateAvailable bool `json:"update_available"` UpdateAvailable bool `json:"update_available"`
AgentReachable bool `json:"agent_reachable"` // the host agent (which performs the swap) is wired 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) 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 TargetImage string `json:"target_image"` // what we would pull + swap to
BackupRunning bool `json:"backup_running"` BackupRunning bool `json:"backup_running"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
@@ -842,4 +842,3 @@ var runCommandStdin = func(stdin, name string, args ...string) (string, error) {
err := cmd.Run() err := cmd.Run()
return out.String(), err return out.String(), err
} }
+5 -5
View File
@@ -25,11 +25,11 @@ type SMBSettings struct {
// SMBShare is one exported folder. Path is an absolute host path under a registered storage root // 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). // (validated by the web layer against the storage registry + deny-list before it ever reaches here).
type SMBShare struct { type SMBShare struct {
Name string `json:"name"` // share name (NetBIOS-safe, ≤15); the [section] in smb.conf and the \\SERVER\<name> path Name string `json:"name"` // share name (NetBIOS-safe, ≤15); the [section] in smb.conf and the \\SERVER\<name> path
Path string `json:"path"` // absolute host path Path string `json:"path"` // absolute host path
ReadOnly bool `json:"read_only,omitempty"` // smb.conf `read only = yes` + a :ro compose bind 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) Offsite bool `json:"offsite"` // [R4] true (default) → backup class mandatory; false → optional (tier-2 only)
CreatedAt string `json:"created_at"` // RFC3339 CreatedAt string `json:"created_at"` // RFC3339
} }
// nbNameRe matches a NetBIOS-safe name: 115 chars, letters/digits/hyphen/underscore, not starting // nbNameRe matches a NetBIOS-safe name: 115 chars, letters/digits/hyphen/underscore, not starting
@@ -63,7 +63,7 @@ func ValidateSMBShareName(name string) error {
if len(name) > 15 { if len(name) > 15 {
return fmt.Errorf("a megosztás neve legfeljebb 15 karakter lehet") 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") 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" // RESERVED NAMESPACE (R-7b). The backup engines key the shares source by the pseudo-stack „_shares"
+2 -2
View File
@@ -43,8 +43,8 @@ type SetupState struct {
// SelectedBackup tracks which backup the user chose. // SelectedBackup tracks which backup the user chose.
type SelectedBackup struct { type SelectedBackup struct {
Source string `json:"source"` // "local" or "hub" Source string `json:"source"` // "local" or "hub"
DrivePath string `json:"drive_path"` // for local DrivePath string `json:"drive_path"` // for local
CustomerID string `json:"customer_id"` CustomerID string `json:"customer_id"`
Timestamp string `json:"timestamp"` Timestamp string `json:"timestamp"`
} }
@@ -41,9 +41,9 @@ func TestCatalogTemplateSlugs_IgnoresLifecycle(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
mk("bookstack", "") // available mk("bookstack", "") // available
mk("plant-it", "abandoned") // withdrawn, but STILL IN THE CATALOG TREE mk("plant-it", "abandoned") // withdrawn, but STILL IN THE CATALOG TREE
mk("someapp", "hidden") // withdrawn, ditto mk("someapp", "hidden") // withdrawn, ditto
m := &Manager{ m := &Manager{
cfg: &config.Config{Paths: config.PathsConfig{DataDir: dataDir}}, cfg: &config.Config{Paths: config.PathsConfig{DataDir: dataDir}},
@@ -10,8 +10,8 @@ import (
// Creating ${USERDATA_PATH}/... then would land app data on the rootfs, shadowed when the drive returns. // 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. // Companion red-proof: removing the gate in ensureUserdataMounts makes the dir get created → this fails.
func TestEnsureUserdataMounts_SkipsAbsentExternalDrive(t *testing.T) { func TestEnsureUserdataMounts_SkipsAbsentExternalDrive(t *testing.T) {
m := newMigManager(t, "") // sysDataPath = /mnt/sys_drive m := newMigManager(t, "") // sysDataPath = /mnt/sys_drive
m.isMountPoint = func(string) bool { return false } // external drive is NOT mounted m.isMountPoint = func(string) bool { return false } // external drive is NOT mounted
stackDir := t.TempDir() stackDir := t.TempDir()
if err := os.WriteFile(filepath.Join(stackDir, "docker-compose.yml"), []byte(beltCompose), 0o644); err != nil { if err := os.WriteFile(filepath.Join(stackDir, "docker-compose.yml"), []byte(beltCompose), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
+4 -4
View File
@@ -51,10 +51,10 @@ type SyncStatus struct {
// SyncResult holds the result of a single sync operation. // SyncResult holds the result of a single sync operation.
type SyncResult struct { type SyncResult struct {
OK bool `json:"ok"` OK bool `json:"ok"`
NewApps []string `json:"new_apps,omitempty"` NewApps []string `json:"new_apps,omitempty"`
Updated []string `json:"updated,omitempty"` Updated []string `json:"updated,omitempty"`
Message string `json:"message"` Message string `json:"message"`
} }
// New creates a new Syncer. rescanFn is called after a successful sync to trigger ScanStacks(). // New creates a new Syncer. rescanFn is called after a successful sync to trigger ScanStacks().
+4 -4
View File
@@ -16,10 +16,10 @@ func debugf(format string, args ...any) {
// SystemInfo holds system resource usage information. // SystemInfo holds system resource usage information.
type SystemInfo struct { type SystemInfo struct {
TotalMemMB uint64 `json:"total_mem_mb"` TotalMemMB uint64 `json:"total_mem_mb"`
UsedMemMB uint64 `json:"used_mem_mb"` UsedMemMB uint64 `json:"used_mem_mb"`
AvailMemMB uint64 `json:"avail_mem_mb"` AvailMemMB uint64 `json:"avail_mem_mb"`
MemPercent float64 `json:"mem_percent"` MemPercent float64 `json:"mem_percent"`
DiskTotalGB float64 `json:"disk_total_gb"` DiskTotalGB float64 `json:"disk_total_gb"`
DiskUsedGB float64 `json:"disk_used_gb"` DiskUsedGB float64 `json:"disk_used_gb"`
+1 -1
View File
@@ -276,7 +276,7 @@ func diskModel(device string) string {
type ProbeStatus int type ProbeStatus int
const ( const (
ProbeConnected ProbeStatus = iota ProbeConnected ProbeStatus = iota
ProbeDisconnected ProbeDisconnected
ProbeTimeout ProbeTimeout
) )
+1 -1
View File
@@ -88,7 +88,7 @@ func CheckBackupDestination(path string) DestinationHealth {
type ProbeStatus int type ProbeStatus int
const ( const (
ProbeConnected ProbeStatus = iota ProbeConnected ProbeStatus = iota
ProbeDisconnected ProbeDisconnected
ProbeTimeout ProbeTimeout
) )
@@ -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 // The empty-subdomain branch still yields the empty string — an app without a subdomain must not get
// a bare domain (the truthiness guard is what produces that, and the fix must not disturb it). // 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) { func TestAppExportDomainEmptyWithoutSubdomain(t *testing.T) {
line := exportScriptLine(t, renderExport(t, "")) line := exportScriptLine(t, renderExport(t, ""))
@@ -236,11 +236,11 @@ func TestBackupsTemplate_Tier3Live(t *testing.T) {
html := renderBackupPage(t, "backups_apps", data) html := renderBackupPage(t, "backups_apps", data)
for _, want := range []string{ 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 "restic → u629488-sub1.your-storagebox.de", // the real off-box host
"Utolsó:", // a relative last-run time is shown "Utolsó:", // a relative last-run time is shown
"Kikapcsolva", // radarr off row "Kikapcsolva", // radarr off row
"Bekapcsolás", // radarr enable link "Bekapcsolás", // radarr enable link
`href="/backups/remote#offbox-section"`, // the cross-page anchor jump target (IA split) `href="/backups/remote#offbox-section"`, // the cross-page anchor jump target (IA split)
} { } {
if !strings.Contains(html, want) { if !strings.Contains(html, want) {
@@ -257,13 +257,13 @@ func TestBackupsTemplate_Tier3Live(t *testing.T) {
} }
} }
for _, banned := range []string{ for _, banned := range []string{
"hamarosan", // the dead placeholder copy "hamarosan", // the dead placeholder copy
"Tier2DriveGroups", // dead template field "Tier2DriveGroups", // dead template field
"ResticPassword", // dead template field "ResticPassword", // dead template field
"restic-pw", // dead element id "restic-pw", // dead element id
"toggleTier", // dead JS fn "toggleTier", // dead JS fn
"details-tier", // dead Részletek markup "details-tier", // dead Részletek markup
"Részletek", // the removed card title "Részletek", // the removed card title
"2026-07-11T02:00:00Z", // raw RFC3339 must never leak (timeAgoStr wraps it) "2026-07-11T02:00:00Z", // raw RFC3339 must never leak (timeAgoStr wraps it)
} { } {
if strings.Contains(html, banned) { if strings.Contains(html, banned) {
+12 -12
View File
@@ -43,18 +43,18 @@ func TestBackupsSplit_SectionsOnExactlyOnePage(t *testing.T) {
// marker → the ONE page it belongs to // marker → the ONE page it belongs to
markers := map[string]string{ markers := map[string]string{
"Tárhely áttekintés": "backups", // Section 0 "Tárhely áttekintés": "backups", // Section 0
"Rendszermentés (teljes mentés)": "backups", // whole-guest "Rendszermentés (teljes mentés)": "backups", // whole-guest
">Adatmentés<": "backups", // stat cards (neutral branch) ">Adatmentés<": "backups", // stat cards (neutral branch)
`id="offbox-section"`: "backups_remote", // the offbox anchor target `id="offbox-section"`: "backups_remote", // the offbox anchor target
"Távoli mentési cél beállítása": "backups_remote", // manual-target form "Távoli mentési cél beállítása": "backups_remote", // manual-target form
"Mely alkalmazások mentődnek": "backups_remote", // toggle list "Mely alkalmazások mentődnek": "backups_remote", // toggle list
"<h3>Ütemezés</h3>": "backups_apps", // schedule "<h3>Ütemezés</h3>": "backups_apps", // schedule
"<h3>Adatbázisok</h3>": "backups_apps", // databases "<h3>Adatbázisok</h3>": "backups_apps", // databases
"Alkalmazások mentési állapota": "backups_apps", // per-app rows "Alkalmazások mentési állapota": "backups_apps", // per-app rows
`id="restore-app"`: "backups_restore", // restore panel `id="restore-app"`: "backups_restore", // restore panel
"Visszaállítás a távoli tárolóból": "backups_restore", // the offsite restore list "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 `href="/backups/restore/app?name=`: "backups_restore", // R-48: the ONE entry per app
} }
for marker, home := range markers { for marker, home := range markers {
for page, html := range pages { for page, html := range pages {
+1 -1
View File
@@ -152,7 +152,7 @@ func (s *Server) escrowPreflightAPIHandler(w http.ResponseWriter, r *http.Reques
agentOK := escrowAgentSupported(agent.AgentVersion()) agentOK := escrowAgentSupported(agent.AgentVersion())
escrowJSON(w, http.StatusOK, map[string]any{ escrowJSON(w, http.StatusOK, map[string]any{
"ok": pf.OK && agentOK, "items": pf.Items, "ok": pf.OK && agentOK, "items": pf.Items,
"agent_supported": agentOK, "agent_supported": agentOK,
"offbox_configured": s.backupMgr != nil && s.backupMgr.OffboxConfigured(), "offbox_configured": s.backupMgr != nil && s.backupMgr.OffboxConfigured(),
"escrow_state": escrowState, "escrow_state": escrowState,
"stale": s.escrowStale(), "stale": s.escrowStale(),
@@ -50,13 +50,13 @@ func TestFabDownload_TraversalGuardAndCleanup(t *testing.T) {
} }
refused := []string{ refused := []string{
"..%2Fdecoy.fab", // ../decoy.fab "..%2Fdecoy.fab", // ../decoy.fab
"..%5Cdecoy.fab", // ..\decoy.fab "..%5Cdecoy.fab", // ..\decoy.fab
"%2Fetc%2Fpasswd", // absolute path "%2Fetc%2Fpasswd", // absolute path
"sub%2Fx.fab", // separator "sub%2Fx.fab", // separator
"..", // bare traversal "..", // bare traversal
".hidden.fab", // not the exporter's naming (leading dot) ".hidden.fab", // not the exporter's naming (leading dot)
"x.txt", // not a .fab "x.txt", // not a .fab
} }
for _, f := range refused { for _, f := range refused {
if rr := fetchDownload(s, f); rr.Code != http.StatusBadRequest { if rr := fetchDownload(s, f); rr.Code != http.StatusBadRequest {
+15 -13
View File
@@ -29,23 +29,25 @@ func (p *fabWebProvider) GetStackDir(string) (string, bool) { return p.stackDir,
func (p *fabWebProvider) GetStackComposePath(string) (string, bool) { func (p *fabWebProvider) GetStackComposePath(string) (string, bool) {
return filepath.Join(p.stackDir, "docker-compose.yml"), true return filepath.Join(p.stackDir, "docker-compose.yml"), true
} }
func (p *fabWebProvider) GetStackHDDMounts(string) []string { return []string{appbackup.UserdataDir(p.hddPath)} } func (p *fabWebProvider) GetStackHDDMounts(string) []string {
func (p *fabWebProvider) GetStackHDDPath(string) string { return p.hddPath } return []string{appbackup.UserdataDir(p.hddPath)}
}
func (p *fabWebProvider) GetStackHDDPath(string) string { return p.hddPath }
func (p *fabWebProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) { func (p *fabWebProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) {
return p.binds, true return p.binds, true
} }
func (p *fabWebProvider) IsStackRunning(string) bool { return false } func (p *fabWebProvider) IsStackRunning(string) bool { return false }
func (p *fabWebProvider) StopStack(string) error { return nil } func (p *fabWebProvider) StopStack(string) error { return nil }
func (p *fabWebProvider) StartStack(string) error { return nil } func (p *fabWebProvider) StartStack(string) error { return nil }
func (p *fabWebProvider) GetStackDisplayName(n string) string { return n } func (p *fabWebProvider) GetStackDisplayName(n string) string { return n }
func (p *fabWebProvider) GetStackNeedsHDD(string) bool { return true } func (p *fabWebProvider) GetStackNeedsHDD(string) bool { return true }
func (p *fabWebProvider) GetDockerVolumes(string) []string { return nil } func (p *fabWebProvider) GetDockerVolumes(string) []string { return nil }
func (p *fabWebProvider) IsStackDeployed(string) bool { return true } func (p *fabWebProvider) IsStackDeployed(string) bool { return true }
func (p *fabWebProvider) GetDecryptedEnv(string) map[string]string { return nil } func (p *fabWebProvider) GetDecryptedEnv(string) map[string]string { return nil }
func (p *fabWebProvider) GetStacksBaseDir() string { return p.stacksDir } func (p *fabWebProvider) GetStacksBaseDir() string { return p.stacksDir }
func (p *fabWebProvider) SaveEncryptedAppConfig(string, map[string]string) error { return nil } func (p *fabWebProvider) SaveEncryptedAppConfig(string, map[string]string) error { return nil }
func (p *fabWebProvider) RefreshStacks() error { return nil } func (p *fabWebProvider) RefreshStacks() error { return nil }
func (p *fabWebProvider) RemoveStackVolumes(string) error { return nil } func (p *fabWebProvider) RemoveStackVolumes(string) error { return nil }
// waitExportDone polls the exporter until the active job finishes. // waitExportDone polls the exporter until the active job finishes.
func waitExportDone(t *testing.T, e *appexport.Exporter) { func waitExportDone(t *testing.T, e *appexport.Exporter) {
@@ -13,8 +13,8 @@ func TestSkipFileBrowserPath(t *testing.T) {
path string path string
want bool want bool
}{ }{
{StableParentDir + "/flash", false}, // external + mounted → keep {StableParentDir + "/flash", false}, // external + mounted → keep
{StableParentDir + "/usb", true}, // external + NOT mounted → skip {StableParentDir + "/usb", true}, // external + NOT mounted → skip
{"/mnt/sys_drive/felhom-data", false}, // system path (not under StableParentDir) → never skip {"/mnt/sys_drive/felhom-data", false}, // system path (not under StableParentDir) → never skip
} }
for _, c := range cases { for _, c := range cases {
@@ -34,9 +34,9 @@ func TestFbNeedsRecreate(t *testing.T) {
compose := []byte("services:\n filebrowser:\n image: x\n") compose := []byte("services:\n filebrowser:\n image: x\n")
cases := []struct { cases := []struct {
name string name string
oldCfg, newCfg, oldCmp, newCmp []byte oldCfg, newCfg, oldCmp, newCmp []byte
want bool want bool
}{ }{
{"unchanged → no recreate", cfg, cfg, compose, compose, false}, {"unchanged → no recreate", cfg, cfg, compose, compose, false},
{"config differs → recreate", cfg, []byte("sources:\n - /srv/hdd\n"), compose, compose, true}, {"config differs → recreate", cfg, []byte("sources:\n - /srv/hdd\n"), compose, compose, true},
+1 -1
View File
@@ -174,7 +174,7 @@ func (s *Server) templateFuncMap() template.FuncMap {
}, },
// isOperational returns true for any state where the stack has containers // isOperational returns true for any state where the stack has containers
// and is not stopped/exited — used by templates for showing action buttons // and is not stopped/exited — used by templates for showing action buttons
"isOperational": isOperationalState, "isOperational": isOperationalState,
"routeUnpublished": routeUnpublished, "routeUnpublished": routeUnpublished,
"logoURL": func(slug string) string { "logoURL": func(slug string) string {
return s.cfg.AppLogoURL(slug) return s.cfg.AppLogoURL(slug)
+9 -9
View File
@@ -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` // 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 // 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: // 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, // 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 // 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 // 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 // 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 — // 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.) // burning its one-shot. That is the bug this fixes.)
// 3. recreates every deployed drive-backed app whose bind is now live (`shouldRecreateOnBoot` is // 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 // state-independent, so a stuck-Exited create-time-failure app is included), then persists the
// new boot-id. // new boot-id.
// //
// Apps on a drive that never goes live within the window are left to the normal gate (stop→return→ // 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. // restart). Single-flight (runs once, before the periodic gate, in driveGateLoop). Best-effort.
+7 -7
View File
@@ -204,7 +204,7 @@ func TestRecreateDriveBackedApps_SyncsFileBrowserAfterRecreate(t *testing.T) {
flash := "/mnt/felhom-drives/felhom-flash" flash := "/mnt/felhom-drives/felhom-flash"
present := map[string]bool{flash: true} present := map[string]bool{flash: true}
stacks := []bootStack{ 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: "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 {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. // gate. Both fail here.
func TestPlanDriveGates(t *testing.T) { func TestPlanDriveGates(t *testing.T) {
paths := []settings.StoragePath{ paths := []settings.StoragePath{
{Path: "/mnt/felhom-drives/usb"}, // present + connected → no action {Path: "/mnt/felhom-drives/usb"}, // present + connected → no action
{Path: "/mnt/felhom-drives/flash"}, // ABSENT + connected → STOP {Path: "/mnt/felhom-drives/flash"}, // ABSENT + connected → STOP
{Path: "/mnt/felhom-drives/back", Disconnected: true}, // present + disconnected → RETURN {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/gone", Disconnected: true}, // ABSENT + disconnected → no action (steady)
{Path: "/mnt/felhom-drives/dead", Decommissioned: true}, // decommissioned → never touched {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/sys_drive/felhom-data"}, // INTERNAL SSD (absent from agent) → never gated
} }
disks := []agentapi.DiskInfo{ disks := []agentapi.DiskInfo{
// present = BoundUnderParent (the usable-in-guest signal), not merely State==attached. // present = BoundUnderParent (the usable-in-guest signal), not merely State==attached.
+7 -7
View File
@@ -24,13 +24,13 @@ func lcStack(name, lifecycle string, deployed bool) stacks.Stack {
// withdrawing an app dangerous in the first place. // withdrawing an app dangerous in the first place.
func TestVisibleCatalogStacks(t *testing.T) { func TestVisibleCatalogStacks(t *testing.T) {
in := []stacks.Stack{ in := []stacks.Stack{
lcStack("bookstack", "", false), // available, not deployed → OFFERED lcStack("bookstack", "", false), // available, not deployed → OFFERED
lcStack("immich", "", true), // available, deployed → shown lcStack("immich", "", true), // available, deployed → shown
lcStack("plant-it", "abandoned", false), // withdrawn, not deployed → HIDDEN lcStack("plant-it", "abandoned", false), // withdrawn, not deployed → HIDDEN
lcStack("plant-it-run", "abandoned", true),// withdrawn, DEPLOYED → shown lcStack("plant-it-run", "abandoned", true), // withdrawn, DEPLOYED → shown
lcStack("oldapp", "hidden", false), // withdrawn, not deployed → HIDDEN lcStack("oldapp", "hidden", false), // withdrawn, not deployed → HIDDEN
lcStack("oldapp-run", "hidden", true), // withdrawn, DEPLOYED → shown lcStack("oldapp-run", "hidden", true), // withdrawn, DEPLOYED → shown
lcStack("typoapp", "bogus", false), // unknown → available → OFFERED (fail-open) lcStack("typoapp", "bogus", false), // unknown → available → OFFERED (fail-open)
} }
got := map[string]bool{} got := map[string]bool{}
for _, st := range visibleCatalogStacks(in) { for _, st := range visibleCatalogStacks(in) {
+5 -5
View File
@@ -65,11 +65,11 @@ func TestMobileCSS_StripRemovedDrawerPresent(t *testing.T) {
// The off-canvas drawer + no-JS fallback are present in the mobile block. // The off-canvas drawer + no-JS fallback are present in the mobile block.
for _, want := range []string{ 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 "translateX(-100%)", // parked off-canvas until opened
".no-js .sidebar", // no-JS path: sidebar renders static inline ".no-js .sidebar", // no-JS path: sidebar renders static inline
"body.nav-open", // scroll lock while the drawer is open "body.nav-open", // scroll lock while the drawer is open
".mobile-topbar", // the sticky top bar is styled inside the mobile block ".mobile-topbar", // the sticky top bar is styled inside the mobile block
} { } {
if !strings.Contains(block, want) { if !strings.Contains(block, want) {
t.Errorf("768px block missing %q — the drawer/fallback rules are incomplete", 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) 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") { !strings.Contains(css, ".mobile-topbar{display:none") {
t.Error("no desktop-default `.mobile-topbar { display:none }` — the top bar would leak onto desktop") t.Error("no desktop-default `.mobile-topbar { display:none }` — the top bar would leak onto desktop")
} }
@@ -49,7 +49,10 @@ func TestNetAddGate_OldAgent_RefusedUpFront(t *testing.T) {
verifyErr: &agentapi.StatusError{Path: "/netstorage/verify-status", Code: http.StatusNotFound}, verifyErr: &agentapi.StatusError{Path: "/netstorage/verify-status", Code: http.StatusNotFound},
} }
s.netAgentFn = func() (netAgent, error) { return agent, nil } 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") w := postNetAdd(t, s, "media")
if w.Code != http.StatusPreconditionFailed { if w.Code != http.StatusPreconditionFailed {
@@ -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 // F6 (VALIDATION-n100) — initialize must end in a USABLE (mounted+registered) drive even when the
// client disconnects mid-format. Two halves: // 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 // RED-PROOF: runStorageInit on a CANCELLED context (the disconnect) aborts at the mount step →
// the pre-fix handler did (it ran the chain on r.Context()). // the device is formatted but NOT registered (the N100-observed state). This is exactly what
// FIX: startStorageInit runs the chain on a DETACHED context → it registers regardless of the // the pre-fix handler did (it ran the chain on r.Context()).
// client, and leaves EXACTLY ONE registry entry (marker-last, Scenario B). // 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) { func TestStorageInit_DetachedSurvivesClientDisconnect(t *testing.T) {
newMock := func() *mockAgent { newMock := func() *mockAgent {
return &mockAgent{ return &mockAgent{
+4 -4
View File
@@ -29,9 +29,9 @@ type storageInitJob struct {
Where string `json:"where,omitempty"` // the registered stable path (done only) Where string `json:"where,omitempty"` // the registered stable path (done only)
Phase string `json:"phase"` // formatting | mounting | registering | done | failed | needs_confirmation | refused Phase string `json:"phase"` // formatting | mounting | registering | done | failed | needs_confirmation | refused
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Reason string `json:"reason,omitempty"` // refusal/confirm reason (Hungarian, from the agent) 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 DurableID string `json:"durable_id,omitempty"` // needs_confirmation: the durable id to confirm against
Opsign string `json:"opsign,omitempty"` // refused: the operator opsign command Opsign string `json:"opsign,omitempty"` // refused: the operator opsign command
StartedAt time.Time `json:"started_at"` StartedAt time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_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. // storageInitParams carries the validated init request into the detached job.
type storageInitParams struct { type storageInitParams struct {
device, fstype, where, label, durableID string device, fstype, where, label, durableID string
setDefault, confirmed bool setDefault, confirmed bool
} }
// storageInitState is the single-flight slot (netAddState shape: acquire/release/set/snapshot, // storageInitState is the single-flight slot (netAddState shape: acquire/release/set/snapshot,
@@ -28,13 +28,13 @@ func TestStorageNetworkTemplate_CanonicalClasses(t *testing.T) {
html := buf.String() html := buf.String()
for _, want := range []string{ for _, want := range []string{
`class="form-control"`, // the canonical input class `class="form-control"`, // the canonical input class
`class="form-group"`, // the canonical field wrapper `class="form-group"`, // the canonical field wrapper
"SMB (Synology, QNAP", // protocol-honest ordering: SMB listed first "SMB (Synology, QNAP", // protocol-honest ordering: SMB listed first
"NFS (TrueNAS, Linux szerver)", // no bare "ajánlott" claim "NFS (TrueNAS, Linux szerver)", // no bare "ajánlott" claim
"Árva", // the orphan badge renders "Árva", // the orphan badge renders
"minden felhasználó leképezése", // the Route-A guidance block "minden felhasználó leképezése", // the Route-A guidance block
"ns-hostid", // the live computed host-id span "ns-hostid", // the live computed host-id span
"/api/storage/netstorage/add/status", // the poll-driven progress wiring "/api/storage/netstorage/add/status", // the poll-driven progress wiring
} { } {
if !strings.Contains(html, want) { if !strings.Contains(html, want) {
@@ -42,8 +42,8 @@ func TestStorageNetworkTemplate_CanonicalClasses(t *testing.T) {
} }
} }
for _, banned := range []string{ for _, banned := range []string{
"form-row", // nonexistent class — the old unstyled-look root cause "form-row", // nonexistent class — the old unstyled-look root cause
"form-input", // nonexistent class "form-input", // nonexistent class
`<summary class="btn`, // the summary-styled-as-button hack `<summary class="btn`, // the summary-styled-as-button hack
} { } {
if strings.Contains(html, banned) { if strings.Contains(html, banned) {