Commit Graph

424 Commits

Author SHA1 Message Date
admin e850402a95 docs(claude): require real UI/user-flow live validation (A4)
Per the F9 storage episode — live validation of a user-facing feature
must exercise the real flow end-to-end, not shortcut via direct
API/agent/CLI calls. Low-level mechanism tests are exempt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:33:21 +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 246213bd11 docs: v0.62.0 CHANGELOG (M18 perf + M19 correctness) + README AppBackup note 2026-06-14 14:18:48 +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 2d3bf64eeb docs(claude): codify trunk-based no-branches rule
All shippable work → main directly; report-only artifacts → felhom.eu/documentation/
(audits/backlog); risky/supervised fixes implemented on main during the supervised
session, not prepared on a branch; unattended escape hatch = revert+report, never park
on a branch. Supersedes the old 'prepared on fix/... branch, pending review' pattern.
2026-06-14 11:03:09 +02:00
admin f2854fcad1 chore: relocate live-drive findings/fixspec to felhom.eu/documentation/audits; drop transient session logs
The live-drive findings + fixspec now live in felhom.eu/documentation/audits/
(with the other audit records); they no longer belong loose at the controller
repo root. Transient SESSION-*.md working logs dropped (their lasting content is
in CHANGELOG/CONTEXT and the audit records). Trunk-based no-branches cleanup.
2026-06-14 11:00:41 +02:00
admin 84979932e7 docs: v0.61.0 CHANGELOG + README (F1 mem/F17 restore/F5 route) + FIXSPEC statuses + REPORT 2026-06-14 10:25:00 +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 d6c428b5dd session log: batch progress 2026-06-14 10:03:26 +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 4938cc8985 F7: tighten status-refresh cadence 30s->10s (dashboard state lag)
The dashboard stacks list served the in-memory map refreshed by a 30s ticker, so
container state lagged Docker health by up to ~30s after a deploy. RefreshStatus
is a cheap docker-ps refresh; 10s (matching health-probes) cuts the lag without
loading Docker.
2026-06-14 09:49:05 +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 8324ed0dc2 Triage: fix-spec for live-drive findings F1-F20 (re-diagnosed at file:line, sequenced batches) 2026-06-14 08:59:53 +02:00
admin 368f5efea1 Live drive: finalize report — exec summary, prioritized issues, UX, couldn't-test, final state 2026-06-14 00:56:11 +02:00
admin 6c0d20d7d1 Live drive: §6/AGENT-001 — gate PASS, but swallowed-error + durable_id mismatch + large-disk timeout corrupts disk (F20) 2026-06-14 00:52:36 +02:00
admin 3b98c7309a Live drive: §8 removal+protected guards PASS (F18); §11 misc PASS (F19); F8 plaintext secrets extended 2026-06-14 00:49:09 +02:00
admin 20463dbaa0 Live drive: §9 import/export PASS (F16); §7 restore CRITICAL — restore does not restore data (F17) 2026-06-14 00:46:01 +02:00
admin d3ac7fb118 Live drive: CTRL-T2-1 PASS (F14), restart-policy nuance (F15) 2026-06-14 00:37:49 +02:00
admin 5954763ed7 Live drive: §5 backup tiers (F13) — tier1+PBS work, tier2 honest no-target, 3-2-1 weakened by F9 2026-06-14 00:31:54 +02:00
admin 6197eadeee Live drive: §4 lifecycle ops PASS (F12) 2026-06-14 00:27:49 +02:00
admin a95168e160 Live drive: §2b crash-loop detection PASS (F10), HDD-app-on-rootfs (F11) 2026-06-14 00:25:08 +02:00
admin d2ce148910 Live drive: F9 CRITICAL — felhom-usb HDD not passed to guest; HDD apps undeployable; disk API misrepresents 2026-06-14 00:19:25 +02:00
admin ddc1ed7fab Live drive: §2/§2b/§3 deploy+health+routing findings (F5 uptime-kuma 404 cascade, F6-F8) 2026-06-14 00:14:11 +02:00
admin ce7f44421f Live drive: baseline + initial findings (F1 mem metric, F2-F4) 2026-06-14 00:05:08 +02:00
admin aa37e6e56e runbook: e2e live-drive (supervised) stressing CTRL-T2-1/CTRL-001/AGENT-001 + session done
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:12:24 +02:00
admin c67c53bc9a docs: correct stale CLAUDE.md de-priv status (slice 8C DONE) + CONTEXT v0.60.0
CLAUDE.md line 31 wrongly said 'bulk strip has NOT happened' — slice 8C executed
the de-privileging (storage/restic/watchdog deleted; disk ops via agentapi).
Corrected to current state + pointer to central docs. CONTEXT banner -> v0.60.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:09:46 +02:00
admin 31af2e7648 session: Part 1 done — v0.60.0 (M25) deployed; M18/M19 branched
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:04:07 +02:00
admin 7dd1abaaf7 docs(changelog): v0.60.0 — M25 data-race fix; M18/M19 branched
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:02:07 +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 23bccf5434 session: backlog-Medium verdicts (M4/5/6 fixed; M18/19/25 live)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:56:34 +02:00
admin d6aacf2863 session: Parts 2-4 complete (agent fix branch, audit records, docs centralized)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:43:23 +02:00
admin 86a61fb170 docs: point controller README/CONTEXT at central docs; drop stray mnt artifact
- controller/README.md: authoritative banner (v0.59.0, docs moved to
  felhom.eu/documentation/controller/, bootstrap-deploy note); legacy body retained.
- CONTEXT.md: banner refreshed to v0.59.0 state.
- Removed controller/mnt/user-data/outputs/... — an accidental Claude-sandbox
  output dir (stale duplicate README) committed in the initial import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:42:36 +02:00
admin 6cd1fc945f docs(bughunt): reconciliation banner — H10 fixed, pointer to authoritative reconcile record
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:28:13 +02:00
admin 6b2c71f45c session: Part 1 done — v0.59.0 fixes deployed+verified on demo 9201
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:19:13 +02:00
admin 7c45fecff3 docs(changelog): v0.59.0 audit fixes (CTRL-001, CTRL-T2-1, H10, M2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:16:16 +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 eea235bd69 docs: REPORT/README/CONTEXT for v0.58.0 prevention layer (9201 re-provision validated)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:44:02 +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 ee87cca16e docs: REPORT/CONTEXT/README for v0.57.0 Part A UI fixes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:30:16 +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 cae2bfbe5b docs: Phase 4 + SLICE COMPLETE — REPORT/CONTEXT for v0.56.0
REPORT (Phase 4 FileBrowser scoping + deploy note + monitoring descriptions; live
validation; full slice summary 1/2/2b/3/4 all shipped + validated v0.52->v0.56).
CONTEXT entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:38:35 +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