gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitc171227ec171227emission 054: hide adult/unknown titles from the public lists and the search; in-app adult-flag backfill (TMDB details + poster per title, resumes), gate + real-data proofmrec171227e/STATUS.md

65.8 KB

  1. # STATUS — tracker.worldapi.org
  2. ## 2026-10-01 — mission 054: adult titles hidden from the public lists + the adult-flag backfill (NOT deployed)
  3. **Built**: `shows.adult` true/false/null(unknown). Public lists (catalog.hl: homepage rows, `/movies`, `/shows`) and the
  4. search (search.hl `titleOk`; "Fetch from web": `include_adult=false` + only results with `adult: false`) show ONLY
  5. `adult == false` (shows.hl `isPublicTitle`) — unknown is hidden, so the first deploy is safe. `/my/*` and the show page
  6. are not filtered (a followed adult title stays reachable). The daily sync, the search import and the backfill store
  7. TMDB's flag. **Backfill** in the app (project.hl `backfillTick` → tmdbsync.hl `backfillTitle`): one title per step on a
  8. 0.1 s clock shared with the daily sync (which wins), `GET /movie|tv/<id>?append_to_response=external_ids` + the poster
  9. when missing, 260 ms pause per API request; 404 → `adultCheck='TMDB 404'` (hidden, never asked again); no answer/429/5xx
  10. → retried (≤ 2 per start); resumes after a restart (queue = what is still unknown); log every 500 + done line. Env
  11. `TRACKER_BACKFILL=0` off, `TRACKER_BACKFILL_LOG_EVERY`. Details: README "What it does (mission 054)".
  12. **Decision / Hybriel finding**: hiding the SHOW PAGE from non-followers was built and dropped. `found = showRow != null &&
  13. !adultHiddenFor(showRow, me)` (a static reaching mpackdb, reading `me`) → after any session face on the page
  14. (`showToggleEpisode`) the browser re-derives `found` and then `rows`, whose `buildRows` reaches mpackdb → console
  15. `'watchesOfUser' is a static of 'Watches' whose initializer reaches 'watchesTable', a name the realm 'client' … cannot bind`,
  16. the click does nothing (gate: "ep3 solid" timeout). A plain copy in between (`hideAdult = hiddenAdult`) did not stop it.
  17. So the show page of an adult title opens for anyone who has the link (it is in no list and no search).
  18. **Gate** (167 → 184 checks):
  19. ```
  20. TRACKER_GATE_SHOTS=/tmp/w054/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  21. (backfill second start: 8 titles left, 6.614 s; 285 pages asked during it, slowest 634 ms; adult backfill done: 8/8 titles, adult=0 notAdult=7 unknown=1 posters=7 ids=2 requests=8 errors=1 retries=0 seconds=6.714)
  22. 184 passed, 0 failed
  23. ```
  24. New block (end of the gate, before the PWA part): `tests/seed-adult.hl` (app stopped) = 1 adult movie alice follows +
  25. 14 unknown (fake TMDB: 1 adult, 1 404). Backfill off: none on `/`, `/movies` (2 pages), `/shows`, search; the adult one on
  26. alice's `/my/movies` and its page; web search drops TMDB's adult result. Backfill on with 300 ms per fake answer: stopped
  27. after 6 → restart asks only the other 8 (resume; mpackdb `update` survives SIGTERM without `persist`), pages < 1.2 s
  28. meanwhile (slowest 634 ms), lists + search updated at once, poster served, 3rd start "nothing to do". All other fixtures
  29. are `adult = false` (seed-show.hl); `startServer()` runs with `TRACKER_BACKFILL=0` unless a block switches it on; on a
  30. throw the gate now prints the browser console + server log tail. Screenshot `adult-home-1280.png` (looked at).
  31. **Real data** (copy of Byrodin `storage/` 2026-10-01 07:37, real TMDB, :8730, `TRACKER_SYNC=0`, deleted afterwards):
  32. ```
  33. adult backfill: start, 9453 titles without an adult flag (65 ms to find them)
  34. adult backfill done: 9453/9454 titles, adult=3450 notAdult=5866 unknown=137 posters=8740 ids=6459 requests=9454 errors=140 retries=1 seconds=5935.021
  35. ```
  36. - 98.9 min, ~0.63 s per title. Adult 3,450 (3,448 movies, 2 series), not adult 5,866, unknown 137 (all TMDB 404 →
  37. `adultCheck`), errors 140 = 137 × 404 + 2 poster 404 + 1 TLS "no answer" (retried OK). Posters: 126 → 8,866 files,
  38. 459 MB (adult titles' posters included). A restart afterwards: `start, 0 titles` / `nothing to do`.
  39. - Lists after: `/movies` 4,242 movies (177 pages), `/shows` 1,624 series (68 pages). EVERY slug on `/`, all 245 list
  40. pages and 10 searches (the, sex, porn, slut, milf, girl, xxx, anal, teen, hot): 6,008 links → all `adult == false`
  41. (0 others). TMDB-non-adult titles like "Porn King: The Rise & Fall of Ron Jeremy" (2022 doc) and "Slut." (2004) stay.
  42. - Page latency DURING the run (curl every ~5 s, 1,155 samples each): `/` avg 0.156 s max 4.87 s, `/movies` avg 0.059
  43. max 1.67, `/shows` avg 0.045 max 1.54, `/search/the` avg 0.075 max 1.33; 14 of 4,620 > 1 s. Cause: 13 steps > 1.5 s
  44. (one 5.8 s) — all a slow TMDB/image answer (movies, no catalog work); hl:fetch blocks the one request loop.
  45. - After: full load (Chrome, signed out) 390/1280: `/` 0.57 (cold)/0.09 s, `/movies` 0.07/0.04, `/shows` 0.04/0.04,
  46. `/search/the` 0.46/0.08 s, 0 console problems. Screenshots `/tmp/w054/real/{home,movies,shows,search-the}{,-bottom}-{390,1280}.png`
  47. on Loreana — looked at: no adult title or poster. Logs `/tmp/w054/real-backfill.log`, `real-latency.log`, `real-restart.log`.
  48. How to repeat (ports 8730/8731; ONLY `storage/`; note the inner ssh needs `< /dev/null` inside `ssh loreana bash -s`):
  49. ```
  50. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  51. TRACKER_PORT=8730 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realsess/ TRACKER_URL=http://127.0.0.1:8730 ./bin/hybriel project.hl
  52. # progress: grep 'adult backfill' in its log; afterwards stop it, rm -rf the copy + sessions
  53. ```
  54. **Open**: (1) deploy: the live app starts the backfill at once (~100 min, ~450 MB of posters into `storage/` → the
  55. nightly backup); until it reaches a title the lists are emptier (all unknown at the first start). (2) A slow TMDB answer
  56. blocks pages for its duration (max seen 4.9 s) — needs a non-blocking fetch in Hybriel or shorter timeouts (20 s now).
  57. (3) Posters of adult titles are downloaded too (served only by their file name; could be skipped to save ~170 MB).
  58. (4) The show page of an adult title is not hidden (Hybriel limit above). (5) 137 titles TMDB no longer has stay hidden.
  59. (6) Seen in passing: signed out at 390 px the header buttons wrap to two lines (pre-existing).
  60. ## 2026-10-01 — mission 053: #15 (TVmaze) and #14 (search) merged into main (with #13)
  61. **Merges** (on main, never pushed): `git merge t15-tvmaze` → `03edc78`, then `git merge t14-search` → `dcc5eec`.
  62. Branches t14-search / t15-tvmaze and worktrees tracker-wt14 / tracker-wt15 removed afterwards.
  63. **Conflicts** (both sides kept): README (intro steps 13/14/15, "What it does" sections, Test block, Files rows merged),
  64. STATUS (all entries, newest on top), `tests/realdata-check.mjs` (#13's page list + #15's env REAL_PORT/REAL_CHROME/
  65. REAL_OUT/REAL_SHOW; REAL_SHOTS still works), `project.hl` (imports of both), `styles.hl` (both blocks),
  66. `tests/browser.mjs` (fixtures: movie 1005 + TVmaze episode lists + thetvdb 4004→504; screenshot list = all pages).
  67. **Behaviour fixes from combining**: (1) #13's 32 extra fixture titles were named "Gate …" and flooded #14's "gate"
  68. search checks → renamed "Pager Movie NN / Pager Future Movie / Pager Undated Movie / Pager Older Series"
  69. (`tests/seed-show.hl`). (2) `search.hl` `importTitle` now calls `catalog.hl` `refreshCatalogShow` → an imported title is
  70. on /movies, /shows and the home rows at once (+1 gate check; the offline check's home Shows row is 4 tiles now).
  71. **Gate**:
  72. ```
  73. TRACKER_GATE_SHOTS=/tmp/w053/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  74. 167 passed, 0 failed
  75. ```
  76. **Real data** (copy of Byrodin `storage/` 2026-10-01, :8730, `az5b2`, `TRACKER_SYNC=0`, deleted afterwards; procedure =
  77. ticket #8; session file `created`/`seen` = now ms): search index 0.58 s at boot. Server (curl): `/` 6 ms, `/movies` 11,
  78. `/shows` 8, `/search/doctor%20who` 14, `/search/the` 56, `/my/shows` 178, `/my/unwatched` 1204, `/my/schedule` 252,
  79. `/my/movies` 6 ms. Full load in Chrome (`REAL_OUT=/tmp/w053/real node tests/realdata-check.mjs` + a search timing),
  80. 390 / 1280: `/` 1.32 (cold) / 0.08 s, `/movies` 0.11 / 0.04, `/shows` 0.10 / 0.09, `/search/doctor%20who` 1.04 (cold) /
  81. 0.06, `/search/the` 0.12 / 0.66, `/my/shows` 1.27 / 0.26, `/my/unwatched` 1.39–2.0 / 1.37–1.39, `/my/schedule` 0.33 /
  82. 0.27, show page 0.11 / 0.11 s; 0 console problems, no sideways scroll. Screenshots `/tmp/w053/real/` + gate shots
  83. `/tmp/w053/shots/` on Loreana (looked at).
  84. **Open**: `/my/unwatched` (1594 rows) is still the slowest page (server 1.2 s); not deployed; open points of #13/#14/#15
  85. below stay (adult titles, imported titles not followed → no daily sync, …).
  86. ## 2026-10-01 — ticket #14 (mission 052, branch t14-search, worktree tracker-wt14): search
  87. **Built**: header magnifier → `/search` / `/search/<text>` (`components/search.hl`), `search.hl` (in-memory word-prefix
  88. index over 9453 titles + 15157 people, built at boot; TMDB `search/multi`; `importTitle` = new show record + the daily
  89. sync's `syncShow`). Signed out: search + web work, "Add" opens the sign-in modal. Details: README "What it does (ticket #14)".
  90. **Decisions**: address `/search/<text>` instead of `/search?q=` (hl:web gives a page no query string; `/search?q=x` opens
  91. the empty page). Min 2 letters. ≤ 30 titles + ≤ 10 people. One TMDB request per "Fetch from web" (button, not per
  92. keystroke). Web list leaves out what we have by TMDB id + kind. Import is not auto-follow (so the daily sync won't
  93. refresh it later — architect's call). Genres matched to ours by name; cast not imported. People link to `/person/<slug>`
  94. (no page yet, like the show page's cast).
  95. **Measured on Hybriel**: a module `static x = f()` is evaluated LAZILY (never, if nothing reads `x`) — the index is built
  96. by `ensureIndex()` called from a `project.hl` member. Static hashes/lists in a module can be mutated in place
  97. (`buckets[k].push(i)` works, 200k pushes 174 ms). The server percent-decodes the path BEFORE routing (`%2F` → 404, invalid
  98. UTF-8 passes through raw); a client-side navigation hands the param still encoded. A record literal entry `id = …`
  99. shadows a local `id` for later entries (bit me: `tmdbId = id` took the new 16-hex id).
  100. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~07:00, deleted afterwards; dev server :8770, az5b2, real TMDB):
  101. index build 0.60 s at boot (log `search index: 9453 titles, 15157 people, 4180 keys, 612 ms`); `dbSearch` 0–1 ms for
  102. "doctor who"/"star trek"/"sopranos", 38–44 ms for "the"/"th" (2000+ candidates); server render `/search/doctor%20who`
  103. 9 ms, `/search/the` 56 ms; browser: per keystroke → results + address 56–76 ms (incl. CDP), `/search` cold 0.85 s / warm
  104. 0.04 s; Fetch from web "pluribus" 0.12–0.23 s (real TMDB); Add "Pluribus (2025)" tv 225171 → `/shows/Pluribus` in
  105. 0.77–1.1 s: Season 1 9 episodes, poster, TMDB/IMDb/TVDB/TVmaze, genres Drama + Sci-Fi & Fantasy; afterwards found in our
  106. database and gone from the web list; 0 console problems. Screenshots `/tmp/w052-t14/real/*.png` on Loreana (looked at).
  107. How to repeat (ports 8770–8772; ONLY `storage/` is copied):
  108. ```
  109. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata
  110. cp -r .scratch/realdata/storage .scratch/probe-store # the import WRITES — work on a second copy
  111. TRACKER_PORT=8770 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_STORAGE=$PWD/.scratch/probe-store/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8770 ./bin/hybriel project.hl
  112. # AFTER the server is up (it sweeps old session files at boot): session file name = sha256(<32-hex id>), JSON
  113. # {"created":<now ms>,"data":{"tag":"t"},"id":"<id>","seen":<now ms>,"user":{"id":"0mujqspn0aqb"}}; the id into .scratch/real-sid
  114. TRACKER_REAL_WEB=pluribus node tests/realdata-search.mjs # TRACKER_REAL_IMPORT=0: no import
  115. # afterwards: stop the server, rm -rf .scratch/realdata .scratch/probe-store .scratch/realdata-sessions .scratch/real-sid
  116. ```
  117. **Gate** (111 → 136 checks):
  118. ```
  119. TRACKER_GATE_SHOTS=/tmp/w052-t14/shots TRACKER_GATE_PORT=8773 TRACKER_GATE_IDENT_PORT=8774 TRACKER_GATE_CHROME=8776-8778 TRACKER_GATE_TMDB_PORT=8779 node tests/browser.mjs
  120. (in-app run 5.413 s; 12 pages asked during it, slowest 1301 ms)
  121. 136 passed, 0 failed
  122. ```
  123. Fake TMDB: `/3/search/multi` (→ `fake.searchRequests`, w92 thumbs → `fake.thumbRequests`, both kept out of
  124. `fake.requests` so the sync checks are untouched); tv 1004 + movie 1005 importable, tv 1006 in the search but 404 (the
  125. failed import = the hybriel#121/#122 check), TVmaze thetvdb 4004 → 504. Screenshots `/tmp/w052-t14/shots/search*.png`
  126. (+ the existing ones; header still one row at 390 with the magnifier) — looked at.
  127. **Merge notes** (#13 likely touches the same): `project.hl` (imports, one route, `searchIndexReady` before `server`),
  128. `components/main.hl` (the magnifier inside `userBox`), `styles.hl` (one block before the `@media` block),
  129. `tests/browser.mjs` (fixtures at the top, a signed-out block before the "#3 borders" checks, `search` in the 390/1280 page
  130. list, a signed-in block before "#10 THE INSTALLABLE APP"), `tests/faketmdb.mjs`, README (intro, a section, Test, Files).
  131. **Open**: no person page (`/person/<slug>` 404s, like the cast links). Imported titles aren't followed → not re-synced.
  132. Web search is open to signed-out visitors (one TMDB request per click; no rate limit of ours). The field's autofocus is proven
  133. on a full load only (real-data run: `focused: searchinput`); after the client-side click on the magnifier it is not checked,
  134. and not on a real phone.
  135. ## 2026-10-01 — ticket #15 (mission 052, worktree tracker-wt15, branch t15-tvmaze): TVmaze merge in the sync
  136. **Built**: `tmdbsync.hl` `mergeTvmaze` (after the TMDB step, series with a tvmzId): `GET /shows/<tvmzId>/episodes`, by
  137. S/E number: ADD missing episodes/seasons, FILL empty/placeholder titles + empty air dates; never overwrites a TMDB
  138. value; TMDB's own placeholder ("Episode 4") no longer overwrites a real title; TMDB later adopts TVmaze rows by
  139. number. `numberingAgrees` skips shows TVmaze numbers differently (≥ 80 % of shared S/E within 1 day or same title).
  140. Details: README "What it does (step 15)". Tool line + totals: `tvmazeEpisodes tvmazeSeasons tvmazeFilled tvmazeSkipped`.
  141. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~07:00, deleted afterwards; procedure = ticket #8 below, server
  142. on :8739, `REAL_PORT=8739 REAL_CHROME=8738 REAL_OUT=/tmp/w052-tvmaze/real REAL_SHOW=/shows/The-Lord-of-the-Rings-The-Rings-of-Power node tests/realdata-check.mjs`):
  143. ```
  144. run 1: shows=126 newEpisodes=0 updatedEpisodes=0 requests=255 tvmazeRequests=126 errors=0 tvmazeEpisodes=0 tvmazeFilled=21 tvmazeSkipped=7 seconds=178.9
  145. run 2: updatedEpisodes=8 (Greek Salad "Épisode n" → "Episode n", after "Épisode" became a placeholder) tvmazeFilled=0 tvmazeSkipped=6 seconds=174.9
  146. run 3: all 0, tvmazeSkipped=6, seconds=169.9 (idempotent)
  147. ```
  148. Filled: Rings of Power S3E1–8 real titles (TMDB "Episode n", airs 2026-11), Last Week Tonight 8x26–30. No episode
  149. was missing on TMDB that day (tvmazeEpisodes=0) — the add path is proven by the gate only. Skipped (checked by hand,
  150. correct): Daily Show (TVmaze seasons by year), Star Trek: Prodigy + Blood & Treasure (shifted numbering), Alex Rider,
  151. Intergalactic, The Rising (other premiere dates + placeholder titles). Loads (390 / 1280): /my/shows 1.51 / 0.26 s,
  152. /my/unwatched 1.46 / 1.41 s, /my/schedule 0.36 / 0.29 s, Rings of Power 0.13 / 0.13 s direct, 0 console problems.
  153. Screenshots `/tmp/w052-tvmaze/real/*.png` on Loreana (looked at), logs `sync{1,2,3}.log`, `skipped*.txt`, `filled.txt`.
  154. **Gate** (111 → 117 checks):
  155. ```
  156. TRACKER_GATE_SHOTS=/tmp/w052-tvmaze/shots TRACKER_GATE_PORT=8732 TRACKER_GATE_IDENT_PORT=8733 TRACKER_GATE_CHROME=8734-8737 TRACKER_GATE_TMDB_PORT=8738 node tests/browser.mjs
  157. 117 passed, 0 failed
  158. ```
  159. Screenshots `tvmaze-{schedule,show}-{390,1280}.png` looked at.
  160. **Open**: adds a new TVmaze-only episode on real data not yet seen live; the daily run takes ~70 s longer (1 TVmaze
  161. request/series). Shows skipped by the numbering check get no TVmaze help (by design).
  162. ## 2026-10-01 — ticket #13 (mission 050): homepage, /movies, /shows, /my/movies
  163. **Built**: `/` (`components/home.hl`): signed in 4 icon tiles → `/my/unwatched` `/my/schedule` `/my/shows` `/my/movies`;
  164. for everyone the text, "Movies" (→ `/movies`, 10 latest RELEASED movies) and "Shows" (→ `/shows`, 10 series by newest
  165. released episode) as poster rows (phone: sideways scroll inside the row; ≥ 40rem grid of 5). `/movies`, `/shows`
  166. (`components/movies.hl`, `allshows.hl` → shared `components/tilelist.hl`): all, 24 per page, pagination =
  167. components.hybriel `pagination.hl` copied verbatim (4b4dabe). `/my/movies` (`components/mymovies.hl`); `/my/shows` is
  168. series only now. `catalog.hl`: lists built once at start, `refreshCatalogShow` after each synced show (`project.hl`
  169. `syncTick`), day roll on first read of a new day. `follows.hl` `insertByFollowDesc` (moved from myshows.hl),
  170. `shows.hl` `allShows`/`allEpisodes`.
  171. **Decisions**: page in the PATH (`/movies/page/2`), not `?page=2` as the mission said — a component can't read the
  172. query (hybriel#11) and Back drops it (hl:web `popstate` uses `location.pathname`), so `?page` would mean full reloads +
  173. Back to page 1. Pagination click → host: the copied component has no data out (hybriel#87 ruling: a passed function),
  174. so a `pager-box { on click }` around it reads the button text — no edit of the copy. One page → no pagination.
  175. /movies = ALL movies incl. future-dated (first) and undated (last); the homepage row only released ones. Series without
  176. a released episode come after the others, by their own `release`. Home text says episodes come from TMDB (TVmaze is
  177. only used for ids — the mission's "TMDB/TVmaze" would be wrong). Sign-in hint only when signed out.
  178. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~06:45, deleted; `az5b2`, :8730, procedure = ticket #8 below; the
  179. session file needs `created`/`seen` = now in ms, `1` is swept at boot): boot → first answer 1.47 s (catalog build).
  180. Server time (curl) 5–7 ms for `/`, `/movies`, `/movies/page/200`, `/shows`, `/shows/page/30`, `/my/movies`. Full load
  181. in a FRESH Chrome per page (document + modules + socket + every poster in view), signed in / out:
  182. | page | 390 in / out | 1280 in / out |
  183. |---|---|---|
  184. | `/` | 1.11 / 1.34 s | 1.13 / 1.29 s |
  185. | `/movies` | 0.30 / 0.91 s | 0.57 / 0.57 s |
  186. | `/movies/page/2` | 0.53 / 0.63 s | 1.31 / 0.50 s |
  187. | `/shows` (24 real posters) | 1.07 / 1.36 s | 1.37 / 1.11 s |
  188. | `/my/movies` | 0.80 / 0.66 s | 0.83 / 0.58 s |
  189. Same Chrome (`node tests/realdata-check.mjs`): `/movies`, `/shows`, `/my/movies` 0.04–0.45 s; a page change by the
  190. pagination 60–75 ms (client-side); Back works; no sideways scroll; 0 console problems. `refreshCatalogShow` for all
  191. 126 followed shows: 0.31 s (SNL 23 ms). Screenshots `/tmp/w050/real/*-{390,1280}.png` (looked at). Not mine but seen:
  192. `/my/unwatched` 1.47–1.64 s (1594 rows), `/my/shows` 1.25 s cold at 390.
  193. **Gate** (111 → 135 checks):
  194. ```
  195. TRACKER_GATE_SHOTS=/tmp/w050/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  196. (in-app run 5.415 s; 11 pages asked during it, slowest 1433 ms)
  197. 135 passed, 0 failed
  198. ```
  199. **Session-sync double rows** (team lead: since hybriel 64527baa every face taking `session` also syncs the members derived
  200. from session; a handler that ALSO appends the returned row shows it twice): checked — no tracker handler appends, every
  201. one assigns the whole list (`rows = r.rows`) or a flag; the new #13 pages have no faces. 4 gate checks "row appears
  202. once" (exact count + unique button values, 400 ms after the face for a late sync): /my/unwatched click, show page
  203. expand, episode click, Follow toggles.
  204. Changed existing checks: the three "page stays empty" (now home content), the in-app run expects `newEpisodes=2` (the
  205. fake TMDB adds a released episode to show 2 too, to prove the in-process re-sort). Screenshots `/tmp/w050/shots/`
  206. (home, movies, movies-page-2, shows-list, my-movies at 390/1280 — looked at).
  207. **Open**: (1) **adult titles** — the migrated movie table has many porn titles (e.g. on /movies page 1 and 4); they
  208. are on the public homepage and /movies. No `adult` flag stored; the creator/architect decides (filter via TMDB `adult`,
  209. hide unfollowed movies, …). (2) Movie posters: only followed shows/movies have real posters → nearly every movie tile is
  210. the placeholder until the sync covers more. (3) If `?page=N` links are wanted anyway, that needs hybriel#11's query
  211. half. (4) Real-phone check after deploy.
  212. ## 2026-10-01 — ticket #12 (mission 049): link icons to TMDB, IMDb, TheTVDB, TVmaze
  213. **Built**: show page header badges `TMDB` `IMDb` `TVDB` `TVmaze` (`components/show.hl` `linkRows`, `shows.hl`
  214. `externalLinksOf`, `styles.hl` `showLinks`/`a.extlink`: CSS-only monochrome badges, accent on hover), only for ids the
  215. show has, `target=_blank rel=noopener`; a movie: TMDB (`/movie/<id>`) + IMDb. Phone: between title row and poster;
  216. desktop: under the title. Sync (`tmdbsync.hl`): details request now `?append_to_response=external_ids` (no extra
  217. request) → fills a MISSING imdbId/tvdbId; still no tvmzId → TVmaze `lookup/shows?imdb=`, on 404 `?thetvdb=` (550 ms
  218. pause each, `pauseMsAfter` in both runners); never overwrites. Totals: `ids=`, `tvmazeRequests=`. New
  219. `tools/count-external-ids.hl` (copy only).
  220. **Bug found + fixed**: movies (`type = 'movie'`, 7814 rows, 1 followed: Star Trek: First Contact, tmdbId 199) were
  221. synced as `/tv/199` — another title. Now `/movie/<id>?append_to_response=external_ids`: imdbId + poster only.
  222. **Live data still carries the damage**: that record has `status = 'Ended'` and `tmdbSummary` = the overview of TV
  223. show 199 ("Kabuhayang Swak na Swak…") — not visible (the page shows `summary`), the new sync leaves both alone; a
  224. one-off fix (set `tmdbSummary = null`, `status` back) is the architect's call.
  225. **Decisions**: TheTVDB link = `https://thetvdb.com/dereferrer/series/<id>` (it and `?tab=series&id=` both 301 to the
  226. slug page, checked 2026-10-01); IMDb `/title/<tt>/`; TVmaze `www.tvmaze.com/shows/<id>`. Badges with text, not
  227. drawn pictograms (no logos copied; legible at 0.75rem). An imdb id is only taken as `tt`+digits, a tvdb/TVmaze id
  228. only as a number > 0. A show TVmaze doesn't know is asked again each daily run (2 lookups/day on real data).
  229. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~06:20, deleted afterwards; `tools/count-external-ids.hl`):
  230. | | shows | tmdbId | imdbId | tvdbId | tvmzId |
  231. |---|---|---|---|---|---|
  232. | followed series, before → after 1 sync | 125 | 125 | 125 | 125 | 123 → **124** |
  233. | followed movie | 1 | 1 | 1 | – | – |
  234. | all series (only followed are synced) | 1639 | 1639 | 907 | 910 | 845 → 846 |
  235. | all movies | 7814 | 7814 | 379 | – | – |
  236. Sync: `shows=126 newSeasons=0 newEpisodes=0 updatedEpisodes=0 updatedSeasons=0 posters=1 ids=1 requests=256
  237. tvmazeRequests=4 errors=0 seconds=105.6` (poster = the movie's real one; id = Greek Salad tvmz 60613; Mapping the
  238. World: TVmaze knows neither tt0395050 nor tvdb 109331). TMDB's external ids match ALL 125 stored imdb/tvdb ids (0
  239. differ). Show pages (az5b2, :8730): Librarians 1.08 s cold / 0.13 s, Greek Salad 0.18 s, Star Trek: First Contact
  240. 0.13 s (TMDB movie/199 + IMDb, real poster now); no sideways scroll, 0 console problems. Screenshots
  241. `/tmp/w049/real/*-{390,1280}.png` (looked at). Procedure = ticket #8 below (sync tool with `.env` sourced).
  242. **Gate** (101 → 111 checks):
  243. ```
  244. TRACKER_GATE_SHOTS=/tmp/w049/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  245. (in-app run 5.416 s; 12 pages asked during it, slowest 1323 ms)
  246. 111 passed, 0 failed
  247. ```
  248. Fixture: show 1 imdb `tt0000001` only (TMDB says tt9999999 + tvdb 4001), show 2 tvdb `4002` only (TMDB: tt0000002 +
  249. 4999), movie `gate-test-movie` (tmdb 1003, followed by a 2nd user so alice's lists are unchanged → the sync now has
  250. 3 shows). Fake TVmaze: imdb tt0000001 → 501, thetvdb 4002 → 502. Screenshots `/tmp/w049/shots/` (show-*, synced-show
  251. {,-390}, synced-movie{,-390} looked at). The new-tab check closes the tab at once (it would load the real site).
  252. **Open**: real-phone tap test after deploy (new tab). Star Trek: First Contact's wrong `tmdbSummary`/`status`
  253. (above). Unfollowed shows never get ids filled (sync covers followed only, by design).
  254. ## 2026-10-01 — mission 047: re-vendor Hybriel (invisible checks), mobile first, show page additions
  255. **Bug** (live, the creator: "i dont see the check marks"): after a CLIENT-SIDE navigation (and after expand/collapse
  256. or a check click) the check `<svg>` was built in the XHTML namespace (circle width 0). Fixed by **re-vendoring Hybriel
  257. master ff51cf46** (bin sha256 `55a724a0…c5`, README "Vendored Hybriel"; old bin/plugins in `.scratch/pre-047/`).
  258. Proof the new check fails on the old build: `/tmp/w047/old-build-gate.log` (`ns: http://www.w3.org/1999/xhtml,
  259. circleWidth: 0`), passes on the new one.
  260. **Built**: `styles.hl` mobile first (phone base rules, one `@media (min-width: 40rem)` block; header one row at
  261. 390 px; /my/unwatched + /my/schedule rows two-line on a phone). Show page: phone order title (+ Follow) → full-width
  262. poster → genres/plot/cast (desktop unchanged); season caret (down/right); episodes not indented; artists orange
  263. `#ce9178`; unwatched check = accent outline circle + accent check; **Follow toggle** (face `showToggleFollow`,
  264. `follows.hl` `isFollowing`/`setFollowed`: new record `at = now`, unfollow deletes); signed out checks + Follow visible,
  265. a click opens the sign-in modal (`components/modal.hl`, copied verbatim from components.hybriel becbd59), nothing sent.
  266. Session-sync workaround in show/myshows/unwatched/schedule: `rows` reads `me` (a plain copy of `userId`), and
  267. `following` goes through a LOCAL static `followingOf` — an imported mpackdb helper called directly in a member
  268. initializer came back false after a client-side navigation (gate caught it; not reduced to a minimal repro yet).
  269. **Duplicate @ids** (hybriel#113) on a copy of live storage (2026-10-01 04:13), `tools/count-duplicate-ids.hl` + the
  270. `*.id.txt` indexes: **0 in every table** (episodes 232384, follows 128, genres 27, persons 15157, seasons 6450, shows
  271. 9453, users 1, watches 11663 rows).
  272. **Real data** (copy, `az5b2`, :8730, `node tests/realdata-check.mjs`, full load incl. socket + posters):
  273. | page | 390 px | 1280 px |
  274. |---|---|---|
  275. | `/my/shows` (128) | 1.29 s (cold posters) | 0.24 s |
  276. | `/my/unwatched` (1594) | 1.51 s | 1.46 s |
  277. | `/my/schedule` (54) | 0.35 s | 0.32 s |
  278. | `/shows/The-Librarians-The-Next-Chapter` direct / client-side | 0.14 / 0.19 s | 0.13 / 0.18 s |
  279. Client-side nav, expand, collapse: 14/26/14 checks, 0 wrong namespace, 0 undrawn; header 46 px at 390, no sideways
  280. scroll. Screenshots `/tmp/w047/real/*.png` (looked at). Procedure = ticket #8 below; the session id goes in
  281. `.scratch/real-sid` (the script reads it); copy + sessions deleted afterwards.
  282. **Gate** (76 → 101 checks):
  283. ```
  284. TRACKER_GATE_SHOTS=/tmp/w047/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  285. 101 passed, 0 failed
  286. ```
  287. Screenshots `/tmp/w047/shots/` ({home,show,my-*}-{390,1280}.png, signed-out-modal-1280.png) — looked at.
  288. Gate note: the follow test re-follows show 1, so the sync's request order between the two shows may flip (the check
  289. accepts both orders) and `/my/shows` after the sync has show 1 on top.
  290. **Open**: the phone poster at full width is tall (2:3 → ~550 px on a 390 phone) — as asked, the creator may want a cap.
  291. The unwatched (outline + check) and watched (filled) icons differ only by fill — as asked. No navigation links to
  292. the `/my/` pages yet. Real-phone check after deploy.
  293. ## 2026-10-01 — ticket #10: installable app (PWA) + icons
  294. **Built**: `icons/` (`icon.svg` source → `icon-192.png`, `icon-512.png`, `apple-touch-icon.png` 180; `favicon.svg`,
  295. `favicon.ico` 16/32/48 — rsvg-convert + magick, commands in README "What it does (step 10)"); `project.hl`:
  296. `appIcons` (192/512 × any/maskable, like calendar), `appTouchIcon`, `appFavicon`, `appThemeColor = darker.value`,
  297. `appBackgroundColor = dark.value` (tokens), `offline = [ Home ]`, file routes for the icons, `/favicon.ico` now a
  298. real file (was `direct = ""`); `components/main.hl` offline note + `netProbe` tick; `styles.hl` `offlineNote`,
  299. `netProbe`, `@keyframes tracker-net-tick`; gate +12 checks.
  300. **Decisions**: icon = a TV (antennas) with a check mark on the screen = "watched shows", green `#4ec9b0` on
  301. rgb(25,30,35), all inside the maskable safe zone so one PNG serves any + maskable. Theme colour = `darker` (the header
  302. bar, so the phone's status bar blends in), background = `dark`. Offline keeps only the shell/`/`; data pages get
  303. hl:web's "Unavailable offline". The offline note is driven by `navigator.onLine`, read on a 1 s CSS-animation tick
  304. (`animationiteration`) — hl:web has no connection-state member and no mount hook (reported as a Hybriel gap).
  305. **Gate** (64 → 76 checks):
  306. ```
  307. TRACKER_GATE_SHOTS=/tmp/w045/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  308. 76 passed, 0 failed
  309. ```
  310. Looked at: `/tmp/w045/icon-48.png`, `icon-192.png`, `icon-512.png`, `maskable-circle.png`, `small-on-white.png`
  311. (favicon 16/32/48), `/tmp/w045/shots/pwa-phone-{online,offline}.png` (390 px) on Loreana.
  312. Test gotcha: CDP `setOffline` only cuts the TAB — the service worker's own fetches still reach the server, so the
  313. gate also stops the server before the offline reload.
  314. **Open**: offline, the header's `<ident-selector>` (script from ident's origin, not cached) shows "choose ident"
  315. next to "Log out". No note when the device is online but the server is down. Real-phone install test (ticket's
  316. **Test**) after deploy: open https://tracker.worldapi.org → "Add to home screen".
  317. ## 2026-10-01 — ticket #9: TMDB sync (new episodes + posters)
  318. **Built**: `tmdbsync.hl` (`syncShow`: details + ≤ 20 seasons per request via `append_to_response`, add/update
  319. seasons+episodes, poster w342), `tools/sync-tmdb.hl` (`--limit N`, paced, prints counts), the daily run in
  320. `project.hl` (`syncTick` on an hl:time `every(1)` clock: at `TRACKER_SYNC_HOUR` UTC (4), once a day, ONE show
  321. per tick, next show after n × 260 ms), `follows.hl` `allFollowedShowIds`, fake TMDB `tests/faketmdb.mjs`,
  322. fixture shows got tmdbIds 1001/1002. Details: README "What it does (step 9)".
  323. **Decisions**: match episode by tmdbId, else by S/E when the local one has no tmdbId (161 migrated); season by
  324. number. Empty TMDB values never overwrite. Season 0 only if the show already has one (else specials flood
  325. /my/unwatched); empty TMDB seasons not created. Own 16-hex ids for new rows (hybriel#113). Posters in
  326. `storage/mpackdb/posters/` (the folder the existing `/posters/:name` route reads — inside `storage/`, so in the
  327. backup) rather than a new `storage/posters/`. Shows synced = all followed (also Ended ones — updates happen).
  328. Sync is off without `TMDB_READ_TOKEN` or with `TRACKER_SYNC=0`.
  329. **Blocking**: `fetch()` blocks the whole hl:web process (measured: requests wait for the running step) — so the
  330. run is cut into one-show steps. Real data: ~0.7 s per step average, SNL (53 seasons, 4 requests) 1.7 s; persist
  331. of the 3 tables 36 ms.
  332. **Real-data proof** (copy of Byrodin's `storage/` in `.scratch/realdata/`, deleted afterwards; procedure as
  333. ticket #8 below; session file needs `created`/`seen` = now in ms, else the boot sweep deletes it):
  334. ```
  335. set -a; . ./.env; set +a
  336. TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb ./bin/hybriel tools/sync-tmdb.hl --limit 128
  337. tmdb sync done: shows=126 newSeasons=20 newEpisodes=800 updatedEpisodes=2651 updatedSeasons=174 posters=125 requests=380 errors=0 seconds=185.718
  338. (again) shows=126 newSeasons=0 newEpisodes=0 updatedEpisodes=0 updatedSeasons=0 posters=0 requests=255 errors=0 seconds=89.888
  339. ```
  340. Fresh-process recount: seasons 6430 → 6450, episodes 231584 → 232384 (exactly +20/+800, no row loss), 0
  341. dangling season/episode refs, 0 duplicate season numbers; 12 duplicate tmdbIds + 1 wrong parent were already in
  342. the migrated data (same before/after). 126 shows (128 follows), 125 posters (one show has none on TMDB).
  343. In-app daily run on the copy (TRACKER_SYNC_HOUR = current hour, real TMDB): 126 shows, 255 requests, 136 s,
  344. 0 errors; `/my/shows` (signed in) polled every 0.5 s meanwhile: 181 answers, avg 0.25 s, max 4.0 s (one), then
  345. 1.8/1.7/1.4 s. Full page loads after the sync (headless Chrome, load + socket + all images):
  346. | page | full load | rows |
  347. |---|---|---|
  348. | `/my/schedule` | 0.7–0.8 s | 55 (was 0) |
  349. | `/my/shows` | 1.3–1.55 s (128 real posters now, 1 placeholder) | 128 |
  350. | `/my/unwatched` | 1.5 s (server 1.3 s) | 1593 (was 831) |
  351. | `/shows/The-Librarians-The-Next-Chapter` | 0.33 s | poster + new S2 (12 eps) |
  352. Screenshots + logs: `/tmp/w044-real/` on Loreana (`after-*.png`, `real-sync{1,2}.log`, `real-lat.txt`).
  353. **Gate** (49 → 64 checks):
  354. ```
  355. TRACKER_GATE_SHOTS=/tmp/w044-shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  356. (in-app run 4.405 s; 10 pages asked during it, slowest 1371 ms)
  357. 64 passed, 0 failed
  358. ```
  359. Screenshots `/tmp/w044-shots/` (`synced-*.png` looked at: posters, S3, schedule rows fine).
  360. **Deploy (architect)**: `TMDB_READ_TOKEN` must be in Byrodin's `.env` (env_file) — without it the log says
  361. `tmdb sync: off (no TMDB_READ_TOKEN)`. First daily run 04:00 UTC after deploy; check `docker logs
  362. tracker.worldapi.org | grep tmdb`. Or stop the container and run the tool once for an immediate catch-up.
  363. **Open**: a step blocks requests up to ~2 s (a non-blocking `fetchStart` + `done()` state machine would fix it).
  364. `/my/unwatched` render grew with the rows (1.3 s server, 1593 rows) — watch it against the 2 s target. A season
  365. watch record stays solid when the sync adds a new episode to that season (existing season-watch semantics).
  366. Dev servers get the real token from `.env` automatically → start them with `TRACKER_SYNC=0`. Shows nobody
  367. follows are never synced (by design).
  368. ## 2026-09-30 — ticket #8: /my/ pages, S01E01, years, speed
  369. **Built**: `/my/unwatched`, `/my/schedule` (routes in `project.hl`); old `/unwatched`, `/schedule` →
  370. **301** (function routes `unwatchedMoved`/`scheduleMoved`). Rows as `S01E02 · <title>` (was
  371. `Ep 2 · …`), list titles with the year `Doctor Who (2005)`, show page `1 episode` singular.
  372. New shared helpers in `shows.hl`: `pad2`, `episodeCode`, `showYear`, `titleWithYear`,
  373. `posterUrlOf`; in `watches.hl`: `watchedSet` (used by `/my/unwatched` and the show page's
  374. `buildRows`; `isWatched` stays for the single-target toggles).
  375. **Decisions**: year = `year`, else `release.slice(0,4)`, else none (197 shows lack `year`, some
  376. have `release = ''`). Year only in LISTS (`/my/*`), not in the show page's `h1`. The 301 drops any
  377. query (the pages take none). A row whose poster FILE is missing gets `/posters/none` (one cached
  378. URL) instead of `/posters/<oldId>.jpg`.
  379. **Why /my/shows was ~25 s live though its data is 0.2 s**: hl:web handles ONE request at a time.
  380. Measured on the copy: `/my/shows` requested 1 s into a `/unwatched` render took 13.7 s (the
  381. `/unwatched` render itself 14.5 s). Every request (pages, 128 poster requests) of every visitor
  382. queued behind a slow `/unwatched`. Plus 128 distinct poster URLs per load, all the placeholder.
  383. **Real-data check** (copy of Byrodin's `storage/` only, `az5b2` = user `0mujqspn0aqb`, dev
  384. server :8730, headless Chrome, navigate → load → websocket open, 1280×900):
  385. | page | before | after |
  386. |---|---|---|
  387. | `/my/shows` (128 rows) | 1.2 s (server 0.19 s) | 0.6–0.8 s, 1 poster request |
  388. | `/unwatched` → `/my/unwatched` (831 rows) | 14.8 s | 0.7–1.2 s (server 0.55–0.65 s) |
  389. | `/schedule` → `/my/schedule` (0 rows, data not synced since 2025-12) | 0.36 s | 0.3 s |
  390. | `/shows/The-Librarians-The-Next-Chapter` | 0.34 s | 0.29 s |
  391. How to repeat (Loreana; never touch Byrodin's folder beyond the tar read):
  392. ```
  393. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata # ONLY storage/
  394. TRACKER_PORT=8730 TRACKER_WATCH=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ ./bin/hybriel project.hl
  395. # session file: name = sha256(<32-hex id>), JSON {"created":1,"data":{"tag":"t"},"id":"<id>","seen":1,"user":{"id":"0mujqspn0aqb"}}
  396. curl -s -o /dev/null -w '%{time_total}\n' -b trackersid=<id> http://127.0.0.1:8730/my/unwatched
  397. # Chrome: tests/cdp.mjs launchBrowser, Network.setCookie trackersid, goto + wait __hl.socket open
  398. # afterwards: stop the server, rm -rf .scratch/realdata .scratch/realdata-sessions
  399. ```
  400. Screenshots: `/tmp/w041-real/{before,after}-{shows,unwatched,schedule,show}.png` (Loreana).
  401. **Gate** (45 → 49 checks: two 301s, the browser landing on `/my/unwatched`, "1 episode"; the
  402. existing ones now expect `/my/…`, `S01E02 · Second`, `S02E01 · Season Two Opener`, titles with
  403. year, `/posters/sh1.png` + `/posters/none`). Fixture: show 1 `image = 'png'` (gate writes a 1×1
  404. `posters/sh1.png`), show 2 `year = null`, `release = '2025-03-01'`.
  405. ```
  406. TRACKER_GATE_SHOTS=/tmp/w041-shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs
  407. 49 passed, 0 failed
  408. ```
  409. Screenshots `/tmp/w041-shots/` (looked at: all fine).
  410. **Open**: `/my/unwatched` rebuilds all rows per check click (~0.6 s, fine). Still no navigation
  411. to the `/my/` pages. The single-request server means any future slow page blocks everyone —
  412. keep every page well under 1 s. `/my/schedule` stays empty until the TMDB sync step.
  413. ## 2026-09-30 — ticket #7: /my/shows
  414. Built `/my/shows` (`components/myshows.hl`, route in `project.hl`, styles in `styles.hl`): every
  415. followed show, newest follow (`follows.at`, epoch ms) first — small poster (2.5rem, `/posters/…` +
  416. placeholder like the show page), title → `/shows/<slug>`, last watched episode `SxxEyy`.
  417. **Decisions**: "last episode" = highest (season, episode) among the user's EPISODE watches of that
  418. show (ticket); a season watch alone doesn't count (the season check watches every episode anyway).
  419. No watch → no episode element. A show without a poster name → `/posters/none` (the route's own
  420. placeholder), so every row has a poster. Read-only page, no face (like `/schedule`).
  421. **Reuse**: new `follows.hl` `followsOfUser` (whole records; `followedShowIds` now builds on it) and
  422. `shows.hl` `episodeById` (one line); `showById`, `posterName`, `watchesOfUser` unchanged.
  423. **Performance** (against a COPY of `storage/mpackdb/` in `.scratch/perf-store`, removed after; live
  424. files md5-verified unchanged): the page touches only the user's follows and episode watches
  425. (one fetch per watch), never all episodes of a followed show — az5b2's 128 follows → 128 rows (123
  426. with an episode, e.g. `S36E11`, `S30E91`) built in **160–255 ms**. For comparison **`/unwatched`
  427. takes ~15 s** on the same copy (805 rows): it fetches every episode of every followed show and
  428. checks each against the user's 11,692 watches with a linear `isWatched` — not in this ticket's
  429. scope, flagged below.
  430. **Gate** (`tests/browser.mjs`, 38 → 45 checks; `tests/seed-show.hl` adds a 2nd show "Unwatched Gate
  431. Show", no seasons/poster name, followed AFTER the first but stored after it; the first follow is now
  432. dated a day back): signed out → sign-in message; signed in → newest follow first, title hrefs,
  433. posters loaded (`naturalWidth > 0`), 40px wide, `S01E02` after /unwatched's click, later `S02E01`
  434. (watched BEFORE season 1's bulk watch → proves highest, not most recent), none for the other show.
  435. ```
  436. TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs
  437. 45 passed, 0 failed
  438. ```
  439. **Open**: `/unwatched` ~15 s on real data (above) — a watched-id map instead of `isWatched` per
  440. episode would fix it (own ticket). No navigation links to `/my/shows` yet (homepage stays empty).
  441. ## 2026-09-27 — ticket #6: /schedule
  442. Built `/schedule` (`components/schedule.hl`): every episode of a show the signed-in user follows
  443. that is NOT yet released (`e.release > today`), soonest first. Creator: "/schedule has a similar
  444. list without check icons for the upcoming episodes of shows followed" — same row shape as
  445. `/unwatched` (`components/unwatched.hl`) minus the check icon.
  446. **Reuse, not copy**: imports the exact same `followedShowIds` (`follows.hl`), `showById` /
  447. `seasonsOfShow` / `episodesOfSeason` (`shows.hl`) that `/unwatched` already uses — nothing new
  448. added to either file. `watches.hl` is not imported at all: a row here is never watched/unwatched,
  449. there is no check icon and no click, so the page needed no per-user watch state and no `on server`
  450. face — the whole component is a single read-only server-side render (`rows` computed once from
  451. `session`, same as every other reactive member here).
  452. **Order**: soonest first, ascending by `release` (a plain `'YYYY-MM-DD'` string, ordering
  453. operators confirmed working against this app's own vendored `bin/hybriel`, same as `/unwatched`'s
  454. STATUS entry) — the ticket named this the architect's own reading ("the creator named the order
  455. only for /unwatched"), so `insertByReleaseAsc` mirrors `/unwatched`'s `insertByReleaseDesc` with
  456. the comparison flipped.
  457. **`decided`**: "upcoming" = `release` strictly after today (not `>=`) — the natural complement of
  458. `/unwatched`'s own `release <= today`, so every released-or-today episode is unambiguously on one
  459. list, never both, never neither.
  460. **Gate** (`tests/browser.mjs`, 35 → 38 checks): reused the same fixture (`tests/seed-show.hl`,
  461. unchanged) — episode 3 already carried a far-future release (`2099-01-01`) for `/unwatched`'s own
  462. "excludes the future one" check; `/schedule` now proves the mirror image with the same fixture:
  463. signed out, no rows, the sign-in message; signed in, only that one future episode shows (the two
  464. already-released ones excluded), no `button.checkicon` anywhere on the page, unaffected by any of
  465. the watch toggles the other checks perform on the same fixture (run in any order — `/schedule`
  466. never reads watch state).
  467. ```
  468. node tests/browser.mjs
  469. 38 passed, 0 failed
  470. ps -eo pid,args | grep [h]l-browser-tier
  471. (nothing)
  472. ```
  473. Updated `README.md` ("What it does (step 6)", Test, Files) and this file.
  474. **Open**: nothing shown on the homepage still points to `/schedule` (same as `/unwatched` before
  475. it — the homepage stays empty per ticket #1's decision).
  476. ## 2026-09-27 — ticket #5: /unwatched
  477. Built `/unwatched`: every episode of a show the signed-in user follows that is already released
  478. (`release` ≤ today, a plain `'YYYY-MM-DD'` string — migrated as-is by `tools/migrate.hl`, never
  479. run through `dateOf`) and not yet watched, newest release first (creator: "/unwatched lists all
  480. unwatched episodes of shows followed in release date desc order").
  481. **Reuse, not copy** (the ticket's own instruction, re "the same check icon and watch logic as the
  482. show page"): `watchClassOf` (watched → `'checkicon solid'`/`'checkicon regular'`) was a private
  483. `static` in `components/show.hl`; moved it to `watches.hl` so both pages import the SAME
  484. definition instead of each declaring their own — `show.hl` now imports it too, nothing duplicated.
  485. `isWatched`/`setWatched` (already in `watches.hl`) needed no change. New `follows.hl`
  486. (`followedShowIds`) and `shows.hl` `showById` (one line) round out the read-only data access this
  487. page and `/schedule` (ticket #6) both need.
  488. **String comparison** (`e.release <= today`, `row.release >= x.release` for the desc sort):
  489. hybriel#2 ("no ordering operators on strings") is filed as still-open in this ticket's brief, but
  490. its own history shows it was fixed and merged to master 2026-09-25 — confirmed directly against
  491. THIS app's own vendored `bin/hybriel` (`'2020-09-03' <= '2026-09-27'` → `true`), so no numeric
  492. date workaround was needed.
  493. **A View text node cannot hold an inline expression** (`"Ep " + row.episodeNumber + " · " +
  494. row.title` inside a tag body) — same rule as View attributes (components/show.hl's own comment:
  495. "a View attribute must be a literal, a member or a field path"), just not documented for text
  496. nodes anywhere yet; it silently rendered `<!-- binary_expression not rendered yet -->` instead of
  497. erroring. Fixed by resolving the label into a plain field (`row.episodeLabel`) once, server-side,
  498. in `unwatchedRowsOf` — the same pattern `show.hl`'s `buildRows` already uses for every other
  499. ternary/concatenation. Not filed as a new Hybriel gap since it's the exact same restriction
  500. already known and worked around elsewhere in this app, just not yet hit for interpolated text.
  501. **Empty state**: signed out (no user, so no follows to show) and signed in with nothing unwatched
  502. both render a message; used `.muted` (colorTextMuted), not `.message` (colorDanger) — the existing
  503. `.message` class is for real errors (the login-failed banner), and an empty list isn't one (a
  504. `decided`, small).
  505. **Gate** (`tests/browser.mjs`, 29 → 35 checks): `tests/seed-show.hl` now takes an optional
  506. `TRACKER_SEED_IDENTITY` and, when given, seeds a fixed user + a follow of the fixture show for it
  507. — the only way to give `/unwatched` a known followed show before the browser ever signs in, since
  508. the real login's `ensureUser` matches an EXISTING `users.db` row by `identity` and reuses its id
  509. rather than minting a new one. The gate learns alice's per-app identity with one throwaway
  510. `ident.exchange` call (consuming one code) before writing the seed. Episode 1/2 of the fixture now
  511. carry a past `release` (`2020-01-01`/`2020-06-15`), episode 3 a far-future one (`2099-01-01`) —
  512. proving both the release-date filter and the desc order. The new checks run signed out (no rows,
  513. the sign-in message), then signed in RIGHT AFTER LOGIN, BEFORE the existing show-page checks that
  514. toggle these same fixture episodes watched (order matters — otherwise `/unwatched` would already
  515. be empty by the time it's tested): both unwatched episodes list newest-first, excluding the
  516. future one; a click on the newest (`Second`, ep 2) removes it from the list; a reload proves the
  517. watch is server-side, leaving only `Pilot` (ep 1).
  518. ```
  519. node tests/browser.mjs
  520. 35 passed, 0 failed
  521. ps -eo pid,args | grep [h]l-browser-tier
  522. (nothing)
  523. ```
  524. Updated `README.md` ("What it does (step 5)", Test, Files) and this file.
  525. **Open**: nothing shown on the homepage still points to `/unwatched` — no navigation exists yet
  526. (the homepage stays empty per ticket #1's decision; a later step, from the creator, presumably
  527. adds it). `/schedule` (ticket #6) is next and reuses `follows.hl`/`shows.hl`/`watches.hl` the same
  528. way.
  529. ## 2026-09-27 — ticket #4: the show page, watch checks (took over a previous half-done session)
  530. Continued from an earlier session that had built the whole page — header, seasons/episodes,
  531. the click-handling redesign to a flat `rows` list rebuilt server-side (`components/show.hl`'s
  532. own header comment explains why: a nested `for` over seasons > episodes risks hybriel#86) — but
  533. left with the gate red: clicking a check never turned it solid, no console error, and the
  534. session ran out of budget before finding why.
  535. **The actual bug**: the show page's own pure helpers (`buildRows`, `genreRowsOf`, `castRowsOf`,
  536. `watchClassOf`, `initialCollapsed`) were plain instance members (`buildRows = (show, …) => {…}`,
  537. no `static`). That shape works fine as a member's own top-level initializer (`rows = found ?
  538. buildRows(showRow, watchRows, collapsed) : []` — this is how the page renders correctly on first
  539. load) but throws when the SAME function is called from inside an `on server` face
  540. (`showToggleEpisode` etc.): the server log showed `'buildRows' is not callable — it holds null`.
  541. Found by running `.scratch/debug-click.mjs` (left by the previous session) with the server's
  542. stdout/stderr captured — the browser console itself stayed silent (the face's error never
  543. reaches the client at all, a separate Hybriel gap, hybriel#92 already covers that half).
  544. Confirmed the fix by grepping other worldapi apps for the same shape:
  545. `calendar.worldapi.org/components/calendar.hl` has `static soonOf = (u) => { … upcoming(u, …) }`
  546. (an mpackdb-backed helper), called from its `on server calLoad`/`calSettingsLoad` faces, and
  547. calendar's own gate is green — so `static` is the proven-safe shape for a component's own helper
  548. that both (a) touches an mpackdb-backed import and (b) needs calling from more than one place
  549. (its own initializer AND a later face). Declared all five helpers in `show.hl` `static`; reran
  550. the gate: 29/29 green, including the season-bulk-toggle and the reload-proves-server-side checks.
  551. Filed **hybriel#115 was already open** (from the previous session, about the CLIENT bundle
  552. omitting a case for such a helper) but this session's finding is the SAME root cause reaching
  553. further than that ticket says: the plain HL interpreter on the SERVER also treats such a helper
  554. as `null` outside its own initializer, not just the JS-compiled client bundle. Left as one
  555. `issues` entry pointing at hybriel#115 rather than opening a near-duplicate ticket — the
  556. repro/observed there already generalizes (a per-instance member function wrapping an
  557. mpackdb-backed import is unreliable wherever it is called from, not just the browser).
  558. Ran `node tests/browser.mjs`: 29 passed, 0 failed (was 20 passed / 1 failed — the click timeout
  559. — before the `static` fix).
  560. **Rehearsed `tools/relink-episode-seasons.hl` against a full COPY** of the real
  561. `storage/mpackdb/` (`.scratch/relink-rehearsal/`, since removed) — the tool's own header claimed
  562. this rehearsal already existed here; it didn't (a leftover claim from the previous session), so
  563. this session actually ran it:
  564. ```
  565. relink: episodes 231584, null season before 36194, linked 36194, still null (no matching season) 0
  566. relink: episodes.db replaced with the relinked file — re-run to confirm idempotency (0 linked the second time)
  567. ```
  568. Ran a second time on the same copy straight after — idempotency confirmed:
  569. ```
  570. relink: episodes 231584, null season before 0, linked 0, still null (no matching season) 0
  571. ```
  572. All 36,194 pre-existing null-season episodes (ticket #2's count) now link to a real season by
  573. `(show, seasonNumber)`; none left over. Not run against the LIVE `storage/mpackdb/` — that needs
  574. the tracker container stopped first (hl:mpackdb does not coordinate two processes on one storage
  575. directory, hybriel#21), the architect's job per the ticket ("the architect runs it live").
  576. Updated README.md ("What it does (step 4)", Test, Files) and this file.
  577. ## 2026-09-27 — ticket #2: old data migrated (took over a previous session's rewritten tool)
  578. Continued from an earlier session that had rewritten `tools/migrate.hl` to avoid a real data-loss
  579. bug (see below) but never run it even once, and had left the real `storage/mpackdb/` holding data
  580. from the OLDER, buggy two-pass version of the tool (put a bare record, then `update()` it once the
  581. other side of a circular reference existed). Took over: moved that buggy data aside
  582. (`.scratch/pre-migrate-buggy-backup`, since removed), kept `storage/mpackdb/users.db` (step 1's
  583. login table, untouched by the migration), and ran the already-fixed tool for the first time.
  584. **The fix already on disk (kept as is):** `Show.seasons`/`Season.show` and `Show.cast`/
  585. `Person.shows` are two-way references, so the id on one side must exist before the record on the
  586. other side can be built. The old approach — `put()` a bare record, come back with `update()` once
  587. the far side's id exists — loses rows: reopening a table in a fresh process after an `update()` had
  588. touched it read back FEWER rows than were ever `put()` (seen directly: `persons.db`'s `count()`
  589. dropped 15157 → 15152 after one round of `update()` — a real `hl:mpackdb` bug, reported to Hybriel,
  590. not something an app is allowed to patch around). The fix assigns every new id itself (8 random
  591. bytes, `hl:crypto`) in one pass over each export file BEFORE building any record, so by the time a
  592. record is actually built every reference is already a known id — one `put()` per row, `update()`
  593. never called.
  594. **Run** (`tools/migrate.hl`, `TRACKER_OLD_DATA=.../old-tracker/mongo-export`,
  595. `TRACKER_STORAGE=$PWD/storage/mpackdb`):
  596. ```
  597. migrate: genres 27 new, 0 already there (old 27 -> new 27)
  598. migrate: persons 15157 new, 0 already there (old 15157 -> new 15157), show refs skipped 0
  599. migrate: shows 9453 new, 0 already there (old 9453 -> new 9453), cast refs skipped 0, genre refs skipped 0, season refs skipped 0
  600. migrate: seasons 6430 new, 0 already there (old 6430 -> new 6430), show refs skipped 0, episode refs skipped 0
  601. migrate: episodes 231584 new, 0 already there (old 231584 -> new 231584), show refs skipped 0, season refs left null (pre-existing) 1
  602. migrate: follows 128 new, 0 already there (old 128 -> new 128), show refs skipped 0
  603. migrate: watches 11692 new, 0 already there (old 11692 -> new 11692), target refs left null (pre-existing) 3
  604. migrate: 0 failed
  605. ```
  606. Every count equals the old export's own document count (`old-tracker/README-RECONSTRUCTED.md`:
  607. Show 9453, Season 6430, Episode 231584, Person 15157, Genre 27, Follow 128, Watch 11692). Ran a
  608. second time straight after (idempotency: the tool's `oldId` index finds each row again) — every
  609. table printed `0 new`, all rows `already there`, counts unchanged, 0 failed: the id-loss bug does
  610. not resurface across a real close-and-reopen.
  611. **Proof beyond the tool's own counters** (`tools/verify.hl`, new): the ticket asks for a check that
  612. does not just trust `migrate.hl`'s own "skipped" tallies. It runs against a plain filesystem COPY
  613. of `storage/mpackdb/` (hl:mpackdb rewrites a file's data on open, so the live store is never opened
  614. a second time) and independently re-derives, from the persisted records alone: every table's
  615. `count()` against the export's own document count, and — by building id sets for every table and
  616. walking every reference field (`Show.genres/cast/seasons`, `Season.show/episodes`,
  617. `Episode.show/season`, `Person.shows`, `Follow.show/user`, `Watch.target/user`) — that every
  618. non-null reference resolves to a real id. It also follows one show end to end (`Raised by Wolves`,
  619. 2 seasons, 18 episodes, 1 follow, 2 season-level watches — all cross-checked back to the show).
  620. ```
  621. $ cp -r storage/mpackdb .scratch/verify-copy && TRACKER_VERIFY_COPY=$PWD/.scratch/verify-copy ./bin/hybriel tools/verify.hl
  622. count ok genres: 27 (expected 27)
  623. count ok persons: 15157 (expected 15157)
  624. count ok shows: 9453 (expected 9453)
  625. count ok seasons: 6430 (expected 6430)
  626. count ok episodes: 231584 (expected 231584)
  627. count ok follows: 128 (expected 128)
  628. count ok watches: 11692 (expected 11692)
  629. users: 1 (expected 1, the creator)
  630. dangling references: 0 (episodes with a pre-existing null season: 36194, watches with a pre-existing null target: 3 — not dangling, the export already had no target)
  631. END-TO-END show 105ad9c91b14d663 "Raised by Wolves" seasons=2
  632. season 1 (be2965a5ab7165ca) episodes=10 show-back-ref-ok=true
  633. season 2 (0fe4e53157ed0367) episodes=8 show-back-ref-ok=true
  634. total episodes across seasons: 18
  635. follows on this show: 1, season-level watches touching it: 2
  636. VERIFY PASS
  637. ```
  638. (36194 episodes with no season = 36193 that never had one in the old export, plus the 1 the tool's
  639. own count already named as a pre-existing dangling `season` reference in the source data — neither
  640. is a reference this migration broke; `dangling references: 0` covers exactly that.)
  641. The single old user (`User.jsonl`, `[email protected]`, password not taken over) is this app's
  642. user for ident short id `az5b2` (`storage/mpackdb/users.db`, the same table step 1's login uses) —
  643. confirmed in the copy: one record, `identity=az5b2`.
  644. **Two Hybriel bugs found while building this** (both already filed as issues on this ticket by an
  645. earlier session, not re-filed): `hl:fs readFile` silently truncates at 10 MiB (worked around here
  646. with `split`, no shell, no JS, see `tools/migrate.hl`'s header comment) and `hl:mpackdb` loses rows
  647. across `update()` + reopen (the reason this tool never calls `update()`).
  648. **Open**: nothing shown yet — this ticket is data-only, on purpose (the next step, from the
  649. creator, is showing the data in the UI).
  650. ## 2026-09-27 — ticket #3: no border on the header or filled buttons
  651. Design notes from the first test (architect): "the application-header should have no bottom
  652. border" and "the buttons also no border except they are inverted". Two changes in
  653. `styles.hl`:
  654. - `applicationHeader`: dropped `borderBottom`.
  655. - `ident-selector::part(button)` (the signed-out "choose ident" button): added
  656. `border = 'none'` — the element's own CSS gave it a 1px border the same colour as its
  657. background (`--_accent`), invisible but still a real border in the computed style; this is
  658. the app-side restyling hook ident's README documents (`::part(button)`), not a change to
  659. ident's vendored `selector.js`.
  660. Left unchanged, already correct: the native `button` rule (filled, e.g. no other filled
  661. buttons on this page yet) already has `border = '0'`; `button.quiet` ("Log out") and the
  662. selector's dropdown `[part="identity"]` rows keep their border (transparent background —
  663. inverted style); `a.button` ("Log in with ident") has no border rule and browsers give `<a>`
  664. none by default. The selector's signed-in "logged in with ident" status pill
  665. (`::part(status)`) is not one of the buttons the ticket names and isn't a button element
  666. (a `<span>`) — left with its border.
  667. **Gate** (`tests/browser.mjs`, now 16 checks): added computed-style checks — the header's
  668. `borderBottomWidth` is `0px`; `#loginbutton`'s border is `0px`; the selector's
  669. `[part~="button"]` (inside its shadow root) border is `0px`; `#logout`'s border is NOT `0px`
  670. (still inverted-bordered) once signed in.
  671. ```
  672. node tests/browser.mjs
  673. 16 passed, 0 failed
  674. ```
  675. ## 2026-09-27 — ticket #1 reopened: login redone to match calendar exactly (header selector, empty page)
  676. The architect rejected the first build of ticket #1: the sign-in sat centered on the page
  677. (its own "Sign in with ident" card, identity + sign-out shown in a `homeCard`) instead of
  678. the header identity selector calendar/gitoria use, and there was no `<ident-selector>` at
  679. all. Rebuilt the login by copying calendar.worldapi.org's own files (unchanged in shape, per
  680. the architect's instruction "Copy calendar.worldapi.org's login unchanged"):
  681. - **`users.hl`** (new): the ident exchange + a `usersTable` (`storage/mpackdb/users.db`,
  682. `identity` → this app's user id), copied from calendar's `users.hl` with `TRACKER_KEY` /
  683. `TRACKER_SECRET` / `TRACKER_URL` / `TRACKER_STORAGE` in place of calendar's names (kept
  684. the names already in this app's `docker-compose.yml`/README/DECISIONS rather than
  685. switching to calendar's generic `IDENT_API_KEY`/`IDENT_API_SECRET`).
  686. - **`login.js`** (new): calendar's bridge between `<ident-selector>`'s `ident-login` DOM
  687. event and the hidden `#identcode` input, copied verbatim (creator: "login.js is correct,
  688. as thats for externals in general").
  689. - **`components/main.hl`**: now the header carries `userBox` (`<ident-selector>` + "Log in
  690. with ident" / "Log out"), the `trackerLogin`/`trackerLogOut` faces and the
  691. `trackerSignedIn`/`trackerSignedOut` client push — calendar's shell renamed to this app's
  692. event names.
  693. - **`components/home.hl`**: now truly empty (`View { home {} }`) — no sign-in link, no
  694. identity/sign-out duplicated on the page; that is entirely the header's job now.
  695. - **`components/loginfailed.hl`** (new) + **`project.hl`**: the login routes are now
  696. `/login/callback` (function route, the button's return AND the code exchange) and
  697. `/login/failed`, copied from calendar's `project.hl` (`safePath`, `failed()`,
  698. `loginCallback`) in place of the old `/login` + `/callback` + `/logout` routes; the old
  699. `/logout` POST route is gone (logout is a face, like calendar's).
  700. - **`styles.hl`**: added the header selector styles (`userBox`, `identSelector`,
  701. `ident-selector::part(...)`, sticky header) from calendar's `styles.hl`; dropped the
  702. now-unused `homeCard`/`accountBar`/`userName` rules from the rejected centered sign-in.
  703. **Gate rewritten** (`tests/browser.mjs`, 12 checks): replaced the old gate (which drove
  704. ident's full email-code UI through a vendored dev copy of ident at `.scratch/ident-dev`)
  705. with calendar's own pattern — `tests/identkit.mjs` (copied from calendar unchanged) starts a
  706. throwaway ident (a fresh copy of `/media/STORAGE/projects/ident.worldapi.org`'s code, no
  707. `.env`/storage copied, codes to a mail sink, never the live ident), signs in over its REST
  708. API and registers this app, then the gate proves the header in a real headless Chrome:
  709. signed out → `#selector` + `#loginbutton` in the header, **`main` is empty** → sign in
  710. (`/login/callback?ident_code=`, the same exchange the selector would trigger) → `#selector`
  711. shows `class="in"`, `#logout` appears, **`main` still empty** → sign out → signed out again
  712. → a reload stays signed out. No console errors.
  713. ```
  714. node tests/browser.mjs
  715. 12 passed, 0 failed
  716. ```
  717. `./deploy.sh --dry-run --target .scratch/deploy-preview --url http://127.0.0.1:0/`: gate
  718. passed as step [1/4], rsync preview held none of `storage/`, `.sessions/`, `.env`,
  719. `.scratch/`, `server.*`, logs. Removed the stale `.scratch/ident-dev` (the old gate's vendored
  720. ident copy) and `.scratch/browser-gate` (the old gate's storage) — unused by the new gate.
  721. **Still open**: the app is registered in ident's dev copies only (this gate's own throwaway
  722. ident). The LIVE ident registration (`TRACKER_KEY`/`TRACKER_SECRET` in `.env` next to
  723. `docker-compose.yml`) is the architect's first-deploy step (README "Deploy"), not built here.
  724. ## 2026-09-27 — ticket #2: data migration — blocked twice on sandbox access, nothing done
  725. Two workers in a row (`s-20260927T1029-f57b0f`, then this session) could not start: the
  726. migration source the ticket names, `/media/STORAGE/projects/old-tracker/mongo-export/*.jsonl`
  727. + its `README-RECONSTRUCTED.md`, is not reachable from this worker's sandbox. Verified fresh
  728. this session:
  729. ```
  730. $ ls -la /media/STORAGE/projects/
  731. antcolony-docs hybriel ident.worldapi.org tracker.worldapi.org # no old-tracker
  732. $ mount | grep STORAGE
  733. /dev/nvme1n1p6 on /media/STORAGE/projects/tracker.worldapi.org type btrfs (rw,...)
  734. /dev/nvme1n1p6 on /media/STORAGE/projects/hybriel type btrfs (ro,...)
  735. /dev/nvme1n1p6 on /media/STORAGE/projects/ident.worldapi.org type btrfs (ro,...)
  736. /dev/nvme1n1p6 on /media/STORAGE/projects/antcolony-docs type btrfs (ro,...)
  737. # old-tracker is not among the mounted subvolumes at all
  738. ```
  739. So this is not a data problem (the export is "complete" per the ticket) — it's the worker
  740. sandbox for this ticket that never got `old-tracker` mounted (read-only, like `hybriel` /
  741. `ident.worldapi.org` / `antcolony-docs` above). Nothing was built or changed this session:
  742. no `storage/`, no `tools/`, no id-mapping — writing the migration tool blind, guessing the
  743. JSONL shape from the ticket text alone, risks silently wrong data and was avoided on purpose
  744. (see the project's "Build it properly" rule). Filed as an antcolony issue so the next worker's
  745. sandbox includes `old-tracker` before another attempt.
  746. ## 2026-09-27 — ticket #1: the empty shell, ident login
  747. Built the app from scratch: vendored `bin/hybriel` + `plugins/` + `shared/tokens.hl` from
  748. `ident.worldapi.org` (newest local build, hybriel master 837fe120, no local patch — see
  749. README "Vendored Hybriel"). `project.hl` (routes `/login`, `/callback`, `/logout`, `/`),
  750. `components/home.hl` (the empty homepage), `components/main.hl` (shell), `styles.hl`
  751. (accent green `#4ec9b0`, per `antcolony/README.md` "look and style" / `CONCEPT.md`).
  752. `docker-compose.yml` / `Dockerfile` / `deploy.sh` following ident's own house pattern
  753. (port 45008, container `tracker.worldapi.org`).
  754. **Login**: the ident login BUTTON flow (ident.worldapi.org README "How apps use ident"),
  755. server-side exchange (`POST <ident>/api/exchange`), this app's own framework session
  756. (`session.user = { identity }`, cookie `trackersid`). ident hands over only the identity's
  757. public **short id** — no display name yet (ident#11, properties handover, is on hold) — so
  758. "your name" on the homepage is `Signed in as <shortid>`, the same identifier every other
  759. worldapi app would show; see `decided` in the report.
  760. **Lesson (Hybriel)**: a face that mutates `session.user` (e.g. `signOut`) does NOT
  761. automatically re-render the page — a component's reactive members (`signedIn`, `identity`,
  762. …) are computed once at mount from the framework's read-only session snapshot; a client
  763. handler that calls `emit server X()` must ALSO flip the affected members itself afterwards
  764. (ident's own `components/home.hl` `on doSignOut(e)` does exactly this: `emit server
  765. signOut()` then `signedIn = false` …). Missing that made the sign-out button silently do
  766. nothing client-side (the ack came back `ok:true`, the server-side session really was
  767. cleared — proven by a reload — but the DOM never updated) until copied.
  768. **Gate** (`tests/browser.mjs`, 13 checks): a fresh copy of `ident.worldapi.org`'s code (no
  769. `.env`, no `storage/`, no `.sessions/` — verified empty of secrets/data before first use)
  770. runs as this gate's own ident, on its own storage and its own mail sink (no live ident, no
  771. real mail). The gate registers this app in that dev ident once over the `/__hl/emit` API
  772. (the same one-time step a person does by hand in ident's `/apps` page), then drives the
  773. REAL login-button flow in a real headless Chrome: signed out → click "Sign in with ident" →
  774. ident's email form → the mail-sink code → the first-login optional-names form (Skip) → the
  775. one-identity choice (one click) → back on tracker, "Signed in as `<shortid>`" → "Sign out" →
  776. signed out again → a reload stays signed out. No console errors. Ran twice for stability, 0
  777. failures; no leftover Chrome/hybriel processes after either run.
  778. ```
  779. node tests/browser.mjs
  780. 13 passed, 0 failed
  781. ```
  782. `./deploy.sh --dry-run --target .scratch/deploy-preview --url http://127.0.0.1:0/` (a local
  783. directory, per ident's own convention for testing deploy.sh without touching Byrodin): gate
  784. passed, rsync preview held none of `storage/`, `.sessions/`, `.env`, `.scratch/`, `server.*`,
  785. logs — confirmed by the same grep the script refuses on.
  786. **Open**: the app is not registered with the LIVE ident yet — `TRACKER_KEY`/`TRACKER_SECRET`
  787. are unset until the architect's first deploy does that (README "Deploy"); until then
  788. `/login` on the live site answers a plain error page instead of redirecting (`/` itself
  789. still renders). This is normal for a brand-new app, the same as ident's own SMTP `.env`
  790. before its first deploy — not something this ticket's worker can set (no `.env` access,
  791. and it is a LIVE ident registration).

Branches

Latest commits

  • c171227emission 054: hide adult/unknown titles from the public lists and the search; in-app adult-flag backfill (TMDB details + poster per title, resumes), gate + real-data proofmre
  • 47a3cae6STATUS: mission 053 merge commit idsmre
  • dcc5eecaMerge branch 't14-search'mre
  • 03edc783Merge branch 't15-tvmaze'mre
  • 71b46345tracker#15: numbering check by date or title, placeholder titles in other languages, docs + real-data proofmre
  • 6bb2daf1tracker#13: homepage (tiles, intro, latest movies/shows), /shows, /movies/page/N, /my/movies; lists cached in memorymre
  • b8bd1157tracker#14: README + STATUS (search, real-data numbers, gate, merge notes)mre
  • 65c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre
  • 34f2c15btracker#15: TVmaze merge in the sync (gaps only: new episodes/seasons, empty titles/air dates; numbering check), fake TVmaze episodes + gatemre
  • cbdc4ea7tracker#12: link icons TMDB/IMDb/TVDB/TVmaze; sync fills missing ids (TVmaze lookup); movies fetched via /movie/mre
  • b105bcd8tracker#11: Hybriel master ff51cf46 (checks no longer vanish), mobile-first styles, carets, follow button, sign-in modal, inverted check, orange castmre
  • 31b758aatracker#10: installable app (manifest, service worker, offline shell), own icon + faviconmre
  • 2fa9d997tracker#9: TMDB sync (followed shows: seasons, episodes, posters), tools/sync-tmdb.hl + daily run 04:00 UTC, fake TMDB in gatemre
  • 49e1f61edeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre
  • 3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre
  • 44b7d9f9tracker#6: /schedule — upcoming episodes of followed shows, soonest firstmre
  • 91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre
  • a97c0295tracker#4: show page /shows/:slug (header, seasons, episodes, watch checks) + tools/relink-episode-seasons.hlmre
  • 05f407c5tracker: no border on any button except inverted ones (Log out, ident status and identities too); header brand weight 100mre