summaryrefslogtreecommitdiffstats
path: root/docs/mediacenter.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-02 17:31:50 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-02 17:31:50 +0200
commit7b25f1c09ba1b8692988d9616f3c33c97af9f3ca (patch)
tree1a7d346ea6a3847269df4aeff3c6b41d515b5d7b /docs/mediacenter.md
parent10f8266e7152d7dc38dbfe2449327829bf020ad1 (diff)
downloadmeshbay-7b25f1c09ba1b8692988d9616f3c33c97af9f3ca.tar.gz
fix(hub): stop the maintenance loop racing the tests, and pin _ASSETS
Two defects found while closing out the Search merge, neither of them in that feature. The maintenance loop. create_app's lifespan starts cleanup_loop as an asyncio task, so every test — each entering that lifespan — ran a purge pass concurrently with its own requests. On SQLite :memory: that is not merely noisy: the engine uses a StaticPool, one connection for the whole process, so the request's session and the cleanup task's session interleave transactions on the same connection. A registration could commit and then be invisible to the login three lines later, surfacing as 401 Invalid credentials for an account created moments before, in roughly one run of test_node_ws_auth.py in four. The purge itself is not at fault and this is not a production condition. A passive SQL listener caught the DELETE removing 0 rows, and the INSERT carrying status='active' — so neither the pending-account mechanism nor the purge filter is involved, and PostgreSQL gives every session its own connection. What the fixture removes is the second user of the shared one. 60 runs of the previously flaky file, 0 failures; reproductions before the fix landed on attempts 4, 6, 13 and 29 of separate loops, so a clean run of 60 has about a 1% chance of being luck. _ASSETS. source-merge.js shipped missing from webapp._ASSETS, the cache-busting hash's input list — exactly the silent failure docs/apps.md §4 step 5 warns about: the file changes, the asset URL does not, and a browser holding the old page keeps the old copy. Harmless this time only because search-page.js changed in the same commit and is listed, which is the worst way for it to go unnoticed. Found by re-reading that checklist for the doc pass, not by any test — so there is a test now, holding _ASSETS to every .js in static/ (sw.js excepted, unversioned on purpose). It was the only one missing. Phase 9 of docs/refactoring-search.md also lands here: mediacenter.md §10.6, musicbay.md §9b, photos.md §10b, apps.md §2b and step 5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbwJDbNTkiRUh7HTWEoyss
Diffstat (limited to 'docs/mediacenter.md')
-rw-r--r--docs/mediacenter.md43
1 files changed, 43 insertions, 0 deletions
diff --git a/docs/mediacenter.md b/docs/mediacenter.md
index dbdf85c..d714366 100644
--- a/docs/mediacenter.md
+++ b/docs/mediacenter.md
@@ -904,6 +904,49 @@ only when there is nothing else, and the lowest *number* rather than the first
entry so it does not quietly depend on `buildSeasons` keeping its sort.
`test_video_default_season.py` — no input it takes can carry a thumbnail.
+### 10.6 The same film twice in the cross-group Search view (2026-09-02)
+
+An operator hosting two groups gave both the *same* video directory — which is
+the point of having two groups: different people are invited to different
+libraries, and one library may be in several of them. **Search files** then
+showed every film as two poster cards and every episode twice in the season
+list, one copy badged per group. Flat list too.
+
+Not a Videos bug. Inside a group it cannot happen: `GroupIndex` is keyed by
+blake3, so the same bytes at two paths are already one entry. `search-page.js`
+concatenates *N* independently keyed indexes into one list, and that is where
+the duplication is born.
+
+`source-merge.js` folds entries on the content hash and resolves **one source
+per unit** — a film, a whole show — rather than per file: a season split across
+two nodes would open two connections and two metadata lookups for one show. A
+group hosted by the reader's own node wins (read from `handshake_ack`'s
+`is_node_admin`, which the node computes from its own record of who it belongs
+to, never a hub claim); failing that the pick is `hash(unitKey + userId)`,
+stable for one reader across renders and reloads — a source that changed
+mid-stream would tear down the connection under a film that is playing — and
+spread across readers.
+
+The units come from **`groupVideoEntries` itself**, called on the un-merged
+list purely to learn them, never a second copy of its keys in the Search page.
+A copy would keep agreeing until one of them changed, and the symptom would be
+a show whose episodes stream from two different nodes.
+
+Two consequences worth knowing:
+
+- **An operator's "Fix match" and "Rematch" go to the chosen source's node.**
+ On the operator's own libraries that is their node, which is what they mean.
+ On a merged entry they do not host, the override lands on whichever group was
+ picked — and the other source keeps its own match.
+- **`PosterGrid.mergedShows` still merges two differently-parsed titles once
+ both resolve to the same TMDB id.** Those were two units when the source was
+ picked, so a merged card can hold two sources. Left alone deliberately:
+ re-picking under a card the reader is looking at is worse than a mixed one.
+
+Full design, the adversary this names, and what must not change:
+`docs/refactoring-search.md`. `test_search_source_merge.py` holds the rules,
+`test_search_media_merge.py` holds this symptom end to end.
+
## 11. Acceptance before shipping
1. Re-run the §3 validation (real TMDB calls, same corpus, same script