docs(report): R-66 implementation, the netns-door decision, and the two-box live validation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UuFPHmHNrCJj1VhY6QdDMU
This commit is contained in:
+20
-1
@@ -7,7 +7,26 @@
|
||||
>
|
||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||
|
||||
Last updated: 2026-07-21 (v0.155.0 — R-48 follow-up: the wizard read the wrong running-flag)
|
||||
Last updated: 2026-07-22 (v0.159.0 — R-66: the box's own address becomes visible)
|
||||
|
||||
> **2026-07-22 — v0.159.0 (R-66), SHIPPED + deployed to BOTH boxes.** Three legs: „Hálózat" card on
|
||||
> Beállítások → Rendszer (Helyi cím / Hálózati név only-while-Megosztás / Átjáró; „—" fallback),
|
||||
> `network` section in the Debug dump (best-effort per item), and the NetBIOS trap named on the NAS
|
||||
> add form (Szerver helper text + a purely lexical hint on `unreachable` for single-label non-IP
|
||||
> names).
|
||||
>
|
||||
> **DECISION (the load-bearing one): all guest-net reads go through the samba netns door.** The
|
||||
> controller is bridge-netns'd, so `/proc/net/route`/resolv.conf/net.Interfaces in-process answer
|
||||
> for the CONTAINER (172.x / 127.0.0.11) — the S-2 trap. `internal/stacks/guestnet.go` docker-execs
|
||||
> into host-networked felhom-samba (one `guestNetExecFn` seam); Megosztás off ⇒ door closed ⇒ „—" /
|
||||
> in-place error strings, never a plausible-wrong substitute (S-5). Nothing stored anywhere.
|
||||
>
|
||||
> Deploy: 0.159.0 on demo-felhom 9201 (open-door path live: .104/.1/\\FELHOM) AND demo-hp 9201 via
|
||||
> G1 break-glass (closed-door path live: dashes, no name row, in-place dump errors; secret shredded).
|
||||
> demo-hp gotcha worth keeping: the controller 404s on direct container-IP probes without the
|
||||
> customer-domain Host header (`felhom.enkisfelhom.hu` there). Red-proofs A2 + C2 run and recorded.
|
||||
> ROADMAP: R-66 SHIPPED; R-64 (pairing blessed, drill = evidence leg) + R-65 (buddy-box replication,
|
||||
> post-alpha spike-first) minted. NAS doc gained the naming-caveat paragraph.
|
||||
|
||||
> **2026-07-21 — v0.155.0.** v0.154.0's wizard sourced "is an op running" from `Manager.IsRunning()`
|
||||
> — the CONCURRENCY single-flight, acquired inside the goroutine, and **`RestoreOffboxScratch` never
|
||||
|
||||
@@ -1,135 +1,98 @@
|
||||
# REPORT — app lifecycle states + R-55 live leg (TASK-F Parts 1 & 3), 2026-07-21
|
||||
# REPORT — R-66: the box's own address becomes visible (controller v0.159.0, 2026-07-22)
|
||||
|
||||
**Versions: v0.158.0 (lifecycle) + v0.158.1 (a defect I shipped in 0.158.0 and caught live — read §4).
|
||||
Baseline `a96226a` (v0.157.0), clean tree. Deployed to guest 9201 and live-verified.**
|
||||
**Spec:** the R-66 prompt (three XS legs, controller-only, v0.158.1 → v0.159.0).
|
||||
**Commit:** `7013a5f` on `main` · image `felhom-controller:0.159.0`
|
||||
(sha256 `5bbc7d62433c464fbbcdbe8ef819636b94075d0cc481631d50471880d99630c5`) · deployed to
|
||||
**demo-felhom 9201** and **demo-hp 9201** (both now above the 0.156.0 floor; floor untouched).
|
||||
|
||||
## 1. What shipped — app lifecycle
|
||||
## 1. The one design decision (spec deviation, recorded)
|
||||
|
||||
`.felhom.yml` gains an optional top-level `lifecycle:`. Absent/empty ≡ `available`, so all existing
|
||||
templates are untouched.
|
||||
The spec sketched Leg A's Átjáró as a `/proc/net/route` read and Leg B's DNS as `/etc/resolv.conf`.
|
||||
**The controller container is bridge-networked** (verified live: `docker inspect` →
|
||||
`NetworkMode=bridge`; the bootstrap `docker run` has no `--network host`) — so every in-process
|
||||
answer describes the CONTAINER's netns: routes → the docker bridge gateway (172.x), resolv.conf →
|
||||
docker's 127.0.0.11 embedded resolver, interfaces → one 172.x eth0. That is exactly the
|
||||
wrong-kind-of-true trap the codebase already documents on `SambaLANAddress` (S-2: "the classic
|
||||
wrong answer that already burned the setup wizard").
|
||||
|
||||
| state | offered for new installs | shown to someone already running it |
|
||||
|---|---|---|
|
||||
| `available` | yes | nothing |
|
||||
| `hidden` | **no** | nothing — "we stopped offering this" is not their problem |
|
||||
| `abandoned` | **no** | „Nem karbantartott" badge + a permanent notice on the app page |
|
||||
**Resolution:** every guest-net read goes through the ONE guest-netns door this process has — a
|
||||
docker-exec into the host-networked `felhom-samba` container (`internal/stacks/guestnet.go`, one
|
||||
`guestNetExecFn` seam for route/link/addr/resolv.conf reads). Accepted consequence, by the spec's
|
||||
own S-5 logic ("an address-less row beats a wrong address"): with Megosztás off the door is closed
|
||||
and the card shows „—" / the dump carries per-item error strings. The spec's §2.1 instruction
|
||||
("check whether it is SMB-conditional… reuse as-is") anticipated exactly this shape.
|
||||
|
||||
**A deployed instance keeps full function in every state.** That is the whole design: the earlier
|
||||
`retired/` directory move un-offered the app but would ALSO have made the orphan detector see the
|
||||
template as gone for anyone running it — flagging a working install `Elavult` and offering a Törlés
|
||||
button. Withdrawing an app must never take a working app away from a customer.
|
||||
## 2. What shipped
|
||||
|
||||
Three design points worth carrying past this feature:
|
||||
- **Leg A** — „Hálózat" card on Beállítások → Rendszer, between „Verzió és frissítés" and „Szerver
|
||||
memória" (`settings_system.html`, `systemPageData`): Helyi cím (LAN) via the reused
|
||||
`sambaLANAddress`; Hálózati név `\\<SMBServerName>` gated on `smb.Enabled` (the NetBIOS name
|
||||
exists only while samba runs); Átjáró via the new `guestGateway`; muted footer („…Távoli
|
||||
hibaelhárításnál ezt az oldalt kérjük felolvasni."). No host/Proxmox address, no interface dump.
|
||||
- **Leg B** — `dump["network"]` in `debugDump`: `interfaces` (name/up/addresses; veth*/docker*/br-*
|
||||
plumbing skipped), `default_route` (gateway + source interface), `dns_servers` (guest
|
||||
resolv.conf — docker copies the HOST's file into a `network_mode: host` container, so the read
|
||||
answers for the box), `lan_address` (the SAME live value Leg A shows, the cross-check anchor).
|
||||
Best-effort per item: a failed read yields `"error: …"` in place, dump never aborts.
|
||||
- **Leg C** — helper text under the Szerver field (`storage_network.html`, naming the »közvetlen
|
||||
cím« row on the Megosztás page), plus `netAddMessage`'s `unreachable` case appends „Tipp: a(z)
|
||||
»<name>« Windows-hálózati névnek tűnik — használja az eszköz IP-címét." when
|
||||
`looksLikeFlatNetworkName(server)` (non-empty ∧ no dot ∧ not `net.ParseIP`-able). Purely
|
||||
lexical; no NetBIOS/mDNS anywhere; agent untouched.
|
||||
- Nothing is stored: no settings.json/app.yaml/hub-report field carries any of these values.
|
||||
|
||||
- **The deploy gate is server-side and fail-CLOSED**, before any mutation, with the ruled message
|
||||
„Ez az alkalmazás jelenleg nem telepíthető." Hiding a button is not a gate — a stale link, a
|
||||
bookmarked deploy form and a direct POST must all be refused. A second check in
|
||||
`stacks.DeployStack` covers callers that do not route through the API.
|
||||
- **Unknown values fail OPEN** (→ `available` + one WARN) — deliberately the opposite posture. A
|
||||
typo, or a state from a catalog newer than this controller, must never silently pull a working app
|
||||
out of every customer's list. Both paths read the same `EffectiveLifecycle`, so they cannot disagree.
|
||||
- **Lifecycle never reaches orphan detection.** `getCatalogTemplateSlugs` keys on directory + compose
|
||||
presence only. Red-proofed.
|
||||
## 3. Tests + red-proofs
|
||||
|
||||
Badge plumbing is generic (`MetaBadge` + the `meta_badge` partial + a `lifecycleBadge` funcmap
|
||||
entry) so R-56's difficulty labels need only a sibling funcmap function — no new markup or CSS.
|
||||
`stacks/guestnet_test.go`: pinned pure parsers (default route incl. garbage/IPv6-gw refusals;
|
||||
interface merge with docker-plumbing filtering; resolv.conf), `GuestGateway` fail-quiet contract,
|
||||
and the B1 best-effort matrix (scripted per-argv exec fake: healthy / resolv.conf-only failure /
|
||||
door fully closed). `web/network_card_test.go`: A1 (all rows render), A2 (Hálózati név ABSENT when
|
||||
sharing off), A3 („—" + „nem állapítható meg", no crash), per-render freshness counter (2 renders ⇒
|
||||
2 resolves — the anti-memoization gate), B1 web-half (dump shape + in-place error + dump
|
||||
completeness), C1/C2/C3 + category containment + IPv6/empty lexical edges.
|
||||
|
||||
## 2. Live validation — endpoint-level, on 9201
|
||||
**Red-proof A2** (enabled-gate dropped in `systemPageData`): FAIL —
|
||||
`Hálózati név row rendered while Megosztás is disabled — a wrong promise`. Restored, green.
|
||||
**Red-proof C2** (lexical check inverted): FAIL — the hint visibly nagged the IP user:
|
||||
`hint wrongly present for an IP: "… Tipp: a(z) »192.168.0.50« Windows-hálózati névnek tűnik …"`
|
||||
(C1/C3 + edge assertions also went red). Restored, green.
|
||||
|
||||
The catalog change arrived through the **normal git-sync**, not a hand-copy:
|
||||
Full green gate: `go build ./... && go vet ./... && go test ./...` — clean. All six template gates
|
||||
(template_id / emoji / native_confirm / offbox_rename / app_row_dedup / mojibake) — OK.
|
||||
|
||||
```
|
||||
$ grep -m1 '^lifecycle:' /opt/docker/stacks/plant-it/.felhom.yml
|
||||
lifecycle: abandoned
|
||||
```
|
||||
## 4. Live validation (endpoint-level — the exact endpoints the UI invokes; no browser on DooPlex)
|
||||
|
||||
| check | result |
|
||||
|---|---|
|
||||
| plant-it on the Alkalmazások page | **0 occurrences** (control app `bookstack`: 10) |
|
||||
| direct `POST /api/stacks/plant-it/deploy` | **HTTP 409** `{"ok":false,"error":"Ez az alkalmazás jelenleg nem telepíthető."}` |
|
||||
| `/apps/plant-it` | 200, carries the notice, **no Telepítés button** |
|
||||
| `/apps/bookstack` (control) | 200, no badge, unaffected |
|
||||
**demo-felhom 9201 (sharing ON — the open-door path).** Deploy via the bootstrap flow; `docker ps`
|
||||
→ `0.159.0 Up (healthy)`; startup log clean (`controller_started (0.159.0)`, hub report pushed).
|
||||
Authed session against container-IP:8080 + Host header (credentials sourced, never echoed):
|
||||
- `GET /settings` — the card renders with **real live values**: `192.168.0.104` (the guest's
|
||||
actual DHCP lease), gateway `192.168.0.1`, `\\FELHOM`, footer present. ASCII-safe greps per the
|
||||
accent-mangling trap.
|
||||
- `GET /api/debug/dump` → `network` = all four sub-keys:
|
||||
`lan_address 192.168.0.104` · `default_route {gateway 192.168.0.1, interface eth0}` ·
|
||||
`dns_servers [192.168.0.1]` · `interfaces [lo up 127.0.0.1/8, eth0 up 192.168.0.104/24]` —
|
||||
docker plumbing correctly absent.
|
||||
- `GET /storage/network` — the Szerver helper text present.
|
||||
|
||||
Method: the exact endpoints the UI calls, authenticated as the customer (no browser on DooPlex).
|
||||
**demo-hp 9201 (sharing OFF — the closed-door path, live).** Access via the hub-vaulted G1
|
||||
break-glass (file→file, env-var use, credential + hub-DB copy **shredded after**; demo-hp remains
|
||||
key-less). 0.156.0 → 0.159.0, `docker ps` → healthy. Same authed method (customer domain
|
||||
`felhom.enkisfelhom.hu` — direct container-IP probing 404s without the right Host, worth knowing):
|
||||
the card renders with „—" rows (×2 „nem állapítható meg"), **no** `\\FELHOM` row — Leg A's A2/A3
|
||||
behaviors observed in production; the dump's `network` section carries all four keys with in-place
|
||||
`error: docker exec …` strings for interfaces/route/dns and an empty `lan_address`. The dump
|
||||
stayed complete.
|
||||
|
||||
## 3. Red-proofs — all five run
|
||||
**Screenshots by description** (rendering is the only untested residual): the card sits between the
|
||||
version card and the RAM card; three `settings-row` lines — „Helyi cím (LAN)" with a mono address
|
||||
(or „— nem állapítható meg" muted in `--text-3`), „Hálózati név" with mono `\\FELHOM` (only while
|
||||
sharing is on), „Átjáró" with the mono gateway — and a muted one-line footer under the grid. On
|
||||
demo-hp the same card shows two dash-rows and no name row.
|
||||
|
||||
| # | pre-fix shape restored | observed failure |
|
||||
|---|---|---|
|
||||
| 1 | remove the API deploy gate | wiring test: *"deployStack never calls Meta.CanInstall() — the lifecycle gate is INERT"* |
|
||||
| 2 | drop `st.Deployed \|\|` from the catalog filter | `"plant-it-run" must be listed`, `"oldapp-run" must be listed` — a customer's running app vanishes from their own page |
|
||||
| 3 | remove the badge line from stacks.html | *"a deployed abandoned app must carry the Nem karbantartott badge"* |
|
||||
| 4 | make orphan detection lifecycle-aware | `catalog set = map[bookstack:true]` — both withdrawn apps read as orphans |
|
||||
| 5 | restore the pointer receivers (§4) | `can't evaluate field IsAbandoned in type interface {}` on every lifecycle value |
|
||||
## 5. Follow-ups
|
||||
|
||||
The wiring test walks the **AST**, not `strings.Contains`, because a commented-out call still
|
||||
contains the string; it also asserts the gate precedes `DeployStack`, since a gate that fires after
|
||||
the mutation is not fail-closed.
|
||||
|
||||
## 4. A defect I shipped in v0.158.0 and caught within the hour
|
||||
|
||||
**`/apps/<slug>` returned HTTP 500 for EVERY app**, not only withdrawn ones.
|
||||
|
||||
`EffectiveLifecycle` / `CanInstall` / `IsAbandoned` were declared with **pointer receivers**.
|
||||
`appDetailHandler` passes `data["Meta"] = found.Meta` — a `stacks.Metadata` VALUE inside a
|
||||
`map[string]interface{}` — and html/template cannot call a pointer-receiver method on a
|
||||
non-addressable value:
|
||||
|
||||
```
|
||||
executing "app_info" at <.Meta.IsAbandoned>: can't evaluate field IsAbandoned in type interface {}
|
||||
```
|
||||
|
||||
Fixed in v0.158.1 with value receivers and the reason recorded at the declaration so it is not
|
||||
"tidied" back.
|
||||
|
||||
**Why every gate missed it, which is the transferable part.** It compiles. `go vet` is silent. All
|
||||
of v0.158.0's tests passed — because none of them rendered `app_info`. The catalog-page tests
|
||||
exercised the *funcmap* route (`lifecycleBadge .Meta`), which takes a value and works with either
|
||||
receiver. **A template method call is only ever checked when that template actually runs**, so a
|
||||
render test is not optional coverage for template-facing methods — it is the only coverage.
|
||||
|
||||
`TestAppInfoRendersForEveryLifecycle` now renders the real template through the production tree with
|
||||
the handler's exact data shape — `"Meta"` as a value in a `map[string]interface{}`, deliberately not
|
||||
a pointer, because a pointer would hide the bug the test exists to catch.
|
||||
|
||||
I found this while live-validating, not from the suite. That is the honest sequence.
|
||||
|
||||
## 5. Part 3 — R-55 live leg, PROVEN
|
||||
|
||||
Operator-present, 2026-07-21. Fixture on 9201 (guest clock = UTC, host = CEST):
|
||||
|
||||
| time (UTC) | event |
|
||||
|---|---|
|
||||
| 14:28:52 | **immich STOPPED through the real UI endpoint** `POST /api/stacks/immich/stop` → `compose down` |
|
||||
| — | pre-state verified: immich **0 containers**, calibre-web 1 running (drive-backed), bookstack 2 running (not drive-backed) |
|
||||
| 14:29:14 | `pct reboot 9201` |
|
||||
| 14:29:28 | `[gate] boot 1784525102-11906045: waiting (≤2m0s) for live drive bind(s) [/mnt/felhom-drives/hdd_1]` |
|
||||
| 14:29:28 | `[gate] … live bind confirmed — recreating drive-backed app calibre-web (state=stopped) onto /mnt/felhom-drives/hdd_1` |
|
||||
| 14:29:29 | `[gate] … 1 drive-backed app(s) left stopped — zero containers means the customer stopped them on purpose` |
|
||||
| 14:29:32 | `[bootrecon] Boot reconciliation: no boot-orphaned apps (nothing to start)` |
|
||||
|
||||
End state: **immich 0 containers / stopped**, calibre-web 1 running, bookstack 2 running. **Zero
|
||||
alerts** (the only `app_start_failed|deadapp` grep hit was the scheduler registering its job).
|
||||
Reboot → steady state in **~15 seconds**.
|
||||
|
||||
**immich is absent from the recreate list.** Hours earlier, the identical fixture brought it back
|
||||
RUNNING — that is the defect R-55 fixed, and this is the same experiment with the opposite result.
|
||||
The `left stopped` INFO line fired in production for the first time, so the honoured path is
|
||||
observable rather than silent.
|
||||
|
||||
## 6. Gates
|
||||
|
||||
`go build ./... && go vet ./... && go test ./...` — **green** (both versions). All five template
|
||||
gates pass (`template_id`, `emoji`, `mojibake`, `app_row_dedup`, `native_confirm`).
|
||||
|
||||
## 7. Observations
|
||||
|
||||
1. **I pushed a commit that does not build.** A `git stash`/`stash pop` used to compare
|
||||
`reuse_refs_check.py` output silently discarded the staged index, so the first lifecycle commit
|
||||
landed only the new test files. Fixed by a follow-up commit rather than a force-push (rewriting
|
||||
pushed history is worse than one bad commit in the log). **Lesson: never `git stash` mid-staging.**
|
||||
2. `REUSE.md` cross-repo citation drift is pre-existing and improved: 8 broken paths → **5**. I fixed
|
||||
the three that were plain prefix errors; the rest cite abbreviated paths the checker resolves
|
||||
against the repo root.
|
||||
3. The `hidden` state currently has no user in the catalog. It is implemented and tested, but only
|
||||
`abandoned` is exercised end to end.
|
||||
- **R-64** (Felhom↔Felhom pairing blessed — evidence drill pending) and **R-65** (buddy-box
|
||||
replication, post-alpha spike-first) minted in `felhom.eu` ROADMAP; **R-66 marked SHIPPED** there.
|
||||
- `documentation/controller/network-storage-nas.md` gained the NetBIOS naming-caveat paragraph.
|
||||
- Operator note: a strict end-to-end UI pass of the card is a manual click-through (no browser on
|
||||
DooPlex).
|
||||
|
||||
Reference in New Issue
Block a user