docs: v0.151.0 REPORT + CONTEXT — deployed, live-verified, 3 red-proofs recorded

This commit is contained in:
2026-07-20 10:54:50 +02:00
parent badf17bebd
commit 5c105fb49b
2 changed files with 139 additions and 226 deletions
+18 -1
View File
@@ -7,7 +7,24 @@
>
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
Last updated: 2026-07-20 (v0.150.0 — green gate restored + F7 export-link fix)
Last updated: 2026-07-20 (v0.151.0 — Megosztás reload loop fixed + connect-address card)
> **2026-07-20 — v0.151.0 (Megosztás).** Closes **S-1/S-2/S-4-core/S-5** of
> `felhom.eu/documentation/audits/DIAG-sharing-2026-07-20.md`; **S-3 (no mDNS/Bonjour) stays OPEN**,
> awaiting Viktor's `smbutil lookup FELHOM` + `dns-sd -B _smb._tcp` from the Mac. **The `/sharing`
> page had been reload-looping at ~1.2 s for every customer with sharing enabled since v0.147.0** —
> `/sharing/status` coerced `idle`→`running` on the JOB phase channel, and the client answers a
> terminal `running` with a one-shot `location.reload()`, so the first poll of every steady-state
> page load re-armed it. The rule this leaves behind, now recorded against R-45 too: **a phase a
> client answers with a one-shot action is an EDGE — never synthesise it from a level, and serve it
> exactly once.** Both halves are server-side; `sharing.html`'s `<script>` is byte-identical to
> v0.150.0. `consumeIfRunning` is the serve-once half (`failed`/`needs_password`/in-flight are never
> consumed). Also new: the „Csatlakozás a megosztáshoz" card — Windows + Mac forms plus the direct
> `smb://<IP>`, read from the SAMBA container's netns because the controller is on a docker bridge
> and would answer `172.x`; **derived per render, cached nowhere** (the guest holds it by DHCP).
> Live-verified endpoint-level on 9201: `phase:"idle"` on 3/3 polls, and the card renders the real
> `192.168.0.104`. 23/23 green twice; 3 red-proofs. **The human "does the page sit still" check is
> still Viktor's** — no browser here.
> **2026-07-20 — v0.150.0.** **`go test ./...` on DooPlex is fully green again (23/23 packages, run
> twice) — no surviving `t.Skip`s, no weakened assertions, no deleted tests.** The 7 red
+121 -225
View File
@@ -1,245 +1,141 @@
# REPORT — green-gate restoration + F7 export-page fix + remediation-arc cleanup
# 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.149.0 → **v0.150.0**) · Trunk, pushed to `main`.
**Origin:** the three open hygiene items from the 2026-07-20 remediation arc (REPORT §Green gate,
§Observations) + R-53.
**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
## 1. 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 |
| Repo | Baseline @ start | Gate | After |
|------|------------------|------|-------|
| felhom-controller | `4646be1` | `HEAD == origin/main`, tree clean ✔ | `9f436c8` (code), then this docs commit |
| felhom.eu | `30fd9d3` | `HEAD == origin/main` ✔ | `1e11819` (ROADMAP) |
Deployed at start: `felhom-controller:0.150.0` on guest 9201 — verified `Up (healthy)`, not assumed.
Deployed controller **before**: `felhom-controller:0.149.0` (Up, healthy) — matched the baseline.
Deployed controller **after**: `felhom-controller:0.150.0` (Up 6 seconds, healthy).
## §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."*
## 2. Part 1 — the 7 red `internal/backup` tests
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.
### 2.1 Classification (1.1) — one class, not several
The v0.149.0 REPORT's device-identity hypothesis is **confirmed**, and the code said so in Hungarian
without anyone having to infer it. `TestSharesTier2MirrorsBothClasses` failed with:
```
LastStatus:no_target
LastError:/tmp/…/hdd_1: nincs másik fizikai meghajtó — a 2. mentéshez 2. meghajtó szükséges
```
Tier-2's whole purpose is to refuse a target on the source disk; it asks
`system.SamePhysicalDevice``isSameBlockDevice``statA.Dev == statB.Dev`
(`internal/system/mounts_linux.go:225`). On DooPlex every `t.TempDir()` lands on one filesystem, so a
fixture's `hdd_1` and `hdd_2` share an `st_dev`, the guard correctly refuses, and the test can never
reach the behaviour it exists to check. **The tests were right and the code was right — the
environment could not express the fixture's premise.**
| Test | Failing assertion | Root-cause class |
|---|---|---|
| `TestSharesTier2MirrorsBothClasses` | shares not mirrored; payload + marker missing; `LastStatus != ok` | device-identity |
| `TestSharesTier2SkipsDeadMountAndContinues` | "the healthy share must still be mirrored" | device-identity |
| `TestSharesTier2ReconcilePrunesRemovedShare` | precondition: dest dir absent after first run | device-identity |
| `TestSharesTier2NotifierNeverLeaksReservedKey` | "precondition: the notifier should have fired" | device-identity |
| `TestTier2V2_MigrationAndMarkerLast` | flat `appdata/` not removed; legs not mirrored; no marker | device-identity |
| `TestTier2V2_Reconcile` | stale sibling / unrelated dirs not reconciled away | device-identity |
| `TestTier2V2_NetworkExclusion` | picked `…/sys/felhom-data` instead of the local drive | device-identity |
**No test failed for a non-environmental reason** — no real defect was found hiding behind the red,
so there is no §1.1 STOP finding.
### 2.2 Fix — tool 2 (minimal seam), once, for all seven
Tool 1 (test-only) was considered and rejected: the only way to get two genuinely distinct devices
on this host is to place one fixture drive on `/mnt/5_hdd` and the other on `/tmp`, which hard-codes
DooPlex's disk layout into the tests — non-hermetic, and it would re-break on any other machine. Tool
3 (`t.Skip`) would have hidden seven real assertions behind an environment excuse.
**The seam** (`internal/backup/backup.go`) — one field + one wrapper, modelled on the package's
existing `tier2Mirror` / `tier2SSDFits` / `sharesPassdbCapture` nil-defaulted field seams:
```go
samePhysicalDevice func(a, b string) bool // nil → system.SamePhysicalDevice
func (m *Manager) sameDevice(a, b string) bool {
if m.samePhysicalDevice != nil { return m.samePhysicalDevice(a, b) }
return system.SamePhysicalDevice(a, b)
}
```
**Seam justification (one line, per §9.4):** the off-drive predicate is the single environmental fact
the tier-2 tests cannot supply on a one-filesystem host; routing it through a nil-defaulted field
makes it injectable while leaving production on the identical `st_dev` call.
Nil-check lives in the wrapper rather than in `NewManager` deliberately: `newSharesEnv` builds
`&Manager{…}` by literal, so a constructor-only default would leave the field nil and panic.
Seven call sites now go through `m.sameDevice(...)`: `tier2.go` ×5 (`selectTier2TargetFrom` ×3,
`RunTier2`, `Tier2Info`), `tier2_shares.go` ×1 (`RunSharesTier2`), `backup.go` ×1
(`hasOffDriveTarget`). Behaviour with a nil seam is byte-for-byte the previous code.
**Test-side** (`device_seam_test.go`, new): `oneDrivePerSubtree` models one drive per directory
subtree — two paths share a device only when one contains the other, which is exactly how real
mountpoints behave. Installed in the two fixtures only (`newTier2V2`, `newSharesEnv`).
Nothing was weakened: the guard still runs and still refuses same-device targets — the
same-drive refusal test `TestSharesTier2NeverTargetsItsOwnSourceDrive` passes under the seam, and it
is the test that would catch a fake that simply said "always different".
**Surviving `t.Skip`s: none.** No test was skipped, deleted, renamed, or had an expected value
changed.
### 2.3 Scenario B — mutation table
Every one of the seven had the defect it guards re-introduced in **production** code, one at a time,
then reverted:
| Test | Mutation | Result |
|---|---|---|
| `TestSharesTier2MirrorsBothClasses` | skip the payload mirror to the target | **FAIL** ✔ |
| `TestSharesTier2SkipsDeadMountAndContinues` | stop skipping `Disconnected/Decommissioned` drives | **FAIL** ✔ (`dead-mount share reached a mirror call`) |
| `TestSharesTier2ReconcilePrunesRemovedShare` | `tier2ReconcileRoots` → no-op | **FAIL** ✔ |
| `TestSharesTier2NotifierNeverLeaksReservedKey` | pass `SharesPseudoStack` raw on the success path | **FAIL** ✔ (`the reserved key reached the notification boundary raw: "_shares"`) |
| `TestTier2V2_MigrationAndMarkerLast` | skip the old-flat-`appdata/` migration cleanup | **FAIL** ✔ |
| `TestTier2V2_Reconcile` | `tier2ReconcileRoots` → no-op | **FAIL** ✔ |
| `TestTier2V2_NetworkExclusion` | disable the auto `sp.IsNetwork()` skip | **FAIL** ✔ |
Honest note on method: my first attempt at two of these mutated the wrong code path (the
mirror-failure notify branch instead of the success one; the unregistered-root skip instead of the
disconnected-drive one) and both tests stayed green. That was a bad mutation, not a hollow test — the
corrected mutations bite, and the notifier one reproduces that test's own documented red-proof
verbatim. Tree verified free of mutation residue afterwards (`grep -c "// MUT:"` → 0).
### 2.4 Scenario A — green gate, twice
| Run | Command | Result |
|---|---|---|
| build / vet | `go build ./...`, `go vet ./...` | **OK** |
| 1 | `go test ./... -count=1` | **exit 0 — 23 packages ok, 0 FAIL** |
| 2 | `go test ./... -count=1` | **exit 0 — 23 packages ok, 0 FAIL** |
`grep -c SKIP` over run 2 → **0**. The gate is genuinely green, not green-by-omission.
---
## 3. Part 2 — F7 / R-53, the export page
`app_export.html` L93 read `var domain = '{{.Stack.Meta.Subdomain}}' ? '{{…}}.{{$.CSRFToken}}' : '';`
— the session CSRF token where the customer domain belongs.
**A spec premise turned out to be wrong, and the fix needed one more line than planned.** §5 states
`{{$.Domain}}` is "set by `baseData`". It is (`handlers.go:120`) — but `exportPageHandler`
(`handler_export.go`) never calls `baseData`; it builds `{"Stack", "Drives"}` and `executeTemplate`
injects only `CSRFField` / `CSRFToken`. Swapping the token alone would have rendered an empty
domain — trading a wrong link for a broken one. So the fix is two lines:
- template: `{{$.CSRFToken}}``{{$.Domain}}`;
- handler: `"Domain": s.cfg.Customer.Domain` added to the map, mirroring `baseData`'s own line.
`csrfH()` and the meta-tag CSRF flow are the correct usage and are untouched.
**Tests** (`app_export_domain_test.go`, new — v0.149.0's render harness): the `var domain` line is
isolated first so an assertion cannot match the token where it legitimately appears (the meta tag).
| Scenario | Test | Result |
|---|---|---|
| C — joined from the customer domain, token absent | `TestAppExportDomainUsesCustomerDomainNotCSRFToken` | **PASS** |
| C — empty-subdomain branch still `''` | `TestAppExportDomainEmptyWithoutSubdomain` | **PASS** |
**Red-proof:** restored the pre-fix template line → both tests fail, showing the defect verbatim:
```
--- FAIL: TestAppExportDomainUsesCustomerDomainNotCSRFToken
export link must be built from the customer domain, got:
var domain = 'photos' ? 'photos.deadbeefcafebabe…' : '';
the CSRF token must NEVER appear in the export URL, got: … (same line)
--- FAIL: TestAppExportDomainEmptyWithoutSubdomain
```
Template restored; both green.
**Live verification (Scenario C)** — authenticated endpoint fetch of the real export page
`/stacks/immich/export` (no browser on DooPlex), ASCII-safe grep per the new gotcha:
```
line 237: var domain = 'photos' ? 'photos.demo-felhom.eu' : '';
meta tag: csrf-token" content="f7f928f1… ← the token lives here, and only here
```
The rendered link is correct and the token appears nowhere in it. Note this also confirms the
handler-side `Domain` key, which the render tests alone could not prove.
---
## 4. §3.4 — the orphaned `dhclient`
Evidence first. Exactly one match, on an interface that does not exist:
```
BEFORE: 1922 /sbin/dhclient -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases eth0
PPID 1652, started Mon Jul 20 07:25:30 (boot)
interfaces: lo enp1s0 wlp2s0 tailscale0 vmbr0 wg-felhom veth9201i0 ← no eth0
vmbr0: iface vmbr0 inet static / address 192.168.0.162/24 ← no dhclient should run at all
neither /run/dhclient.eth0.pid nor /var/lib/dhcp/dhclient.eth0.leases existed
```
Killed. Re-checked immediately and again ~25 min later: `pgrep -a dhclient`**none, no respawn**.
`vmbr0` still `192.168.0.162/24`, `felhom-agent` still `active`. No other host mutation.
---
## 5. Files changed
## Files changed (all in commit `badf17b`)
| File | Change |
|------|--------|
| `internal/backup/backup.go` | seam field + `sameDevice` wrapper; 1 call site routed |
| `internal/backup/tier2.go` | 5 call sites routed through the seam |
| `internal/backup/tier2_shares.go` | 1 call site routed; unused `system` import dropped |
| `internal/backup/device_seam_test.go` | **new**`oneDrivePerSubtree` |
| `internal/backup/tier2_v2_test.go`, `shares_test.go` | seam injected in the two fixtures (1 line each) |
| `internal/web/templates/app_export.html` | 1 token: `{{$.CSRFToken}}``{{$.Domain}}` |
| `internal/web/handler_export.go` | `"Domain"` added to the export page's data map |
| `internal/web/app_export_domain_test.go` | **new** — 2 tests + 2 helpers |
| `CLAUDE.md` | 2 gotchas in the live-validation section |
| `CHANGELOG.md` / `CONTEXT.md` / `REPORT.md` | v0.150.0 bookkeeping |
|---|---|
| `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 |
**Commits:** felhom-controller `9f436c8` (code) + this docs commit; felhom.eu `1e11819` (ROADMAP).
## 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.
## 6. ROADMAP corrections (felhom.eu)
| 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 |
- **R-50 factual correction.** The entry claimed an address move needs the cert's SAN set to cover
the new address. Read against source (`internal/agentapi/client.go` L105129): the controller→agent
leg sets `InsecureSkipVerify: true` and replaces chain verification with a `VerifyPeerCertificate`
that does a raw **SHA-256 match on the leaf DER**. Hostname/SAN never enters verification on this
leg, so an address move most likely needs **no cert re-issuance** — only the endpoint the guest
dials. The entry now says so, and still requires the spike to confirm it empirically.
- **R-53 collapsed** to its one-liner + **SHIPPED (controller v0.150.0)**, noting the handler-side
`Domain` key as part of the fix.
### 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. |
## 7. Not done / open
### Other gates
R-50 (durable F1 — spike-first, not started), R-51, R-52, R-39(b)/F6 — all untouched, as scoped.
`template_id_gate` · `emoji_gate` · `native_confirm_gate` · `offbox_rename_gate`**all OK**.
---
**`sharing.html`'s `<script>` block is byte-identical to v0.150.0** — verified by extracting the
block from `git show HEAD:…` and from the working tree and comparing (one block each, equal). Both
fixes are server-side, so the client contract is proven fixed rather than worked around.
## 8. Observations (noticed, not acted on)
## Deploy
1. **Four files in `internal/backup` are not `gofmt`-clean at HEAD**`offbox_3a_test.go`,
`recovery_unit.go`, `tier2.go`, `tier2_v2_test.go`. I verified this is **pre-existing** (checked
each file's HEAD blob through `gofmt -l` before my edits) and left them alone: `gofmt` is not part
of the green gate (`build`/`vet`/`test`), and reformatting them would have buried this task's real
diff in noise. Worth a one-shot `gofmt -w` pass in its own commit.
2. **`system.SamePhysicalDevice` is mount/device-granular, not disk-granular** — its own doc comment
says two partitions on one physical disk look "different" here, with the agent's durable-id as the
stronger guarantee. Unchanged by this work, but it means the tier-2 off-drive promise is
partition-level on a box that partitions one disk.
3. **The tier-2 tests silently stopped exercising their subject rather than failing loudly at the
premise.** Each failed on a downstream assertion ("share was not mirrored") instead of on "this
fixture needs two devices and this host has one". A fixture-level precondition check would have
turned a week of ambiguous red into one clear message.
```
./build.sh 0.151.0 --push → sha256:0141b62fe64e3d471446d8a1a8c1a719b3669dab26888e50acbf13644677510b, 145M
docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'
gitea.dooplex.hu/admin/felhom-controller:0.151.0 Up 26 seconds (healthy)
```
Clean-tree gate held before the build: `git status --porcelain` empty, `HEAD == origin/main`.
## Live validation
**Method: endpoint-level** — the exact endpoints the UI invokes, so no server logic is skipped, only
rendering. `claude-in-chrome` is not available on DooPlex.
1. **`GET /sharing/status` ×3, authenticated** → `{"data":{"error":"","phase":"idle","running":true},"ok":true}`
on all three calls. Before the fix the same call returned `phase:"running"` every time. The
client's reload path is reachable only from `phase === "running"`, so this is the server-side
proof that the loop's trigger is gone.
2. **`GET /sharing`, authenticated** → the connect card is present (2 `Csatlakoz` hits — the heading
and the Mac line) showing `smb://FELHOM` **and `smb://192.168.0.104`**. That address is the
guest's real current DHCP lease and is a DERIVED value: it matches what `ip -4 -o addr show eth0`
reports inside the samba container, and it exists as a literal nowhere in the codebase. Grepped
with ASCII-safe substrings per the CLAUDE.md trap.
3. **Human check — OPEN, Viktor's to give:** that the page visibly sits still. A browser is the one
method not available here.
## DIAG finding-status flips (`felhom.eu@eaa2173`)
S-1 → **FIXED v0.151.0** · S-2 → **SHIPPED v0.151.0** · S-4 → **CORE SHIPPED v0.151.0** (the unified
async-job layer stays R-45, which gained a one-line note recording the lesson) · S-5 → **SHIPPED
v0.151.0** · S-3 → **OPEN**, awaiting `smbutil lookup FELHOM` and `dns-sd -B _smb._tcp` from the Mac.
## Observations — noticed, not acted on
- **The poll timer's idle path.** `tick()` does call `stop()` in the idle branch, so on a steady-state
page the interval now ends after the first response — but the `.catch()` branch deliberately keeps
polling on a transient failure, so a page whose status endpoint is erroring polls forever at 1.5 s.
Recorded, not fixed: the JS was read-only in this task, and this is the next thing anyone auditing
that block will ask about.
- **Multi-tab success banner.** With two `/sharing` tabs open during a real bring-up, only the tab
that polls first sees „A megosztási szolgáltatás fut." Accepted, and documented at the method
itself; both tabs still show the true state, which comes from the `running` level and the
server-rendered badge.
- **`SMBRunning` in `sharingPageData` had no nil-manager guard** while the status handler beside it
did. Added (one `!= nil`) — noted because it is the only change in this task not traceable to a
DIAG finding.
- **`setup.DetectLocalIPs` is the same problem answered differently.** It needs a `HOST_IP` env var
because the controller sits on a docker bridge; the new read goes through the samba container
instead, which needs no env plumbing but only works while sharing is up. Neither is wrong — worth
knowing both exist if a third caller ever wants a guest IP.
- **The samba container has no `ps`** (found while establishing what the exec path could rely on) but
does have `/sbin/ip`. Any future in-container probe should verify its tool the way the catalog
healthcheck rule already requires.
- **The `.162` half of the original report is still worth a product answer.** The connect card now
gives customers the right address, but nothing stops a *stale saved favourite* on the Mac from
failing silently forever. Out of scope here; it is the kind of thing the S-3 mDNS work would make
moot by putting the box in the Finder sidebar.