v0.151.0 — the Megosztás page stops reloading, and says how to connect

S-1: /sharing/status coerced idle->running on the PHASE channel, so the first
poll of every steady-state page load reported a terminal job that never ran and
the client's repaint-reload fired ~1.2s apart, forever. The coercion's real duty
(liveness must never be contradicted) belongs to the 'running' LEVEL field
beside it, and is now pinned by its own regression test.

S-4 core: a terminal 'running' is served exactly once, so a REAL bring-up cannot
re-arm the reload on the page it just caused. failed/needs_password/in-flight
are never consumed. Unified async-job feedback stays the ROADMAP item.

S-2/S-5: new connect card with the Windows form, the Mac form and the direct
smb://<IP>, read from the SAMBA container's netns (the controller is on a docker
bridge and would answer 172.x). Derived per render, cached nowhere - the address
is a DHCP lease. Underivable => the line is omitted.

sharing.html's <script> block is byte-identical to v0.150.0. Red-proofed three
ways. 23/23 packages green.
This commit is contained in:
2026-07-20 10:46:21 +02:00
parent 8db9232dea
commit badf17bebd
13 changed files with 663 additions and 14 deletions
+39 -9
View File
@@ -122,7 +122,17 @@ func (s *Server) sharingPageData() map[string]interface{} {
data["SMBEnabled"] = smb.Enabled
data["SMBServerName"] = smb.EffectiveServerName()
data["SMBUserSet"] = smb.UserSet
data["SMBRunning"] = s.stackMgr.SambaRunning()
data["SMBRunning"] = s.stackMgr != nil && s.stackMgr.SambaRunning()
// „Csatlakozás a megosztáshoz" (v0.151.0, S-2): the page has always shown the configured NAME and
// never an address, so a customer whose network does not resolve the name had nothing to fall
// back on and guessed — which is how this diagnosis started, with the Proxmox HOST's IP typed
// into Finder (DIAG-sharing-2026-07-20.md). Derived FRESH on every render and cached nowhere:
// the guest holds this address by DHCP, so a stored copy is a copy that eventually misdirects
// people (S-5). "" simply omits the line — an address-less page beats a wrong address.
if smb.Enabled {
data["SMBDirectAddress"] = s.sambaLANAddress()
}
type shareRow struct {
Name string
@@ -177,6 +187,18 @@ func (s *Server) sharingPageData() map[string]interface{} {
return data
}
// sambaLANAddress resolves the connect-address seam. Never cached at this level either — the seam
// exists so tests can supply an address without docker, not so anyone can memoize one.
func (s *Server) sambaLANAddress() string {
if s.sambaAddrFn != nil {
return s.sambaAddrFn()
}
if s.stackMgr == nil {
return ""
}
return s.stackMgr.SambaLANAddress()
}
func (s *Server) sharingPageHandler(w http.ResponseWriter, r *http.Request) {
data := s.sharingPageData()
if f := strings.TrimSpace(r.URL.Query().Get("flash")); f != "" {
@@ -231,20 +253,28 @@ func (s *Server) sharingEnableHandler(w http.ResponseWriter, r *http.Request) {
sharingRedirect(w, r, "Beállítás mentve. A megosztási szolgáltatás előkészítése folyamatban…")
}
// sharingStatusHandler is the 4b poll target (GET /sharing/status). Reports the ensure job's phase
// plus the live container state, so a page loaded AFTER the job finished (or after a restart, when
// the in-memory job is gone) still shows the truth.
// sharingStatusHandler is the 4b poll target (GET /sharing/status). It carries TWO independent
// channels in one envelope, and keeping them apart is the whole point of this handler:
//
// phase — the ensure JOB. The client treats a terminal `running` as an EDGE ("the bring-up I was
// watching just succeeded") and reloads once to repaint the server-rendered badge.
// running — the service LEVEL, straight from the liveness probe. True whenever the container is up,
// with or without a job, and it is what makes a page loaded after the job finished (or
// after a controller restart, when the in-memory job is gone) still show the truth.
//
// v0.147.0 coerced `idle` → `running` here so that liveness could never be contradicted by a missing
// job. That duty belongs to — and was already discharged by — the `running` field beside it; on the
// phase channel the same value reads as a fresh terminal edge, so the client reloaded on the FIRST
// poll of every steady-state page load and the page looped at ~1.2s forever
// (felhom.eu/documentation/audits/DIAG-sharing-2026-07-20.md, S-1). The coercion is gone: no job,
// no edge. See consumeIfRunning for the other half — a REAL bring-up must be reported exactly once.
func (s *Server) sharingStatusHandler(w http.ResponseWriter, r *http.Request) {
phase := sambaPhaseIdle
errMsg := ""
if job := s.sambaEnsure.snapshot(); job != nil {
if job := s.sambaEnsure.consumeIfRunning(); job != nil {
phase, errMsg = job.Phase, job.Error
}
running := s.stackMgr != nil && s.stackMgr.SambaRunning()
// A stale `idle`/`running` job must never contradict reality: liveness wins on a fresh page.
if phase == sambaPhaseIdle && running {
phase = sambaPhaseRunning
}
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{
"phase": phase, "error": errMsg, "running": running,
})