Commit Graph

71 Commits

Author SHA1 Message Date
admin 3b70a9e9ab docs(shares): R-7b v0.145.0 — CHANGELOG, CONTEXT, REUSE, README; caveats cleared
The samba_classify.go SCOPE NOTE and the README KNOWN GAP both described a gap that
R-7b closes; both now describe the sibling-shares-source execution instead.
2026-07-18 13:06:41 +02:00
admin c81df55dcb feat(shares): R-7b Parts 1-2 — shares payload builder + tier-2 shares job (Model B')
Sibling shares source for the local cross-drive tier. Reuses the tier2Mirror seam,
selectTier2TargetFrom (narrow source-drive seam extracted from selectTier2Target),
tier2ReconcileRoots (pure extraction), tier2SafeRemove, the marker-LAST discipline
and the recordTier2* helpers. Per-app paths are untouched.

- shares_payload.go: deterministic _shares-manifest.json + best-effort passdb capture
- tier2_shares.go: per-source-drive legs -> cross-drive target, payload, marker LAST
- infra.SambaContainerName/SambaPassdbVolume/Mount: single source of truth for the
  container identity (renderer, stacks execs, backup execs, monitor all read it)
- RESERVED-NAME finding: ValidateSMBShareName did NOT exclude a leading underscore,
  so "_shares" was an accepted share name. Now refused; RunAllTier2 additionally
  skips a "_shares" stack loudly as defense in depth.
- fix: shareSourceDrive returned a slash-normalised path, which made the target
  selector's source-drive equality check miss (a group could target its own drive)
2026-07-18 12:45:57 +02:00
admin 4f08e5e7c3 feat(samba): Megosztas page + guarded folder picker (R-7 slice 1, Part 3)
New top-nav category with the Halozati megosztas page: enable/server-name card,
household password, shares table (Nev/Mappa/Irasvedett/Felhomentes/Torles), and
a create flow (new folder under <storage>/shares or an existing folder via the
browse modal). Every customer path goes through sharingResolvePath: absolute ->
EvalSymlinks -> containment in a registered live storage root -> deny-listed
system subtree check -> is-a-directory. Refusals are UNIFORM so the picker is
never a filesystem oracle. Deny-list derived from ProtectedHDDPaths (provably a
subset); the drive root is an exact-match denial so user-data folders under it
stay shareable. samba infra metadata + i-share icon. Gates green.
2026-07-18 11:45:18 +02:00
admin 1d26a69dd4 feat(samba): backup classification from the shares registry (R-7 slice 1, Part 4)
ClassifiedBinds("samba") resolves from the shares registry instead of catalog
metadata (samba has no .felhom.yml; its binds are absolute share paths). [R4]
Offsite ON -> mandatory (offsite + tier-2); OFF -> optional (tier-2 only);
smb.conf/passdb never classified. Verified through the REAL ComputeCaptureSet
tier filter incl. the negative (optional NOT in offsite). Zero engine edits.

Part-4 Step-1 finding: tier-2 (RunTier2) short-circuits on os.Stat(unitDir)
BEFORE GetStackClassifiedBinds, and the offsite runner enumerates
settings.GetOffboxApps() — both are recovery-unit shaped, which a share-only
infra stack has not. Wiring share data into a live run is therefore more than an
enumeration tweak; reported as a design fork per the STOP clause, not improvised.
2026-07-18 11:35:09 +02:00
admin 0dcbea90b2 feat(samba): lifecycle — ensureSamba/ReconcileSamba/password/disable (R-7 slice 1, Part 2)
ensureSamba joins EnsureBaseStack after filebrowser, gated on SMB.Enabled
(cloudflared conditional precedent); reconcile is idempotent (unchanged config +
running container = ZERO compose calls, asserted via seam). Atomic tmp+fsync+
rename config writes. Password applied via smbpasswd on STDIN (never argv/log/
settings). Disable = compose down, volumes + folders KEPT. samba added to
IsProtectedStack in code (controller.yaml is golden-generated and predates it),
which also makes the app-backup loops correctly skip it.
2026-07-18 11:30:04 +02:00
admin 2668ac4da3 Capture-set computation (INERT; Task 3-core, v0.133.0)
Pure appbackup.ComputeCaptureSet(binds, hasClassification, tier, hddPath) → CaptureSet
{HasClassification, Paths, Skipped}: legacy short-circuit → tier filter (§2) → structural
guards → equal-Abs collapse (mandatory>optional) → containment dedup → sort. Slash algebra,
no filepath/FS/log. Structural guards (traversal / bare HDD drive-root / reserved backups/)
are load-bearing (the compose parser does not reject ..). Pure CrossAppOverlaps advisory
(WARN wiring deferred to 3a/3b). INERT — no engine consumes it yet.

Tests: Groups A-F (appbackup) + F-S3 no-seam wiring (stacks); all 6 §10 red-proofs verified.
Docs: architecture §3 aligned (felhom.eu 8d85da7).
2026-07-14 21:53:05 +02:00
admin 0649f9a3e6 Backup classification: schema + parser + pure classifier (INERT, v0.132.0)
Task 2 of the backup-classification-redesign arc. Ships the referential-
coupling classification as DATA + PARSER + PURE CLASSIFIER, deliberately
inert — no backup tier changes behavior. Task 3 (tier policy engine) and
Task 4 (manual .fab UI) consume it.

- appbackup/classify.go: BackupSpec/BindSpec/ComposeBind/ClassifiedBind;
  ClassifyBinds (SQ5 two-level default — explicit beats :ro; unlisted
  writable→mandatory, unlisted :ro→excluded; nil spec→legacy/false);
  ValidateBackupSpec (whole-block-reject on any defect, first defect named).
- stacks/classify_binds.go: ParseComposeClassifiableBinds — ${VAR}-relative
  binds + :ro flag (NOT ParseComposeHDDMounts/ExportDataMounts, the traps).
- Metadata.Backup + LoadMetadata as the single validation choke point (bad
  catalog block → nil + one ERROR → legacy, within one sync cycle).
- Manager.ClassifiedBinds + StackDataProvider.GetStackClassifiedBinds seam
  (delegated by stackAdapter, nil-stubbed in every fake) — wired + tested
  now so Task 3 consumes a tested seam.

INERT: full pre-existing suite green with zero test-logic edits. +14 tests;
red-proofs RP-1..RP-4 confirmed. The 13 catalog backup: blocks ship in the
same app-catalog change (this controller deploys first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
2026-07-14 18:46:32 +02:00
admin 68f0e0cf5c F-S2 + F-S3: compose-derived appdata dir resolution (v0.131.0)
The controller assumed an app's HDD appdata dir is always appdata/<stackName>.
paperless-ngx writes appdata/paperless (stack paperless-ngx), so every consumer
keying by stack name silently missed it via a stat-and-skip. One canonical
resolver appbackup.AppDataDirNames derives the real dir name(s) from the app's
compose ${HDD_PATH} binds; all consumers use it.

- F-S2 (tier-2): RunTier2 mirrors the resolved appdata/<name> (paperless docs
  got NO tier-2 copy before). Tier2Info size + RestoreTier2Files live dir use it.
  WARN when a declared appdata dir is absent. New tier2Mirror seam.
- F-S3 (migrate, NEW): all six per-app appdata legs (collision/size/copy/verify/
  cleanup/skip-set) now loop resolved names. scope="app" migration of paperless
  previously copied nothing and left an empty media dir (scope="all" was saved by
  the merge walk). WARN on missing declared dir in the copy leg.
- Multi-dir (N>1) refusal: tier-2 backup/info/restore refuse loudly (Hungarian);
  migrate supports N. No catalog app hits it today; lifted by Task 3.
- Display: storage page sums resolved dirs.
- Truth repair: the v0.130.0 "tier-2 copies the namespace wholesale" claim is
  false; corrected in CHANGELOG + main.go export-adapter comment.

+9 tests; red-proofs RP-1..RP-5 all confirmed. Controller-only, no agent/hub
coupling. Task 1 of the backup-classification-redesign arc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
2026-07-14 17:45:53 +02:00
admin 8967ba7cb4 C6B-F1 cause 2: .fab export mount discovery resolves the userdata convention
exportAdapter.GetStackHDDMounts now returns stacks.ExportDataMounts — the
${HDD_PATH} binds UNIONed with the ${USERDATA_PATH} ROOT (captured at the
root, not per-bind, so the manifest's basename keying round-trips through the
existing import mapping without touching restore). Containment-aware dedupe
both directions. The backup-side stackAdapter is intentionally unchanged.
Red-proof: pre-fix behavior fails TestExportDataMounts_UserdataConvention/
MixedBindsUnion/LiteralUserdataBindDeduped (run->fail->revert recorded).
2026-07-14 15:10:45 +02:00
admin d8f6069b46 controller: fix-3 dead-app alerting + fix-6 ring cap/spill/spam (WIP, pre-build)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
2026-07-12 10:07:47 +02:00
admin 26a43708b7 v0.116.0: observability pass — always-on debug ring + leveled sweep + agent tab + self-log pull — MinAgent: 0.81.0
Capture layer: LogBuffer always exists; logger = MultiWriter(LevelFilterWriter
(stdout, logging.level), ring) so DEBUG detail exists remotely without a config
flip while docker logs keep respecting the level. New internal/logx leveled
helpers. Report ACK gains controller_log_requested (additive); next report
ships controller_log_tail (128KB, consume-once, app-tail wire byte-compatible).
Debug page: Vezérlő|Ügynök tabs; agent tab proxies agent /debug/logs with the
pre-0.83 notice on typed 404. Sweep: netstorage_job phases, netprobe, handler
validation refusals + orphan WARN, SupportsWithSource gate line, agentapi
per-call DEBUG, migrate phase lines, tier2/offbox unswallowed persists.
Red-proofs: filter-disabled, drain-removed, dropped-phase-line all FAIL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-11 16:45:57 +02:00
admin a52851e79e fix(backup): O4 — generate a replacement for unrecoverable resettable secrets on restore
The proceed-path for a missing RESETTABLE secret redeployed the app with the
secret blank (compose "Defaulting to a blank string" → exit 1, live-hit in the
2026-07-04 drill Phase 5). Now the restore generates a fresh credential instead:

- stacks.Manager.GenerateSecretForField: replacement value from the field's
  catalog generate spec via the deploy flow's generateValue (no logic copied);
  refuses data-keys (defense-in-depth), spec-less and non-secret fields.
- backup.Manager.SetSecretGenerator seam (wired in main.go), consulted in
  RestoreFromRecoveryUnit AFTER the untouched fail-closed gate, for missing
  names NOT in DataKeyEnvVars. The generated value rides fullEnv into
  RecreateStackFromUnit → RedeployFromEnv → SaveAppConfig, so it persists
  encrypted in the guest app.yaml and round-trips on the next backup/restore
  (no second write path). reconcileRestoreSecrets stays pure and untouched.
- WARNs now discriminate: "generated replacement for X (credential was reset)"
  vs "X unrecoverable and has no generator — app may fail to start". Values are
  never logged (asserted in test).
- Residual case (documented, not pretended away): if a restored volume tar
  carries the OLD internal credential hash, the app may still fail auth until a
  manual in-DB reset — generation fully fixes only the fresh-init case.

Companion red-proof: pre-fix behaviour (generation skipped) fails
TestRestoreGeneratesMissingResettableSecret on the non-empty DB_PASSWORD
assertion (verified, reverted). Data-key gate proven unreachable by generation
in TestRestoreGenerationNeverReachesDataKeys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-05 11:52:04 +02:00
admin a405505e81 v0.89.0: app-email plaintext-only listener (:2526) + split-From mapping
Gap 1: third shim listener :2526, plaintext, does NOT advertise STARTTLS (TLSConfig
nil) — for opportunistic-STARTTLS clients with no cert-skip (cal.com, nextcloud).
Gap 2: SMTPMapping tls_mode (picks port 2525/2526/2465) + from_domain_var (split
local-part + domain for nextcloud's MAIL_FROM_ADDRESS/MAIL_DOMAIN). Default keeps
existing apps on 2525. Hub untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 13:13:40 +02:00
admin 0e20eb19c1 v0.88.0: app-email SMTP relay (in-process shim + per-app injection)
In-process go-smtp shim (Shape 1): apps → shim → hub → Resend, Resend key stays
hub-side. From-header allowlist (reject 5xx pre-hub), single-shot raw-MIME forward,
status→SMTP mapping. Global + per-app toggles gate compose-time env injection from
.felhom.yml smtp_mapping. Hungarian UI on settings + app config pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 08:45:04 +02:00
admin 1705d71dd5 v0.84.0: catalog-driven initial_credentials — read an app's auto-generated first-login from a file and show it on the app page 2026-06-26 11:00:06 +02:00
admin 2d6df0f6d5 v0.83.0: Traefik scoped insecure-skip-verify serversTransport for self-signed HTTPS backends (fixes crafty 502) 2026-06-26 10:28:58 +02:00
admin 904e787dda v0.77.0: per-app open_path for the Megnyitás link
Optional .felhom.yml open_path appended to the open-app URL in dashboard/deploy/app-info
(via .Meta.OpenPath). Empty=bare root. Fixes apps whose UI isn't at / (gokapi /admin, ghost /ghost/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:01:12 +02:00
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 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 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 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 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 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 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 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
admin 2da23462c9 Phase 1 gate: lock deploy/backup HDD path agreement (no doubled felhom-data)
The deploy-side double-nest fix lives in the app catalog (templates dropped the
extra felhom-data segment). This adds the controller-side invariant test that
ties the deploy path (ParseComposeHDDMounts) to the backup path
(AppDataDir/NamespaceRoot) so they can't drift again, plus the v0.52.0 CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 09:25:31 +02:00
admin 63484a0bd4 v0.51.0: offsite-backup UI (felhom-pbs DR) + Model-A double-nest fix
- Backups page: whole-guest backup shown as real DR — target label "Biztonsági szerver –
  külön hardver (PBS)"; app-data "Távoli mentés" card now reflects the PBS offsite tier
  (guestBackupView.Offsite) instead of "nincs beállítva".
- Model-A double-nest fix: appbackup path helpers take a felhom-data NAMESPACE ROOT (no
  internal felhom-data join); backup.Manager.namespaceRoot/AppNamespaceRoot resolve
  HDD-vs-systemDataPath provenance so a drive-resident app's backups land single-nested
  (<drive>/backups/... on the guest = <drive>/felhom-data/backups/... on the host) instead
  of .../felhom-data/felhom-data/.... Writes, deletion (GetStackBackupData/RemoveStack/
  ProtectedHDDPaths), wipe-warning scan, and export updated coherently; legacy double-nest
  dirs kept protected. New appbackup test asserts no doubled segment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:26:52 +02:00
admin e61e7dd8fc v0.42.1: wildcard cert via controller route (entrypoint domains don't issue)
Empirically (staging on 9201): traefik v3 issues a cert from a router-level
tls.domains but NOT from the entrypoint http.tls.domains. So the wildcard moves
to RenderControllerRoute (the always-present anchor): when DNS-01 ACME is
configured it carries tls.certResolver+domains *.<domain>+apex, and every other
router serves that wildcard by SNI (no per-app labels). Reverts v0.42.0's dead
entrypoint-domains + TraefikData.Domain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:04:39 +02:00
admin 84c3e84641 v0.42.0: real Let's Encrypt cert via wildcard proactive issuance
traefik's websecure entrypoint now declares http.tls.domains *.<domain>+apex so
it proactively obtains the wildcard via Cloudflare DNS-01 at startup (cert ready
before first client, every router serves it by SNI). Gated on CFAPIToken (DNS-01).
TraefikData gains Domain; ensureTraefik wires cfg.Customer.Domain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:48:15 +02:00
admin 2bed7cee2a v0.41.2: fix controller-route auto-connect + dead dashboard cross-drive block
containerOnNetwork misread the absent-key '<nil>' as "already attached", so
wireController skipped docker network connect -> traefik 502'd felhom.<domain>.
Now lists network names and matches exactly. Also removed dashboard.html's dead
CrossDrive* block (slice-8C leftover) that 500'd the dashboard via gt <nil> 0,
exposed once v0.41.1 made the dashboard reachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:48:50 +02:00
admin 91736eb015 v0.41.1: wire the controller dashboard into traefik (felhom.<domain> routing)
EnsureBaseStack now writes a traefik file-provider route
(Host(felhom.<domain>) -> http://felhom-controller:8080) and joins the
controller to traefik-public. Done post-pull (domain known) and idempotently
(write-if-changed + skip-if-connected), so felhom.<domain> reaches the
controller. Completes the v0.41.0 base-infra bring-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:40:43 +02:00
admin abbd9488c6 v0.41.0: first-boot base-infra bring-up + self-heal (+ Section-G mount fix)
New internal/infra package renders traefik/cloudflared/filebrowser from config
(pinned images, single source of truth; web filebrowser path delegates here).
stacks.EnsureBaseStack deploys the traefik-public network + the three stacks,
single-flight + idempotent + non-fatal; wired to first boot and every health
tick. monitor.EffectiveProtected drops cloudflared when no tunnel token.
Section-G fix lives in felhom-agent build-golden.sh (same-path stacks bind).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:56:42 +02:00
admin 68fc153d9c slice 8B (controller half): app-consistent backup quiesce loop (v0.36.0)
internal/quiesce: poll /backup/due -> quiesce (stop app stacks) -> POST /backup
-> poll /backup/status -> unquiesce (restart exactly those). Crash-safety:
persisted marker before stopping, guaranteed unquiesce (defer), max-quiesce
guard, startup Recover, single-flight. agentapi BackupDue/StartBackup/
BackupStatus; stacks.RunningAppStacks(); config QuiesceConfig; main wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:44:52 +02:00
admin af1dd14933 fix: standardize log prefixes, remove duplicates, add missing module tags
Second-pass logging cleanup: consistent [LEVEL] [module] format across
all 41 files. Remove stale prefixes ([CF], [SYNC], [SCHED], [API],
[STORAGE], [HEALTH], [ROLLBACK]). Remove 5 duplicate log lines. Gate
ungated DEBUG lines. Fix wrong log levels (restore start WARN→INFO).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:20:09 +01:00
admin 8e61cd7ec4 feat: comprehensive INFO/WARN/ERROR logging across all controller modules
Add structured operational logging at INFO, WARN, and ERROR levels to
every controller module. Standardize custom prefixes ([GEO], [SCHED],
[SYNC]) to use [INFO/WARN/ERROR] [module] format. Fix misleveled logs
(WARN->ERROR for data loss scenarios, WARN->INFO for routine operations).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:58:27 +01:00
admin 95c821deb2 feat: comprehensive debug logging across all controller modules
Add detailed [DEBUG] logging to every controller module when
logging.level is set to "debug". Each module with stateful debug
uses SetDebug(bool) wired from main.go. Covers stacks, backup,
cloudflare, integrations, system, monitor, settings, scheduler,
web handlers, storage, metrics, API, selfupdate, and assets.

Also includes the app export/import (.fab bundles) feature from
v0.32.0 and its debug page integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:14:43 +01:00
admin 0a5840a255 feat: app-to-app integration framework + OnlyOffice handlers
Generic integration system for connecting deployed apps via toggle UI.
First handlers: OnlyOffice→FileBrowser (config.yaml patch) and
OnlyOffice→Nextcloud (occ CLI). Lifecycle hooks auto-suspend on
stop and re-apply on start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:06:20 +01:00
admin 2e9634e50f health-probes: clear stale results on start/restart, fast 10s probing until healthy
- Clear HealthProbe on StartStack/RestartStack so stale unhealthy state
  isn't re-applied by RefreshStatus
- Use 10s probe interval for unhealthy/new stacks (nil HealthProbe probes
  immediately on next tick), switch to normal 5m interval once healthy
- Scheduler frequency 1m → 10s to support fast probing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 14:59:25 +01:00
admin db83db383c fix: deep bug hunt II — concurrency, security & optimization (25 files)
Critical: watchdog mutex panic safety, SetGeoAppOverride nil guard,
SSD-only app DB restore fallback.

High: double deploy race (atomic Deploying flag), delete/remove during
deploy guard, ScanStacks overwrite protection, FileBrowser mount mutex,
PushEvent history, PushOnce error handling, DB dump sync+close before
rename, restic retry fresh context, encrypt failure logging, cross-backup
path traversal validation, deepCopyStack completeness.

Security: constant-time API key comparison, login rate limiting (5/min),
git credential masking in logs, storage path prefix traversal fix.

Concurrency: MigrateEncryption lock ordering, SubdomainInUse I/O outside
lock, scheduler late-registered jobs, SQLite WAL verification, metrics
shutdown context, telemetry scan error logging, asset sync lock scope.

Optimization: streaming file copy for DB dumps, restic stats dedup,
atomic infra config copy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 14:21:09 +01:00
admin 8b8c04a487 fix: P0+P1 critical bug fixes across controller (24 files)
Concurrency fixes:
- Deep-copy stacks in GetStack/GetStacks to prevent shared state mutation (C04)
- Add per-state mutex to watchdog pathProbeState (C05)
- Guard MetricsCollector.Start() with sync.Once against double-start (C06)
- Hold diskJobMu across entire raw mount operation (C07)
- Add mutex to SetEncryptionKey (C08), MigrateEncryption write lock (H03)
- Use sync.Once for sync.Stop() channel close (H08)
- Set syncing=true before releasing lock in TriggerSync (H09)
- Deep-copy lastDBDump/lastBackup in GetFullStatus (H11)
- Add WaitGroup for stderr goroutine in MigrateDrive (H19)
- Add mutex to SetBackupRunningCheck (M18)

Security fixes:
- Validate Bearer token against Hub API key in CSRF middleware (H16)
- Validate backup paths start with expected prefix in RemoveStack (M12)
- Guard uuid[:8] slice with length check (H20)
- Parse fstab fields exactly for mount target matching (H21)

Bug fixes:
- Use decrypted env vars for compose deploy (C01)
- Log decrypt failures in DecryptMap instead of swallowing (C02)
- Move Deployed=false inside lock in runComposeDeploy (C03)
- Fix activeDrives() to skip disconnected drives (H02)
- Fix Snapshot() stderr extraction from exec.ExitError (H01)
- Check unlockCmd.Run() error in restic (H01)
- Buffer template rendering via bytes.Buffer (H07)
- Thread context.Context through cloudflare client (H10)
- Fix leaf-name collision detection in cross-drive backup (H15)
- Add nil check for crossDriveRunner (H17)
- Use strings.TrimSpace instead of slice on command output (H18)
- Make SaveAppConfig atomic with write-to-tmp+rename (H04)
- Pass encKey on deploy failure SaveAppConfig (H05)
- Fix IPv6 address format in TCP health probe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:39:45 +01:00
admin 4c5d430b1a feat: controller-side HTTP/TCP health probes
Add network-level health probing from the controller to deployed apps.
The controller probes containers over the shared Docker network and
overrides stack state to "unhealthy" if the service isn't responding.

Three probe types: http (any response = alive), api (validates status
code and body content), tcp (port reachability). Configured per-app
via healthcheck: section in .felhom.yml. Runs every minute, per-app
interval defaults to 5 minutes.

This replaces Docker-level healthchecks for distroless images (e.g.
Vikunja) that lack shell utilities, and complements existing Docker
healthchecks for other apps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:11:21 +01:00
admin 44f7fd2f19 feat: encrypt sensitive values in app.yaml with AES-256-GCM
Passwords and secrets from deploy fields (type: password/secret) are now
encrypted at rest in app.yaml using a per-node 32-byte key. Values stored
as ENC:base64(nonce+ciphertext), decrypted transparently for docker-compose
and web UI. Key included in infra backup bundle for disaster recovery.
Existing plaintext values migrated automatically on startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 19:12:24 +01:00
admin aaf479356a fix(stacks): RestartStack now uses up -d with env vars
Previously used bare "docker compose restart" which doesn't inject
env vars or pick up template changes. Now matches StartStack behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:17:23 +01:00
admin 563cf07ec8 feat(deploy): async compose-up for instant UI feedback (v0.28.2)
Deploy API now returns immediately after validation + config save.
docker compose up -d runs in a background goroutine so the UI shows
progress during image pulls instead of blocking for 30-60s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:08:08 +01:00
admin ad4c005e01 v0.27.3: Use real system memory everywhere, add monitoring memory bar
Deploy page, pre-start check, and deploy validation now use actual
/proc/meminfo usage instead of declared mem_request sums. New
GetMemoryMB() helper for lightweight real-time memory reads. Monitoring
page gains a stacked memory distribution bar showing per-container
usage, OS overhead, and free memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:06:03 +01:00
admin e99067ca60 v0.27.2 — copyable error popups, Tier2 hub reporting, memory bar fixes, new labels
- Replace native alert() with custom showAlert() modal (text selectable)
- Manual Tier2 backup now pushes infra backup to Hub
- CommittedMemory() excludes stopped/exited apps
- Pre-start memory check blocks start if insufficient RAM
- Add hungarian_ui metadata field + "Magyar felület" badge
- Add "USB" badge on storage cards in settings page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:28:29 +01:00
admin 66817709ad v0.27.0 — user-configurable app subdomains
Users can now customize the subdomain for each app during deployment
instead of using a fixed value. The deploy page shows an editable text
input with the default pre-filled and the base domain as a suffix.

New "subdomain" deploy field type with DNS-safe format validation,
reserved name blocklist, and uniqueness check across deployed stacks.
Locked after deploy — changing requires Remove + Redeploy.

Backward compatible: InjectMissingFields() auto-fills SUBDOMAIN from
.felhom.yml defaults for existing deployed apps on next sync/restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:06:22 +01:00
admin f7556b0dad v0.26.2 — show full app URL on deploy page
Domain field now displays subdomain.base_domain (e.g. wiki.demo-felhom.eu)
instead of just the base domain, matching the app card display.
Applies to both pre-deploy and post-deploy settings pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:51:39 +01:00