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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 <stack>_postgres / <stack>-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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
// 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:
|
||||
// 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
|
||||
|
||||
@@ -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
|
||||
// .felhom-tier2-layout (marker file, content "2" — written LAST, after all legs + reconcile)
|
||||
// recovery-unit/ (the unit leg, layout-identical to v1)
|
||||
// hdd/<relpath>/ (per-bind HDD legs, relpath-mirroring)
|
||||
// userdata/<relpath>/ (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/<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
|
||||
// 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}{
|
||||
{
|
||||
|
||||
@@ -321,4 +321,3 @@ func buildStacksReport(stackMgr *stacks.Manager) StacksReport {
|
||||
|
||||
return sr
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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\<name> 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\<name> 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"
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -276,7 +276,7 @@ func diskModel(device string) string {
|
||||
type ProbeStatus int
|
||||
|
||||
const (
|
||||
ProbeConnected ProbeStatus = iota
|
||||
ProbeConnected ProbeStatus = iota
|
||||
ProbeDisconnected
|
||||
ProbeTimeout
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
type ProbeStatus int
|
||||
|
||||
const (
|
||||
ProbeConnected ProbeStatus = iota
|
||||
ProbeConnected ProbeStatus = iota
|
||||
ProbeDisconnected
|
||||
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
|
||||
// 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, ""))
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
"<h3>Ütemezés</h3>": "backups_apps", // schedule
|
||||
"<h3>Adatbázisok</h3>": "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
|
||||
"<h3>Ütemezés</h3>": "backups_apps", // schedule
|
||||
"<h3>Adatbázisok</h3>": "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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
`<summary class="btn`, // the summary-styled-as-button hack
|
||||
} {
|
||||
if strings.Contains(html, banned) {
|
||||
|
||||
Reference in New Issue
Block a user