# REPORT — v0.151.0: Megosztás status contract (S-1 + S-4 core) + connect-address card (S-2 + S-5) **Date:** 2026-07-20 · **Repo:** felhom-controller (v0.150.0 → **v0.151.0**) · Trunk, pushed to `main`. **Origin:** `felhom.eu/documentation/audits/DIAG-sharing-2026-07-20.md`. **Class:** implementation (code + tests + build + deploy + live verify). **S-3 (no mDNS) deliberately out of scope** — it awaits Viktor's two Mac probes and is an image change, not a controller one. ## Baselines used | Repo | start `main` @ | end `main` @ | version | |---|---|---|---| | felhom-controller | `8db9232` (== `origin/main`, clean tree) | `badf17b` | v0.150.0 → **v0.151.0** | | felhom.eu | `0c48869` | `eaa2173` | docs only | Deployed at start: `felhom-controller:0.150.0` on guest 9201 — verified `Up (healthy)`, not assumed. ## §4.1 — what `b5d78d1` actually intended, and what became of it Read in full before touching the coercion. It was **defensive, not a fix for a named repro**: the v0.147.0 commit message describes slice 4b as detached-and-polled bring-up feedback and never mentions a status-contradiction bug, and the code's own comment states the intent outright — *„A stale `idle`/`running` job must never contradict reality: liveness wins on a fresh page."* That intent is legitimate and survives; it was simply asserted on the wrong channel. Liveness is already carried by the `running` field in the same JSON object, which is where a LEVEL belongs. So instead of a repro-derived regression test there is a named test on that field: `TestSharingStatus_LivenessStillReportedWithoutAnyJob` asserts that with **no job in memory at all**, `running` tracks `SambaRunning()` in both directions — exactly the guarantee the coercion was written to provide, minus the phase-channel side effect that produced the loop. ## Files changed (all in commit `badf17b`) | File | Change | |---|---| | `controller/internal/web/sharing_handlers.go` | A.1 coercion deleted + the two-channel contract documented; B page data gains `SMBDirectAddress` (enabled-only); `sambaLANAddress()` seam resolver; `SMBRunning` gains the nil-manager guard the status handler already had | | `controller/internal/web/samba_ensure_job.go` | A.2 `consumeIfRunning()` — serve-once for terminal `running`, and only while the single-flight slot is free | | `controller/internal/web/server.go` | `sambaAddrFn` seam field | | `controller/internal/web/templates/sharing.html` | B.2 „Csatlakozás a megosztáshoz" section — **markup only** | | `controller/internal/stacks/samba.go` | `SambaLANAddress()` + the `sambaLANAddr()` seam + the pure `parseIPv4FromIPAddrOutput` | | `controller/internal/stacks/manager.go` | `sambaAddrFn` field; `SetSambaRunProbe` (exported seam setter — internal/web's tests need a live-container world from another package, same precedent as `SetMigrationDoneHook`) | | `controller/internal/infra/samba.go` | `SambaHostInterface` — single source for the nic named by smb.conf's `interfaces =`, the container's `FELHOM_IFACE`, and the address read | | **new** `web/sharing_status_contract_test.go`, `web/sharing_connect_card_test.go`, `stacks/samba_lanaddr_test.go` | Scenarios A/B/C/D + the parser matrix | | `CHANGELOG.md`, `REUSE.md`, `controller/README.md`, `REPORT.md`, `CONTEXT.md` | docs | ## Test results **23/23 packages green, run twice** — the second run with `-count=1` (no cache). Package count unchanged: this task added tests, not packages. | Test | Scenario | |---|---| | `TestSharingStatus_SteadyStateProducesNoEdge` | **A** — two polls, live container, empty slot → `phase:"idle"`, `running:true` both times | | `TestSharingStatus_LivenessStillReportedWithoutAnyJob` | the `b5d78d1` intent regression (both liveness directions) | | `TestSharingStatus_RealBringUpServedExactlyOnce` | **B** — first poll `running`, second `idle`, `running:true` on both | | `TestSharingStatus_RunningNotConsumedWhileInFlight` | the in-flight window: not consumed while the slot is held, served once after `release()` | | `TestSharingStatus_NonEdgePhasesStaySticky` | **C** — `failed`/`needs_password`/`pulling`/`starting` each persist over 3 polls | | `TestSharingStatus_EnvelopeShapeUnchanged` | exactly the 3 keys `phase`/`error`/`running` | | `TestSharingStatus_NilStackManagerIsQuiet` | a nil manager does not panic the poll | | `TestSharingConnectCard_ShowsNameAndDirectAddress` | **D** — Windows + Mac + direct forms present, seam invoked on this render | | `TestSharingConnectCard_AddressDerivedFreshEveryRender` | **D** — one derivation per render; a changed address reaches the page | | `TestSharingConnectCard_NoAddressOmitsTheLine` | **D** — `""` omits the address block, keeps the name lines | | `TestSharingConnectCard_UsesConfiguredName` | **D** — `OTTHON` renders and `FELHOM` appears nowhere | | `TestSharingConnectCard_AbsentWhenSharingDisabled` | card gated on the feature; zero derivations when off | | `TestSharingConnectCard_UnsetNameFallsBackToEffective` | unset name → the effective default, never an empty `smb://` | | `TestParseIPv4FromIPAddrOutput` | 3 accepted forms (incl. verbatim live output) + 8 rejected (link-local, unspecified, loopback-only, IPv6-only, docker error text, …) | | `TestSambaLANAddressFailsQuiet` | error → `""`, address → passthrough | ### Red-proofs (§10) — all three mutated → FAILED → restored → green | # | Mutation | Result | |---|---|---| | **A** | reinstated `if phase == idle && running { phase = running }` | `TestSharingStatus_SteadyStateProducesNoEdge` **FAILED** — `call 1: phase = "running", want "idle"`, and again on call 2. Restored → green. | | **B** | deleted the `s.cur = nil` serve-once clear | `TestSharingStatus_RealBringUpServedExactlyOnce` **FAILED** — `second call: phase = "running", want "idle"`; `TestSharingStatus_RunningNotConsumedWhileInFlight` also failed at its post-release assertion. Restored → green. | | **D** | replaced fresh derivation with a package-level cached value | `TestSharingConnectCard_AddressDerivedFreshEveryRender` **FAILED** on all three assertions — `total derivations = 1, want 2`, the new address absent, the old address still on the page. Restored → green. | ### Other gates `template_id_gate` · `emoji_gate` · `native_confirm_gate` · `offbox_rename_gate` — **all OK**. **`sharing.html`'s `