Plex (R-1045): the controller asks Plex itself whether it is claimed — after the claim and on the household's „Kész”; no template probe (an older controller would refuse the press forever)
gates / gates (push) Successful in 45s
gates / gates (push) Successful in 45s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -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
|
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
|
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).
|
(`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
|
- **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`
|
folder, `moveFiles:false` editor, rescan, old root removed, Radarr's collections too. `TestRepairOldRoot_K7`
|
||||||
(red-proved: moveFiles=true is caught).
|
(red-proved: moveFiles=true is caught).
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ func (s *Server) appPlexClaimHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i := 0; i < 10; i++ {
|
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) {
|
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)
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"))
|
escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.probe_not_done"))
|
||||||
return
|
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 err := s.stackMgr.OpenSetupGate(found.Name, stacks.SetupGateByHousehold); err != nil {
|
||||||
if errors.Is(err, stacks.ErrSetupGateNotClosed) {
|
if errors.Is(err, stacks.ErrSetupGateNotClosed) {
|
||||||
escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.not_closed"))
|
escrowJSON(w, http.StatusConflict, nil, s.msg(r, "err.setup_gate.not_closed"))
|
||||||
|
|||||||
Reference in New Issue
Block a user