From 372d6e599477fdb4fc459b0ebb923a6aabb56990 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 15 Oct 2026 10:29:47 +0200 Subject: [PATCH] =?UTF-8?q?Plex=20(R-1045):=20the=20controller=20asks=20Pl?= =?UTF-8?q?ex=20itself=20whether=20it=20is=20claimed=20=E2=80=94=20after?= =?UTF-8?q?=20the=20claim=20and=20on=20the=20household's=20=E2=80=9EK?= =?UTF-8?q?=C3=A9sz=E2=80=9D;=20no=20template=20probe=20(an=20older=20cont?= =?UTF-8?q?roller=20would=20refuse=20the=20press=20forever)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- CHANGELOG.md | 3 ++ controller/internal/web/media_events.go | 24 ++++++++++++++- controller/internal/web/plex_claim_test.go | 35 ++++++++++++++++++++++ controller/internal/web/setup_gate.go | 7 +++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c91a784..52bd341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,9 @@ Not released yet (bake → vouch → floor, `09` §3 decision 247). Design: felh removal pass's alone (`TestPlanDriveGates_SkipsSafelyRemovedDrive`, red before the fix); (2) the removal pass read the agent's `state`, which stays „attached" for a registered disk that is gone — presence is now a backing device (`TestEjectStep_G4` from the two measured records, red-proved). +- **„Kész" never opens an unclaimed Plex (R-1045)** — the controller asks Plex itself (`/identity` with the JSON Accept + header) both after the claim and on the household's press. The live template carries NO probe: a controller before + 0.313.0 would send no Accept header and refuse the press forever. `TestPlexDonePress_RefusedWhileUnclaimed` (red-proved). - **Old Sonarr/Radarr installs repaired by themselves (R-1047)** — only when /media is no longer bound: same-path root folder, `moveFiles:false` editor, rescan, old root removed, Radarr's collections too. `TestRepairOldRoot_K7` (red-proved: moveFiles=true is caught). diff --git a/controller/internal/web/media_events.go b/controller/internal/web/media_events.go index e5f86f9..9aaaf63 100644 --- a/controller/internal/web/media_events.go +++ b/controller/internal/web/media_events.go @@ -385,7 +385,7 @@ func (s *Server) appPlexClaimHandler(w http.ResponseWriter, r *http.Request) { return } for i := 0; i < 10; i++ { - if has, done, _, perr := s.stackMgr.SetupGateProbe("plex"); has && perr == nil && done { + if plexClaimed() { if oerr := s.stackMgr.OpenSetupGate("plex", stacks.SetupGateByProbe); oerr != nil && !errors.Is(oerr, stacks.ErrSetupGateNotClosed) { s.logger.Printf("[WARN] [web] plex: claimed, but opening the setup gate failed: %v", oerr) } @@ -442,3 +442,25 @@ func ejectStep(sp settings.StoragePath, disks []agentapi.DiskInfo) (int, *agenta } return ejectBack, disk } + +// plexClaimed asks the running Plex itself whether it is claimed (`/identity`, JSON only with this Accept header — +// measured 2026-10-15). A seam for tests. The live template carries NO setup_done_probe on purpose: a controller older +// than 0.313.0 would send no Accept header, read XML as „not JSON" and refuse the household's „Kész" forever. +var plexClaimed = func() bool { + req, err := http.NewRequest(http.MethodGet, "http://plex:32400/identity", nil) + if err != nil { + return false + } + req.Header.Set("Accept", "application/json") + resp, err := (&http.Client{Timeout: 5 * time.Second}).Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + var id struct { + MediaContainer struct { + Claimed bool `json:"claimed"` + } `json:"MediaContainer"` + } + return resp.StatusCode == http.StatusOK && json.NewDecoder(resp.Body).Decode(&id) == nil && id.MediaContainer.Claimed +} diff --git a/controller/internal/web/plex_claim_test.go b/controller/internal/web/plex_claim_test.go index 9270d37..0d7c69a 100644 --- a/controller/internal/web/plex_claim_test.go +++ b/controller/internal/web/plex_claim_test.go @@ -1,6 +1,10 @@ package web import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" "strings" "testing" @@ -37,3 +41,34 @@ func TestPlexClaimCodeShape(t *testing.T) { } } } + +// R-1045: the household's „Kész" never opens an UNCLAIMED Plex (it answers everything to a private address with no +// token); once Plex says claimed, the press opens it. No template probe is needed (an old controller would refuse the +// press forever with one). COMPANION RED-PROOF: drop the plexClaimed check in appSetupGateOpenHandler → the first press +// opens the gate (200). Restored. +func TestPlexDonePress_RefusedWhileUnclaimed(t *testing.T) { + s := gateHarness(t) + app := filepath.Join(s.cfg.Paths.StacksDir, "gapp") + if err := os.WriteFile(filepath.Join(app, ".felhom.yml"), []byte("display_name: Plex\nslug: plex\nsetup_gate: true\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := s.stackMgr.ScanStacks(); err != nil { + t.Fatal(err) + } + claimed := false + old := plexClaimed + plexClaimed = func() bool { return claimed } + defer func() { plexClaimed = old }() + press := func() int { + w := httptest.NewRecorder() + s.appSetupGateOpenHandler(w, httptest.NewRequest(http.MethodPost, "/apps/plex/setup-gate/open", nil), "plex") + return w.Code + } + if c := press(); c != http.StatusConflict { + t.Fatalf("„Kész” on an unclaimed Plex: HTTP %d, want 409 — the gate must stay closed", c) + } + claimed = true + if c := press(); c != http.StatusOK { + t.Fatalf("„Kész” on a claimed Plex: HTTP %d, want 200", c) + } +} diff --git a/controller/internal/web/setup_gate.go b/controller/internal/web/setup_gate.go index 1ffb72f..0a627fc 100644 --- a/controller/internal/web/setup_gate.go +++ b/controller/internal/web/setup_gate.go @@ -295,6 +295,13 @@ func (s *Server) appSetupGateOpenHandler(w http.ResponseWriter, r *http.Request, escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.probe_not_done")) return } + // R-1045 (v0.313.0): an UNCLAIMED Plex answers everything to a private address with no token — „Kész" never opens it + // before Plex itself says it is claimed (the claim box on the same page does that). + if found.Meta.Slug == "plex" && !plexClaimed() { + s.logger.Printf("[INFO] [web] setup gate plex: the household's press refused — Plex is not claimed yet (R-1045)") + escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.plex_claim.not_claimed")) + return + } if err := s.stackMgr.OpenSetupGate(found.Name, stacks.SetupGateByHousehold); err != nil { if errors.Is(err, stacks.ErrSetupGateNotClosed) { escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.not_closed"))