Commit Graph

299 Commits

Author SHA1 Message Date
admin 0b2605c5a8 v0.75.0: gate userdata MkdirAll on a live mountpoint (no writes into an absent drive)
Belt (ensureUserdataMounts) + FileBrowser sync skip ensure/mount when an external
drive root is not a live mountpoint -> no 'mkdir userdata: permission denied' + no
rootfs-shadow during a drive-absent window. System/local path never gated. Reuses
system.IsMountPoint; matches planDriveGates external-only rule. T1-T4 + red-proofs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:47:08 +02:00
admin 2a5b88fbf9 v0.74.0: fix controller->agent connection leak (reuse one agentapi client)
agentClient() built a new agentapi.Client (new bare http.Transport, IdleConnTimeout:0)
per call and discarded it without closing idle conns -> one leaked idle ESTABLISHED
socket per call to the agent :8443, exhausting the ephemeral port range after ~5 days
(EADDRNOTAVAIL). Memoize one shared client via sync.Once; harden Transport
(MaxIdleConns/PerHost + IdleConnTimeout 90s). Agent/firewall untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:19:24 +02:00
admin f3146ac7bf controller v0.73.0: DR recipe — emit secret-free customer+apps half in hub report
DR recipe slice (controller half), grounded in SPIKE-dr-recipe-2026-06-16. The
controller emitter is the BOUNDARY enforcement point: v1 ships an explicit
allowlist {catalog_ref, enabled, storage_bindings} and reads NOTHING from
AppConfig.Env, so no ENC:/token/password can leak. storage_bindings parsed from
the compose (${HDD_PATH}/${USERDATA_PATH} volume binds -> {container_path,
drive, subpath}).

Load-bearing tests: TestBuildAppRecipe_NoSecrets (synthetic-secret app -> none
leak) + TestBuildAppRecipe_AllowlistIsLoadBearing (red-proof companion) +
TestAppStorageBindings + TestBuildDRRecipeAppHalf. Red-proofed live: forcing the
emitter to dump Env makes the boundary test fail. recipe_version=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:38:59 +02:00
admin 6ea25388d7 controller v0.72.0: FileBrowser converges on boot-recreate
processGuestBootChange recreated the drive-backed app stacks but never re-synced
FileBrowser (base-infra, no HDD_PATH), so its drive mounts went stale after a
reboot. Now, AFTER pollLiveBinds confirms the live binds and the apps are
recreated, trigger go s.SyncFileBrowserMounts() so FileBrowser converges against
the now-live drives. Refactored into pure recreateDriveBackedApps(stacks, present,
recreate, syncFB). Tests: FB sync runs once after recreate (red-proofed companion);
runs even when nothing recreated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:37:12 +02:00
admin e2de234325 controller v0.71.0: fix guest-reboot recovery (boot-race + agent-path blocker)
Live diagnosis of drive-backed apps stuck Exited after a pct reboot pinned THREE
sub-causes, fixed together (hardening the existing processGuestBootChange, not a
parallel mechanism):

1. Agent-path blocker (live root cause): agentClient() returned "agent not
   configured" (cfg.LocalAPI.Endpoint empty), so processGuestBootChange AND the
   whole drive gate bailed at the first guard. bootstrap.json had a complete
   local_api block, but MaybeIngest returned immediately on "already configured"
   so a controller.yaml seeded before local_api existed never got the agent path.
   Fix: MaybeIngest now calls ensureLocalAPI on the already-configured path,
   merging local_api from bootstrap.json into the existing controller.yaml when
   missing (no hub re-pull, config preserved; idempotent + fail-safe).

2. Boot-race readiness gate: processGuestBootChange sampled BoundUnderParent once
   during fast startup, racing the ~18s rebind, recreated nothing, burned its
   boot-id one-shot. Fix: gate on the REAL live in-guest bind -- driveBindLive
   checks /mnt/felhom-drives/<drive> is a mountpoint in the controller's own /mnt
   rslave /proc/self/mountinfo; pollLiveBinds waits for it (bounded ~120s) before
   recreating via the normal pipeline. shouldRecreateOnBoot stays state-independent
   so stuck-Exited create-time-failure apps are included.

3. Single-shot fragility: processGuestBootChange ran only once at startup; a
   briefly-unreachable agent right after a guest reboot stranded recovery. Fix:
   driveGateLoop runs it every periodic tick too (idempotent, boot-id gated).

