> Ask Claude Code: "Please update CONTEXT.md with what we did today"
Last updated: 2026-07-02 (v0.98.0 — Tárhely split into Meghajtók + Hálózati tárhely subpages; live on 9201)
Last updated: 2026-07-03 (docs: REUSE.md introduced)
> **2026-07-03 — `REUSE.md` exists at the repo root** (canonical helpers / patterns / traps / seams, code-verified). Check it before writing new code; update it in the same commit that adds/changes a shared helper (maintenance rule now in CLAUDE.md).
> **2026-07-02 — v0.98.0 (deployed on 9201): Tárhely IA follow-up.** User feedback on D1: NAS-add and
> local-drive enrollment buttons sat side by side — confusing. `/storage` split into two subpages
> Before writing new code, check here. Canonical helpers, patterns to copy, traps to avoid.
> Maintenance: update in the SAME commit that adds/changes/deprecates a shared helper.
> Entries cite file + symbol. Line numbers are landmarks only — reconfirm before editing.
## 1. Canonical helpers (MUST reuse — do not reinvent)
### Paths & namespaces (felhom-data layout)
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `NamespaceRoot` | controller/internal/appbackup/paths.go | `(drivePath string, inGuestDrive bool) string` | Resolve felhom-data root for a drive | `inGuestDrive=true` returns path AS-IS (Model A: guest mount IS the ns root); false appends `felhom-data`. Never double-nest |
| `PrimaryBackupPath` / `RecoveryUnitPath` / `RecoveryUnitComposePath` / `RecoveryUnitManifestPath` | controller/internal/appbackup/paths.go | `(nsRoot[, stackName]) string` | All backup dir layout | Take the NAMESPACE ROOT, not a bare drive path |
| `validStackName` | controller/internal/web/validate.go | `(name string) bool` | Any stack name from a request | Single-segment, no `/ \ ..` — blocks path traversal into stacks/userdata |
| `ValidateSegment` | controller/internal/appexport/validate.go | `(kind, s string) error` | Any attacker-controlled path segment (.fab manifest fields) | CTRL-001 guard; deliberately NOT for dotfile ConfigFiles |
| `Settings.save` (unexported) | controller/internal/settings/settings.go | via mutator methods only | ALL settings.json persistence | tmp+rename, then `.bak` last-known-good AFTER rename succeeds. Never write settings.json by hand |
| `Loop.writeMarker` / `Recover` | controller/internal/quiesce/quiesce.go | `(m Marker)` / `()` | Quiesce crash-safety | Marker written BEFORE stopping stacks; Recover restarts stranded stacks at boot |
### Compose ops / stack lifecycle
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `Manager.DeployStack` | controller/internal/stacks/deploy.go | `(req DeployRequest) (string, error)` | Full deploy flow | Sets in-memory `Deployed` BEFORE compose up (slow-pull race), reverts on failure |
| `Manager.RedeployFromEnv` | controller/internal/stacks/deploy.go | `(name, env map[string]string) error` | Re-up with changed env (migration flip, config edits) | `compose up -d`, never `restart` (restart won't pick up images/env) |
| `Manager.StartStack/StopStack/RestartStack/UpdateStack` | controller/internal/stacks/manager.go | `(name string) error` | Lifecycle | Protected stacks refuse stop; all funnel through composeExec |
| `Manager.DeleteStack` / `RemoveStack` | controller/internal/stacks/delete.go | `(name, removeHDDData[, backupPaths])` | THE guarded removal paths | Orphan/protected/deploying/running checks + ProtectedHDDPaths filter before any RemoveAll |
| `resolveContainerState` / `aggregateState` | controller/internal/stacks/manager.go | `(dockerState, dockerStatus)` / `([]ContainerInfo)` | State classification | `.State` says "running" even when unhealthy — `.Status` parse is the fix |
| `Manager.logPostStartStatus` | controller/internal/stacks/manager.go | `(name, stackDir, env)` | Async post-start verification | compose up exits 0 on crash-loops; this is the detection. Goroutine + 3s, never blocks |
| `Manager.EnsureBaseStack` | controller/internal/stacks/infra.go | `() error` | Traefik/cloudflared/FileBrowser infra convergence | Renders from `internal/infra` templates |
| `backup.Manager.DumpAppVolumesSafe` | controller/internal/backup/backup.go | `(stackName) error` | Volume tar of a live app | Stops → dumps → restarts; surfaces BOTH errors (app may be left stopped) |
| `Manager.acquireRunning`/`releaseRunning`, `acquireMigrating` | controller/internal/backup/backup.go, controller/internal/stacks/migrate.go | `() error` | Single-flight for long ops | Copy this mutex-flag pattern for any new long-running manager op |
### Secrets hygiene
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `crypto.Encrypt/Decrypt/IsEncrypted/DecryptMap` | controller/internal/crypto/crypto.go | AES-256-GCM, `ENC:` prefix | app.yaml sensitive values | `Decrypt` errors on non-ENC input — use `DecryptMap` for whole env maps (passes through + warns) |
| `reconcileRestoreSecrets` | controller/internal/backup/restore_unit.go | `(nonSecretEnv, recoveredSecrets, secretNames, dataKeyNames)` | Recovery-unit restore env merge | Units are secret-FREE by design; secrets come from live app.yaml |
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `Settings.AddStoragePath/RemoveStoragePath/RepointStoragePath` | controller/internal/settings/settings.go | registry CRUD | ALL drive registration | `AddStoragePath` dedupes (double-register is clean no-op); `AutoDiscoverStoragePaths` never re-adds a known-in-any-state path |
| `Settings.SetDisconnected/ClearDisconnected/SetDecommissioned` | controller/internal/settings/settings.go | state flags + stopped-stacks memo | Drive lifecycle state | Records `stoppedStacks` so reconnect restarts exactly those |
| `registerStoragePath` | controller/internal/web/storage_handlers.go | `(where, label, setDefault) error` | Post-enroll registration | The single funnel used by init/attach/manual-add |
| `system.IsMountPoint` / `IsWritable` / `PathsOverlap` | controller/internal/system/mounts_linux.go | `(path) bool` | Mount checks | `_other.go` stubs return permissive values — Linux behavior is the real one |
| `planDriveGates` / `Server.ReconcileDriveGates` | controller/internal/web/intermediary.go | pure plan + executor | Drive appear/disappear reactions | `planDriveGates` is PURE (unit-testable); loop at `driveGateLoop` |
| `Server.runStorageInit` / `runStorageAttach` | controller/internal/web/storage_handlers.go | wizard pipelines | New-drive enroll / re-attach | Format goes through the agent's two-step confirm (below) |
### Agent local-API client (cross-repo edge)
| Symbol | File | Short signature | Use for | Gotchas |
| `Server.agentClient` | controller/internal/web/agent_disk_handlers.go | `() (*agentapi.Client, error)` | THE memoized client accessor | Always use this, never a fresh `agentapi.New` per request (§3) |
| `Server.ProbeAgentChannel` | controller/internal/web/agent_disk_handlers.go | `(ctx) (constructionErr bool, err error)` | Channel health probe | Probes via the PRODUCTION client on purpose (self-heals, mirrors UI) |
| `Client.EjectDisk` / `Decommission` / `AssignDisk` / `GuestAttach` / `ListCandidates` | controller/internal/agentapi/client.go | disk lifecycle | Delegate ALL disk ops to agent | Controller holds no Proxmox creds — never shell out to disk tools in-guest |
| `Client.AddNetStorage/ListNetStorage/RemoveNetStorage` | controller/internal/agentapi/client.go | NAS mounts (A1) | Network storage | Password passes through to agent's 0600 cred file; controller NEVER persists it |
### Notifications / hub sync
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `Notifier.PushEvent` | controller/internal/notify/notifier.go | `(eventType, severity, message, details)` | Hub events | Async goroutine, 3 attempts/3s backoff. NEW event types MUST be added to hub `allowedEventTypes` or POST /event 400s; hub only emails `warning`/`error` from this path |
| `Notifier.Notify*` convenience methods | controller/internal/notify/notifier.go | typed wrappers (backup/DB/storage/channel/DR…) | Standard events | Add a typed wrapper rather than raw PushEvent calls |
| `Server.RequireAuth` / `CsrfProtect` / `csrfField` | controller/internal/web/auth.go + csrf.go | middleware | Any new authed route/form | csrfField emits the hidden input; setup wizard has its OWN csrf (§6) |
| `LogBuffer` | controller/internal/web/logbuffer.go | ring buffer io.Writer | In-memory log capture for debug UI | — |
| `monitor.RunHealthCheck` / `EffectiveProtected` | controller/internal/monitor/healthcheck.go | system health report | Health + protected-container list | — |
| `util.TruncateStr` | controller/internal/util/strings.go | `(s, maxLen) string` | Rune-safe truncation | The intended shared helper; stacks still uses its byte-based twin (§6) |
## 2. Canonical patterns (copy structure from THE named file)
| Two-step confirmed destructive op | controller/internal/web/storage_handlers.go `handleStorageWipe` | server-side type-to-confirm + probe(unconfirmed) → sentinel error → resubmit bound to agent durable-id; agent re-checks role regardless |
| Crash-safe long job (journal + recover) | controller/internal/stacks/migrate.go | state machine + `writeJournal` per transition + `RecoverMigration` at startup + single-flight acquire/release + done-hook |
| Quiesce/marker loop | controller/internal/quiesce/quiesce.go | marker BEFORE side effects, guaranteed undo (defer + max bound), `Recover()` once at startup, `TriggerNow` 409 single-flight |
| Platform split | controller/internal/system/mounts_linux.go + mounts_other.go | `_linux.go`/`_other.go` twins; other = permissive no-op stubs for dev on Windows |
| Post-start async verification | controller/internal/stacks/manager.go `logPostStartStatus` | goroutine + sleep, INFO log, never blocks/fails the operation |
| Startup wiring order | controller/cmd/controller/main.go | init-only setters (`SetStackProvider` M2 contract: exactly once, before scheduler/HTTP), scheduler registration block |
## 3. Dangerous lookalikes — do NOT reuse
| Trap | Why it bites | Use instead |
|---|---|---|
| `rsyncMirror` (controller/internal/backup/tier2.go) | `rsync -a --delete` — DESTROYS anything extra at dst; correct only for tier-2 mirror dirs | `rsyncCopy` + `rsyncVerify` (controller/internal/stacks/migrate.go) for any move/copy; they are documented "NEVER --delete" |
| raw `os.RemoveAll` on drive/HDD paths | Bypasses the protected-set; wipes appdata/backups/media | `Manager.DeleteStack`/`RemoveStack` (controller/internal/stacks/delete.go) — gated by `ProtectedHDDPaths` + orphan/protected/running checks |
| `timeAgo` on an RFC3339 string field | Template 500 (OffboxTarget.LastRun bug, fixed v0.96.0) | `timeAgoStr` |
| `backup.Manager.DumpAppVolumes` on a running DB app | Inconsistent tar of live DB volume | `DumpAppVolumesSafe` (stop → dump → restart, both errors surfaced) |
| `stacks.Manager.execCommand` / `composeExecCustomEnv` for NEW long-running calls | No context/timeout — a hung docker CLI blocks forever | `exec.CommandContext` + explicit timeout (copy `rsyncCopy` or appexport `composeExecEnv`) |
| `config.LoadPermissive` | Skips validation — setup-mode only (customer.id/domain may be unset) | `config.Load` everywhere else |
| `docker compose restart` (any wrapper) | Does not pick up new images or env | `RedeployFromEnv` / composeExec `up -d` |
## 4. Seams & interfaces (testing + cross-repo)
| Interface | Defined in | Implemented by | Fakes/tests at |
- `controller/internal/agentapi/client.go` ↔ **felhom-agent** local API (`/storage`, `/disks*`, `/backup*`, `/netstorage*`, `/guest/*`): pinned leaf SHA-256 + per-guest bearer token from bootstrap.json.
- `controller/internal/report/pusher.go` → **hub**`/api/v1/report` ingest; ACK `config_version` drives config_refresh.go; `controller/internal/notify/notifier.go` → hub `/api/v1/event` (hub-side `allowedEventTypes` allowlist must include new types).
- `controller/internal/sync/sync.go` ← **app-catalog-felhom.eu**: copies ONLY `docker-compose.yml` + `.felhom.yml` per app (SHA-256 change detection); NEVER overwrites `app.yaml` (deployed secrets).
## 5. Extension points (where new features plug in)
- **New storage web endpoint**: switch in `ServeStorageAPI` (controller/internal/web/storage_handlers.go); disk ops in `ServeDiskAPI` (controller/internal/web/agent_disk_handlers.go); backup in `ServeBackupAPI`; export in `ServeExportAPI`; debug in `handleDebugAPI` (debug-mode gated).
- **New REST endpoint**: path dispatch in `Router.ServeHTTP` (controller/internal/api/router.go); use `writeJSON` + `limitBody`.
- **New background job**: `sched.Every`/`sched.Daily` registration block in controller/cmd/controller/main.go.
| DB wait/import ×2 | controller/internal/appbackup/dbdump.go `waitDBReady`/`ImportDump` vs controller/internal/appexport/restore.go `waitForDB`/`importDBDump` |
| compose exec ×2 | controller/internal/stacks/manager.go `composeExecCustomEnv` vs controller/internal/appexport/restore.go `composeExecEnv` (the latter has ctx+timeout; the former has the userdata belt) |
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.