Tests (non-hollow, pre-fix companions, red-proofed): pollLiveBinds waits then
reports live / never-live stays absent / single early sample misses; ensureLocalAPI
merges local_api into a configured controller.yaml that lacks it / no-ops when
present. Live-accepted with repeated pct reboot 9201.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:12:49 +02:00
admin 25e5cb5850 controller v0.71.0: fix guest-reboot boot-race stranding drive-backed apps
Sub-cause: on guest pct reboot, in-guest dockerd auto-starts unless-stopped
drive-backed apps ~18s BEFORE the agent re-binds the drive; the create-time
volume bind fails (mkdir /mnt/felhom-drives/<drive>/userdata: permission denied)
and RestartCount=0 means it's never retried -> stuck Exited. The existing
recovery (processGuestBootChange) RAN but raced the rebind: it sampled the
agent's BoundUnderParent once during fast startup (not live yet), recreated
nothing, and persisted the new boot-id -> burned its one-shot. The periodic gate
never recovered them either (first observation after the rebind -> no transition).

Fix (harden the existing mechanism, no parallel one): processGuestBootChange now
gates on the REAL live in-guest bind. driveBindLive checks whether
/mnt/felhom-drives/<drive> is an actual mountpoint in the controller's own /mnt
(rslave) /proc/self/mountinfo -- true only once the agent's bind propagated,
exactly when docker can recreate the app. pollLiveBinds waits for that (bounded
~120s, poll 2s; rebind lands ~18s) and only then recreates via the normal
pipeline, including stuck-Exited create-time-failure apps (shouldRecreateOnBoot
is state-independent). Single-flight; absent-after-window drives left to the gate;
host-reboot path unaffected; guest-only reboot path now covered.

Tests: pollLiveBinds waits through the rebind then reports live (recreate fires);
never-live drive stays absent; pre-fix companion (single early sample misses the
not-yet-live bind). Red-proofed against a no-wait single-sample.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:46:51 +02:00
admin 9b7a26585c fix(settings): render restart button unconditionally
The Vezérlő újraindítása card was accidentally placed inside the
{{if .RetrievalPassword}} block, so it was hidden whenever no retrieval
password is set (e.g. the demo). Moved it outside so the self-serve restart
is always available. Found during live verification of v0.70.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:13:18 +02:00
admin 09d75c1e2e controller: fix country autocomplete — reveal dropdown with display:block (C)
Root cause (diagnosed live, not the hypothesised JS throw): filterCountries runs
fine and correctly populates the list, but reveals it with `list.style.display=''`.
The .geo-country-list CSS default is `display:none` (style.css), so clearing the
inline style falls back to none and the populated list never shows — no console
error, just an invisible dropdown. Latent since the geo feature's first commit.
Fix: reveal with 'block'. Verified live (typing "Német" now lists Németország).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:01:11 +02:00
admin 02a4ba0491 controller: immediate hub report push on geo change + always-report geo (B)
- Geo settings save and manual geo sync now fire an out-of-band, non-blocking hub
  report push (Router.reportPushNow seam, wired in main.go to BuildReport+Push in a
  goroutine) so the hub reflects the new geo state / clears a stale last_sync_error
  within seconds instead of after the next ~15-min cycle. Scope: geo handlers only.
- builder.go always populates report.GeoRestriction (Enabled=false, empty countries
  when nil/disabled) via new buildGeoRestrictionReport helper, so the hub always
  renders the geo section ("Inaktív" when off) instead of hiding it via omitempty.
- Tests: geo save success → push once; invalid country → no push (companion);
  buildGeoRestrictionReport(nil) → non-nil disabled (companion vs old nil-omit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:00:32 +02:00
admin ba87412508 controller: config-apply self-restart + manual restart button (A)
POST /api/config/apply now takes effect via a graceful SELF-RESTART instead of
logging "restart needed" and leaving stale in-process singletons (the CF client
is built once at startup, so a rotated Cloudflare token never applied until a
manual LXC restart). Container is restart:unless-stopped, so a clean os.Exit(0)
auto-restarts with fresh config.

- New gracefulSelfRestart helper behind an injectable Restarter seam (Router.restart
  + SetRestarter) so the exit is unit-testable.
- configApply: no-op guard (byte-identical re-push → no write, no restart), else
  write → 200 (flushed) → restart. Removed stale "restart needed" wording.
- Removed the dead OnConfigApplied hook (Phase-1-retired infra-backup push; the
  self-restart reloads everything and a fresh report is pushed on startup).
- New POST /api/selfrestart (auth+CSRF via /api/ mount) + "Vezérlő újraindítása"
  settings button: confirm → POST → poll GET / every 2s → reload.
- Tests: changed→restart once; identical→not called (companion); invalid→not called;
  selfrestart→restart once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:56:51 +02:00
admin 9f59bc2146 controller v0.69.0: remove dead infra-backup stubs + unused restic-password report field
Controller half of Phase-1 Infra Backup retirement (hub v0.12.0;
SPIKE-infra-backup-2026-06-15). Pure dead-code removal, no behaviour change.

- Remove Pusher.PushInfraBackup (caller-less; hub endpoint gone).
- Remove Notifier.NotifyBackupCompleted / backup_completed event (caller-less
  since slice 8C; hub deadline check now reads agent host-report PBS snapshots).
- Remove report.BackupReport.ResticPassword — builder never sets it post-8C
  (confirmed in source + live), but it historically leaked the restic password
  into the hub's plaintext reports store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:11:56 +02:00
admin a821a9d7d0 fix: Beállítások endless-refresh loop after a migration (v0.68.3)
The migration journal keeps returning the last completed job; the resume-view
watched any job and migWatch's done-branch reloads the page -> endless reload
loop after any migration. Resume-view now watches only in-progress jobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:43:00 +02:00
admin 37ed7570ca fix: stack-card state-badge clipping on unhealthy apps (v0.68.2)
.stack-title-row gains flex:1+min-width:0 so it shrinks/wraps; .stack-state-badge
gains flex-shrink:0 so the nowrap badge is never compressed. Only unhealthy cards
clipped (the route-unpublished warning inflated the title-row).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:24:33 +02:00
admin 7a85732fc1 controller v0.68.1: boot-id recreate ALL deployed drive-backed apps (state-independent)
E1 caught it: filtering on State!=stopped missed apps docker hadn't auto-restarted
yet at the one-shot instant (5 apps exited after host reboot). Now recreates every
deployed present drive-backed app regardless of state (deployed=should run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:42:21 +02:00
admin 42f69dadda controller v0.68.0: storage lifecycle on intermediary model (H2/H3/M1/M3 + boot-id)
H2 decommission UI button (migrate / anyway); H3 one-click re-enroll of a
decommissioned drive; M1 default reassignment (auto-promote + block-if-none);
M3 migrate re-asserts 2775 setgid on userdata dirs; deterministic guest-reboot
recreate via agent boot_id (replaces the timed sample). Fixes the {path}/{where}
H1 JS bug. Non-hollow tests + companions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:26:02 +02:00
admin 783c79016a controller v0.67.5: startup recreate waits for stack scan
recreateBootStaleApps ran before the stack manager finished scanning (GetStacks
empty) so it found no apps; add a bounded wait for stacks before the one-time
boot-stale recreate. Deterministic guest-reboot convergence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:51:26 +02:00
admin be95a16246 controller v0.67.4: startup recreate also recovers exited/restarting drive-backed apps
recreateBootStaleApps recreates a present drive-backed app when boot-stale OR
exited/restarting/unhealthy (the recency-only gate missed already-exited apps).
Still skips healthy long-running + cleanly user-stopped apps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:46:08 +02:00
admin 91a6dcfa75 controller v0.67.3: startup recreate of boot-stale drive-backed apps
Completes guest-reboot convergence. driveGateLoop runs recreateBootStaleApps once
at startup: deployed drive-backed apps whose drive is present (BoundUnderParent)
and whose containers started recently (fresh guest boot, not a controller-only
restart) are recreated (down+up) onto the re-propagated drive. Paired with agent
v0.35.0's drive re-propagation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:40:19 +02:00
admin 3adfa41a09 controller v0.67.2: gate keys present on BoundUnderParent (reboot convergence)
The drive-absent gate treats a stable path usable only when bound under the parent
(BoundUnderParent), not merely host-mounted. Makes a host reboot converge: apps
stay gated until the agent binds the drive under the parent, then are restarted
(recreated) on the populated path. Test updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:10:02 +02:00
admin 38294d4eb5 controller v0.67.1: gate only acts on external drives under /mnt/felhom-drives/
Fix caught live: planDriveGates falsely marked the internal SSD path
/mnt/sys_drive/felhom-data disconnected (agent never reports it), which would
block starting SSD-resident apps. Gate now skips non-/mnt/felhom-drives/ paths.
Regression case added. No apps were stopped (none depended on the SSD path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:54:33 +02:00
admin 55c896624f controller v0.67.0: intermediary-mount — HDD_PATH repoint + drive-absent gate + H1 routes
Drives are visible in-guest only at the STABLE /mnt/felhom-drives/<name>; the
registered path + HDD_PATH + FileBrowser source repoint there while agent calls
map back to raw /mnt/<name> (agentWhere). Enroll binds-under-parent before
register. Drive-absent GATE (planDriveGates + 30s driveGateLoop) stops/blocks
apps when a drive vanishes and auto-restarts on return; start-gate refuses start
when the drive is absent. H1 endpoints (disconnect/reconnect/restart-apps) routed
onto host-side ops. Non-hollow tests + companions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:45:13 +02:00
admin 98d5504097 v0.66.2: FileBrowser umask 002 via entrypoint wrapper
gtstef/filebrowser is a single Go binary that ignores a UMASK env (verified live:
-e UMASK=002 leaves PID1 0022), so RenderFileBrowserCompose wraps the entrypoint
sh -c 'umask 002; exec /home/filebrowser/filebrowser'. Customer-created folders now
come out 2775 (group-writable) so group-1000 apps can write into them. Test asserts
the wrapper is rendered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:00:47 +02:00
admin 3b7d08979c v0.66.1: inject USERDATA_PATH on first deploy too (shared withUserdataPath)
DeployStack's initial compose-up builds env from deploy values (not stackEnv), so
v0.66.0 missed USERDATA_PATH on first deploy → ${USERDATA_PATH} resolved to '' and
Docker bound a root-owned dir at the container root (found live: radarr /media/movies
was 0:0 755). Shared withUserdataPath injector now used by stackEnv AND
composeExecWithEnv. Regression test included.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:23:36 +02:00
admin c48f95fe06 v0.66.0: userdata layout + shared-storage ownership convention
appbackup/userdata.go: EnsureUserdataDir (MkdirAll + explicit setgid Chmod 2775 +
chown gid 1000), UserdataSkeleton, EnsureUserdataSkeleton; linux chown/StatGID +
non-linux stubs. stackEnv injects USERDATA_PATH=<HDD_PATH>/userdata. Skeleton
pre-created on register + FileBrowser sync; deploy belt (composeExecCustomEnv on
'up') pre-creates every ${USERDATA_PATH} bind source. FileBrowser mounts userdata
(was appdata) — uid 1000 can now write into 2775 setgid. #8: migrate merge walk +
copyFile preserve source setgid+group so the convention survives MigrateAll.
Non-hollow tests incl. Linux setgid assertions + migration-preserve companion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:58:49 +02:00
admin f2596ea433 B2b: decommission orchestration + missing-storage indicator + re-enroll fix (v0.65.0)
agentapi.Decommission + handleStorageDecommission (migrate-all-or-none, Change 2):
migrate-then-decommission via the migration done-hook, or decommission-anyway (stop
apps, keep HDD_PATH). 'Hiányzó tárhely' badge on dashboard/stacks/app card when an
app's drive is decommissioned/disconnected/absent. Change 4: registerStoragePath
clears the decommissioned marker on re-enroll (ClearDecommissioned had no callers).
Non-hollow tests incl. mutation-proven Change-4 companion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:08:52 +02:00
admin 16a4c3e878 B1: migrate UI wiring — /api/storage/migrate{,-app,/status} + settings & app pages
ServeStorageAPI gains POST /api/storage/migrate (whole-namespace), POST
/api/storage/migrate-app (single app), GET /api/storage/migrate/status (poll).
settings.html: the greyed migrate-all span becomes a real target-select + button +
shared progress panel; app_info.html gains a per-app 'Áthelyezés másik tárhelyre'
control. Both poll the shared status endpoint and render Hungarian phase progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:48:22 +02:00
admin b57150e3ab B1: data-migration engine (MigrateAll + MigrateApp) + backup mutual-exclusion
internal/stacks/migrate.go: crash-safe, resumable namespace migration over the
controller's /mnt RW mount. Two entry points (whole-namespace + per-app) share one
journaled pipeline: validate -> stop -> copy (rsync -a --checksum, additive; conflict-
merge walk for non-app content) -> verify -> flip+redeploy (RedeployFromEnv) -> cleanup.
CLEANUP (the only destructive step) is gated on all units verified AND all apps
redeployed. Single-flight; mutual exclusion with the backup orchestrator (Change 3).
Non-hollow tests incl. mutation-proven collision + cleanup-gate companions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:42:57 +02:00
admin 2d4d43203f v0.64.0: additive storage discovery (A1) + internal-SSD label (A2)
A1: AutoDiscoverStoragePaths no longer bails on a non-empty registry;
registers only deployed-app paths missing from the registry. Never
mutates/removes existing entries, never re-adds or reactivates a path
present in ANY state (incl. Decommissioned), never flips IsDefault.
A2: InferStorageLabel maps base==felhom-data namespace dir to
'Belső SSD (rendszer)' to disambiguate the internal system volume.
Table-driven tests incl. a companion that fails without the
skip-by-presence guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:33:08 +02:00
admin 688ba0d2a5 v0.63.0: surface agent wipe_durable_id (F20-BUG2) + guest_attached (F9) on /api/disks
The agent (v0.31.0) now returns these on /disks, but the controller dropped them when
re-marshalling into its agentapi.DiskInfo mirror. Added both fields (additive) so they reach
/api/disks + the dashboard: wipe_durable_id (gate scheme, for the wipe-confirm) and guest_attached
(drive bound into THIS guest vs merely host-present). Controller behaviour otherwise unchanged.
2026-06-14 15:21:29 +02:00
admin f8afe5c055 M18: cache DB-dump validation; skip re-validate on unchanged dumps
ListDumpFiles ran ValidateDump (line-by-line scan) for every dump on every ~5-min
RefreshCache cycle — wasted I/O+CPU on large customer dumps. ListDumpFiles now takes
an optional cached(name,size,mod) lookup; on a (size+modtime) match it reuses the
prior result and skips ValidateDump. settings.DBValidationCache gains Size+ModTime;
listAllDumpFiles builds the lookup from the persisted cache and writes back only fresh
validations (cache miss), so an unchanged dump triggers neither a re-validation nor a
settings.json write each cycle. nil cached = legacy validate-always (back-compat).
Tests: cache-hit skips validate (sentinel), cache-miss validates, nil validates.
2026-06-14 14:13:09 +02:00
admin 6bab68b132 M19: deriveStackName cross-references deployed stacks (fix DB-container misattribution)
deriveStackName pure-suffix-stripped on '-' (postgres/db/mariadb/.../cache), so a
stack whose slug ENDS in a role token (e.g. 'my-cache') was misattributed (stripped
to 'my') — filing its DB dump under the wrong/nonexistent stack. Now threads the set
of deployed stack names (m.knownStackNames() <- ListDeployedStacks) into
DiscoverDatabases and cross-references: candidate suffix-strip if known, else the
container name if it IS a known stack, else longest known stack that is a prefix
(handles <stack>_postgres / <stack>-1), else legacy strip. nil/empty known = legacy
behaviour (appexport passes nil). Table test incl. the my-cache case (fails pre-fix).
2026-06-14 14:08:56 +02:00
admin 4989513a96 F1 (rework): source guest RAM cap from docker info; deploy guard uses committed memory
The cgroup-only approach was a no-op on the demo: the controller container's OWN
cgroup is unlimited (the 2GB cap is on the LXC ancestor, hidden), and /proc has no
lxcfs, so it kept reporting the host's 16GB. The Docker daemon runs IN the LXC, so
'docker info' MemTotal reports the guest's real cap (2048MB) — now the authoritative
source (cgroup limit preferred when present, e.g. non-nested). The deploy memory
guard now uses the controller's committed-app memory (sum of running mem requests)
for 'used' — accurate and cheap — instead of host /proc RSS (unobservable per guest;
would make the guard never/always fire). /api/system/info reports the guest cap as
total and committed memory as used. Tests: cgroup-limit path, docker-info fallback
(nested case), GuestMemTotalMB fallback (dockerMemTotalFn stub).
2026-06-14 10:18:22 +02:00
admin 0b9450e356 F17: per-app restore now replays the captured .sql DB dump (.sql wins)
The restore paths (RestoreFromRecoveryUnit + the RestoreApp fallback) repopulated
Docker volume tars but NEVER replayed the captured <stack>-<dbtype>.sql dump, so
DB-resident data (e.g. rows in a DB whose data dir is a bind mount) did not come
back — the romm marker round-trip in the audit lost the row.

New appbackup.ImportDump (read-side counterpart to DumpOne) replays a .sql/.sql.gz
into the running DB using the live container's OWN discovered credentials (no env
threading; reuses DiscoveredDB + getMariaDBPassword). backup.reimportDBDumps
orchestrates it AFTER volume restore + stack bring-up, so the logical dump WINS
over any volume-tar copy of the DB (operator-chosen precedence). pg_dump
--clean --if-exists and mariadb-dump (default --add-drop-table) make replay
idempotent; psql ON_ERROR_STOP=1 surfaces real import errors.

Also: volume-restore per-volume failures and DB-import failures now SURFACE (the
restore returns an error) instead of a swallowed WARN, so a failed data restore
cannot read as success.

Tests (restore_db_test.go, injectable discover/import seams): imports when dump+DB
present, failure surfaces, no-dump skips discovery, dump-but-no-matching-DB is a
non-fatal skip. Live DB round-trip to be validated post-deploy.
2026-06-14 10:02:58 +02:00
admin 803ce50578 F5 (dashboard): surface 'route unpublished' for unhealthy/restarting deployed apps
Traefik only publishes a route to a healthy container, so an unhealthy deployed app
returns 404 at its URL though the container runs — previously shown only as 'Nem
egészséges' with no hint the URL is dead. New routeUnpublished() funcmap helper +
a distinct indicator on the dashboard and stacks cards (gated on .Deployed). Tests:
routeUnpublished across all states, real templateFS parses with the funcmap, and the
card guard renders the indicator only for deployed+unhealthy.
2026-06-14 09:56:30 +02:00
admin 68684892d8 F8: persist controller.yaml as 0600 (holds infra secrets)
The Hub config-apply handler wrote controller.yaml 0644; it holds cf_api_token,
cf_tunnel_token and hub api_key in plaintext. New writeConfig0600 helper writes
0600 atomically (tmp+rename, bind-mount fallback) and chmods to enforce 0600 even
when the file pre-existed 0644 (os.WriteFile doesn't chmod existing files).
Test asserts mode 0600 (Linux; skipped on Windows). Setup path already used 0600.
2026-06-14 09:50:50 +02:00
admin 56fe5749a5 F4+F6: 405 for non-POST /stacks/rescan; deploy POST reports 'started' not 'deployed'
F4: GET /api/stacks/rescan fell through to GET /stacks/{name} → misleading
'stack not found: rescan'. Now returns 405 + Allow: POST.
F6: the deploy POST returns before compose/health complete (async; UI polls).
Message changed 'Stack X deployed' → 'Telepítés elindítva…' and status 200→202
Accepted, so API/script consumers aren't told a deploy finished when it hasn't.
UI checks data.ok (not HTTP status), so 202 is safe.
2026-06-14 09:48:08 +02:00
admin 2cf3fadaec F20-BUG1: surface swallowed format error in agentapi.FormatDisk
A failed agent format (e.g. 502 'device is mounted', ok:false, data:null) fell
through FormatDisk's trailing 'return out, nil', so the web layer reported a
zero-value FormatResult as ok:true — a failed DESTRUCTIVE format read as success.
postWithStatus now returns the full envelope; FormatDisk returns a non-nil error
on any non-2xx/ok:false that is not a recognized refusal (403/needs-confirmation).
Test TestFormat_MountedFailureSurfacesError (502 → non-nil err) fails on old code.
2026-06-14 09:46:28 +02:00
admin 0550b3117e F1: read cgroup memory limit, not host RAM (restores deploy OOM guard)
/api/system/info reported the Proxmox host's 16GB (the controller container
reads host /proc/meminfo with no lxcfs), defeating the deploy memory-headroom
hard-block (deploy.go uses GetMemoryMB). readMemInfo now prefers the cgroup
memory limit (v2 memory.max / v1 memory.limit_in_bytes; sentinels = unlimited)
when finite and below the host total; used = memory.current/usage_in_bytes.
Test info_cgroup_test.go (cgroup v2 cap wins, v2 max sentinel, v1 unlimited,
v1 finite) — fails on pre-fix code.
2026-06-14 09:44:28 +02:00
admin 6953899045 fix(M25): atomic.Pointer for Server.integrationMgr (constructor-goroutine race)
NewServer launches the SyncFileBrowserMounts goroutine (reads integrationMgr)
from the constructor, BEFORE main.go's SetIntegrationManager write — so the
init-only happens-before that covers the other Set* fields does NOT hold here,
making it a genuine data race (handlers.go:358/360/1433 reads vs server.go:162
write). Converted the field to atomic.Pointer[integrations.Manager]; setter
Stores, all 3 readers Load(). Regression test reproduces the concurrent access
(clean under -race; flags on the pre-fix plain-pointer field).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:58:59 +02:00
admin 092cbbe804 fix(M2): make backup.Manager.stackProvider init-only contract explicit
The single write (SetStackProvider, main.go:225) was mutex-guarded while all 11
reads were unlocked — the lock implied a runtime concurrency the reads don't
honour. It is called once during single-threaded startup before any goroutine,
so the write happens-before every read and no race exists. Removed the
misleading lock and documented the init-only contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:15:38 +02:00
admin 5a80739799 fix(CTRL-T2-1,H10): crash-safe deploy state + fail-closed secret encryption
deploy.go, one slice (both edit SaveAppConfig / the deploy goroutine):

CTRL-T2-1 (ghost-deployed on crash): DeployStack wrote app.yaml Deployed:true to
disk BEFORE the async 'docker compose up -d'; a crash during the image-pull
window left a ghost-deployed stack (Deployed:true, no containers) that DeployStack
then refused to redeploy. Now the env is persisted with Deployed:false
(transitional), and Deployed:true is written by runComposeDeploy ONLY after up -d
succeeds. In-memory Deployed stays true during the pull to preserve the
no-stale-Telepítés-button UX. On a post-success save failure, revert so the stack
is redeployable.

H10 (plaintext secret on encrypt failure): SaveAppConfig logged a WARN then fell
through to persist the secret in PLAINTEXT. Now fail-closed: return an error on
crypto.Encrypt failure, never write plaintext. Callers already propagate it.

Regression tests: H10 fail-closed (+ good-key encrypts) and the CTRL-T2-1
transitional durable-state contract (transitional reads not-deployed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:13:58 +02:00
admin c20ff56e4a fix(CTRL-001): reject path traversal in .fab import manifest
manifest.AppName / HDDSubdirs / VolumeNames are attacker-controlled JSON inside
an imported .fab and reach filepath.Join+MkdirAll/extractTar with a trusted base
(restore.go:339/606/678). UnmarshalManifest did zero validation, so '../..' in
any of them escaped the stacks / HDD destination dir.

- New appexport.ValidateSegment + validateManifestPaths; UnmarshalManifest now
  fails the parse on a traversal segment (the chokepoint).
- Defence-in-depth ValidateSegment guards at the HDD-subdir and volume-name join
  loops in restore.go.
- ConfigFiles deliberately NOT validated (holds dotfiles like .felhom.yml; never
  used in a restore join).
- Permanent regression test (was the deep-sweep failing audit test) now asserts
  rejection of traversal + acceptance of legit names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:09:47 +02:00
admin 76ec322c28 v0.58.0: infra-protection prevention layer for the OS/Docker-data split (Phase 2)
Reserved-buffer headroom guard on the Docker-data volume (system/dockervol.go,
max(5GB,10%)); deploy-time hard gate refuses (HTTP 507) when below the buffer
(api/router.go); deploy page warns + disables the button (deploy.html); runtime
disk monitor confirmed to watch the Docker volume above the buffer. Log rotation
baked into the golden (agent side). Phase 1 = felhom-agent v0.29.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:38:40 +02:00
admin 13c6a0929a v0.57.0: stable host-storage list + per-app Tier-2 config panel
Part A of the UI-fixes/storage-spike spec.

A1: enrichHostStorageTargets sorts /api/host-metrics storage_targets
server-side and attaches friendly Hungarian labels + purpose, fixing the
#host-storage-bars reorder-on-poll bug. Display labels only — PVE storage
ids are never renamed.

A2: new GET/POST /stacks/{name}/backup Tier-2 config panel; the "2. mentés"
Beállítás button is repointed there from the dead-end deploy page. Customer
can pin a target drive or disable Tier 2; preference is preserved across the
runner's status writes. Always visible (single-SSD + non-HDD apps included).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:23:34 +02:00
admin 476a97376f v0.56.0: Phase 4 — FileBrowser scoping + deploy DB-on-SSD note + monitoring descriptions
4A: scope FileBrowser bind to <drive>/appdata (recovery units + Tier 2 copies under
backups/ are no longer mounted into FileBrowser — customer can't browse/delete the
thing that restores them). 4B: deploy storage-selection step states the chosen drive
holds files while the DB runs on the fast internal SSD + is backed up with the app.
4C: buildStorageBars stable sort + purpose description on the monitoring storage list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:35:43 +02:00
admin d2071430ea v0.55.0: Phase 3 — auto off-drive Tier 2 (rootfs-headroom guard)
Tier 2 rsync-mirrors each HDD app's recovery unit + appdata to a DIFFERENT physical
disk (the only off-drive protection bind-mounted userdata can get; PBS can't reach it).
Auto-enabled, auto-target: prefer another registered drive (different physical disk via
system.SamePhysicalDevice), else the internal SSD for SMALL units only — with a
size-aware headroom guard that REFUSES rather than fill the ~8G guest rootfs, recording
an honest "needs 2nd HDD" status. Status persisted via the surviving CrossDriveBackup;
"2. mentés" UI card now populated. Daily tier2-backup job + POST /api/backup/tier2.

- backup/tier2.go (engine+selection+headroom), tier2_test.go (headroom arithmetic)
- system.SamePhysicalDevice (linux Stat_t.Dev + stub)
- handlers.go Tier2 UI population + tier2DestLabel; backups.html honest no-target reason
- fixed stale TestBackupCopiesOnPath (old felhom-data layout -> in-guest layout)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:24:49 +02:00
admin e02292aa1a test: Phase 2b restore orchestration coverage + nil-safe isDebug
Adds an in-process orchestration test for RestoreFromRecoveryUnit: success path
calls recreate with non-secret env + recovered secrets merged; data-key-missing
path is REFUSED and recreate is never called. Makes Manager.isDebug nil-safe
(behavior-neutral in prod; cfg is always set) so the gate/orchestration are testable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:17:08 +02:00
admin 7863e62f29 v0.54.0: Phase 2b — restore-from-recovery-unit + fail-closed data-key gate
Restore recreates an app from its on-drive unit + the guest's own secrets,
regenerating nothing. reconcileRestoreSecrets (pure, unit-tested) merges the unit's
non-secret env with secrets recovered from the live app.yaml and FAILS CLOSED if a
data-encrypting key is unrecoverable (refuse — a PBS whole-guest restore is needed —
rather than regenerate and corrupt). Resettable secrets missing → warn + proceed.

- backup: RestoreFromRecoveryUnit (manifest -> recover secrets -> gate -> restore
  volumes -> recreate definition + redeploy w/ re-pull); falls back to volume-only.
- seams: RecoverStackSecrets/RecreateStackFromUnit (adapter +encKey),
  stacks.RedeployFromEnv. Wired into /backup/restore.
- tests: gate (refuse/proceed/verbatim) + data_key parsing.

Gate + reconcile + data_key parsing unit-tested; capture live-validated (v0.53.1).
Full readable-data e2e vs AdventureLog needs the auth-gated dashboard restore — pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:12:43 +02:00
admin eefeeabea3 v0.53.1: refresh recovery units on periodic cache cycle (idempotent)
CaptureRecoveryUnit now builds content in memory and skips writes when the unit
is already current (checksum + dump-set + version), so it can run from RefreshCache
(startup + every 5m) without thrashing the USB drive. Units now exist shortly after
startup and track config changes without waiting for the daily DB dump. +idempotency test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:27:35 +02:00
admin 70eb521cd0 v0.53.0: Phase 2 capture side — per-app secret-free recovery unit
Each app's on-drive backup becomes a self-contained, recreatable recovery unit:
compose/ (docker-compose.yml + .felhom.yml + secret-stripped app.yaml) alongside
the existing db-dumps/ + volume-dumps/, plus a secret-free manifest.json (image
pins, secret env-var NAMES, data_key names, checksums). The unit stores no secret
value, no data-key, and not the image — secrets are recovered at restore from the
guest's own app.yaml (live/PBS), never regenerated.

- appbackup: RecoveryUnit* path helpers, RecoveryInfo + GetStackRecoveryInfo,
  ParseComposeImages; AppDBDump/Volume refactored onto RecoveryUnitPath.
- backup: recovery_unit.go (manifest + CaptureRecoveryUnit), wired into RunDBDumps;
  capture test proves secret-free.
- stacks: DeployField.DataKey + Metadata.DataKeyEnvVars(); main.go stackAdapter
  implements GetStackRecoveryInfo (excludes secret-named + encrypted values).
- Restore-from-unit recreate + fail-closed gate + live AdventureLog validation: next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:20:37 +02:00