gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitfbb903ccfbb903cctracker#33/#37 (mission 032): a title without a TMDB poster gets its backdrop (w780, posterFromBackdrop, shown 2:3 centre-cropped); movies store runtime, the page shows release date + runtime; art backfill (public titles without poster file / movies without runtime) and person photo backfill (tmdbProfile / photoCheck) as the last start jobs (TRACKER_ART, TRACKER_PHOTOS; off in every gate start); gates 379/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy: art 4977 titles in 42 min (115 posters, 15 backdrops, 4609 runtimes), The Remaining shows its backdrop + 7 minmrefbb903cc/STATUS.md

183.3 KB

  1. # STATUS — tracker.worldapi.org
  2. ## 2026-10-04 — mission 032: backdrop as poster, release + runtime, art + photo backfills (NOT deployed)
  3. **The Remaining**: live had TMDB's overview/genres/cast already; missing was the poster (TMDB has none, only a backdrop) and
  4. runtime/release (never shown). **Done**: syncPoster falls back to the backdrop (w780, `posterFromBackdrop`, shown 2:3 centre-cropped);
  5. movies store `runtime`, the page shows "17 September 2026 · 7 min"; ART BACKFILL (public titles without poster file / movies without
  6. runtime) and PHOTO BACKFILL (people without photo source → `tmdbProfile` / `photoCheck`) as the last start jobs (`TRACKER_ART`,
  7. `TRACKER_PHOTOS`; README "What it does (mission 032: backdrop …)").
  8. **Tests**: gates browser 379/0, kinds 32/0, franchises 53/0, pager 229/0, franchiseseed 26/0, check-theme 0 (`.scratch/w089/gate-art2-*.txt`).
  9. Real copy (pristine2, tree with a link to `.env`, only merge + art + photos on): merge as #39; `art backfill done: 4977/4977 titles,
  10. posters=115 backdrops=15 runtimes=4609 failed=0 requests=5107 seconds=2527`; The Remaining → `/posters/tmdb-movie-1775752.jpg` (its
  11. backdrop) + "17 September 2026 · 7 min" (`.scratch/w089/remaining/after/`, before = live `.scratch/w089/remaining/live-before-*.png`);
  12. `photo backfill: start, 53106 people` — after ~5 min /people pages 1/2/3: 8/3/4 of 24 with a photo (before 0), 425 checked "none"
  13. (`.scratch/w089/photos-after/`); RSS 1.5–1.7 GB. Stopped there (the whole photo run ≈ 7.5 h).
  14. **How to repeat**: copy as #39; tree = rsync + `ln -s $PWD/.env <tree>/.env` (never read it); start with TRACKER_SYNC/BACKFILL/
  15. REPAIR/KINDS/CREDITS/COLLECTIONS/SHORTIDS/FRANCHISE_SEED=0 (env as tests/realdata-m028.sh), watch `<copy>/server.log`.
  16. **Open**: after the deploy the photo job runs ~7.5 h (≤ 40 req/10 s, pages served); 15 backdrops only — most poster-less public
  17. titles have no TMDB art at all; series runtime not stored (movies only, as asked).
  18. ## 2026-10-04 — merge t38 + t40 into main (w092) (NOT deployed)
  19. **Merged**: b638e99 = t38 (#38 pagination, #35 Returning/Airing), e5945d2 = t40 (#40 curated franchises, Franchises menu, superseded
  20. collections) onto f8ffa4d (#37, #39). Resolved: LOG/STATUS keep every section; deploy.sh GATES = browser, kinds, franchises, pager,
  21. franchiseseed, check-theme (and the `|| true` fix); components/search.hl imports #37's `personPhotoOf` and #40's `importWithCredits`;
  22. the pager gate pages /people (everyone, last updated first: seed `updatedAt`) instead of the removed /people/<letter>, and runs with
  23. `TRACKER_FRANCHISE_SEED=0`. **Gates on the merged tree** (ports 8732–8738): browser 374/0, kinds 32/0, franchises 53/0, pager
  24. 229/0, franchiseseed 26/0, check-theme 0 (`.scratch/w092/merge/m2-*.txt`).
  25. **Live copy** (storage 2026-10-04 ~07:40, `git archive HEAD` + bin, no .env, merge job + franchise seed on, other jobs off):
  26. merge 850 double episodes in 58 s; franchise seed built 17 franchises (superseded=8, imported 0: no token). /movies pager 2→2, 3→3,
  27. Next→4, Previous→3 at 390/1280; /people page 1 = 24 tiles, 4377 pages; Twisted Metal "Returning"; "Franchises" in the desktop
  28. header and the phone sidebar; Star Trek: First Contact ONE widget (Star Trek — Prime, Voyager ‹ › Insurrection); The Flash S9 =
  29. 13 episodes; 0 console problems. Shots looked at: `.scratch/w092/mergeshots/<w>-<page>.png` (script `.scratch/w092/mergecheck.mjs`).
  30. ## 2026-10-04 — tracker#39 double episodes (mission 032 queue) (NOT deployed)
  31. **Cause**: migrated episodes carry TMDB ids TMDB has since replaced; sync.hl matched by number only when an episode had NO tmdbId
  32. → the sync/repair added a second episode per number. Live copy: 850 extra in 112 series (166 in Specials).
  33. **Done**: sync adopts a stale-id episode by number (`migratedTmdbId`); merge.hl step 3 merges each season's doubles (keeper: most
  34. watches > synced > first; watches moved/parked; tombstones out of `episodes` into `mergedEpisodes`); `tools/count-duplicate-episodes.hl`.
  35. **Tests**: gates browser 373/0, kinds 32/0, franchises 52/0 (`.scratch/w089/gate-t39b-*.txt`); the #39 sync check fails with the
  36. old sync.hl (`gate-t39-oldsync-browser.txt`). Real copy (live storage 2026-10-04 ~06:40 CEST, `.scratch/w089/pristine2`,
  37. merge on, other jobs off): `merge done: … doubleSeasons=186 doubleEpisodes=850 items=374 slowestStepMs=1391 seconds=64`; count
  38. 850 → 0; creator watch rows 12,525 = 12,525 (live 12,522 → 12,481: 41 repeats parked); /my/unwatched 993 → 905 rows. Flash S9 22 →
  39. 13, Specials 74 → 72; Discovery S5 20 → 10, S4 15 → 13, Specials 81 → 70; Scrubs S1 48 → 24 … Screenshots
  40. `.scratch/w089/flash-shots/{before,after}-{390,1280}-*.png` (looked at).
  41. **How to repeat**: copy as below; `sed 's/TRACKER_MERGE=0/TRACKER_MERGE=${MERGE:-0}/' tests/realdata-m028.sh > x.sh; MERGE=1 bash x.sh
  42. start <tree> <copy> 8731`, wait for `merge done` in `<copy>/server.log`, stop, `tools/count-duplicate-episodes.hl`.
  43. **Open**: Specials stay big (TMDB's own: Flash 72, Discovery 70) — hide/collapse Specials? (creator); 68 same-tmdbId pairs with
  44. different numbers (old tracker) untouched; the deploy runs the merge once (~1 min, pages served meanwhile).
  45. ## 2026-10-04 — mission 032: tracker#37 /people, people in the search, /settings (NOT deployed)
  46. **Done**: `/people` (+ `/page/<n>`) = everyone, last updated first (newest of `updatedAt` — new, stamped by the person fill —
  47. `filmographyAt`, `added`), photo + name tiles (TileList `tileClass="tile person-tile"`, name in the person colour), pagination like
  48. /movies; letters + `/people/<letter>` removed. A view built at boot (`lib/people.hl warmPeopleView`, ~4 s incl. the cast photos);
  49. created/updated people move to the front at once (`personTouched`). Photo = our file, else `tmdbProfile` (TMDB w185), else a cast/crew
  50. entry's `profile` (in-memory map at boot), else the new "no photo" placeholder. New people keep `tmdbProfile` (cast, crew,
  51. created_by). Search people rows have the photo. `/settings` = `<h1>Settings</h1>` only. util.hl `sortDesc` starts from sorted runs
  52. (same result; 105k people 1.6 s → 0.15 s).
  53. **Tests**: gates browser **369/0** (+4 #37 checks, 3 extended), kinds 32/0, franchises 52/0, check-theme 0 (`.scratch/w089/gate-final3-browser.txt`, `gate-final2-{kinds,franchises}.txt`; a browser run that crosses a full UTC hour fails the 9 #18/#35 in-app-run checks — TRACKER_SYNC_HOUR = the hour at start — seen in `gate-final2-browser.txt`, a known flake: rerun).
  54. Real copy (live storage 2026-10-04 ~04:45, tree without .env, jobs off, 8731; base 96ba683 on 8730): boot log `people view: 105018
  55. people, 51922 cast photos in 3753 ms`; boot RSS ≈ base (+~20 MB; peak noise ±150 MB). /people pages 1/2/3 server 13–17 ms, deep page
  56. 4376 ~15 ms; full load to hydrated (1280, signed in, median of 10) 85/67/77 ms. Search "Jared Harris" → Jared Harris with his photo
  57. (`/profiles/617a9bfadb28af7657ccb0ec.jpg`), 16 ms server. Screenshots 390/1280 in/out LOOKED at: `.scratch/w089/shots-final/` (0
  58. console problems, no sideways scroll).
  59. **How to repeat**: gates `.scratch/w089/gates.sh $PWD <label>` (8720–8727, `GATES="browser kinds franchises"`). Real copy:
  60. `tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage` (Byrodin) → `.scratch/w089/pristine/`; `cp -a` to a copy; tree =
  61. `rsync -a --exclude /.env --exclude /storage/ --exclude /.scratch/ --exclude /.git/ ./ <tree>/`; `tests/realdata-m028.sh start
  62. <tree> <copy> 8731`; shots `REAL_PORT=8731 REAL_CHROME=8729 REAL_SID_FILE=<copy>/sid REAL_OUT=<dir>
  63. REAL_PATHS=/people,/people/page/2,/search/Jared%20Harris,/settings node tests/realdata-m031.mjs`; load `.scratch/w089/ab.mjs`
  64. (`ONLY_NEW=1` when the base has no such page).
  65. **Open**: pages 1–3 on live are ALL crew people (the credits job's last titles' writers/directors: crew entries had no profile
  66. before) → "no photo" until their page is opened or their title's credits are stored again — a photo backfill (TMDB /person per
  67. person, ~50k requests) would fix it, not built; a one-time WRITE of the cast profiles into the people took 511 s at a start → kept
  68. in memory instead; mission 033 (worktree t38) may touch tilelist.hl (this mission added `tileClass`).
  69. ## 2026-10-04 — mission 033 part 2: tracker#35 status label "Returning" / "Airing" (branch t38, NOT deployed)
  70. **Done** (`lib/shows.hl statusOf` + `isAiring`, `lib/util.hl dayStr`): TVmaze "Running" → label "Airing" when an episode of the two
  71. newest numbered seasons has `release` in today ±7 days, else "Returning"; specials (season 0) ignored (Family Guy's 2026-10-05
  72. special between S24/S25 would else say Airing). Data unchanged. **Tests**: browser 366/0 (#35 fixture +2 series, +1 check; the
  73. offline homepage row 7 → 10 shows), kinds 32/0, franchises 52/0, pager 229/0, check-theme 0. Real copy (live storage 2026-10-04
  74. 05:55, `.scratch/w092/live`): Twisted Metal "Returning", FBI "Airing", Family Guy "Returning" at 390/1280, 0 console problems;
  75. shots `.scratch/w092/shots35/m033-<w>-<slug>.png`. Airing candidates on a copy: `.scratch/airing.hl` in the worktree (not committed).
  76. ## 2026-10-04 — mission 033: tracker#38 pagination jumps around (branch t38, NOT deployed)
  77. **Cause** (reproduced live, real clicks, `node tests/repro-t38.mjs https://tracker.worldapi.org 1280|390`): a pager click runs
  78. TWO handlers — `components/pagination.hl`'s own (moves to the page, rebuilds its list INTO THE SAME `<button>`s) and
  79. `tilelist.hl`'s `pager-box` (bubbled), which read `e.target.textContent`. On a REAL click the browser drains microtasks between the
  80. two listeners, so the rebuild lands first and the text is the label now at that index; a script `el.click()` (what browser.mjs
  81. used) runs the rebuild after → the old gate never saw it. Live /movies: "2" →
  82. reads "1" → no navigation (pager says 2, tiles page 1); "Next" on 3 → reads "199" → last page. All 5 lists (all via tilelist).
  83. **Fix** (`components/tilelist.hl`, pagination.hl untouched): if the pagination already marks another page `li.current`, go there
  84. (it resolved Previous/Next/number against its own page = the address's), else the button's own not-yet-rebuilt text; the pager-box is passed from the click (`e.currentTarget`) since the
  85. handler runs after the dispatch and "Next" → last page removes the clicked button.
  86. **Tests**: new gate `tests/pager.mjs` (5th in deploy.sh): /movies /series /shows /genres/Pagerg /people/p (since the t38 merge: /people, tracker#37), 11 pages each
  87. (`tests/seed-pager.hl`), 390 + 1280 real clicks + 1280 script clicks: 2,3,Next,Previous,11,10,Previous,Next,1,Back,Back,Forward,Next,Back — address,
  88. tiles (count/first/last), current, same document, stable 600 ms: **229/0** (main, real clicks only: 113/40). browser 365/0, kinds 32/0, franchises 52/0, check-theme 0
  89. (`.scratch/w092/gate-*.txt`).
  90. Real copy (live storage, `.scratch/w092/copy`, 8733 fixed / 8734 main): main jumps like live, fixed goes 2,3,4,3 + Back/Forward.
  91. **How to repeat**: `TRACKER_GATE_PORT=8732 TRACKER_GATE_CHROME=8736-8739 node tests/pager.mjs`; live check read-only with repro-t38.
  92. **Open**: components.hybriel's Pagination still has no data out (hybriel#87) — a `paginationGo` reaching the host would make the
  93. DOM read unnecessary; deploy + creator confirmation.
  94. ## 2026-10-04 — mission 034: tracker#40 the curated franchises (branch t40, NOT deployed)
  95. **Superseded collections (separate commit)**: a TMDB collection timeline whose titles are all in one curated timeline is hidden
  96. (`supersededBy`, kept: own page + editor finder), set by the collection seed on creation and by the curated build; partly covered
  97. ones join that franchise. Gate franchiseseed 26/0 (+ Gate Kelvin Collection hidden at creation, collection 900 superseded, Extras
  98. joined, one widget each), browser 365/0, kinds 32/0, franchises 53/0, theme 0. Real copy (seeded copy + collection seed with the
  99. real token, `TRACKER_COLLECTIONS=1 tests/realdata-m034.sh start …`): `collections done: 149/149 movies … timelines=16 titles=61
  100. superseded=16`, curated build `superseded=8`; restart `superseded=0` (idempotent). 24 superseded: Alien, Ant-Man, Aquaman, AVP,
  101. Black Panther, Captain America, Fantastic Beasts, Harry Potter, Iron Man, Man of Steel, Planet of the Apes (Original), (Reboot),
  102. Predator, Prometheus, Shazam!, Star Trek: Alternate Reality, Star Trek: TNG, The Avengers, Fast and the Furious, The Hobbit, LOTR
  103. (Animated), LOTR, The Matrix, Thor. Joined/attached, still shown: I Am Groot → MCU; Justice League (DCAU), Superman / Batman
  104. (Animated), Superman → DC. First Contact, Fast Five, Discovery, Into Darkness, Star Trek (2009): ONE widget each (`.scratch/w093/
  105. shots-sup/`, LOOKED at). /franchises: 102 loose timelines.
  106. **Menu (separate commit, deployable on its own)**: "Franchises" → /franchises in the main menu (desktop header after People, phone
  107. sidebar), marked on franchise/timeline pages (`components/main.hl` menu, `styles.hl` :has rules); gate franchises.mjs +1 check
  108. (53/0), browser.mjs expects the 6 entries (365/0). **First Contact "no widget"** (creator): NOT reproducible — one record (tmdb
  109. 199, not merged) is in the TNG collection timeline; all 282 timeline members resolve to live records (0 tombstones/missing,
  110. `.scratch/w093/fc.hl` on a live copy); live HTML + hydrated page show the widget, signed in as az5b2 on a live copy with the
  111. deployed code too (6 navigation paths). Needs the creator's device/path/screenshot.
  112. **Done**: `data/franchises.json` — 17 franchises, 31 timelines, 285 distinct TMDB titles (movies + series; ids checked against
  113. themoviedb.org), order `in-universe` or `release` each; 12 TMDB collection timelines attached. `lib/franchiseseed.hl` + jobs.hl
  114. `franchiseSeedTick` (last start job; imports missing titles via `details.hl importWithCredits` = the search's Add, one per step,
  115. paced; then one build step; `franchiseseed.db` records what it made — editor changes win, the creator's same-name franchise
  116. adopted / timeline left alone; resumable, idempotent). Timeline heads say "N titles · in-universe order" (`orderKind`; the
  117. head now wraps so a long count never squeezes the name — phone bug found in the screenshots). Series pages show the widget.
  118. `TRACKER_FRANCHISE_SEED=0` added to browser/kinds/franchises gates; new 5th gate in deploy.sh.
  119. **Tests**: gates browser **365/0**, kinds 32/0, franchises 52/0, **franchiseseed 24/0**, check-theme 0 (`.scratch/w093/gate-final-*.txt`
  120. in the t40 worktree `/media/STORAGE/projects/tracker-wt40`). Real copy (live storage 2026-10-04 ~05:20, all other jobs off, `.env` link
  121. for the run, removed after): `franchise seed done: franchises=17 (created=17 adopted=0) timelines=31 (created=31 …) collections=12
  122. titles=285 had=89 imported=196 failed=0 missing=0 placed=290 requests=636 tvmazeRequests=93 seconds=429.55`; RSS ≤ 2.7 GB; restart
  123. → `unchanged=31`, nothing imported. Pages: /franchises 0.03 s, Star Trek 0.09 s, MCU 0.56 s (54 titles, series' last-episode scan),
  124. Discovery 0.10 s. Screenshots (390/1280, in/out, LOOKED at) `.scratch/w093/shots-real2/` (+ `390-widget-*.png`): Discovery
  125. ‹ Enterprise | Star Trek — Prime | Strange New Worlds ›, Into Darkness ‹ Star Trek (2009) | Kelvin | Beyond ›, no sideways scroll, 0
  126. console problems.
  127. **How to repeat** (Loreana, worktree tracker-wt40, ports 8760–8769): gates `.scratch/w093/gates.sh $PWD <label>` (8760–8764, 8767).
  128. Real copy: `tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage` (Byrodin) → `.scratch/w093/pristine/`; tree = `rsync -a
  129. --exclude /.env --exclude /storage/ --exclude /.scratch/ --exclude /.git/ ./ <tree>/` + `ln -s …/tracker.worldapi.org/.env <tree>/.env`
  130. (never read; `rm` after); `tests/realdata-m034.sh start <tree> <copy> 8768`; wait for `franchise seed done`; stop; screenshots
  131. `REAL_PORT=8768 REAL_CHROME=8769 REAL_SID_FILE=<copy>/sid REAL_OUT=<dir> REAL_PATHS=/franchises,/franchises/Star-Trek,… node
  132. tests/realdata-m031.mjs`; widget shots `node .scratch/w093/widgetshot.mjs <dir>`.
  133. **Open**: the creator should correct the list/order (report 034 has it); "Star Trek: The Animated Series" is "Star Trek" on TMDB
  134. (→ /series/Star-Trek-2, "Star Trek (1973)"), Kelvin's "Star Trek" is /movies/Star-Trek-3; imported movies get their TMDB collection
  135. checked on the NEXT start (new loose timelines like "Star Wars Collection" will appear — attach them via the file's `collections` or
  136. the editor); the TMDB "Star Trek Collection" etc. give a second widget on those movies; MCU page 0.56 s; no rename/delete in the
  137. editor (#19); Spider-Man (Raimi/Webb/Sony), X-Men (Fox), Superman/Batman films before the DCEU, Short Treks, Star Wars Visions /
  138. Tales, Stargate Infinity, Torchwood specials and Doctor Who specials as movies are NOT in the file.
  139. ## 2026-10-04 — mission 031: the WorldAPI theme + tracker#32 header/menu (NOT deployed)
  140. **Done**: `theme/` vendored verbatim from layouts.worldapi.org (85b5654), `shared/tokens.hl` byte-equal; `components/styles.hl`
  141. inherits it (accent green-dark, type colours 1 Movie yellow · 2 Series green · 3 Show purple · 4 Genre blue · 5 Person orange · 6
  142. Franchise); own base/header rules removed; row lines, the timeline/widget lines, genre-pill and inverted-button frames gone (the
  143. season foldable keeps its line). `node theme/check-theme.mjs .` 21 → **0** (deploy.sh GATES #4). Header = theme
  144. AppHeader/MainMenu/UserMenu/Sidebar/ContentFirst (`components/main.hl`), as #32 says (README "What it does (mission 031)"). New:
  145. `/genres`, `/people` (+ `/<letter>`, `/page/<n>`), `/settings`, `/find?q=` → `/search/<q>`. Main actions `class="primary"`.
  146. **Tests**: gates browser **365/0**, kinds 32/0, franchises 52/0, check-theme 0 (`.scratch/w088/gate-run1-*.txt`,
  147. `gate-run2-browser.txt`; shots `/tmp/w088/shots/header-*.png` on Loreana). Real copy (live storage 2026-10-04 ~11:00, tree without
  148. .env, jobs off): every page 390/1280 in/out, header 52 px, no sideways scroll, **0 console problems**; screenshots LOOKED at:
  149. `.scratch/w088/shots-final/` (before: `shots-base/`). Load (interleaved A/B vs a386dc9, 10 loads, median to hydrated, 1280 signed
  150. in): +4…+16 ms per page (sum of 13 medians 790 → 866 ms); server +3–5 ms per small page. Cause: 5 more component modules
  151. (theme/shell/*) and +19 KB page seed (their blueprints). Big pages unchanged (Picard 155 → 127, /my/unwatched 35 → 40).
  152. **How to repeat** (Loreana, ports 8720–8731): gates `.scratch/w088/gates.sh $PWD <label>` (8720–8727). Real copy:
  153. `tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage` (Byrodin) → `.scratch/w088/pristine/`; a tree = `rsync -a --exclude
  154. /.env --exclude /storage/ --exclude /.scratch/ --exclude /.git/ ./ <tree>/`; `tests/realdata-m028.sh start <tree> <copy> 8731`;
  155. screenshots `REAL_PORT=8731 REAL_CHROME=8729 REAL_SID_FILE=<copy>/sid REAL_OUT=<dir> node tests/realdata-m031.mjs`; A/B:
  156. `.scratch/w088/ab.mjs` (base on 8730, new on 8731).
  157. **Open**: the hl:web slot bug (a page slot not at a line start is rendered once per line — WebFramework.hl `document()`) worked
  158. around, not fixed → Hybriel ticket?; `aria-current` on the marked menu entry (a shell gets no path); the theme doc's
  159. `ContentFirst { Sidebar … main { slot } }` pattern breaks in a `parent` shell (tell layouts.worldapi.org); /people order = name
  160. words, lower case (punctuation ignored); "Settings" has nothing to set yet; load +~10 % from the theme's shell modules.
  161. ## 2026-10-04 — mission 030: tracker#34 Follow under the poster + tracker#35 status pill (NOT deployed)
  162. **Done** (commit d36ea6e): Follow sits directly under the poster, as wide as it (phone: full width, desktop 10rem; a posterless
  163. title: under the placeholder). A series'/show's title row carries an INVERTED status pill (filled, dark `colorAccentText`, no
  164. border; theme tokens only): TVmaze's `tvmazeStatus` when stored, else TMDB's `status` (`Returning Series` → "Returning"); TVmaze
  165. Ended + TMDB Canceled → "Canceled" (TVmaze has no canceled). Tones (`shows.hl statusOf`): running green (`colorSuccess`), pending
  166. (In Production/Development, To Be Determined, Planned, Pilot) yellow (`colorWarning`), canceled red (`colorDanger`), ended/other
  167. muted. Movies: no pill.
  168. - Kept current: the sync's TVmaze merge stores it (followed series, every completion); the daily delta asks `GET /shows/<id>` for
  169. an UNFOLLOWED series TVmaze's change list names (status-only step, or with its light step) — `dailysync.hl syncRunStep`,
  170. `sync.hl syncTvmazeStatus`; backfill = the details repair's second part (`jobs.hl statusTick`, `backfill.hl
  171. tvmazeStatusTitle`): every series with a TVmaze id and none, 1 TVmaze request + 550 ms, resumable, 404 → `tvmazeStatusCheck`.
  172. **Tests**: gates 354/0, 32/0, 52/0 (`tests/seed-status.hl`, fake TVmaze `statuses`/`missing`; #34 geometry 12 loads, #35 pill
  173. colours, backfill + resume, delta status-only). Real copy (029's repaired copy, real TVmaze): `tvmaze status done: 1558/1558
  174. series, stored=1558 notFound=0 errors=0 seconds=1092`, RSS max 3.0 GB. Pills (histogram of non-adult series): Ended 1,210,
  175. Canceled 321, Running 161 + Returning 61, To Be Determined 44, Pilot 4, In Production 2, In Development 2. Screenshots (390/1280,
  176. in/out) `/tmp/w079-real/after/` on Loreana: Foundation Running, Star Trek: Picard Ended, Raised by Wolves Canceled, The Orville To
  177. Be Determined, Mapping the World Returning (TMDB only); Follow under the poster, same width, every load; no sideways scroll.
  178. **How to repeat**: copy as mission 029, `tests/realdata-m028.sh start <git archive HEAD + bin> <copy> 8731`, then
  179. `REAL_PORT=8731 REAL_CHROME=8728 REAL_SID_FILE=<copy>/sid REAL_OUT=/tmp/x REAL_PATHS=/series/Foundation,… node tests/realdata-m029.mjs`
  180. (prints poster/Follow boxes + the pill's text/colours per load). The status backfill itself needs the jobs on (real `.env`).
  181. **Open**: `/shows/The-Daily-Show` is a Dutch 2011 show (TMDB 38609, Canceled) — an old-tracker id mix-up, not the US one
  182. (TMDB 2224); the signed-out Follow is the inverted button WITH its 1px border (existing rule "inverted keeps its border" —
  183. borderless design rules: tracker#32); genre pills still bordered (#32).
  184. ## 2026-10-04 — mission 029: tracker#33 reduced titles (NOT deployed)
  185. **Why The Remaining looked reduced**: it is NOT a partial import — complete per TMDB (search import, `detailsAt` set): TMDB has no
  186. poster (`/movie/1775752/images`: posters []), no IMDb id, 5 cast, a 7-minute Finnish short. The page showed an empty poster column
  187. → now the placeholder ("no poster") in the poster's place (`components/show.hl noPoster`).
  188. **The real reduced ones**: #26's `isIncomplete` only knew `minimal`/`imported` — the old tracker's MIGRATED rows that were never
  189. synced passed. Complete now = TMDB's details went through this app once (`detailsAt` or `tmdbSync`) or given up (`detailsCheck`).
  190. Live copy 2026-10-03 22:2x: **5,834 reduced** (4,206 movies + 1,491 series non-adult, 137 adult flag unknown); their pages missed:
  191. seasons 703, genres 554, ids 506, cast 371, poster 324, release 177, summary 162.
  192. **Done** (commit 7d7d448): `shows.hl isIncomplete` + `missingParts`; repair queue visibly reduced first; `tools/count-incomplete.hl`
  193. (`reduced titles: N`, `--list`); gate fixtures stand for synced titles (`tmdbSync = 1`), `tests/seed-reduced.hl` + 5 #33 checks.
  194. **Tests**: gates 347/0, 32/0, 52/0. Real copy, real TMDB/TVmaze, only the repair on: `details repair done: 5834/5835 titles,
  195. complete=5697 failed=137 newSeasons=1933 newEpisodes=65433 posters=11 cast=276370 requests=7903 tvmazeRequests=2170 seconds=7567`
  196. (2 h 6 min); RSS 2.3 → max 2.6 GB → 2.1 GB (flat); pages meanwhile (/movies, Picard, First Contact, / every 200 ms, 3 min): median
  197. 33 ms, p99 0.63 s, max 1.5 s; 43 slow steps > 1.5 s, worst Today (15,716 episodes) 11.5 s, This Morning 5.3 s, a 5.5 s one (the
  198. finish step reads every episode for the TVmaze merge — known since #18). `count-incomplete`: **5,834 → 0**; gaps left = TMDB has
  199. none (seasons 28, genres 383, poster 367, ids 566, cast 371). The creator's /my/unwatched 1,278 / /my/series 127 / /my/schedule
  200. 64 rows — same before and after. Screenshots `/tmp/w079-real/{before,after}/` (The Remaining placeholder, Sat af: no seasons → 2).
  201. **How to repeat** (Loreana, ports 8720–8731): `tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage` from Byrodin into
  202. `.scratch/<w>/copy/`; count: `TRACKER_STORAGE=$PWD/.scratch/<w>/copy/storage/mpackdb ./bin/hybriel tools/count-incomplete.hl`;
  203. repair: this tree (its `.env` = real token, never read) on the copy with `TRACKER_SYNC=0 TRACKER_BACKFILL=0 TRACKER_KINDS=0
  204. TRACKER_CREDITS=0 TRACKER_COLLECTIONS=0 TRACKER_SHORTIDS=0 TRACKER_PORT=8731 TRACKER_STORAGE=<copy>/storage/mpackdb …` (full env:
  205. `tests/realdata-m028.sh`), wait for `details repair done`, stop, count again. Pages without TMDB: `tests/realdata-m028.sh start`.
  206. **Open**: runtime is not stored (TMDB has it; nothing shows it); the deploy runs this repair once (~2 h, + the 030 status part
  207. ~18 min) — slow steps block pages up to ~11 s on Today/This Morning.
  208. ## 2026-10-03 — mission 028: code order (no behaviour change) (NOT deployed)
  209. **Rules** (creator, README "Code order"): `let` only when reassigned; `project.hl` = the map; no `.hl` in the root but
  210. `project.hl`; one file per topic in `lib/` with its central logic, helpers beside it, shared ones once in `lib/util.hl`; thin
  211. route handlers and faces.
  212. **Done** (commits 1/5 move · 2/5 util + topics · 3/5 project.hl · 4/5 thin faces + last copies · 5/5 let · comments):
  213. | | before (4f47f18) | after |
  214. |---|---|---|
  215. | `.hl` files in the root | 15 | 1 (`project.hl`) |
  216. | `project.hl` lines | 914 | 261 (config, routes, thin wrappers, boot wiring, `jobs = new Jobs()`, the feature → file index) |
  217. | `let` (code, all .hl) | 1,211 — 706 never reassigned outside a loop | 525 = 264 in loop bodies + 234 reassigned + 27 kept (name also a member / outer / free name) |
  218. | lib files | — | 25: util, users, shows, follows, watches, catalog, search(+helpers), people, credits(+helpers), details, tmdb, sync(+helpers), dailysync, backfill, shortids, franchises, collections, merge, jobs, images, export |
  219. - Copies removed: envOr ×5, storageDir ×4, textOr/newId/todayStr/dateOr/listOr/firstOf ×2–3, hexDigits ×3, sortDesc, the slug
  220. builder (franchises), the poster dir, the export readers (tools), todayStr/listOf in components, the login exchange (two logins).
  221. - `lib/jobs.hl` is a CLASS (one instance, `jobs = new Jobs()` after the server): the job runners' state is reassigned every
  222. step and a static cannot be (tested: "Cannot reassign immutable variable"); its code moved unchanged from project.hl.
  223. - Behaviour notes: `postersDir`/`profilesDir` now via `envOr` (trimmed; project.hl took a set-but-empty `TRACKER_STORAGE` as
  224. is — the tables never did); `tools/old-short-ids.hl` now stops on an export that splits into no chunk (as migrate.hl did).
  225. - Hybriel facts found: a plain declaration in a loop body fails on the 2nd pass; a plain `x = …` in a function REBINDS a file
  226. member of that name; a plain declaration copies like `let`; client JS emits `let` → `var` but a plain one → block `let`.
  227. **Tests** (ports 8720–8731; outputs `.scratch/w078/`): gates after every step and at the end `342/0`, `32/0`, `52/0`
  228. (`.scratch/w078/gates.sh <tree> <label>`). Real copy (live `storage/` 19:xx, signed in as az5b2, all jobs off, no `.env`), 24
  229. pages + 10 redirects/404s, 2 runs each tree: **normalized page HTML and hydrated text identical** (49 files; only the build hash,
  230. `"at"`, `"who"` and source positions differ). Browser full load, mean of 2×10 loads: sum 7,358 → 7,258 ms (0.99×; single pages
  231. 0.86–1.17×, ±noise); boot 48–51 s both, boot RSS 2,216 MB both. Client modules 835.6 → 842.8 KB (+0.9 %). Tools old vs new on
  232. the export/copy: old-short-ids byte-identical, migrate + verify output identical (ids aside), 7 count tools identical.
  233. Jobs on the real copy (final tree + a link to `.env`, `tests/realdata-074.sh jobs`, forced delta run): `tmdb sync done: mode=delta
  234. changeRequests=114 shows=345 newSeasons=78 newEpisodes=5857 … requests=629 errors=0 seconds=609`, `details repair done: 24/24
  235. complete=24 failed=0`, kinds/credits/merge/adult/short ids nothing to do, `collections done: 17/17 errors=0`; no error line; RSS
  236. 2.37 → 2.49 GB max (mission 027: 2.54); after it `/my/series` 0.10 s, `/my/unwatched` 0.13 s, Picard 0.09 s (curl).
  237. How to repeat (Loreana, this folder; ports 8729–8731; base = `git archive 4f47f18` + bin/ in `.scratch/w078/base-tree`):
  238. ```
  239. W=$PWD/.scratch/w078; ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C $W/pristine
  240. # a tree = git archive <commit> + cp -a bin; NO .env (tests/realdata-m028.sh refuses one)
  241. rm -rf $W/run-x && mkdir -p $W/run-x && cp -a $W/pristine/storage $W/run-x/ && tests/realdata-m028.sh start <tree> $W/run-x 8730
  242. REAL_PORT=8730 REAL_CHROME=8729 REAL_SID_FILE=$W/run-x/sid REAL_OUT=$W/out-x REAL_LOADS=10 node <tree>/tests/realdata-m028.mjs
  243. tests/realdata-m028.sh stop x $W/run-x 8730 # same for the other tree into out-y, then:
  244. for f in $W/out-x/html/*.norm.html; do cmp -s $f $W/out-y/html/$(basename $f) || echo DIFF $f; done # timing.json per run
  245. python3 tools/let-audit.py --list project.hl lib/*.hl components/*.hl tools/*.hl tests/*.hl # the kept lets + why
  246. ```
  247. **Open**: deploy note — rsync has no `--delete`: the 14 old root `.hl` files stay on Byrodin (unused; delete them by hand after the
  248. deploy). The search import face (components/search.hl) still stores the credits itself (search.hl cannot import credits.hl:
  249. credits → people → search). Topic files left long: merge.hl 599, sync.hl 446, franchises.hl 445, people.hl 404 lines.
  250. ## 2026-10-03 — mission 027: Hybriel master 06617221 vendored (plugin allocator fixes 3a781359 + 413f60e4) (NOT deployed)
  251. **Vendor**: 190aa11d (`feb597c4…`, live with dc40d85) → master **06617221** (`af6dfcea…`), built ReleaseFast from a read-only archive
  252. (`~/scratch-077`, removed). Changed: crypto fetch fs http http1 mpackdb proc smtp time (.so + sources), web (WebFramework/client: a
  253. dev-watch build check only). Old vendor in `.scratch/pre-027/`. Lambda audit: no lambda/parameter semantic change since 190aa11d.
  254. **Gates** (ports 8720–8727, outputs `.scratch/w077/gate-*.txt`): `TRACKER_GATE_SHOTS=/tmp/m027/shots TRACKER_GATE_PORT=8720
  255. TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs` → `342 passed, 0 failed`;
  256. same env `tests/kinds.mjs` → `32/0`, `tests/franchises.mjs` → `52/0` (shots dirs must exist: `mkdir -p /tmp/m027/{shots,kinds,fr}`).
  257. **Real copy** (one tar of live `storage/` 01:33, az5b2; first-start jobs = forced delta, 423 titles, ~11.5 min, both binaries the same
  258. `tmdb sync done` line). Browser full-load ms, mean of 3 runs (200 + 2×100 loads; server ms + per-load RSS in `.scratch/w077/logs/{new,old}/`):
  259. | page | old fresh | new fresh | old after jobs | new after jobs |
  260. |---|---|---|---|---|
  261. | /my/series | 228 | 190 | 200 | 217 (1.08×) |
  262. | /my/unwatched (1,266) | 549 | 441 | 426 | 465 (1.09×) |
  263. | Picard + Show all | 397 | 319 | 321 | 267 |
  264. | /series | 99 | 90 | 108 | 82 |
  265. | /franchises | 118 | 101 | 118 | 98 |
  266. RSS (10 s samples, jobs + 400 loads after): **new 2.27 → 2.54 GB at 200 s, then flat 2.54–2.59 GB** to the end; old 2.26 → 5.58 GB
  267. (climbing ~6 MB/s during the sync, peak 5.61). Fresh 200 loads: both flat (new 2241 → 2189 MB, old 2262 → 2256). Boot RSS ~2.25 GB both.
  268. Loreana load 8–11 (other workers): single runs vary ±30 %; the first after-jobs new run was 1.3× on /my/ pages, the 2 repeats ≤ old.
  269. Only fresh /series server time is 25 vs 18 ms mean (single outliers 43/347 ms); its browser time is 0.90×.
  270. How to repeat (ports 8729–8731; old tree = rsync of this folder without .scratch/storage/.env/.git + `.scratch/pre-027` bin/plugins + absolute `.env` link):
  271. ```
  272. W=$PWD/.scratch/w077; ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C $W/pristine
  273. for w in new old; do mkdir -p $W/real-$w && cp -a $W/pristine/storage $W/real-$w/; done
  274. tests/realdata-074.sh fresh $PWD $W/real-new 8730; R=$W/real-new; REAL_LOADS=40 REAL_PORT=8730 REAL_CHROME=8729 REAL_SID_FILE=$R/sid REAL_PID=$(cat $R/server.pid) node tests/realdata-027-bench.mjs; tests/realdata-074.sh stop x $R 8730
  275. tests/realdata-074.sh jobs $PWD $W/real-new 8730; tests/realdata-074.sh jobs $W/oldtree $W/real-old 8731 # wait for "tmdb sync done" in real-*/server-jobs.log, RSS in rss-jobs.log
  276. # then the bench against each (REAL_PORT 8730 / 8731), stop both by PID
  277. ```
  278. ## 2026-10-03 — mission 026: tracker#31 duplicate titles merged + deploy.sh waits for the boot (NOT deployed)
  279. **Cause**: not this app — the OLD tracker. Live copy (Byrodin `storage/` 2026-10-03 ~00:30): all 157 records of the 68 duplicated
  280. type+tmdbId pairs are migrated rows (Mongo `oldId`, consecutive ids made 2022–2023, e.g. Archive 81 `62f53674…35`–`…3a`). The
  281. app's creating paths (search import, filmography fill) look up `search.hl haveTmdb` first and their `oldId tmdb-<kind>-<id>` is
  282. UNIQUE in the `!oldId` index → no app path made one. Seasons: 449 migrated seasons name a title without being in its list; the
  283. sync goes by the list → it made a twin where TMDB has that number (16 twins, Reacher S3 with the creator's 8 watches + check).
  284. **Fix** (README "mission 026"): `merge.hl` (repair job on its own clock, before the TMDB jobs; keeper = follows/watches, else old
  285. short id, else oldest; tombstones `mergedInto`, never deleted; stray twins merged, watched strays linked); `shows.hl` slug map with
  286. ranks + `liveShowOf` + `movedSlugs` (301 routes at boot, project.hl `slugMoved`), `isPublicTitle` false for tombstones; search
  287. import looks up again before its put, `searchTitleMerged`; shortIdPath / filmography / timelines (`franchises.hl titleMerged`)
  288. resolve to the keeper; need* of repair/credits/adult/kinds skip tombstones; deploy.sh step 5 waits up to 90 s.
  289. **Decision**: stray seasons WITHOUT a twin are linked only when watched (2: Supergirl S0, The Peripheral S0). Linking all 393 with
  290. episodes (first try) put +1,091 never-watched old talk-show episodes on the creator's /my/unwatched (1,273 → 2,364). The other
  291. 431 strays (391 with episodes) stay as they were (not shown); when the sync makes a season of that number later, the next start merges the stray in.
  292. **Real copy** (TMDB jobs off, `tests/realdata-m026.sh`; before = 2667da0 on the same copy):
  293. | | before | after |
  294. |---|---|---|
  295. | type+tmdbId held by >1 live record (count-tmdb-ids) | 68 (157 records) | **0** (89 tombstones) |
  296. | Archive 81 live records / search "archive 81" hits | 6 / 6 | **1 / 1** (`/series/Archive-81`, was the keeper at `Archive-82`) |
  297. | Reacher S3 (listed) watched by az5b2 / page check | 0 of 8 / regular | **8 of 8 / solid** |
  298. | /my/series Reacher · Archive 81 | S03E08 · S01E08 → /series/Archive-82 | S03E08 · S01E08 → /series/Archive-81 |
  299. | /my/unwatched rows | 1,273 (16 Reacher incl. S03) | 1,265 (8 Reacher, S04 only) |
  300. | az5b2 follows rows/live · watches rows/live | 130/130 · 12,139/12,136 | 130/130 · 12,139/12,136 |
  301. | stray seasons (naming a title, not listed) | 449: 16 with a listed twin, 5 watched | 431, none with a twin or a watch (0 work left) |
  302. - Merge run: `merge: start, 68 duplicate groups (157 records), 15 titles with stray seasons (1632 ms to find them)` → `merge done:
  303. groups=68 tombstones=89 follows=0 watches=9 seasonsMerged=29 seasonsLinked=2 episodesMoved=22 episodesMerged=187 cast=16 people=44
  304. timelines=0 slugs=1 … items=83 slowestStepMs=187 seconds=9.869`. Next start: `moved slugs: 28`, `merge: nothing to do` (1.2 s scan).
  305. - Redirects (curl): `/series/Archive-82` → 301 `/series/Archive-81`; `/727vk` (a tombstone's short id) → 301 same; `/movies/Ecstasy-10`
  306. → 301 `/movies/Ecstasy-9`. Boot (listening) 24 s before → 26–27 s after (slug map at boot); The View page 3 loads ~200 ms both.
  307. - Screenshots (looked at): `/tmp/m026/real/{before,after}-*-{390,1280}.png` on Loreana (Reacher S3 regular → solid; search 6 → 1
  308. row; Archive 81 page all 8 checked, "Following", cast 22).
  309. - deploy.sh step 5 tested against a fake server (502 for 12 s → `after 0 s: 502`, `after 12 s: 200`, exit 0; with
  310. DEPLOY_URL_WAIT=6 → REFUSED "answered 502, not 200, after 6 s").
  311. - Gates (ports 8720–8727): `TRACKER_GATE_SHOTS=/tmp/m026/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726
  312. TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs` → `342 passed, 0 failed` (+15 #31 checks: fixture, merge log, /my/series, keeper page,
  313. /my/unwatched, stray seasons, movie keeper, old slug before restart, short ids 301, search once, credit + timeline, counts, the
  314. two creating paths, 301 after restart, browser follows it); same env `tests/kinds.mjs` → `32 passed, 0 failed`, `tests/franchises.mjs`
  315. → `52 passed, 0 failed`. Gate shots looked at: `/tmp/m026/shots/dupe-{keeper,stray}-1280.png`.
  316. **Open**: 431 unwatched stray seasons stay unlinked (391 with episodes) (architect: link them? +1,091 /my/unwatched rows for the creator); 224 episodes
  317. share season+number inside one season (migrated, untouched); the 301 for a slug merged while the process runs needs the next start.
  318. How to repeat (Loreana, this folder; ports 8728–8731):
  319. ```
  320. W=$PWD/.scratch/w075; mkdir -p $W/pristine; ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C $W/pristine
  321. for c in before after; do rm -rf $W/$c; mkdir -p $W/$c; cp -a $W/pristine/storage $W/$c/; done
  322. TRACKER_STORAGE=$W/before/storage/mpackdb ./bin/hybriel tests/realdata-m026-counts.hl # the numbers (app stopped)
  323. tests/realdata-m026.sh start $W/after 8730 # merge runs (~10 s, "merge done" in $W/after/server.log); stop, counts, start again (moved slugs)
  324. REAL_PORT=8730 REAL_CHROME=8729 REAL_SID_FILE=$W/after/sid REAL_SHOTS=/tmp/m026/real REAL_LABEL=after node tests/realdata-m026.mjs
  325. tests/realdata-m026.sh stop $W/after 8730 # "before" pages: the 2667da0 tree (git archive + bin/) with the script copied in, on $W/before
  326. ```
  327. ## 2026-10-02 — mission 025: tracker#30 /my/ pages fast after the jobs + timeline name once + widget position + First Contact (NOT deployed)
  328. **Cause of #30**: every /my/series load fetched each followed title's whole record (cast + crew) and one episode per watch (11,648
  329. fetches); /my/unwatched + /my/schedule every season + episode of 128 titles. With a big heap after the jobs each allocated byte
  330. feeds the byte-triggered GC. **Fix** (shows.hl, watches.hl, README "mission 025"): slim caches `cardOf` / `episodeRowsOf` /
  331. `episodeKeyOf` + `watchedSetOf`, warmed at boot (~460 ms), refreshed in `catalog.hl refreshCatalogShow`; also used by the tiles.
  332. /my/series keeps going by the watched EPISODE's own `show` (Reacher S03 watches sit on a migrated duplicate season outside
  333. the title's seasons — the first cut via the seasons gave S02E08 instead of S03E08; caught by the before/after text diff).
  334. **Real copy** (live storage 14:50 — taken while live ran its seed, so the copy's first start = forced delta (316–333 titles) +
  335. the rest of the seed (1,802 movies), ~20 min; signed in as az5b2; 20 loads/page, `server / browser ms avg`):
  336. | page | before (3909810), after jobs | after (this), after jobs | after, fresh |
  337. |---|---|---|---|
  338. | /my/series (126) | 1767 / 1927 | **63 / 123** | 53 / 113 |
  339. | /my/shows (3) | 3539 / 3397 | 39 / 73 | 64 / 73 |
  340. | /my/unwatched (1,288) | 4063 / 4540 | **176 / 329** | 172 / 336 |
  341. | /my/movies | 114 / 155 | 8 / 38 | 6 / 36 |
  342. | /my/schedule (56) | 4518 / 4543 | 14 / 52 | 13 / 58 |
  343. | /series | 50 / 67 | 18 / 62 | 16 / 55 |
  344. | Picard + Show all | 164 / 207 | 159 / 228 | 147 / 221 |
  345. | RSS over the run | 4.7 → 21.3 GB (peak), 10.6 end | 1.36 → 1.68 GB | 1.83 → 2.13 GB |
  346. - Before was benched while the after-process ran its jobs; after was benched with 3 GB of it swapped out (the before-process had
  347. pushed Loreana into swap). Fresh before vs after (5 node fetches): /my/series 166–277 → 21–41 ms, /my/unwatched 500–730 → 87–125.
  348. - **Correctness**: page text of /my/series, /my/shows, /my/unwatched, /my/movies, /my/schedule, /series, / identical between
  349. before and after (fresh), and between the after-process that ran the jobs (caches refreshed by 333 synced titles, 6,361 new
  350. episodes) and a fresh process on the same storage.
  351. - **Timeline page** (components/franchise.hl): heads carry `hasName`; a timeline's own page shows no `a.timeline-name`.
  352. - **Widget** (show.hl/styles.hl): `FranchiseWidget` inside `showMeta` before the genres; hidden when empty (`:has`).
  353. - **First Contact**: tmdbId 199 is correct (TMDB movie 199); tmdbSummary was TV 199's ("Kabuhayang Swak na Swak…"), status
  354. `Ended`, seasonsCount/episodesCount 0 — from the #9 `/tv/` bug (report 006). `details.hl hasTvLeftovers` puts such a movie into
  355. the details repair; on the copy the next start logged `details repair: start, 1 incomplete titles` → `done: 1/1 complete=1`,
  356. record afterwards: tmdbSummary = the movie's text, status Released, counts null. Only 1 such movie on real data.
  357. - **Shared TMDB ids** (`tools/count-tmdb-ids.hl`): 0 records share a TMDB id with a different title. 68 type+id pairs are held by
  358. 2–6 records of the SAME title (mostly adult filmography titles, also Archive 81 ×6, Ally McBeal ×2, The Flight Attendant ×2;
  359. several even share one slug) — duplicate imports, not fixed here.
  360. - Gates: browser 327/0 (+2: TV-leftover repair, repaired once), franchises 52/0 (+2: widget position 390/1280, timeline name
  361. once), kinds 32/0. Shots looked at: gate `/tmp/w074/{shots,fr,kinds}`, real `/tmp/w074/real/*-{390,1280}.png` (First Contact
  362. full page, A Kid in… timeline, /my/*) on Loreana.
  363. **Open**: duplicate title records (above) — which import path makes them (search import / filmography racing?) is a ticket of
  364. its own; the page caches assume one process per storage (as watches.hl already does).
  365. How to repeat (Loreana, this folder; ports 8728–8730):
  366. ```
  367. W=$PWD/.scratch/w074; mkdir -p $W/pristine $W/real-after
  368. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C $W/pristine
  369. cp -a $W/pristine/storage $W/real-after/
  370. tests/realdata-074.sh jobs $PWD $W/real-after 8730 # wait for "collections done" in real-after/server-jobs.log
  371. R=$W/real-after; REAL_PORT=8730 REAL_CHROME=8729 REAL_SID_FILE=$R/sid REAL_PID=$(cat $R/server.pid) REAL_OUT=$R/bench-jobs.json node tests/realdata-074-bench.mjs
  372. tests/realdata-074.sh stop x $R 8730 # "before": a tree snapshot (rsync without .scratch/storage/.env, absolute .env link) as <tree>
  373. ```
  374. ## 2026-10-02 — mission 024 (old 071): Hybriel master 190aa11d measured, NOT adopted (vendor stays 8efba065, NOT deployed)
  375. **Candidate**: master `190aa11d` (#127 both shapes d98926c6 + 04df4428, GC correctness fc838894, closure scopes 038d84b3),
  376. built ReleaseFast read-only (`.scratch/hybriel-071`), bin sha256 `feb597c4…dedc8`; kept ready in `.scratch/w071/vendor-190aa11d/`
  377. (bin/ + plugins/; only `plugins/web/{WebFramework,compile}.hl` differ in git). Lambda audit: no semantics change since 8efba065,
  378. `tools/lambda-audit.py .` lists the same 13. Gates on it: browser 325/0, kinds 32/0, franchises 50/0.
  379. **Verdict: rule "every big page ≤ 1.3× the old live binary AND RSS flat over 20 loads" fails** → bin/plugins restored from
  380. `.scratch/pre-071` (= 8efba065, 8081350's vendor); gates on the restored tree 325/0, 32/0, 50/0.
  381. **Gate fix (committed)**: `tests/kinds.mjs` sets `TRACKER_COLLECTIONS=0` — the collection seed (on by default since #19) made a
  382. TMDB request now and then before check 1 ("kinds off … no TMDB request"), 1 of 4 runs failed.
  383. **Real copy** (live `storage/` 2026-10-02 10:50, real TMDB, all first-start jobs + a forced delta run; signed in as az5b2; 20
  384. loads per page, each = node fetch + Chrome full load; `browser / server ms, RSS before→after (max)`; Loreana shared with other
  385. workers, ±30 % noise between runs):
  386. | page | old 8590df63 fresh | new 190aa11d fresh | old after jobs | new after jobs |
  387. |---|---|---|---|---|
  388. | /my/unwatched (1,288 rows) | 1089 / 974, 7.8→10.3 GB (15.3) | 910 / 793, 1.7→2.1 (2.2) | 3004 / 2688, 13.0→25.6 (26.4) | 2967 / 2879, 9.4→8.2 (12.2) |
  389. | Picard + Show all (212) | 240 / 109 | 245 / 225 (repeat 222 / 135) | 270 / 124 | 333 / 322 |
  390. | /my/series (126) | 292 / 194, 6.5→8.2 | 529 / 398 (repeat 380 / 277), 1.9→2.1 | 1827 / 1834, 17.9→30.1 | **4410 / 4401**, 7.4→10.8 (11.6) |
  391. | /series | 49 / 22 | 82 / 49 (repeat 89 / 30) | 102 / 64 | 166 / 160 |
  392. | /franchises | 52 / 20 | 70 / 31 (repeat 75 / 20) | 79 / 262 | 126 / 23 |
  393. | a timeline | 47 / 44 | 55 / 9 | 50 / 10 | 85 / 18 |
  394. - Boot RSS (fresh, 60 s after listening): old 7.96 GB, new 1.69–1.74 GB, 8efba065 2.39 GB, new+`HL_GC_BYTES=0` 3.3 GB.
  395. - First-start jobs (delta 288 titles, kinds 1,789, seed 4,774 → 110 timelines): new 57 min (sync 448 s, kinds 1,188 s, seed
  396. 2,151 s), RSS at 60 s 4.7 GB, **peak 9.8 GB**, median 6.4; old 53 min (430/1,082/2,020 s), 7.7 GB, peak 13.6, median 10.3.
  397. - Fails: after the jobs new `/my/series` 2.4× (4.4 s), `/series` 1.6×; fresh `/my/series` 1.3–1.8×, `/series` ~1.7× (tens of ms).
  398. Not flat: after the jobs new swings 7.4–12.2 GB (old 13–31 GB!). Fresh new is flat (1.7–2.2 GB) and `/my/unwatched` faster.
  399. - vs current main 8efba065 (fresh, same copy): /my/unwatched 4098 / 3933, /my/series 407 / 347, Picard 303 / 177 — 190aa11d is
  400. better than what main has now (/my/unwatched 4.5× faster; 070's after-jobs 8.3 s and +25 MB/load are gone: 2.9 s, no climb).
  401. - New + `HL_GC_BYTES=0`: fresh all ≤ 1.2× old (unwatched 577, /my/series 299, /series 59) at 3.3→3.9 GB; after its jobs (peak
  402. 16.1 GB, 54 min) speed ≈ old (/my/series 1999, /series 135 = 1.3×) but RSS 15.8→25.9 GB — the old binary's memory.
  403. - So: GC by bytes with a big live heap still costs (the /my/series and /series renders); hybriel #127 fixed the growth, not the
  404. per-collection cost. Decision for the creator/architect: adopt 190aa11d anyway (better than main in every after-jobs number but
  405. /my/series), or wait for a GC fix. Shots: gate shots `/tmp/w071/{shots,kinds,fr}` on Loreana.
  406. How to repeat (Loreana, this folder; the old tree = this tree with `.scratch/pre-070` bin/plugins + an absolute `.env` link):
  407. ```
  408. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/w071/pristine
  409. for w in new old; do mkdir -p .scratch/w071/real-$w && cp -a .scratch/w071/pristine/storage .scratch/w071/real-$w/; done
  410. tests/realdata-071.sh jobs new 8758 # all jobs + delta run; wait for "collections done" in real-new/server-jobs.log (~57 min)
  411. R=$PWD/.scratch/w071/real-new; REAL_PORT=8758 REAL_CHROME=8759 REAL_SID_FILE=$R/sid REAL_PID=$(cat $R/server.pid) REAL_OUT=$R/bench-jobs.json node tests/realdata-071-bench.mjs
  412. tests/realdata-071.sh stop new 8758; tests/realdata-071.sh fresh new 8758 # prints boot RSS; then the bench again (bench-fresh.json)
  413. # same with "old"; HL_GC_BYTES=… tests/realdata-071.sh … passes through; tools/realdata-071-table.py label=.scratch/w071/real-new/bench-jobs.json … → the table
  414. ```
  415. ## 2026-10-02 — mission 023 (old 070): t18 + t20 + t19 merged into main, migrated data kept, Hybriel 8efba065 (NOT deployed)
  416. **Commits on main** (not pushed): `20e09d8` merge t18 (#18), `daf49fe` merge t20 (#20/#21), `749019b` merge t19 (#19), `46b2138`
  417. migrated data kept, `1ad19c8` re-vendor + `&` params, then the docs commit. Worktrees tracker-wt18/-wt19/-wt20 + branches removed.
  418. **Merges** (conflicts README/STATUS/show.hl/project.hl/search.hl/components/search.hl/browser.mjs/faketmdb.mjs; both sides kept):
  419. - show.hl: `pageShowOf.summary = summaryOf(s)` (#18 on 062's small object), `slugInfoOf` (#20) beside 062's slug helpers, the
  420. franchise widget (#19) from `showRow.id`.
  421. - project.hl clock: adult backfill → details repair → **kind backfill** → credits job; the collection seed (own clock) now waits
  422. for ALL of them and the daily run (one TMDB pacing at a time) — first start = kinds, then the seed.
  423. - details.hl `withDetailsFields` also stores `tmdbType` + `kind` (delta light step, #26 completion).
  424. - #19 pages: typed headings "Franchises", "Franchise | …", "Timeline | …" (also the timeline editor); title links via `titlePath`.
  425. - search import: `path` (#21) + `details` (#28), log line with the real path.
  426. - Gates: browser.mjs URLs → `/series/` `/movies/`, typed-h1 selectors; kinds.mjs/franchises.mjs: repair/credits/kinds off,
  427. fixture name = seed name, default ports 8700–8710 (deploy.sh runs all three now).
  428. **Migrated data is never destroyed** (conductor): the one-time step MOVES a copied summary to `migratedSummary` (marker
  429. `summaries-moved.txt`); a migrated record's (oldId not `tmdb-…`) title/genres/homepage/tagline → `migratedTitle`/`migratedGenres`/
  430. `migratedHomepage`/`migratedTagline` before TMDB's first replaces them; seasons/episodes (oldId not `tmdb-`/`tvmaze-`)
  431. `migratedTitle`/`migratedSummary`. Set once. Real copy after the jobs (`tools/count-migrated.hl`): summary 9,647, title 16
  432. (e.g. "The Daily Show with Trevor Noah → The Daily Show", "Kaun Banega Crorepati(कौन बनेगा करोड़पति) → Kaun Banega Crorepati"),
  433. genres 71, tagline 16, homepage 4, episodes title 635 / summary 409, seasons 3 / 3. Gate: 3 checks (move at first start, renamed
  434. show/movie/episode keep the old value, a second rename keeps the first).
  435. **Hybriel 8efba065** (README "Vendored Hybriel"). Lambda audit (#48, `tools/lambda-audit.py`): 13 lambdas change a parameter —
  436. details.hl `withCredits` 236, `withDetailsFields` 265, `addSum` 359, `firstStep` 412 (stores the record itself); franchises.hl
  437. `storeOrder` 373; people.hl `startFill` 222, `fillStep` 269 (p, jobIn); tmdbsync.hl `mergeTvmaze` 279 (seasonsIn, episodesIn),
  438. `syncMovie` 442 (show, res), `addTotals` 826 — every caller uses the RETURNED value or the stored record (the maps of
  439. `mergeTvmaze` are not read after the call) → nothing to fix. **But the copy per call was slow**: 319 read-only parameters got `&`
  440. (`tools/ref-params.py --apply`): /my/schedule 2.35 → 0.46 s, /my/unwatched 13.7 → 5.7 s (real copy, fresh process). Micro test:
  441. a 20k-row list passed 2,000× = 15.5 s copied, 0.015 s with `&`. /my/unwatched: one merge sort instead of n² inserts (5.7 → 4.8 s).
  442. **Gates** (final tree, new binary):
  443. ```
  444. TRACKER_GATE_SHOTS=/tmp/w070/new-shots TRACKER_GATE_PORT=8751 TRACKER_GATE_IDENT_PORT=8752 TRACKER_GATE_CHROME=8753-8756 TRACKER_GATE_TMDB_PORT=8757 node tests/browser.mjs
  445. 325 passed, 0 failed
  446. TRACKER_GATE_SHOTS=/tmp/w070/new-kinds TRACKER_GATE_PORT=8750 TRACKER_GATE_IDENT_PORT=8751 TRACKER_GATE_CHROME=8752-8756 TRACKER_GATE_TMDB_PORT=8757 node tests/kinds.mjs
  447. 32 passed, 0 failed
  448. TRACKER_GATE_SHOTS=/tmp/w070/new-fr TRACKER_GATE_PORT=8750 TRACKER_GATE_IDENT_PORT=8751 TRACKER_GATE_CHROME=8752-8756 TRACKER_GATE_TMDB_PORT=8757 node tests/franchises.mjs
  449. 50 passed, 0 failed
  450. ```
  451. **Real data** (copy of Byrodin `storage/` 2026-10-02 ~04:40, real TMDB/TVmaze, all jobs on, the daily run forced as a delta
  452. from yesterday). Jobs: delta run 261 titles (14 followed), 447 requests, 377–612 s; kind backfill 1,789 titles in 970–1,246 s;
  453. credits/repair/adult nothing to do; seed 4,774 movies, 110 timelines / 272 titles, 1,920–2,228 s. RSS (sampled every 10 s):
  454. | | after boot | jobs peak | 200 page loads after the jobs (fresh process) | /my/unwatched |
  455. |---|---|---|---|---|
  456. | old 8590df63 | 8.0 GB | 15.2 GB (in swap pressure, see below) | 8.0 → 12.2 GB | 1.15 s |
  457. | new 8efba065, final tree | 2.1 GB | 11.8 GB, median 2.7 GB | 2.1 → 2.8 GB | 4.8 s |
  458. | new, `HL_GC_BYTES=268435456` | 2.7 GB | 12.4 GB at 24 min (run aborted) | 2.7 → 3.5 GB | 2.2 s |
  459. | new, `HL_GC_BYTES=0` | 6.2 GB | — | 6.2 → 8.7 GB | 1.15 s |
  460. - The SAME new process right after its jobs: 200 loads 3.0 → 8.1 GB, then 8.1 → 13.6, 13.6 → 18.9 GB; /my/unwatched 8.3 s.
  461. After a restart + one delta run: 4.3 → 5.8 → 7.5 GB per 200 loads. → restart after the first-start jobs (README "Deploy").
  462. - The first parallel run (old + new at once) was killed by **systemd-oomd** at 05:38 (Loreana's memory + swap > 90 %, other
  463. workers' tests too); its curves are in `.scratch/w070/real-{new,old}/rss.log`, the controlled numbers above ran one at a time.
  464. - Pages (curl, 200 loads, final tree, fresh process): `/` 0.02 s, lists 0.01–0.05, `/franchises` 0.04, a series 0.25–0.45,
  465. `/my/series` 0.4, `/my/shows` 0.33, `/my/schedule` 0.42, `/my/unwatched` 4.8, a person 0.4.
  466. - Browser (`tests/realdata-070.mjs`, signed in as az5b2, 390/1280, after a delta run): lists 39–146 ms, `/franchises` 104–129,
  467. a timeline ~40, Star Trek: First Contact with its widget (‹ Generations | TNG Collection | Insurrection ›) 136–346, Picard
  468. (202 cast) 192–368, SNL 444–459, /my/series 825–1,064, /my/shows ~770, /my/unwatched 5.6–6.1 s, /my/schedule ~780, search
  469. 77–476; the first `/` after boot 15 s (first hydration), then 121 ms. No sideways scroll, 0 console problems. Shots looked at:
  470. Loreana `/tmp/w070/real/*-{390,1280}.png`. No series is in a timeline on real data (the seed makes movie timelines only), so
  471. "a series page with widget" was checked in the franchise gate (Gate Test Show), cast on Picard.
  472. How to repeat (Loreana, from this folder; ONE tar of live storage):
  473. ```
  474. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/w070/pristine
  475. mkdir -p .scratch/w070/real-final/sessions && cp -a .scratch/w070/pristine/storage .scratch/w070/real-final/
  476. tests/realdata-070-run.sh final 8750 # all jobs, the delta run now, session for az5b2 → real-final/sid, RSS → real-final/rss.log
  477. grep 'done' .scratch/w070/real-final/server.log # wait for "collections done" (~50 min)
  478. tests/realdata-070-load200.sh final 8750 # 200 signed-in loads: per-path avg/max + RSS before/after
  479. REAL_PORT=8750 REAL_CHROME=8752 REAL_OUT=/tmp/w070/real REAL_SID_FILE=.scratch/w070/real-final/sid node tests/realdata-070.mjs
  480. tests/realdata-070-bench.sh new 8750 [HL_GC_BYTES=…] # a FRESH process on a done copy: boot RSS, 200 loads, stop
  481. kill $(cat .scratch/w070/real-final/server.pid); rm -rf .scratch/w070/pristine .scratch/w070/real-*/storage
  482. ```
  483. (`.scratch/w070/oldtree` = this tree with `.scratch/pre-070` bin/plugins and a `.env` link — removed again.)
  484. **Open**: (1) deploy + restart once after `collections done`; a daily restart after the 04:00 run? (2) Hybriel (architect):
  485. GC by bytes — with a 2+ GB live heap every 64 MiB of plugin data costs a full collection (/my/unwatched 1.2 → 4.8 s); a process
  486. that fetched for a while keeps ~8–25 MB per page load (fresh: 3.5 MB) — repro: the bench above; the #48 copy of a big list per
  487. call (micro test in this entry). (3) `HL_GC_BYTES` stays default: 256 MiB gives /my/unwatched 2.2 s, its full jobs peak was not
  488. measured (aborted at 12.4 GB). (4) Video → Series? (#21, creator). (5) #18's 24 kept "own" summaries.
  489. ## 2026-10-01 — ticket #19 (mission 020 (old 066)): franchises and timelines (branch t19 from fa1f9df, NOT merged, NOT deployed)
  490. **Built** (details [docs/franchises.md](docs/franchises.md)): tables `franchises.db`, `timelines.db`, `collectionchecks.db`
  491. (`franchises.hl`); `/franchises`, `/franchises/<slug>` (each timeline: Timeline | Release toggle, a series by its LAST released
  492. episode), `/timelines/<slug>`; the widget `‹ Prequel | Timeline | Sequel ›` with the franchise above on every show/movie page
  493. (one per timeline); the creator's editor `/franchises/edit` + `/timelines/<slug>/edit` (`TRACKER_EDITORS`, default az5b2,
  494. checked in every face); the TMDB collection seed in the app (own clock, one movie per step, 260 ms per request, waits for the
  495. daily sync and the adult backfill, resumes).
  496. **Decisions**: a collection becomes a timeline only with ≥ 2 of our non-adult movies (a 1-title widget says nothing); seeded
  497. timelines get no franchise (franchises are hand-made); pages/widget skip non-public titles (adult != false), the editor shows
  498. all; sort toggle client-side; styles in styles.hl (own section), the widget a composed component so show.hl changes by 4 lines.
  499. **Merge notes** (existing files touched): `components/show.hl` (1 import, 1 static, 1 member, 1 View line before
  500. `showSeasons`), `project.hl` (4 component imports, 1 franchises import, 5 routes after `/genre/:genre`, `franchisesReady` at
  501. boot, the seed clock block before `on Error`), `styles.hl` (a section before "/ (tracker.worldapi.org#13)" + 2 lines in the
  502. media block), `tests/browser.mjs` (`TRACKER_COLLECTIONS: '0'` in startServer), `tests/faketmdb.mjs` (`collections`,
  503. `belongs_to_collection`, `at` on requests), `README.md`, `STATUS.md`. New: franchises.hl, components/{franchises,franchise,
  504. franchisewidget,franchiseedit,timelineedit}.hl, tests/{franchises.mjs,seed-franchises.hl,realdata-066.mjs}, docs/franchises.md.
  505. **Gates** (both must be green):
  506. ```
  507. TRACKER_GATE_SHOTS=/tmp/w066/shots TRACKER_GATE_PORT=8732 TRACKER_GATE_IDENT_PORT=8733 TRACKER_GATE_CHROME=8734-8736 TRACKER_GATE_TMDB_PORT=8737 node tests/franchises.mjs
  508. 48 passed, 0 failed
  509. TRACKER_GATE_SHOTS=/tmp/w066/main-shots TRACKER_GATE_PORT=8732 TRACKER_GATE_IDENT_PORT=8733 TRACKER_GATE_CHROME=8734-8736 TRACKER_GATE_TMDB_PORT=8737 node tests/browser.mjs
  510. 266 passed, 0 failed
  511. ```
  512. Shots looked at: Loreana /tmp/w066/shots/{franchises,franchise,timeline,widget-*,franchise-edit,timeline-edit*}-{390,1280}.png.
  513. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~22:47, :8738, real TMDB): seed **4,774 movies** checked in **2,459 s**
  514. (~41 min), 4,957 TMDB requests, **110 timelines / 272 titles**, 235 collections with < 2 of our movies, 4,143 movies without a
  515. collection, 124 TMDB 404, 0 retries, slowest step 860 ms. Pages during the seed (curl ×40): `/` avg 0.08 s (one 2.2 s outlier),
  516. `/movies` 0.01, `/shows/Foundation` 0.18, `/franchises` 0.11. No Star Wars / Alien MOVIES in our data (only series) → checked
  517. Star Trek: The Next Generation (4), Harry Potter (7), Lord of the Rings (3): order right, widget right on the middle title
  518. (e.g. First Contact: ‹ Generations | TNG Collection | Insurrection ›). Full loads: `/franchises` 74–109 ms (83 KB, 110
  519. timelines), timeline pages 49–213 ms, movie pages with widget 403–464 ms (curl: 0.38 s vs 0.34 s for a movie without a
  520. timeline). Creator (az5b2): Edit link + editor forms present. 0 console problems. Shots /tmp/w066/real/*.png (looked at).
  521. How to repeat:
  522. ```
  523. tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage | ssh loreana 'tar xzf - -C <wt>/.scratch/realdata' # from Byrodin
  524. ln -s ../tracker.worldapi.org/.env .env # the real TMDB token for the worktree (never read it); rm afterwards
  525. TRACKER_PORT=8738 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_BACKFILL=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8738 TRACKER_OLD_SHORT_IDS=$PWD/data/old-short-ids.json HL_HOST=127.0.0.1 nohup ./bin/hybriel project.hl > /tmp/w066/real/server.log 2>&1 &
  526. grep '^collections' /tmp/w066/real/server.log # wait for "collections done"
  527. # session file for 0mujqspn0aqb (name sha256(id)) in .scratch/realdata-sessions/, id → .scratch/real-sid
  528. REAL_MATCH='Star Trek|Harry Potter|Lord of the Rings' REAL_PORT=8738 REAL_CHROME=8739 REAL_OUT=/tmp/w066/real node tests/realdata-066.mjs
  529. kill <pid>; rm -f .env; rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid
  530. ```
  531. **Open**: (1) deploy: the first start runs the seed ~40 min (TMDB pace), after the adult backfill. (2) Not built: rename /
  532. delete franchises and timelines, edit a franchise summary, franchise `shortId` (field null). (3) New parts of a collection that
  533. already has a timeline are not added by a later seed; titles imported later are not seeded unless their check row is
  534. removed. (4) The widget sits under the whole header — on a phone after the cast (the creator may want it higher). (5) A
  535. timeline page shows its name twice (page title + block head). (6) A show page costs ~40 ms more with a widget.
  536. ## 2026-10-02 — tickets #20 + #21 (mission 022 (old 068)): typed headings, Series / Shows split (branch t20, NOT merged, NOT deployed)
  537. **Built** (details: [docs/kinds.md](docs/kinds.md)):
  538. - #20: headings "<Type> | <name>" on the title page (Movie / Series / Show), genre, person. The type word is in its colour, the
  539. bar muted, the name white. /movies, /series, /shows, /my/series, /my/shows show their type word; so do the homepage row
  540. headings, the search labels and the filmography labels.
  541. - Colours: Movie yellow, Series greenDark (the accent), Show purple, Genre blueDark, Person orange. Franchise blueLight and
  542. Timeline greenLight are classes only (for t19).
  543. - #21: TV titles get `kind` = series | show, rule in `shows.hl kindOfTitle`:
  544. - show = TMDB type Reality / Talk Show / News / Video, or one of our genres Reality / Talk / News;
  545. - everything else = series.
  546. - Who stores `kind` + `tmdbType`: the daily sync, the adult backfill, the import, and a new kind backfill (`TRACKER_KINDS`,
  547. after the adult backfill, 1 request per title without a show genre, resumes).
  548. - Pages: /series and /shows (+ /page/N); /series/<slug>, /shows/<slug>, /movies/<slug>; /my/series + /my/shows; home has
  549. 5 tiles and 3 rows.
  550. - Old /shows/<slug> of a series → 301 /series/<slug>; of a movie → 301 /movies/<slug>.
  551. **Decisions**:
  552. - **Movies moved to /movies/<slug>**: /shows/ now means TV shows; old links answer 301.
  553. - **/shows/<slug> is a FUNCTION route** (`project.hl showsRoute`), because hl:web cannot redirect from a component route. For a
  554. show it renders the Show component itself through hl:web internals (`server.router.match`, `server.page`, `hostOf`,
  555. `requestHeaders`, `sessions`). Re-check when hl:web is re-vendored; the kinds gate covers it.
  556. - Consequence: a link to a show is a full page load. Series and movie pages stay client-side navigations.
  557. - Search "Fetch from web" rows say **TV** (TMDB's search has no TV type).
  558. - `/my/series` signed out: "Sign in to see the series you follow."
  559. **Gates**:
  560. ```
  561. mkdir -p /tmp/w068/shots /tmp/w068/main-shots
  562. TRACKER_GATE_SHOTS=/tmp/w068/shots TRACKER_GATE_PORT=8761 TRACKER_GATE_IDENT_PORT=8762 TRACKER_GATE_CHROME=8763-8765 TRACKER_GATE_TMDB_PORT=8766 node tests/kinds.mjs
  563. 32 passed, 0 failed
  564. TRACKER_GATE_SHOTS=/tmp/w068/main-shots TRACKER_GATE_PORT=8761 TRACKER_GATE_IDENT_PORT=8762 TRACKER_GATE_CHROME=8763-8765 TRACKER_GATE_TMDB_PORT=8766 node tests/browser.mjs
  565. 266 passed, 0 failed (URLs, h1 selectors and the home checks updated; TRACKER_KINDS=0 in its startServer)
  566. ```
  567. One run of browser.mjs failed once at the short-id resume check (`count-short-ids.hl`: mpackdb LockTimeout right after
  568. stopServer, under load). The rerun passed; nothing of #21 is involved.
  569. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~23:35, :8760, real TMDB through a `.env` symlink that was removed afterwards):
  570. ```
  571. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata
  572. ln -s ../tracker.worldapi.org/.env .env # token only, never read; rm .env afterwards
  573. .scratch/w068/start.sh 8760 TRACKER_BACKFILL=0 TRACKER_KINDS=1 # = TRACKER_SYNC=0, TRACKER_STORAGE=copy, own sessions dir
  574. # session file for 0mujqspn0aqb in .scratch/w068/sessions (as mission 016 (old 060)), id → .scratch/w068/real-sid
  575. REAL_PORT=8760 REAL_CHROME=8767 REAL_OUT=/tmp/w068/real node tests/realdata-068.mjs
  576. TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb ./bin/hybriel tools/count-kinds.hl # server stopped
  577. ```
  578. - **Backfill**: 1,789 TV titles in **838 s** (1,613 requests).
  579. - Kinds: series 1,577, shows 212 (176 by genre without a request, 36 by TMDB type).
  580. - 11 TMDB 404s, 0 retries, no slow step.
  581. - TMDB types: Scripted 1,337, Miniseries 179, Documentary 50, Reality 16, Talk Show 15, Video 4, News 1.
  582. - **Public lists**: before (genres only) 1,601 series / 173 shows; after **1,565 series / 209 shows**; 4,650 movies.
  583. - **Spot checks** (search label → link):
  584. - Show: Saturday Night Live, The Tonight Show Starring Jimmy Fallon, Last Week Tonight, The Late Show with Stephen Colbert,
  585. Hot Ones.
  586. - Series: Reacher, Foundation, Doctor Who (both), Chernobyl (Miniseries), Severance (2022).
  587. - All correct.
  588. - **Pages**, curl every 5 s during the backfill (156 each, avg / max):
  589. | Page | avg | max |
  590. |---|---|---|
  591. | `/` | 0.094 s | 3.5 s |
  592. | `/series` | 0.055 s | 2.6 s |
  593. | `/shows` | 0.024 s | 0.21 s |
  594. | `/series/Reacher` | 0.187 s | 0.48 s |
  595. - 4 answers over 1 s, about every 170 s. Idle afterwards: avg 0.029, max 1.0 (1 of 196).
  596. - Cause not found; probably mpackdb rewriting shows.db.
  597. - Before the backfill: /series/Reacher 0.17 s, /shows/Saturday-Night-Live (function route) 0.28 s.
  598. - **Browser** (signed in as the creator, full load to hydrated, ms 390 | 1280):
  599. | Page | 390 | 1280 |
  600. |---|---|---|
  601. | `/` | 74 | 98 |
  602. | `/series` | 97 | 43 |
  603. | `/shows` | 351 | 43 |
  604. | `/movies` | 123 | 43 |
  605. | `/series/Reacher` | 290 | 243 |
  606. | `/shows/Saturday-Night-Live` | 395 | 380 |
  607. | `/movies/Star-Trek-First-Contact` | 251 | 246 |
  608. | `/genres/Drama` | 46 | 44 |
  609. | `/person/Alan-Ritchson` | 94 | 35 |
  610. | `/my/series` (126 rows) | 1410 | 384 |
  611. | `/my/shows` (3) | 220 | 217 |
  612. | `/search/reacher` | 37 | 90 |
  613. - Every heading's text and colour is right, nothing scrolls sideways, 0 console problems.
  614. - Shots `/tmp/w068/real/*-{390,1280}.png` on Loreana (looked at).
  615. - Copy, sessions and the `.env` link were removed and the server stopped.
  616. **Open**:
  617. 1. Merge with main (062) and t19 / t26. They also change `components/show.hl`, `project.hl`, `styles.hl`, `tests/browser.mjs`,
  618. `tests/faketmdb.mjs`. t19's franchise pages need the typed h1 (`type-franchise` / `type-timeline`), and its links to
  619. `/shows/<slug>` should become `titlePath`.
  620. 2. TMDB type **Video** = anime OVAs here (Halo Legends, .hack//Liminality, Carnival Phantasm, Halo: The Fall of Reach). They
  621. became Shows by the architect's rule; maybe Video → Series (creator).
  622. 3. Deploy: the first start runs the kind backfill after the adult backfill (~14 min).
  623. 4. The 2–3.5 s answers during the backfill (above).
  624. 5. Titles added by the person fill have no stored kind until the next start (the genres decide meanwhile).
  625. ## 2026-10-02 — ticket #18 (mission 021 (old 067), branch t18 from t26's head 10bb3f9, worktree tracker-wt18): daily sync by change lists, show record refresh, summary priority (NOT merged, NOT deployed)
  626. **Built**: `deltasync.hl` (new): the daily run first reads TMDB `/tv/changes` + `/movie/changes` (since the stored day, all pages)
  627. and TVmaze `/updates/shows?since=day|week|month`, one request per clock step → our changed titles (search index lookups, no scan),
  628. followed first. Followed → full `syncShow` (as before); unfollowed → `syncShowDelta` (light: details + ids + poster + record + only
  629. new/grown/newest seasons, no TVmaze). Full walk on: no `sync-state.txt`, gap > 14 days, failed change list (day not advanced).
  630. `details.hl withDetailsFields` (title, tmdbSummary, homepage, tagline, status, genres, adult; shared with #26's completion) +
  631. `applySynced` (fields + credits) replace `applyCredits` in the sync paths. TVmaze request = `/shows/<id>?embed=episodes` (summary →
  632. `tvmazeSummary`, `stripHtml`). `summary` never written (sync, search import, person fill, completion); page `summaryOf`: summary >
  633. tmdbSummary > tvmazeSummary. One-time boot clear of copied summaries (marker `summaries-cleared.txt`). Search: `searchTitleRenamed`,
  634. `titleIdByTvmaze`. Tool: `tools/sync-tmdb.hl --delta [--since D] [--plan]`. Details: README "What it does (ticket #18)".
  635. **Decisions (mine)**: (1) unfollowed changed series get the LIGHT step — the first real run with full steps blocked the server 20.8 s
  636. for Today (15,716 episodes) and EastEnders (7,349) each and grew RSS to 14 GB + 2.9 GB swap; (2) TVmaze stays a followed-series thing
  637. (its list only brings followed series); (3) the day stored = the day the finished run STARTED (overlapping windows, no gap); (4) light
  638. step's seasons = missing, TMDB count > ours (fewer = TVmaze extras, skipped), newest; (5) a renamed title keeps its slug;
  639. (6) `syncShowPart` reads only its part's seasons' episodes unless it finishes with the TVmaze merge (also speeds up #26's parts).
  640. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~21:45 UTC = pre-#26/#28 live data, real TMDB/TVmaze, :8750, sync-state = 2026-09-30):
  641. - Change lists for one day: TMDB tv 3,088 + movie 9,559 ids (31 + 96 pages), TVmaze 312 → **336 of our titles (28 followed)**
  642. (152 series / 197 movies in the first variant).
  643. - Delta run (final code): `tmdb sync done: mode=delta changeRequests=128 shows=336 newSeasons=79 newEpisodes=4475 updatedEpisodes=1305
  644. updatedSeasons=70 posters=1 ids=0 requests=607 tvmazeRequests=29 errors=0 … seconds=536.359`; slowest step 2.5 s; pages meanwhile
  645. (`/`, `/movies`, `/shows` every 0.5 s, n=925): median 45 / p95 226 / p99 693 / max 2216 ms. Same day again (steady state):
  646. `shows=336 newSeasons=0 newEpisodes=0 updatedEpisodes=0 … requests=605 tvmazeRequests=29 errors=0 seconds=511.205`, slowest 2.06 s,
  647. RSS 2.18 → 5.7 GB.
  648. - OLD full walk on the same copy (no state file): `mode=full shows=128 … requests=259 tvmazeRequests=128 errors=1 seconds=312.241`
  649. (the error: one TLS handshake failure, transient), RSS 2.1 → 4.9 GB.
  650. - First variant (full step for every title): 349 titles, 659 + 155 TVmaze requests, 755 s, 46 slow steps up to 20.8 s, RSS 14 GB.
  651. - Summaries: `summaries: cleared 9647 copies of tmdbSummary, kept 24 own texts, errors 0` (one time; ~+2 GB RSS at that boot). The 24
  652. kept are all FOLLOWED shows whose tmdbSummary the #9 sync changed since the migration → older TMDB texts, likely no rewrites:
  653. The-Flash, Magnum-P.I., Alex-Rider, Star-Trek-Strange-New-Worlds, Star-Trek-First-Contact (its tmdbSummary is ANOTHER title's —
  654. "Kabuhayang Swak na Swak": wrong tmdbId on that record), Night-Sky, Good-Omens, Quantum-Leap-2, His-Dark-Materials, Not-Dead-Yet,
  655. Star-Wars-Andor, The-Rookie, Will-Trent, Unstable-2, Reacher, Wednesday, Citadel, Miracle-Workers, FUBAR, The-Night-Agent,
  656. The-Wheel-of-Time, Ahsoka, Percy-Jackson-and-the-Olympians, The-Daily-Show-with-Trevor-Noah.
  657. - Record refresh, first day: 24 renamed (Star Wars: Andor → Andor, The Daily Show with Trevor Noah → The Daily Show, WWE Raw → Raw,
  658. 편의점 샛별이 → Backstreet Rookie, "Kaun Banega Crorepati(कौन बनेगा करोड़पति)" → "Kaun Banega Crorepati" — the old tracker's
  659. "Name(original)" form goes), 79 tmdbSummary changed, 173 taglines.
  660. - Screenshots (looked at): Loreana `/tmp/w067/real/{Star-Wars-Andor,Breaking-Bad,Kaun-Banega-Crorepati}-{390,1280}.png` (Andor: new
  661. title; Breaking Bad: TMDB's current text after the clear), full load 134–649 ms, 0 console problems; gate `/tmp/w067-shots/`
  662. (delta-show-1280, delta-movie-1280).
  663. How to repeat (ports 8750–8759; ONLY `storage/`; the worktree has no `.env` → `ln -s ../tracker.worldapi.org/.env .env`, remove after):
  664. ```
  665. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  666. echo <yesterday> > .scratch/realdata/storage/mpackdb/sync-state.txt # none = the full walk
  667. TRACKER_PORT=8750 TRACKER_SYNC=1 TRACKER_SYNC_HOUR=$(date -u +%-H) TRACKER_WATCH=0 TRACKER_BACKFILL=0 TRACKER_REPAIR=0 TRACKER_CREDITS=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8750 HL_HOST=127.0.0.1 ./bin/hybriel project.hl > .scratch/delta.log 2>&1 &
  668. REAL_PORT=8750 REAL_LOG=.scratch/delta.log node tests/realdata-018-poll.mjs # latency until "tmdb sync done"
  669. grep '^tmdb sync\|^summaries' .scratch/delta.log; RSS: grep VmRSS /proc/<pid>/status
  670. REAL_TITLES=Star-Wars-Andor,Breaking-Bad REAL_OUT=/tmp/w067/real node tests/realdata-018.mjs
  671. # afterwards: kill <pid>; rm -rf .scratch/realdata .scratch/realdata-sessions; rm .env
  672. ```
  673. **Gate** (249 → 261; 12 new #18 checks; the TVmaze episode paths are `/tvmaze/shows/<id>?embed=episodes` now; the #26 completion
  674. check now expects TMDB's overview — `summary` is no longer filled by the fill/completion; the seed movie's summary is a copy of its
  675. tmdbSummary; `tests/peek-shows.hl` reads records with the app stopped):
  676. ```
  677. TRACKER_GATE_SHOTS=/tmp/w067-shots TRACKER_GATE_PORT=8751 TRACKER_GATE_IDENT_PORT=8752 TRACKER_GATE_TMDB_PORT=8753 TRACKER_GATE_CHROME=8754-8756 node tests/browser.mjs
  678. 261 passed, 0 failed
  679. ```
  680. **Open (for the architect)**:
  681. - NOT "a handful of requests": movies change a lot on TMDB (9,559 ids/day → 96 pages) — a day = 128 change-list requests + ~480 title
  682. requests + 29 TVmaze ≈ 634 (old full walk: 387) but every title stays current, followed ones only when they changed (28 vs 128).
  683. Dropping unfollowed MOVIES would save ~96 pages + ~200 requests — creator's call.
  684. - MEMORY: ~10 MB per synced title are never freed (Hybriel, see #28) → +3.5 GB RSS per daily run until a restart (old run +2.8 GB);
  685. the live container already used 11.4 GiB. A daily restart or the runtime fix is needed either way.
  686. - Titles: TMDB's en-US name replaces the migrated title (24 on day one, incl. the "Name(original)" form). If the creator wants to keep
  687. some, a creator-title field like `summary` would be needed.
  688. - The 24 kept "own" summaries look like older TMDB copies (list above) — clear them too? (architect's rule was "identical only").
  689. - Merge: t18 is on t26's 10bb3f9; t19 (franchises) and the 062 merge also touch people.hl/search.hl/README/STATUS — expect conflicts
  690. in README/STATUS only (code: people.hl 1 line, search.hl small additions).
  691. ## 2026-10-01 — mission 018 (old 062): t26 (#26 + #28) merged into main, short ids for new people, lazy guests, lean watch clicks (NOT deployed)
  692. **Merge**: `git merge t26` into main (fa1f9df). Conflicts were in README/STATUS, `components/show.hl` (header comment;
  693. skeleton poster + short id) and `tests/browser.mjs` (startServer env, import checks, #27 + #26/#28 blocks). Both sides kept.
  694. **Fixes**:
  695. 1. **Short ids**: people.hl `castPersonId` calls `claimShortId('person', …)`. It is the only place that creates people:
  696. - #26 completion, credits job, daily sync, `tools/sync-tmdb.hl`, search import;
  697. - the guest route.
  698. - shortids.hl also finds `../data/old-short-ids.json` from tools/.
  699. 2. **Guests are no person records**: details.hl `mergedCast` keeps a guest on the title as
  700. `{ person: null|existing, tmdbId, actor, character, profile, episodes, guest }`.
  701. - Link: `/person/tmdb/<id>?show=<show id>` → project.hl `guestRoute` → people.hl `guestPersonId`.
  702. - It creates the person with a short id (from that title's entry; without `?show=`, from TMDB `/person/<id>`), then 302.
  703. 3. **Watch/follow clicks**:
  704. - `showRow` is a small `pageShowOf` object. The whole record (~300 cast + crew) was shipped with the page and re-sent with
  705. every session sync.
  706. - cast/crew derive from the slug (`castOfSlug`/`crewOfSlug`; adult titles: none).
  707. - `watchesOfUser` is cached per user until `setWatched`.
  708. - The faces return rows only when the page opened/closed a season (`collapsedChanged`). Otherwise the sync's rows answer.
  709. **Gate** (266 main + 35 t26 → 311; mission 018 (old 062): 10 new checks, the #26 new-person check extended, sync-tool expectation +27 → +25 people):
  710. - count-short-ids runs after the sync tool and after the credits job;
  711. - import and #26 people have short ids at once;
  712. - guest links, guest click → person page with short id + TMDB fill, again → 302, TMDB fallback, 404s;
  713. - a watch click's frames carry no cast/crew;
  714. - a client-side navigation + watch click gets rows by the sync only.
  715. ```
  716. mkdir -p /tmp/w062-shots
  717. TRACKER_GATE_SHOTS=/tmp/w062-shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  718. 311 passed, 0 failed
  719. ```
  720. Shots looked at: `/tmp/w062-shots/guest-person-{390,1280}.png`, `credits-series-all-390.png`.
  721. **Real data** (copy of Byrodin `storage/` 2026-10-01 22:05, real TMDB, :8730; the copy is deleted):
  722. - **Jobs**:
  723. - Details repair: 557/557 in 836 s, 0 failed.
  724. - Credits job: 5,867 titles.
  725. - Run 1 (1,961 titles) ended at ~22:41 with no message; the cause is not known (no OOM line visible).
  726. - The restart resumed: `credits job done: 3906/3906 … people=47242 … seconds=2028`. The next start: nothing to do.
  727. - `short ids: 112298 records, 112298 with a short id, 0 without` (every person created had one at once).
  728. - **RSS** (`ps` every 60 s, `/tmp/w062/rss.log`):
  729. - During the jobs: 1.2–6.3 GB, swinging; it falls back by GBs at times.
  730. - 2 slow steps of 4.9–5.6 s, where RSS dropped too, so probably the runtime freeing memory. Pages wait meanwhile.
  731. - **People / boot**:
  732. | | people | search index | RSS ~30–60 s after boot |
  733. |---|---|---|---|
  734. | main fa1f9df, same copy (no token, no jobs) | 15,157 | 0.66 s | 4.3–4.5 GB |
  735. | merged, after the jobs (jobs off) | 102,424 | 3.0 s | 6.4–7.0 GB |
  736. | merged, after the jobs (jobs on, nothing to do) | 102,287 | 2.7 s | 8.1–8.8 GB |
  737. - w061 (all guests as people): 160,084 people.
  738. - Movie casts (all "main") are most of the 87k new people.
  739. - **Pages** (`REAL_OUT=/tmp/w062/real node tests/realdata-062.mjs`, signed in as az5b2):
  740. - Star Trek: Picard: 202 cast (94 guests linked lazily) + 45 crew, HTML 143 kB. Full load 201–342 ms; the first after boot
  741. took 2.3 s.
  742. - Superman: 90 cast, 8 crew, 117–173 ms.
  743. - Guest click (Stephanie Czajkowski) → person created (short id 401md) + filled from TMDB (18 films) in 0.74–1.3 s. Again: 29–66 ms.
  744. - **Watch click** (page clock, click → answer frame):
  745. | | before | after |
  746. |---|---|---|
  747. | Picard | 184–200 ms (w061) | 159–174 ms, 4.9 kB |
  748. | The Simpsons | 276–731 ms (w061) | 156–214 ms, 15 kB (was 29 kB) |
  749. | Follow | | 119–221 ms |
  750. About 1 in 6 clicks took 550–970 ms (same pauses as above).
  751. - Server timing (temporary logs, removed): the face is ~40 ms, the page's re-mount ~25 ms of our code; the rest (~120 ms) is
  752. hl:web's session-face path.
  753. - Screenshots (looked at): `/tmp/w062/real/{Star-Trek-Picard,Superman}{,-credits}-{390,1280}.png`, `guest-person-{390,1280}.png`.
  754. How to repeat (ports 8730/8731; ONLY `storage/`):
  755. ```
  756. mkdir -p .scratch/realdata && ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  757. TRACKER_PORT=8730 TRACKER_URL=http://127.0.0.1:8730 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_WATCH=0 TRACKER_SYNC=0 TRACKER_CREDITS_LOG_EVERY=250 ./bin/hybriel project.hl
  758. # (the runtime reads .env → real TMDB: repair ~14 min, then credits ~50 min; RSS: ps -o rss= -p <pid> every minute)
  759. # session for 0mujqspn0aqb as in mission 016 (old 060) → .scratch/real-sid; then:
  760. REAL_OUT=/tmp/w062/real REAL_CHROME=8731 node tests/realdata-062.mjs
  761. # afterwards: kill the server; rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid
  762. ```
  763. **Deploy (what to watch)**:
  764. - After the start: the details repair (~14 min), then the credits job (~50 min).
  765. - `docker stats`: expect RSS to roughly double after boot (4.3 → ~8 GB) and swing during the jobs.
  766. - Log: `details repair done`, `credits job done`, `slow step`, `guest opened`.
  767. **Open**:
  768. 1. Deploy.
  769. 2. 102k people is still ~6.8× the old count. Most are movie cast. If memory is too high, apply the same lazy rule to a
  770. movie's cast after the first N (not built: the mission kept "main cast").
  771. 3. The Hybriel runtime: RSS swings, multi-second pauses, a silent process end in run 1. For the architect.
  772. 4. Watch clicks: about 1 in 6 is slow; the base cost is hl:web's session-face path (~120 ms).
  773. 5. Adult titles show no cast/crew now (they never get credits; at most a migrated cast was hidden).
  774. ## 2026-10-01 — ticket #29 (mission 019 (old 063)): unwatched checks looked "active" (NOT deployed)
  775. **Cause**: NOT a code regression of #23 — on real data (as az5b2) all 1,287 `/my/unwatched` checks are `checkicon regular`
  776. (fill transparent), fresh load and after a client-side navigation; live CSS = HEAD. The look itself was the problem: #11's
  777. "inverted check" (outline circle + outline check) was drawn in the ACCENT colour, so a page full of them read as ticked
  778. (creator: "in /my/unwatched all checkboxes are active"). **Fix** (`styles.hl`): the unwatched outline + check are muted grey
  779. (`colorTextMuted`, rgb(145,150,155)), accent only on hover; watched unchanged (solid accent disc, dark check). Keeps #11's
  780. shape (not a bare empty circle). Applies everywhere the check is used: /my/unwatched, show seasons + episodes, movie page.
  781. /my/schedule has no checks; /my/movies only marks watched movies (#24).
  782. **Gate** (259 → 266): `checkLook(sel)` in tests/browser.mjs measures a REAL check on the page (no hidden copy any more):
  783. computed circle fill/stroke + check stroke, topmost element, and the SCREENSHOT pixel inside the disc (svg 12,6) →
  784. `lookEmpty` / `lookSolid`. On /my/unwatched (both rows), show page (unwatched episode + season, watched episode, #23 mixed
  785. state), movie page (both states), /my/movies (watched mark). Against the old styles.hl it fails 4 checks (stroke accent).
  786. ```
  787. mkdir -p /tmp/w063/shots # the gate needs the shots dir to exist
  788. TRACKER_GATE_SHOTS=/tmp/w063/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  789. 266 passed, 0 failed
  790. ```
  791. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~19:30; procedure as mission 016 (old 060) — the inner ssh from Loreana to
  792. Byrodin was refused once, so the tar was piped from Byrodin into Loreana instead; server :8730 also needs
  793. `TRACKER_OLD_SHORT_IDS=$PWD/data/old-short-ids.json`): `REAL_OUT=/tmp/w063/real node tests/realdata-063.mjs` → /my/unwatched
  794. 1,287 checks all regular/muted/unfilled (390: 1913 ms, 1280: 1050 ms full load), Foundation 11 muted + 2 solid, Star Trek:
  795. First Contact movie check muted, /my/movies none (creator has no watched movie); one click 1287 → 1286 rows in 1.5 s, gone
  796. after a reload; 0 console problems. Shots /tmp/w063/real/{my-unwatched,show,my-movies}-{390,1280}.png (looked at).
  797. **Open**: deploy; creator to confirm the muted look (if they want a bare empty circle that reverses #11).
  798. ## 2026-10-01 — ticket #27 (mission 016 (old 060)): short ids for titles and persons (NOT deployed)
  799. **Built** (details README "What it does (mission 016 (old 060))"): `shortId` (5 × `[a-z0-9]`) on shows (movies + series) and persons, unique
  800. across both through one in-memory map (`shortids.hl`). The 702 old ids come back from `data/old-short-ids.json` (made once by
  801. `tools/old-short-ids.hl` from the old export); they are reserved for new ids. New records get one at creation (search import, the
  802. person fill's titles). A background backfill (own 0.1 s clock, 40 ms batches, resumes) gives everyone else one. `#shortid` under
  803. the poster / photo (no photo: under the facts), small, muted, monospace. `/<shortId>` → 301 to the page, else 404.
  804. **Decisions**: backfill in the background, not at start (all 25k at once ≥ 16 s blocked; batches measured ≤ 62 ms). Uniqueness via
  805. the in-memory map, not an mpackdb `!shortId` index (per table only; adding an index to the live tables is untested, hybriel#21).
  806. Old-id file in the code (`data/`, deployed), not in `storage/`. Upper case in `/<id>` is read as lower case. A record shows no id
  807. until the backfill reaches it (first ~5 min after the deploy).
  808. **Gate** (238 → 259):
  809. ```
  810. TRACKER_GATE_SHOTS=/tmp/w060/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  811. 259 passed, 0 failed
  812. ```
  813. The gate gets its own old-id file (`TRACKER_OLD_SHORT_IDS`: sh1 → 100jh). Shots looked at: Loreana /tmp/w060/shots/shortid-*.
  814. **Real data** (copy of Byrodin `storage/` taken 2026-10-01 ~14:09 Loreana time: 10,010 shows, 15,157 persons; :8730):
  815. - Backfill: 25,167 records in **297 s** (another run 307 s), 1,859 batches, slowest batch 62 ms, slowest persist 31 ms; boot map 0.12–0.22 s.
  816. - `tools/count-short-ids.hl` afterwards: all 10,010 + 15,157 have one, old kept **702/702**, lost 0, old id on another record 0,
  817. **duplicates 0**; `tools/count-duplicate-ids.hl`: 0 duplicate @ids in all tables.
  818. - Pages by curl, back to back, during the backfill (avg / max / answers > 1 s of ~520 each): `/shows/Foundation` 0.234 / 1.56 / 9,
  819. `/person/Jared-Harris` 0.170 / 1.37 / 9, `/` 0.070 / 1.34 / 2, `/movies` 0.073 / 1.18 / 2. Without a backfill (120 s, 436 each):
  820. 0.173 / 1.34 / 5, 0.046 / 1.10 / 5, 0.022 / 0.99 / 0, 0.015 / 1.01 / 1 — the > 1 s answers happen without it too (Loreana was
  821. busy with other workers); the backfill adds ~0.1 s on average.
  822. - Browser (`node tests/realdata-060.mjs`, signed in as the creator, full loads ms 390 | 1280): Raised by Wolves (via `/100jh`)
  823. 1372/297/310 | 335/266/301, Foundation (`100sp`, an old id too) 302/306/250 | 1246/205/195, Star Trek: First Contact (`aafif`)
  824. 275/287/275 | 211/235/212, Jared Harris (`yg7lk`) 164/138/122 | 104/119/121; each id under its image; every `/<id>` 301 in 1–27
  825. ms and the browser lands on the page; `/zzzzz` 404; 0 console problems. Shots /tmp/w060/real/{old-id,series,movie,person}-{390,1280}.png (looked at).
  826. How to repeat (ONLY `storage/`; inner ssh with `< /dev/null`):
  827. ```
  828. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  829. TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_OLD_SHORT_IDS=$PWD/data/old-short-ids.json ./bin/hybriel tools/count-short-ids.hl
  830. OUT=/tmp/w060 tests/realdata-060-backfill.sh 1 450 "" # starts :8730 on the copy, curl-times 4 pages until "short ids done"
  831. # (3rd arg `full` = keep timing until MAX — the baseline once nothing is left). Then, server still up: session file for user
  832. # 0mujqspn0aqb (name sha256(<32-hex id>), JSON {"created":<now ms>,"data":{"tag":"t"},"id":"<id>","seen":<now ms>,"user":{"id":"0mujqspn0aqb"}}
  833. # in .scratch/realdata-sessions/), id → .scratch/real-sid
  834. REAL_OUT=/tmp/w060/real node tests/realdata-060.mjs # REAL_SERIES, REAL_MOVIE, REAL_PERSON, REAL_OLD, REAL_REPEAT
  835. kill $(cat /tmp/w060/real.pid); TRACKER_STORAGE=… tools/count-short-ids.hl again; rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid
  836. ```
  837. **Open**: (1) deploy — the first start runs the backfill (~5 min, pages ~0.1 s slower meanwhile; ids appear as it goes).
  838. (2) Inside the app a record costs ~3 ms vs ~0.6 ms in a plain script (cause not found; a big `static` list read through a field
  839. was copied on every access — fixed by keeping the queue a plain `static` list). (3) Nothing creates persons in the app today; if a
  840. later step does, it must call `claimShortId('person', …)`. (4) `/<shortId>` of an adult title redirects; its page says "Not found"
  841. to non-followers as before.
  842. ## 2026-10-01 — tickets #22–#25 (mission 014 (old 058)): air dates, season check, movie watched check, genre pages (NOT deployed)
  843. **Built** (details README "What it does (mission 014 (old 058))"): #22 every episode row shows its air date (server rows + opened
  844. seasons). #23 a season's check is DERIVED = all its episodes watched (live with the face's answer; a season click on a partly
  845. watched season watches the rest). #24 a movie's page has `#moviewatch` (watch `targetType 'movie'`, face `showToggleMovie`;
  846. signed out → modal), `/my/movies` says "N movies · M watched" + a check per watched row. #25 `/genres/<genre>` (+ `/page/<n>`,
  847. `components/genre.hl`, catalog.hl `index.genres`): movies AND series, newest first, 24 per page, adult hidden; pills link there,
  848. `/genre/<x>` → 301.
  849. **Decisions**: the season record is still written by a season click but no longer read for the check. A series' place in a
  850. genre list = its newest released episode date (else its release), a movie's = its release (one comparable date). Genre by
  851. `urlSegment` (case-insensitive fallback). TMDB's TV and movie genres stay separate (see Open).
  852. **Gate** (214 → 238):
  853. ```
  854. TRACKER_GATE_SHOTS=/tmp/w058/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  855. 238 passed, 0 failed
  856. ```
  857. One old check changed: "an unwatched check is the inverted icon" now styles a detached copy classed `regular` (with #23 every
  858. check on that page is solid at that point). Fixture: genre Action (`g0000000000000g2`) on Pager Movie 01–30, Pager Future Movie,
  859. Pager Older Series + the adult `Hidden Action Film`. Shots looked at (Loreana /tmp/w058/shots/: show-open-*, movie-watched-*,
  860. genre-action-*, my-movies-*).
  861. **Real data** (copy of Byrodin `storage/` 2026-10-01 12:03 UTC, :8730, creator 0mujqspn0aqb, `node tests/realdata-058.mjs`, full
  862. load goto → hydrated, 3 repeats, ms 390 | 1280): `/shows/Foundation` 549/277/221 | 224/228/225 (10 open rows all dated, top season
  863. check == all its episodes); `/shows/Star-Trek-First-Contact` (movie) 234/250/250 | 237/238/239; `/genres/Action` 118/991/42 |
  864. 92/39/43 (643 titles, 27 pages; server 6–8 ms by curl — the spikes are poster loads); page 2 41/103/749 | 93/48/110; `/my/movies`
  865. 66/63/66 | 68/63/63. Movie check on the copy: click 361 ms → reload solid → click back → reload regular. Genre kinds page 1:
  866. Action 24 movies/0 series, Drama 5/19, Action-Adventure 0/24. 0 console problems. Screenshots /tmp/w058/real/ on Loreana (looked at).
  867. How to repeat (ONLY `storage/`; inner ssh with `< /dev/null`):
  868. ```
  869. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  870. TRACKER_PORT=8730 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_BACKFILL=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8730 HL_HOST=127.0.0.1 ./bin/hybriel project.hl
  871. # session file for 0mujqspn0aqb (name sha256(id), created/seen = now ms), id → .scratch/real-sid
  872. REAL_OUT=/tmp/w058/real node tests/realdata-058.mjs # REAL_SHOW, REAL_MOVIE, REAL_GENRE, REAL_REPEAT
  873. # afterwards rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid
  874. ```
  875. **Open**: (1) deploy. (2) TMDB's TV genres ("Action & Adventure", "Sci-Fi & Fantasy", "War & Politics") are separate genres from
  876. the movie ones, so `/genres/Action` has no series on real data — merge them? (creator). (3) A series tile shows its start year
  877. while it is placed by its newest episode (same as /shows). (4) The season's own watch record can now disagree with its episodes
  878. (not read anywhere for the check).
  879. ## 2026-10-01 — ticket #28 (mission 017 (old 061), branch t26 on top of #26, worktree tracker-wt26): full cast + crew (NOT merged, NOT deployed)
  880. **Found** (live copy 2026-10-01 ~14:40): the page showed `show.cast` — the old tracker's migrated list for 1,820 of 10,011 titles
  881. (Star Trek: Picard **4**: Stewart, Ryan, Hurd, Speleers; Dune (1984) 39), nothing for the rest (Superman 1978: **0**); #26 only
  882. added TMDB's first 20 to filmography titles; no crew anywhere; the daily sync and the search import never stored cast.
  883. **Built**: details.hl `castOf`/`crewOf`/`mainMinOf`/`withCredits`/`applyCredits` + the credits job (`creditsQueue`/`creditsStep`,
  884. project.hl `creditsTick`); tmdbsync.hl asks `external_ids,aggregate_credits|credits` in the details request it makes anyway and
  885. returns it (`res.details`); search.hl `importTitle` asks once (ids + credits) and hands that answer to `syncShowWith` (one request
  886. less), components/search.hl stores the credits; tools/sync-tmdb.hl too; show page: crew block (job labels) + cast `Name (Character)`
  887. collapsed after 20 with a client-only "Show all (N)" (crew: 6 per job, "Show all crew (N)"), "Guest stars" label; shows.hl
  888. `showBySlug` by a slug → id map. Details: README "What it does (ticket #28)".
  889. **Decisions**: cast ≤ 300 (`TRACKER_CAST_MAX`; SNL 2,771 / Grey's 3,243 one-episode guests would each be a person), crew ≤ 30 per
  890. job; guest = fewer than half the series' episodes (≤ 10, ≥ 1) — Picard (30 ep.): ≥ 10; composer = Original Music Composer /
  891. Composer / Music; Writer, Screenplay, Story separate groups (Teleplay not shown). The credits job runs after the backfill and the
  892. repair, only public titles (`adult == false`), one request per title + 260 ms, pauses above `TRACKER_CREDITS_MAX_MB` (16000).
  893. **Hybriel finding (not filed; for the architect)**: the native runtime never gives back what a `fetch` body or a decoded record
  894. took — repro (10 lines): a node server answering a 440 KB JSON; a .hl loop `fetch(url)` + `r.json()` 300× → RSS +2.7 GB (fetch
  895. without `.json()`: +0.43 GB), sampled with `ps -o rss=`; nothing is given back while the process lives. The same
  896. for `showsTable.find(null, null)`: the old `showBySlug` decoded every title per call → run 1 below grew to 20 GB in 20 min. The LIVE
  897. container used **11.4 GiB** today (docker stats, before any of this). Fix in this branch: the slug map (no per-page scan) + the job's
  898. RSS limit; the leak itself is Hybriel's.
  899. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~14:40, real TMDB/TVmaze, :8750; copy deleted):
  900. - Before → after: **Star Trek: Picard 4 → 202 cast** (17 main ≥ 10 episodes incl. season-3 regulars Gates McFadden, Todd Stashwick, then
  901. guests from John de Lancie (9)) + 45 crew (Created by Kurtzman, Beyer, Goldsman, Chabon; Directed by Frakes …; Written by; Story;
  902. Music Jeff Russo …). **Superman (1978) 0 → 90 cast** + Directed by Richard Donner, Written by Tom Mankiewicz, Screenplay Newman /
  903. Benton / Puzo / Newman, Story Puzo, Music John Williams. Dune (1984) 39 → 41. The Simpsons 6 → 300 (cap) + 67 crew.
  904. - Run 1 (`TRACKER_REPAIR=1 TRACKER_CREDITS=1`): `details repair done: 557/557 … cast=30286 … seconds=800`, then the credits job
  905. `start, 5867 titles`; stopped after 714 (RSS 20 GB — pages scanned every show per call, see the Hybriel finding). Run 2 (slug map,
  906. light persist, `TRACKER_CREDITS_MAX_MB=16000`): resumed `start, 5153 titles` → `credits job done: 5153/5153 titles, done=5153
  907. failed=0 cast=262634 crew=39624 people=104614 requests=5153 retries=0 slowestStepMs=1316 seconds=2681` (44.7 min; whole job ≈ 51
  908. min); RSS 3.8 → 6.3 GB (the pause did not trigger; it is gate-tested). Next start: `credits job: start, 0 titles` / `nothing to do`.
  909. - Page latency meanwhile (`/movies`, Picard, Foundation, `/` in turn every 0.5 s, n=4836): median 28 ms, p95 194, p99 436, max 1125 ms.
  910. (Run 1 with the old `showBySlug`: Picard median 318 ms, max 3.8 s; during the repair median 179 ms, max 4.1 s.)
  911. - After (no job running): show page full load 105–235 ms (Picard 206 ms, first after boot 2.1 s = the slug map), Picard HTML 166 kB,
  912. Simpsons 364 kB; "Show all" 2–18 ms, 0 faces sent; no sideways scroll at 390. Signed-in watch click (session face re-derives the cast
  913. rows too): Picard 184–200 ms (first 1.3 s), The Simpsons 276–731 ms. Boot: search index 10,011 titles / **160,084 people** (was
  914. 15,157) in **4.3 s (was 0.66 s)**, RSS after boot **4.9 GB (was 1.6 GB)**.
  915. - Screenshots (looked at) on Loreana `/tmp/w061/real/{Star-Trek-Picard,Superman,Dune,The-Simpsons}-{collapsed,all}-{390,1280}.png`;
  916. gate `/tmp/w061/shots/credits-{movie-1280,series-collapsed-390,series-all-390,series-1280}.png`. 0 console problems.
  917. How to repeat (ports 8750–8753; ONLY `storage/`; the worktree has no `.env` → `ln -s ../tracker.worldapi.org/.env .env`, remove after):
  918. ```
  919. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  920. TRACKER_PORT=8750 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_BACKFILL=1 TRACKER_REPAIR=1 TRACKER_CREDITS=1 TRACKER_CREDITS_LOG_EVERY=250 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8750 HL_HOST=127.0.0.1 ./bin/hybriel project.hl
  921. # latency meanwhile: a node loop fetching /movies, /shows/Star-Trek-Picard, /shows/Foundation, / every 0.5 s until 'credits job done'
  922. # (mission 017 (old 061)'s: .scratch/w061/poll.mjs); grep '^credits job\|^details repair' in the log; RSS: ps -o rss= -p <pid>
  923. REAL_TITLES=Star-Trek-Picard,Superman,Dune,The-Simpsons REAL_OUT=/tmp/w061/real node tests/realdata-028.mjs
  924. # afterwards: stop the server, rm -rf .scratch/realdata .scratch/realdata-sessions; rm .env
  925. ```
  926. **Gate** (231 → 249; 18 new #28 checks; fake TMDB fixtures: show 1001 aggregate_credits + created_by, movie 1003 credits, import
  927. 1004, Backfill Movies' credits; the sync/import request paths now carry the credits; `TRACKER_CREDITS=0` by default):
  928. ```
  929. TRACKER_GATE_SHOTS=/tmp/w061/shots TRACKER_GATE_PORT=8754 TRACKER_GATE_IDENT_PORT=8755 TRACKER_GATE_CHROME=8756-8758 TRACKER_GATE_TMDB_PORT=8759 node tests/browser.mjs
  930. 249 passed, 0 failed
  931. ```
  932. **Merge notes** (main has missions 014 (old 058)/016 (old 060) meanwhile): `components/show.hl` — header comment, statics after `genreRowsOf`
  933. (`castShown`…`crewCountOf`, replacing `castRowsOf`), `completedPageOf`'s answer, members after `hasCast`, the View's crew + cast
  934. blocks (replacing the old `showCast` loop), `completeTick` assignments, handlers `toggleCastAll`/`toggleCrewAll` before
  935. `toggleEpisode`. `styles.hl` — the cast/crew block replaces the two `.artist-link` lines. `shows.hl` — `showBySlug`. `tmdbsync.hl` —
  936. details paths + `res.details`. `search.hl` — `importTitle` (one request, `syncShowWith`). `project.hl` — `readFile` import, sync tick
  937. `applyCredits`, the credits job block before the clock, the clock's tick. Gate expectations that name request paths changed (sync tool,
  938. import, failed-import message). **Deploy**: repair (~557 titles, ~13 min) then credits job (~5,900 titles, ~51 min); watch RSS
  939. (`docker stats`) — live already used 11.4 GiB before this.
  940. **Open**: (1) merge + deploy. (2) The Hybriel leak (fetch bodies / decoded records never freed) — architect to file; the live app
  941. grows with every sync/backfill too. (3) 160k people: boot +3.6 s, +3.3 GB RSS — if too much, store cast without creating people for
  942. guests (link resolved on click) — not built. (4) Session faces (watch/follow clicks) re-derive the cast rows (`castRows` derives from
  943. `showRow`, which reads the session): Simpsons click up to 0.7 s; deriving them from the slug would avoid it (adult pages need care).
  944. (5) A followed title's credits refresh with the daily sync; others are never refreshed. (6) Cast persons' slugs as #26 (`-2` rule).
  945. ## 2026-10-01 — ticket #26 (mission 015 (old 059), branch t26, worktree tracker-wt26): filmography titles complete (NOT merged, NOT deployed)
  946. **Found**: the person fill (#16, people.hl `addMinimalTitle`) stores only title, year/release, overview, type, tmdbId, adult, genres,
  947. language and that one person as cast — no poster, seasons/episodes, imdb/tvdb/tvmaze ids, homepage/tagline/status, `tmdbSync`; the
  948. show page just showed that, forever. A search import (`importTitle` + `syncShow`) has all of it except the cast. Live copy: 321 such
  949. titles (6 people filled so far).
  950. **Built**: `details.hl` (`completeStep`), show page loading state (skeleton poster / cast line / 3 season rows, probe → face
  951. `showComplete` until done, no reload), `project.hl repairTick` (the details repair, after the adult backfill), tmdbsync.hl
  952. `syncShowWith`/`syncShowPart` (sync with a given details answer, a series in parts), people.hl `castPersonId` + `minimal = true`,
  953. search.hl `slugBaseOf`/`indexPerson`. Details: README "What it does (ticket #26)".
  954. **Decisions**: complete = what a full import has + the cast (TMDB's first 20, billing order; series: aggregate_credits; adult
  955. people left out; new people get a record + slug + search entry, their page fills on first visit). Marker `detailsAt`; incomplete =
  956. `minimal` or `imported` without `tmdbSync`. Adult titles are never completed (not on open — the public face refuses them — not by
  957. the repair). TMDB 404 → `detailsCheck`, not asked again. A series in steps (≤ 300 episodes per step) — the first real run did the
  958. Late Show in ONE 10.7 s step, pages waited up to 10.3 s. Titles a fill adds while the app runs: completed on open or at the next
  959. start (the queue is built at start).
  960. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~13:55, real TMDB/TVmaze, :8750, `TRACKER_BACKFILL=0`; copy deleted):
  961. - Click from a person page (fill first), `node tests/realdata-026.mjs` — ms from the click: skeleton shown / complete / poster:
  962. | title (person, width) | shown | complete | poster | server (log) |
  963. |---|---|---|---|---|
  964. | Violent Night 2, movie (Jared Harris, 390) | 186 | 712 | 765 | 1 step, 255 ms, cast 14 |
  965. | Royal Kill List, series (Jared Harris, 390) | 227 | 1361 | 1363 | 3 steps, 592 ms, 1 season / 3 ep |
  966. | Teen Wolf: The Movie (Tyler Hoechlin, 1280) | 216 | 662 | 662 | 1 step, 189 ms, cast 20 |
  967. | Wolf Watch, series (Tyler Hoechlin, 1280) | 258 | 1316 | 1318 | 3 steps, 537 ms, 3 seasons / 28 ep |
  968. | Avengers: Doomsday (Patrick Stewart, 1280) | 178 | 642 | 694 | 1 step, 247 ms |
  969. | Bait, series (Patrick Stewart, 1280) | 181 | 1262 | 1263 | 3 steps, 594 ms |
  970. /movies asked every 100 ms meanwhile: max 283–586 ms. Reload afterwards: complete at once, 157–205 ms full load, same parts.
  971. Direct open (1280): Late Show with David Letterman 11.4 s (23 steps, 4,252 episodes), Watch What Happens Live 11.1 s (18 steps,
  972. 3,220 ep); /movies meanwhile max 1.3 / 1.5 s (before the steps: one 10.7 s step).
  973. - **Repair** (restart with `TRACKER_REPAIR=1` on a fresh copy): `details repair done: 321/321 titles, complete=321 failed=0 adult=0
  974. newSeasons=638 newEpisodes=16528 posters=299 cast=5196 requests=787 tvmazeRequests=218 retries=0 seconds=507.248`; no slow step
  975. (> 1.5 s); /movies + /shows/Foundation every 0.5 s meanwhile: n=1298, median 0.13 s, p95 0.25 s, p99 1.03 s, max 2.35 s. Next
  976. start: `start, 0 incomplete titles` / `nothing to do`. (First try, one step per title: 401 titles in 628 s, max wait 10.3 s.)
  977. - Screenshots (looked at) on Loreana `/tmp/w059/real/`: `{movie,series}-{skeleton,complete,reload}-{390,1280}.png`,
  978. `direct-*-1280.png`, `repaired-{Licorice-Pizza,Late-Show-with-David-Letterman}-{390,1280}.png`. 0 console problems.
  979. How to repeat (ports 8750–8753; ONLY `storage/`; the worktree has no `.env` → `ln -s ../tracker.worldapi.org/.env .env` for the
  980. real token, remove afterwards):
  981. ```
  982. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  983. TRACKER_PORT=8750 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_BACKFILL=0 TRACKER_REPAIR=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8750 HL_HOST=127.0.0.1 ./bin/hybriel project.hl
  984. REAL_PEOPLE=Jared-Harris:390,Tyler-Hoechlin:1280,Patrick-Stewart:1280 REAL_OUT=/tmp/w059/real node tests/realdata-026.mjs
  985. REAL_PEOPLE= REAL_TITLES=Late-Show-with-David-Letterman node tests/realdata-026.mjs
  986. # repair: restart with TRACKER_REPAIR=1, poll /movies, grep '^details repair' in the log (~8.5 min)
  987. # count incomplete: the log line 'details repair: start, N incomplete titles' (N = 321 on 2026-10-01)
  988. ```
  989. **Gate** (214 → 231; 17 new #26 checks, the offline home check now expects 6 series):
  990. ```
  991. TRACKER_GATE_SHOTS=/tmp/w059/shots TRACKER_GATE_PORT=8754 TRACKER_GATE_IDENT_PORT=8755 TRACKER_GATE_CHROME=8756-8758 TRACKER_GATE_TMDB_PORT=8759 node tests/browser.mjs
  992. 231 passed, 0 failed
  993. ```
  994. Gate env: `TRACKER_REPAIR=0` by default, `TRACKER_DETAILS_PART_EPISODES=1` (a season per step), the repair block sets
  995. `TRACKER_REPAIR_FAST=1`. Shots `title-skeleton-1280.png`, `series-skeleton-1280.png` (looked at).
  996. **Merge notes** (main has mission 014 (old 058) meanwhile — #22–#25 touch `components/show.hl`): my show.hl changes are additive — an
  997. import, 2 statics before "reactive members", 4 members after `adultShow`, the poster/cast/loading blocks in the View, the
  998. `completeTick` handler before `toggleEpisode`, the face `showComplete` first among the faces. If 058 changes `buildRows`' signature,
  999. `completedPageOf` calls `buildRows(show, null, cl)`. `tmdbsync.hl`: `syncMovie(show, res, pre)` gained a parameter, `syncShow` is
  1000. now `syncShowPart(showId, null, null)`. `project.hl`: 1 import, the repair block before the clock, the clock's tick. Gate: fixtures
  1001. (movies 2001/2004/2005/2101–2178, tv 2002, TVmaze 602, person 3010), startServer defaults, the #26 block inside #16, the repair
  1002. block before #10, the offline home check. **The live deploy**: the repair waits for the adult backfill (~80 min), then ~9 min.
  1003. **Open**: (1) merge + deploy. (2) A search import still has no cast (could use `completeStep`). (3) Client-side navigation keeps
  1004. the person page's scroll position on the title page (hl:web; seen on the phone shots, not new). (4) Titles added while the app runs
  1005. are not queued for the repair (open or next start). (5) Cast persons of a title are never refreshed; people's slugs from cast use
  1006. the same `-2` rule as the migration.
  1007. ## 2026-10-01 — ticket #17 (mission 013 (old 057)): season caret up/down, skeleton rows, faster opening (NOT deployed)
  1008. **Built** (`components/show.hl`, `styles.hl`, gate): caret DOWN closed / UP open. Opening a season: caret up + min(episodes, 8)
  1009. shimmering skeleton rows at once (client-side), then the sessionless face `showSeasonEpisodes(seasonId)` → `filledRows`
  1010. replaces them. Closing: client-only, no request. Watch state from member `watchedEpisodes` (per season `{ all, except }`).
  1011. Adult title pages (followers only) keep the session face `showRows(slug, collapsed)` (signature changed: no toggle inside).
  1012. Details: README "What it does (ticket #17)".
  1013. **Why it was slow** (measured): server work per open was small (probe on the copy: showBySlug 60–78 ms linear scan,
  1014. watchesOfUser 20 ms, watchedSet 6, episodesOfSeason 6) — but the face took `session`, so hl:web RE-MOUNTED the page on the
  1015. server and shipped every session-derived member (`rows` with the initial seasons!) as `sync`, which the client repainted,
  1016. then the handler repainted again. TRAP for future faces: a face with `session` = a full page re-render server-side + sync.
  1017. **Real data** (copy of Byrodin `storage/` 2026-10-01 12:25, :8730, creator az5b2, `node tests/realdata-057.mjs`, ms in the
  1018. page from click; 3 repeats; 390 and 1280 alike):
  1019. | | Daily Show S30 (142 ep) | Daily Show S28 (123) | LWT S12 (30) | LWT S10 (21) |
  1020. |---|---|---|---|---|
  1021. | before: episodes visible | 380–450 | 355–385 | 265–1093 | 235–285 |
  1022. | after: skeleton visible | 77–106 | 76–90 | 25–54 | 24–38 |
  1023. | after: episodes visible | 233–288 | 226–268 | 58–102 | 54–74 |
  1024. | close before → after | 420–1388 → 135–175 | 420–500 → 135–165 | 285–390 → 28–45 | 260–370 → 26–30 |
  1025. Skeleton rows 8 (cap) on all; faces sent: only showSeasonEpisodes, never showRows; client-filled watch state == the server's
  1026. rebuild (Daily Show 296 checks/121 solid, LWT 74/29). Page load unchanged (Daily Show ~310–360 ms warm both; HTML 157 → 167 kB
  1027. from `watchedEpisodes` — a plain id map was 274 kB, hence `{ all, except }`). Adult path checked once on a 2nd copy with LWT
  1028. set `adult = true` (:8728): signed out "Not found", creator opens seasons via showRows (skeleton 8, rows 300–400 ms, no dup).
  1029. The Daily Show's skeleton needs ~80 ms because hl:web re-renders the whole flat row list (~264 rows) on every `rows` write.
  1030. How to repeat (ONLY `storage/`; inner ssh with `< /dev/null`):
  1031. ```
  1032. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  1033. TRACKER_PORT=8730 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_BACKFILL=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8730 HL_HOST=127.0.0.1 ./bin/hybriel project.hl
  1034. # session file for user 0mujqspn0aqb (mission 012 (old 056) entry: name sha256(id), created/seen = now ms), id → .scratch/real-sid
  1035. REAL_OUT=/tmp/w057/real-after node tests/realdata-057.mjs # REAL_SHOWS, REAL_SEASONS (indexes, default 1,3), REAL_REPEAT
  1036. # afterwards rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid
  1037. ```
  1038. **Gate** (208 → 214):
  1039. ```
  1040. TRACKER_GATE_SHOTS=/tmp/w057/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  1041. 214 passed, 0 failed
  1042. ```
  1043. Skeleton state tested with a FAKE DELAY: an init script holds the websocket frame naming `showSeasonEpisodes` back 1.5 s
  1044. while `window.__delayFace` is set. Shots `show-skeleton/open/closed-{390,1280}.png` looked at (Loreana /tmp/w057/shots/).
  1045. **Open**: (1) deploy. (2) Episode/season check clicks still use session faces that rebuild all rows (+ the re-mount sync) —
  1046. not measured here; the same split (client patch + a lean face) could speed them up. (3) Big-show skeleton ~80 ms (whole-list re-render in hl:web).
  1047. (4) The adult fallback is not in the gate (no fixture series is adult; changing seed-adult shifts many counts).
  1048. ## 2026-10-01 — mission 012 (old 056): #16 merged into main, 3 fixes, Hybriel master 8590df63 (NOT deployed)
  1049. **Commits** (main, not pushed): 2c53d5e merge t16-person (worktree tracker-wt16 + branch removed; its `.scratch/w055` moved to
  1050. `.scratch/w055-wt16`), 2b7fdd6 header + backfill posters, f40c250 Hybriel + adult show page, + this docs commit.
  1051. - **Merge**: all conflicts additive. Filmography (people.hl `filmographyOf`) lists only `isPublicTitle` (`adult == false`)
  1052. titles whose credit is not adult — unknown hidden like the public lists.
  1053. - **Header** (signed out, phone): ident's "choose ident" + "Log in with ident" wrapped (73 px). Now nowrap, the phone says
  1054. "Log in" (`span.wide` " with ident" from 40rem): 46 px at 320/390, 54 px at 1280. TRAP: a style key twice in one block
  1055. REPLACES the first (a 2nd `'ident-selector::part(button)'` dropped its padding/border) — merge into one line.
  1056. - **Backfill**: an adult title's poster is not downloaded (flag + ids stored). Real data: posters 5,433 (was 8,740), dir
  1057. 269 MB (was 459 MB) → ~190 MB saved.
  1058. - **Hybriel** master 8590df63 (≥ 73267707: #122 18cf2fb9, #121 426944c7), sha256 `ca830ef8…`; old in `.scratch/pre-056/`.
  1059. Duplicate @ids on live copy: 0 in all 8 tables (episodes 232,384, shows 9,453, watches 11,663, …). Workarounds: none removed
  1060. (static helpers kept). SVG namespace check already in the gate (047).
  1061. - **Adult show page** (components/show.hl `visibleShow`/`adultHiddenFor`): `adult == true` → "Not found" for non-followers,
  1062. signed out too; no member carries the row (`showRow` itself is null — every member is shipped to the browser). A follower
  1063. sees it; Unfollow on the page hides it at once (session sync, #122; old build: only after reload). Faces showRows/
  1064. showToggle* refuse it. TRAP (why 054's try failed): `collapsed` derived from `found`/`showRow` (session-derived) gets
  1065. re-derived on the server with MOUNT state after each session face → seasons snap back, 2nd click dead. `collapsed`
  1066. now from the slug (`initialCollapsedOf`). Unknown (`null`) pages are not hidden.
  1067. - **Gate fixes**: the backfill "start" line comes with the clock's first tick, after the server answers → wait for it
  1068. (was a deterministic fail after the merge); every `stopServer()` first sends the tab to about:blank (websocket reconnect
  1069. errors failed the console check intermittently).
  1070. **Gate** (203 after merge → 208):
  1071. ```
  1072. TRACKER_GATE_SHOTS=/tmp/w056/shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 TRACKER_GATE_TMDB_PORT=8727 node tests/browser.mjs
  1073. 208 passed, 0 failed
  1074. ```
  1075. Proven failing without the fix: poster check (forced download → FAIL), Unfollow-hides check on the old Hybriel (FAIL, page
  1076. stayed until reload). Shots `header-signed-out-{320,390,1280}.png`, `person*.png` looked at.
  1077. **Real data** (copy of Byrodin `storage/` 2026-10-01 09:47, :8730, real TMDB, backfill run on the copy 78 min, copy deleted):
  1078. ```
  1079. adult backfill done: 9453/9455 titles, adult=3450 notAdult=5866 unknown=137 posters=5433 ids=6459 requests=9455 errors=139 retries=2 seconds=4701.056
  1080. ```
  1081. `node tests/realdata-056.mjs` (full load ms 390/1280): `/` 426/91, `/movies` 92/90, `/movies/page/2` 93/37, `/shows` 287/41,
  1082. `/shows/page/2` 92/42, `/search/the` 336/126, `/search/sex` 442/91, `/person/Jared-Harris` 242/102 (first visit filled 97
  1083. credits, 80 added, 487 ms), `/shows/Foundation` 189/145; signed in (az5b2): `/my/shows` 650/219, `/my/movies` 41/38,
  1084. `/my/unwatched` 1478/1655 (repeats 1.5–2.4 s, server 1.33 s, 1,595 rows — the same with the old Hybriel), `/my/schedule`
  1085. 457/322, show 170/267. Header one row on every page (46/54–56 px). Adult page `/shows/This-Aint-Star-Trek-XXX`: "Not found"
  1086. signed out (slug not in the HTML) and for az5b2 (follows no adult title). Every link of all 251 list pages, `/`, 6 searches
  1087. (the, sex, porn, xxx, girl, love) and the person: 5,914 slugs → `tools/check-public-slugs.hl`: notAdult=5914, adult=0,
  1088. unknown=0. 0 console problems. Screenshots `/tmp/w056/real/{out,in}-*-{390,1280}.png` on Loreana (looked at).
  1089. How to repeat (ports 8730/8731; ONLY `storage/`; inner ssh with `< /dev/null`):
  1090. ```
  1091. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  1092. TRACKER_PORT=8730 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_BACKFILL=1 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8730 ./bin/hybriel project.hl
  1093. # wait for 'adult backfill done' (~80 min); session file for user 0mujqspn0aqb as in ticket #8 (created/seen = now ms), id → .scratch/real-sid
  1094. REAL_PERSON=Jared-Harris REAL_SHOW=/shows/Foundation REAL_ADULT=This-Aint-Star-Trek-XXX node tests/realdata-056.mjs
  1095. # stop the server, then: TRACKER_STORAGE=… SLUGS=/tmp/w056/real/slugs.txt USER_ID=0mujqspn0aqb ./bin/hybriel tools/check-public-slugs.hl
  1096. # afterwards rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid
  1097. ```
  1098. **Open**: (1) deploy: the live backfill (~80 min, now ~270 MB posters). (2) `/my/unwatched` 1.5–2.4 s full load at the 2 s
  1099. target (1,595 rows). (3) hl:fetch blocks the server during a slow TMDB answer (max step 5 s in this run). (4) Unfollowing an
  1100. adult title on its page turns the page into "Not found" at once (wanted: hidden for non-followers). (5) 137 TMDB-404 titles
  1101. stay hidden; adult people are not hidden; people are never refreshed.
  1102. ## 2026-10-01 — ticket #16 (mission 011 (old 055), branch t16-person, worktree tracker-wt16): person pages
  1103. **Built**: `/person/<slug>` (`components/person.hl`, `people.hl`, route + `/profiles/:name` in `project.hl`, styles block, search.hl
  1104. `titleIdByTmdb`): photo, facts, short bio, filmography (poster, title, year, type, role; newest first; adult hidden). Incomplete
  1105. person → "Loading filmography…" and a step-by-step TMDB fill without reload (1 TMDB request + photo, then 40 titles per face
  1106. call), missing titles added as minimal records with TMDB's `adult`, person marked `filmographyAt`. Details: README "What it does
  1107. (ticket #16)".
  1108. **Decisions**: the fill is driven by the page (face `personFill` per step; first step from an invisible CSS-animation tick — hl:web
  1109. has no mount hook), so it needs no server timer and other requests are served between steps. No sign-in needed. Credits stored in
  1110. the migrated `person.shows` (+ `posterPath`, `adult`); migrated credits TMDB no longer lists are kept. Rows without our poster file
  1111. show TMDB's w92 thumb (like search's web rows) — posters of added titles otherwise come later (sync/backfill). Crew credits count
  1112. (role = job). New titles get this person in `cast` (other existing titles' `cast` is not touched). Bio clamped to 4 lines, click =
  1113. all. Hidden: `show.adult == true` OR the credit's TMDB `adult`.
  1114. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~10:00, deleted afterwards; dev server :8777, signed out, `TRACKER_SYNC=0`,
  1115. real TMDB; `node tests/realdata-person.mjs`): cast click on the show page → person page shown / filled (ms), titles added, server log:
  1116. | person (show) | shown | filled | credits | added | rows | steps | TMDB request | other requests waited max |
  1117. |---|---|---|---|---|---|---|---|---|
  1118. | Tyler Hoechlin (Superman & Lois, 33 migrated credits) | 131 | 643 | 36 | 7 | 38 | 2 | 249 ms | 678 ms |
  1119. | Grant Gustin (The Flash) | 68 | 593 | 33 | 21 | 33 | 2 | 242 ms | 203 ms |
  1120. | Jared Harris (Foundation) | 85 | 869 | 97 | 80 | 97 | 4 | 287 ms | 220 ms |
  1121. | Niamh Algar (Raised by Wolves) | 76 | 584 | 39 | 20 | 40 | 2 | 250 ms | 209 ms |
  1122. | Eric Roberts (stress, direct load) | — | 3.8 s fill | 758 | 712 | 758 | 20 | 646 ms | 612 ms |
  1123. Second visit (complete, full load in Chrome): 41–120 ms at 390/1280, no loading, same rows; row → show page client-side 122–149 ms;
  1124. 0 console problems, no sideways scroll at 390. Screenshots `/tmp/w055/real/*.png` on Loreana (looked at).
  1125. How to repeat (ports 8777–8779; the fill WRITES — use a copy):
  1126. ```
  1127. mkdir -p .scratch/realdata && ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata
  1128. TRACKER_PORT=8777 TRACKER_SYNC=0 TRACKER_WATCH=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ TRACKER_URL=http://127.0.0.1:8777 ./bin/hybriel project.hl
  1129. node tests/realdata-person.mjs # REAL_CASES='/shows/X:Person-Slug,…' for others; grep 'person fill' in the server log
  1130. # afterwards: stop the server, rm -rf .scratch/realdata .scratch/realdata-sessions
  1131. ```
  1132. **Gate** (167 → 186 checks):
  1133. ```
  1134. TRACKER_GATE_SHOTS=/tmp/w055/shots TRACKER_GATE_PORT=8770 TRACKER_GATE_IDENT_PORT=8771 TRACKER_GATE_CHROME=8772-8775 TRACKER_GATE_TMDB_PORT=8776 node tests/browser.mjs
  1135. 186 passed, 0 failed
  1136. ```
  1137. Screenshots `/tmp/w055/shots/person{,-complete}-{390,1280}.png` (looked at).
  1138. **Merge notes**: shared files touched additively — `project.hl` (1 import, `profilesDir`/`profileRoute` before the "old addresses"
  1139. block, 2 routes after `/search/:q?`), `search.hl` (`titleIdByTmdb` before "the query as it comes in the address"), `styles.hl` (one
  1140. block before `@media` + 2 lines at the end of it), `tests/faketmdb.mjs` (`persons`, `personRequests`, `personDelayMs`), `tests/seed-show.hl`
  1141. (Test Actor gets `tmdbId = 3002`, genre Drama `tmdbId = 18`, two persons), `tests/browser.mjs` (`gatePersons` fixture, a block
  1142. before "#10 THE INSTALLABLE APP"), README/STATUS. Mission 010 (old 054) (`adult`): people.hl already reads `show.adult == true` and writes `adult`
  1143. on added titles; if 054 also filters the public lists, the gate's "Hidden Adult Film" (2012) leaves /movies — no gate check here counts it.
  1144. **Open**: the TMDB step blocks the server for its request (~0.25–0.65 s; hl:fetch is blocking). People are never refreshed after
  1145. complete (the ticket's "daily sync may refresh followed people later" — not built). Adult PEOPLE are not hidden (only titles). Added
  1146. titles have no poster file / seasons until synced. Signed-out header at 390 wraps "choose ident" / "Log in with ident" to two lines
  1147. (not from #16 — seen in the real-data shots, every page).
  1148. ## 2026-10-01 — mission 010 (old 054): adult titles hidden from the public lists + the adult-flag backfill (NOT deployed)
  1149. **Built**: `shows.adult` true/false/null(unknown). Public lists (catalog.hl: homepage rows, `/movies`, `/shows`) and the
  1150. search (search.hl `titleOk`; "Fetch from web": `include_adult=false` + only results with `adult: false`) show ONLY
  1151. `adult == false` (shows.hl `isPublicTitle`) — unknown is hidden, so the first deploy is safe. `/my/*` and the show page
  1152. are not filtered (a followed adult title stays reachable). The daily sync, the search import and the backfill store
  1153. TMDB's flag. **Backfill** in the app (project.hl `backfillTick` → tmdbsync.hl `backfillTitle`): one title per step on a
  1154. 0.1 s clock shared with the daily sync (which wins), `GET /movie|tv/<id>?append_to_response=external_ids` + the poster
  1155. when missing, 260 ms pause per API request; 404 → `adultCheck='TMDB 404'` (hidden, never asked again); no answer/429/5xx
  1156. → retried (≤ 2 per start); resumes after a restart (queue = what is still unknown); log every 500 + done line. Env
  1157. `TRACKER_BACKFILL=0` off, `TRACKER_BACKFILL_LOG_EVERY`. Details: README "What it does (mission 010 (old 054))".
  1158. **Decision / Hybriel finding**: hiding the SHOW PAGE from non-followers was built and dropped. `found = showRow != null &&
  1159. !adultHiddenFor(showRow, me)` (a static reaching mpackdb, reading `me`) → after any session face on the page
  1160. (`showToggleEpisode`) the browser re-derives `found` and then `rows`, whose `buildRows` reaches mpackdb → console
  1161. `'watchesOfUser' is a static of 'Watches' whose initializer reaches 'watchesTable', a name the realm 'client' … cannot bind`,
  1162. the click does nothing (gate: "ep3 solid" timeout). A plain copy in between (`hideAdult = hiddenAdult`) did not stop it.
  1163. So the show page of an adult title opens for anyone who has the link (it is in no list and no search).
  1164. **Gate** (167 → 184 checks):
  1165. ```
  1166. 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
  1167. (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)
  1168. 184 passed, 0 failed
  1169. ```
  1170. New block (end of the gate, before the PWA part): `tests/seed-adult.hl` (app stopped) = 1 adult movie alice follows +
  1171. 14 unknown (fake TMDB: 1 adult, 1 404). Backfill off: none on `/`, `/movies` (2 pages), `/shows`, search; the adult one on
  1172. alice's `/my/movies` and its page; web search drops TMDB's adult result. Backfill on with 300 ms per fake answer: stopped
  1173. after 6 → restart asks only the other 8 (resume; mpackdb `update` survives SIGTERM without `persist`), pages < 1.2 s
  1174. meanwhile (slowest 634 ms), lists + search updated at once, poster served, 3rd start "nothing to do". All other fixtures
  1175. are `adult = false` (seed-show.hl); `startServer()` runs with `TRACKER_BACKFILL=0` unless a block switches it on; on a
  1176. throw the gate now prints the browser console + server log tail. Screenshot `adult-home-1280.png` (looked at).
  1177. **Real data** (copy of Byrodin `storage/` 2026-10-01 07:37, real TMDB, :8730, `TRACKER_SYNC=0`, deleted afterwards):
  1178. ```
  1179. adult backfill: start, 9453 titles without an adult flag (65 ms to find them)
  1180. 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
  1181. ```
  1182. - 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 →
  1183. `adultCheck`), errors 140 = 137 × 404 + 2 poster 404 + 1 TLS "no answer" (retried OK). Posters: 126 → 8,866 files,
  1184. 459 MB (adult titles' posters included). A restart afterwards: `start, 0 titles` / `nothing to do`.
  1185. - Lists after: `/movies` 4,242 movies (177 pages), `/shows` 1,624 series (68 pages). EVERY slug on `/`, all 245 list
  1186. pages and 10 searches (the, sex, porn, slut, milf, girl, xxx, anal, teen, hot): 6,008 links → all `adult == false`
  1187. (0 others). TMDB-non-adult titles like "Porn King: The Rise & Fall of Ron Jeremy" (2022 doc) and "Slut." (2004) stay.
  1188. - 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
  1189. 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
  1190. (one 5.8 s) — all a slow TMDB/image answer (movies, no catalog work); hl:fetch blocks the one request loop.
  1191. - After: full load (Chrome, signed out) 390/1280: `/` 0.57 (cold)/0.09 s, `/movies` 0.07/0.04, `/shows` 0.04/0.04,
  1192. `/search/the` 0.46/0.08 s, 0 console problems. Screenshots `/tmp/w054/real/{home,movies,shows,search-the}{,-bottom}-{390,1280}.png`
  1193. on Loreana — looked at: no adult title or poster. Logs `/tmp/w054/real-backfill.log`, `real-latency.log`, `real-restart.log`.
  1194. How to repeat (ports 8730/8731; ONLY `storage/`; note the inner ssh needs `< /dev/null` inside `ssh loreana bash -s`):
  1195. ```
  1196. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdata
  1197. 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
  1198. # progress: grep 'adult backfill' in its log; afterwards stop it, rm -rf the copy + sessions
  1199. ```
  1200. **Open**: (1) deploy: the live app starts the backfill at once (~100 min, ~450 MB of posters into `storage/` → the
  1201. nightly backup); until it reaches a title the lists are emptier (all unknown at the first start). (2) A slow TMDB answer
  1202. blocks pages for its duration (max seen 4.9 s) — needs a non-blocking fetch in Hybriel or shorter timeouts (20 s now).
  1203. (3) Posters of adult titles are downloaded too (served only by their file name; could be skipped to save ~170 MB).
  1204. (4) The show page of an adult title is not hidden (Hybriel limit above). (5) 137 titles TMDB no longer has stay hidden.
  1205. (6) Seen in passing: signed out at 390 px the header buttons wrap to two lines (pre-existing).
  1206. ## 2026-10-01 — mission 009 (old 053): #15 (TVmaze) and #14 (search) merged into main (with #13)
  1207. **Merges** (on main, never pushed): `git merge t15-tvmaze` → `03edc78`, then `git merge t14-search` → `dcc5eec`.
  1208. Branches t14-search / t15-tvmaze and worktrees tracker-wt14 / tracker-wt15 removed afterwards.
  1209. **Conflicts** (both sides kept): README (intro steps 13/14/15, "What it does" sections, Test block, Files rows merged),
  1210. STATUS (all entries, newest on top), `tests/realdata-check.mjs` (#13's page list + #15's env REAL_PORT/REAL_CHROME/
  1211. REAL_OUT/REAL_SHOW; REAL_SHOTS still works), `project.hl` (imports of both), `styles.hl` (both blocks),
  1212. `tests/browser.mjs` (fixtures: movie 1005 + TVmaze episode lists + thetvdb 4004→504; screenshot list = all pages).
  1213. **Behaviour fixes from combining**: (1) #13's 32 extra fixture titles were named "Gate …" and flooded #14's "gate"
  1214. search checks → renamed "Pager Movie NN / Pager Future Movie / Pager Undated Movie / Pager Older Series"
  1215. (`tests/seed-show.hl`). (2) `search.hl` `importTitle` now calls `catalog.hl` `refreshCatalogShow` → an imported title is
  1216. on /movies, /shows and the home rows at once (+1 gate check; the offline check's home Shows row is 4 tiles now).
  1217. **Gate**:
  1218. ```
  1219. 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
  1220. 167 passed, 0 failed
  1221. ```
  1222. **Real data** (copy of Byrodin `storage/` 2026-10-01, :8730, `az5b2`, `TRACKER_SYNC=0`, deleted afterwards; procedure =
  1223. ticket #8; session file `created`/`seen` = now ms): search index 0.58 s at boot. Server (curl): `/` 6 ms, `/movies` 11,
  1224. `/shows` 8, `/search/doctor%20who` 14, `/search/the` 56, `/my/shows` 178, `/my/unwatched` 1204, `/my/schedule` 252,
  1225. `/my/movies` 6 ms. Full load in Chrome (`REAL_OUT=/tmp/w053/real node tests/realdata-check.mjs` + a search timing),
  1226. 390 / 1280: `/` 1.32 (cold) / 0.08 s, `/movies` 0.11 / 0.04, `/shows` 0.10 / 0.09, `/search/doctor%20who` 1.04 (cold) /
  1227. 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 /
  1228. 0.27, show page 0.11 / 0.11 s; 0 console problems, no sideways scroll. Screenshots `/tmp/w053/real/` + gate shots
  1229. `/tmp/w053/shots/` on Loreana (looked at).
  1230. **Open**: `/my/unwatched` (1594 rows) is still the slowest page (server 1.2 s); not deployed; open points of #13/#14/#15
  1231. below stay (adult titles, imported titles not followed → no daily sync, …).
  1232. ## 2026-10-01 — ticket #14 (mission 008 (old 052), branch t14-search, worktree tracker-wt14): search
  1233. **Built**: header magnifier → `/search` / `/search/<text>` (`components/search.hl`), `search.hl` (in-memory word-prefix
  1234. index over 9453 titles + 15157 people, built at boot; TMDB `search/multi`; `importTitle` = new show record + the daily
  1235. sync's `syncShow`). Signed out: search + web work, "Add" opens the sign-in modal. Details: README "What it does (ticket #14)".
  1236. **Decisions**: address `/search/<text>` instead of `/search?q=` (hl:web gives a page no query string; `/search?q=x` opens
  1237. the empty page). Min 2 letters. ≤ 30 titles + ≤ 10 people. One TMDB request per "Fetch from web" (button, not per
  1238. keystroke). Web list leaves out what we have by TMDB id + kind. Import is not auto-follow (so the daily sync won't
  1239. refresh it later — architect's call). Genres matched to ours by name; cast not imported. People link to `/person/<slug>`
  1240. (no page yet, like the show page's cast).
  1241. **Measured on Hybriel**: a module `static x = f()` is evaluated LAZILY (never, if nothing reads `x`) — the index is built
  1242. by `ensureIndex()` called from a `project.hl` member. Static hashes/lists in a module can be mutated in place
  1243. (`buckets[k].push(i)` works, 200k pushes 174 ms). The server percent-decodes the path BEFORE routing (`%2F` → 404, invalid
  1244. UTF-8 passes through raw); a client-side navigation hands the param still encoded. A record literal entry `id = …`
  1245. shadows a local `id` for later entries (bit me: `tmdbId = id` took the new 16-hex id).
  1246. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~07:00, deleted afterwards; dev server :8770, az5b2, real TMDB):
  1247. index build 0.60 s at boot (log `search index: 9453 titles, 15157 people, 4180 keys, 612 ms`); `dbSearch` 0–1 ms for
  1248. "doctor who"/"star trek"/"sopranos", 38–44 ms for "the"/"th" (2000+ candidates); server render `/search/doctor%20who`
  1249. 9 ms, `/search/the` 56 ms; browser: per keystroke → results + address 56–76 ms (incl. CDP), `/search` cold 0.85 s / warm
  1250. 0.04 s; Fetch from web "pluribus" 0.12–0.23 s (real TMDB); Add "Pluribus (2025)" tv 225171 → `/shows/Pluribus` in
  1251. 0.77–1.1 s: Season 1 9 episodes, poster, TMDB/IMDb/TVDB/TVmaze, genres Drama + Sci-Fi & Fantasy; afterwards found in our
  1252. database and gone from the web list; 0 console problems. Screenshots `/tmp/w052-t14/real/*.png` on Loreana (looked at).
  1253. How to repeat (ports 8770–8772; ONLY `storage/` is copied):
  1254. ```
  1255. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata
  1256. cp -r .scratch/realdata/storage .scratch/probe-store # the import WRITES — work on a second copy
  1257. 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
  1258. # AFTER the server is up (it sweeps old session files at boot): session file name = sha256(<32-hex id>), JSON
  1259. # {"created":<now ms>,"data":{"tag":"t"},"id":"<id>","seen":<now ms>,"user":{"id":"0mujqspn0aqb"}}; the id into .scratch/real-sid
  1260. TRACKER_REAL_WEB=pluribus node tests/realdata-search.mjs # TRACKER_REAL_IMPORT=0: no import
  1261. # afterwards: stop the server, rm -rf .scratch/realdata .scratch/probe-store .scratch/realdata-sessions .scratch/real-sid
  1262. ```
  1263. **Gate** (111 → 136 checks):
  1264. ```
  1265. 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
  1266. (in-app run 5.413 s; 12 pages asked during it, slowest 1301 ms)
  1267. 136 passed, 0 failed
  1268. ```
  1269. Fake TMDB: `/3/search/multi` (→ `fake.searchRequests`, w92 thumbs → `fake.thumbRequests`, both kept out of
  1270. `fake.requests` so the sync checks are untouched); tv 1004 + movie 1005 importable, tv 1006 in the search but 404 (the
  1271. failed import = the hybriel#121/#122 check), TVmaze thetvdb 4004 → 504. Screenshots `/tmp/w052-t14/shots/search*.png`
  1272. (+ the existing ones; header still one row at 390 with the magnifier) — looked at.
  1273. **Merge notes** (#13 likely touches the same): `project.hl` (imports, one route, `searchIndexReady` before `server`),
  1274. `components/main.hl` (the magnifier inside `userBox`), `styles.hl` (one block before the `@media` block),
  1275. `tests/browser.mjs` (fixtures at the top, a signed-out block before the "#3 borders" checks, `search` in the 390/1280 page
  1276. list, a signed-in block before "#10 THE INSTALLABLE APP"), `tests/faketmdb.mjs`, README (intro, a section, Test, Files).
  1277. **Open**: no person page (`/person/<slug>` 404s, like the cast links). Imported titles aren't followed → not re-synced.
  1278. Web search is open to signed-out visitors (one TMDB request per click; no rate limit of ours). The field's autofocus is proven
  1279. on a full load only (real-data run: `focused: searchinput`); after the client-side click on the magnifier it is not checked,
  1280. and not on a real phone.
  1281. ## 2026-10-01 — ticket #15 (mission 008 (old 052), worktree tracker-wt15, branch t15-tvmaze): TVmaze merge in the sync
  1282. **Built**: `tmdbsync.hl` `mergeTvmaze` (after the TMDB step, series with a tvmzId): `GET /shows/<tvmzId>/episodes`, by
  1283. S/E number: ADD missing episodes/seasons, FILL empty/placeholder titles + empty air dates; never overwrites a TMDB
  1284. value; TMDB's own placeholder ("Episode 4") no longer overwrites a real title; TMDB later adopts TVmaze rows by
  1285. number. `numberingAgrees` skips shows TVmaze numbers differently (≥ 80 % of shared S/E within 1 day or same title).
  1286. Details: README "What it does (step 15)". Tool line + totals: `tvmazeEpisodes tvmazeSeasons tvmazeFilled tvmazeSkipped`.
  1287. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~07:00, deleted afterwards; procedure = ticket #8 below, server
  1288. 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`):
  1289. ```
  1290. run 1: shows=126 newEpisodes=0 updatedEpisodes=0 requests=255 tvmazeRequests=126 errors=0 tvmazeEpisodes=0 tvmazeFilled=21 tvmazeSkipped=7 seconds=178.9
  1291. run 2: updatedEpisodes=8 (Greek Salad "Épisode n" → "Episode n", after "Épisode" became a placeholder) tvmazeFilled=0 tvmazeSkipped=6 seconds=174.9
  1292. run 3: all 0, tvmazeSkipped=6, seconds=169.9 (idempotent)
  1293. ```
  1294. Filled: Rings of Power S3E1–8 real titles (TMDB "Episode n", airs 2026-11), Last Week Tonight 8x26–30. No episode
  1295. was missing on TMDB that day (tvmazeEpisodes=0) — the add path is proven by the gate only. Skipped (checked by hand,
  1296. correct): Daily Show (TVmaze seasons by year), Star Trek: Prodigy + Blood & Treasure (shifted numbering), Alex Rider,
  1297. Intergalactic, The Rising (other premiere dates + placeholder titles). Loads (390 / 1280): /my/shows 1.51 / 0.26 s,
  1298. /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.
  1299. Screenshots `/tmp/w052-tvmaze/real/*.png` on Loreana (looked at), logs `sync{1,2,3}.log`, `skipped*.txt`, `filled.txt`.
  1300. **Gate** (111 → 117 checks):
  1301. ```
  1302. 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
  1303. 117 passed, 0 failed
  1304. ```
  1305. Screenshots `tvmaze-{schedule,show}-{390,1280}.png` looked at.
  1306. **Open**: adds a new TVmaze-only episode on real data not yet seen live; the daily run takes ~70 s longer (1 TVmaze
  1307. request/series). Shows skipped by the numbering check get no TVmaze help (by design).
  1308. ## 2026-10-01 — ticket #13 (mission 007 (old 050)): homepage, /movies, /shows, /my/movies
  1309. **Built**: `/` (`components/home.hl`): signed in 4 icon tiles → `/my/unwatched` `/my/schedule` `/my/shows` `/my/movies`;
  1310. for everyone the text, "Movies" (→ `/movies`, 10 latest RELEASED movies) and "Shows" (→ `/shows`, 10 series by newest
  1311. released episode) as poster rows (phone: sideways scroll inside the row; ≥ 40rem grid of 5). `/movies`, `/shows`
  1312. (`components/movies.hl`, `allshows.hl` → shared `components/tilelist.hl`): all, 24 per page, pagination =
  1313. components.hybriel `pagination.hl` copied verbatim (4b4dabe). `/my/movies` (`components/mymovies.hl`); `/my/shows` is
  1314. series only now. `catalog.hl`: lists built once at start, `refreshCatalogShow` after each synced show (`project.hl`
  1315. `syncTick`), day roll on first read of a new day. `follows.hl` `insertByFollowDesc` (moved from myshows.hl),
  1316. `shows.hl` `allShows`/`allEpisodes`.
  1317. **Decisions**: page in the PATH (`/movies/page/2`), not `?page=2` as the mission said — a component can't read the
  1318. query (hybriel#11) and Back drops it (hl:web `popstate` uses `location.pathname`), so `?page` would mean full reloads +
  1319. Back to page 1. Pagination click → host: the copied component has no data out (hybriel#87 ruling: a passed function),
  1320. so a `pager-box { on click }` around it reads the button text — no edit of the copy. One page → no pagination.
  1321. /movies = ALL movies incl. future-dated (first) and undated (last); the homepage row only released ones. Series without
  1322. a released episode come after the others, by their own `release`. Home text says episodes come from TMDB (TVmaze is
  1323. only used for ids — the mission's "TMDB/TVmaze" would be wrong). Sign-in hint only when signed out.
  1324. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~06:45, deleted; `az5b2`, :8730, procedure = ticket #8 below; the
  1325. session file needs `created`/`seen` = now in ms, `1` is swept at boot): boot → first answer 1.47 s (catalog build).
  1326. Server time (curl) 5–7 ms for `/`, `/movies`, `/movies/page/200`, `/shows`, `/shows/page/30`, `/my/movies`. Full load
  1327. in a FRESH Chrome per page (document + modules + socket + every poster in view), signed in / out:
  1328. | page | 390 in / out | 1280 in / out |
  1329. |---|---|---|
  1330. | `/` | 1.11 / 1.34 s | 1.13 / 1.29 s |
  1331. | `/movies` | 0.30 / 0.91 s | 0.57 / 0.57 s |
  1332. | `/movies/page/2` | 0.53 / 0.63 s | 1.31 / 0.50 s |
  1333. | `/shows` (24 real posters) | 1.07 / 1.36 s | 1.37 / 1.11 s |
  1334. | `/my/movies` | 0.80 / 0.66 s | 0.83 / 0.58 s |
  1335. Same Chrome (`node tests/realdata-check.mjs`): `/movies`, `/shows`, `/my/movies` 0.04–0.45 s; a page change by the
  1336. pagination 60–75 ms (client-side); Back works; no sideways scroll; 0 console problems. `refreshCatalogShow` for all
  1337. 126 followed shows: 0.31 s (SNL 23 ms). Screenshots `/tmp/w050/real/*-{390,1280}.png` (looked at). Not mine but seen:
  1338. `/my/unwatched` 1.47–1.64 s (1594 rows), `/my/shows` 1.25 s cold at 390.
  1339. **Gate** (111 → 135 checks):
  1340. ```
  1341. 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
  1342. (in-app run 5.415 s; 11 pages asked during it, slowest 1433 ms)
  1343. 135 passed, 0 failed
  1344. ```
  1345. **Session-sync double rows** (team lead: since hybriel 64527baa every face taking `session` also syncs the members derived
  1346. from session; a handler that ALSO appends the returned row shows it twice): checked — no tracker handler appends, every
  1347. one assigns the whole list (`rows = r.rows`) or a flag; the new #13 pages have no faces. 4 gate checks "row appears
  1348. once" (exact count + unique button values, 400 ms after the face for a late sync): /my/unwatched click, show page
  1349. expand, episode click, Follow toggles.
  1350. Changed existing checks: the three "page stays empty" (now home content), the in-app run expects `newEpisodes=2` (the
  1351. fake TMDB adds a released episode to show 2 too, to prove the in-process re-sort). Screenshots `/tmp/w050/shots/`
  1352. (home, movies, movies-page-2, shows-list, my-movies at 390/1280 — looked at).
  1353. **Open**: (1) **adult titles** — the migrated movie table has many porn titles (e.g. on /movies page 1 and 4); they
  1354. are on the public homepage and /movies. No `adult` flag stored; the creator/architect decides (filter via TMDB `adult`,
  1355. hide unfollowed movies, …). (2) Movie posters: only followed shows/movies have real posters → nearly every movie tile is
  1356. the placeholder until the sync covers more. (3) If `?page=N` links are wanted anyway, that needs hybriel#11's query
  1357. half. (4) Real-phone check after deploy.
  1358. ## 2026-10-01 — ticket #12 (mission 006 (old 049)): link icons to TMDB, IMDb, TheTVDB, TVmaze
  1359. **Built**: show page header badges `TMDB` `IMDb` `TVDB` `TVmaze` (`components/show.hl` `linkRows`, `shows.hl`
  1360. `externalLinksOf`, `styles.hl` `showLinks`/`a.extlink`: CSS-only monochrome badges, accent on hover), only for ids the
  1361. show has, `target=_blank rel=noopener`; a movie: TMDB (`/movie/<id>`) + IMDb. Phone: between title row and poster;
  1362. desktop: under the title. Sync (`tmdbsync.hl`): details request now `?append_to_response=external_ids` (no extra
  1363. request) → fills a MISSING imdbId/tvdbId; still no tvmzId → TVmaze `lookup/shows?imdb=`, on 404 `?thetvdb=` (550 ms
  1364. pause each, `pauseMsAfter` in both runners); never overwrites. Totals: `ids=`, `tvmazeRequests=`. New
  1365. `tools/count-external-ids.hl` (copy only).
  1366. **Bug found + fixed**: movies (`type = 'movie'`, 7814 rows, 1 followed: Star Trek: First Contact, tmdbId 199) were
  1367. synced as `/tv/199` — another title. Now `/movie/<id>?append_to_response=external_ids`: imdbId + poster only.
  1368. **Live data still carries the damage**: that record has `status = 'Ended'` and `tmdbSummary` = the overview of TV
  1369. show 199 ("Kabuhayang Swak na Swak…") — not visible (the page shows `summary`), the new sync leaves both alone; a
  1370. one-off fix (set `tmdbSummary = null`, `status` back) is the architect's call.
  1371. **Decisions**: TheTVDB link = `https://thetvdb.com/dereferrer/series/<id>` (it and `?tab=series&id=` both 301 to the
  1372. slug page, checked 2026-10-01); IMDb `/title/<tt>/`; TVmaze `www.tvmaze.com/shows/<id>`. Badges with text, not
  1373. drawn pictograms (no logos copied; legible at 0.75rem). An imdb id is only taken as `tt`+digits, a tvdb/TVmaze id
  1374. only as a number > 0. A show TVmaze doesn't know is asked again each daily run (2 lookups/day on real data).
  1375. **Real data** (copy of Byrodin `storage/` 2026-10-01 ~06:20, deleted afterwards; `tools/count-external-ids.hl`):
  1376. | | shows | tmdbId | imdbId | tvdbId | tvmzId |
  1377. |---|---|---|---|---|---|
  1378. | followed series, before → after 1 sync | 125 | 125 | 125 | 125 | 123 → **124** |
  1379. | followed movie | 1 | 1 | 1 | – | – |
  1380. | all series (only followed are synced) | 1639 | 1639 | 907 | 910 | 845 → 846 |
  1381. | all movies | 7814 | 7814 | 379 | – | – |
  1382. Sync: `shows=126 newSeasons=0 newEpisodes=0 updatedEpisodes=0 updatedSeasons=0 posters=1 ids=1 requests=256
  1383. tvmazeRequests=4 errors=0 seconds=105.6` (poster = the movie's real one; id = Greek Salad tvmz 60613; Mapping the
  1384. World: TVmaze knows neither tt0395050 nor tvdb 109331). TMDB's external ids match ALL 125 stored imdb/tvdb ids (0
  1385. differ). Show pages (az5b2, :8730): Librarians 1.08 s cold / 0.13 s, Greek Salad 0.18 s, Star Trek: First Contact
  1386. 0.13 s (TMDB movie/199 + IMDb, real poster now); no sideways scroll, 0 console problems. Screenshots
  1387. `/tmp/w049/real/*-{390,1280}.png` (looked at). Procedure = ticket #8 below (sync tool with `.env` sourced).
  1388. **Gate** (101 → 111 checks):
  1389. ```
  1390. 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
  1391. (in-app run 5.416 s; 12 pages asked during it, slowest 1323 ms)
  1392. 111 passed, 0 failed
  1393. ```
  1394. Fixture: show 1 imdb `tt0000001` only (TMDB says tt9999999 + tvdb 4001), show 2 tvdb `4002` only (TMDB: tt0000002 +
  1395. 4999), movie `gate-test-movie` (tmdb 1003, followed by a 2nd user so alice's lists are unchanged → the sync now has
  1396. 3 shows). Fake TVmaze: imdb tt0000001 → 501, thetvdb 4002 → 502. Screenshots `/tmp/w049/shots/` (show-*, synced-show
  1397. {,-390}, synced-movie{,-390} looked at). The new-tab check closes the tab at once (it would load the real site).
  1398. **Open**: real-phone tap test after deploy (new tab). Star Trek: First Contact's wrong `tmdbSummary`/`status`
  1399. (above). Unfollowed shows never get ids filled (sync covers followed only, by design).
  1400. ## 2026-10-01 — mission 005 (old 047): re-vendor Hybriel (invisible checks), mobile first, show page additions
  1401. **Bug** (live, the creator: "i dont see the check marks"): after a CLIENT-SIDE navigation (and after expand/collapse
  1402. or a check click) the check `<svg>` was built in the XHTML namespace (circle width 0). Fixed by **re-vendoring Hybriel
  1403. master ff51cf46** (bin sha256 `55a724a0…c5`, README "Vendored Hybriel"; old bin/plugins in `.scratch/pre-047/`).
  1404. Proof the new check fails on the old build: `/tmp/w047/old-build-gate.log` (`ns: http://www.w3.org/1999/xhtml,
  1405. circleWidth: 0`), passes on the new one.
  1406. **Built**: `styles.hl` mobile first (phone base rules, one `@media (min-width: 40rem)` block; header one row at
  1407. 390 px; /my/unwatched + /my/schedule rows two-line on a phone). Show page: phone order title (+ Follow) → full-width
  1408. poster → genres/plot/cast (desktop unchanged); season caret (down/right); episodes not indented; artists orange
  1409. `#ce9178`; unwatched check = accent outline circle + accent check; **Follow toggle** (face `showToggleFollow`,
  1410. `follows.hl` `isFollowing`/`setFollowed`: new record `at = now`, unfollow deletes); signed out checks + Follow visible,
  1411. a click opens the sign-in modal (`components/modal.hl`, copied verbatim from components.hybriel becbd59), nothing sent.
  1412. Session-sync workaround in show/myshows/unwatched/schedule: `rows` reads `me` (a plain copy of `userId`), and
  1413. `following` goes through a LOCAL static `followingOf` — an imported mpackdb helper called directly in a member
  1414. initializer came back false after a client-side navigation (gate caught it; not reduced to a minimal repro yet).
  1415. **Duplicate @ids** (hybriel#113) on a copy of live storage (2026-10-01 04:13), `tools/count-duplicate-ids.hl` + the
  1416. `*.id.txt` indexes: **0 in every table** (episodes 232384, follows 128, genres 27, persons 15157, seasons 6450, shows
  1417. 9453, users 1, watches 11663 rows).
  1418. **Real data** (copy, `az5b2`, :8730, `node tests/realdata-check.mjs`, full load incl. socket + posters):
  1419. | page | 390 px | 1280 px |
  1420. |---|---|---|
  1421. | `/my/shows` (128) | 1.29 s (cold posters) | 0.24 s |
  1422. | `/my/unwatched` (1594) | 1.51 s | 1.46 s |
  1423. | `/my/schedule` (54) | 0.35 s | 0.32 s |
  1424. | `/shows/The-Librarians-The-Next-Chapter` direct / client-side | 0.14 / 0.19 s | 0.13 / 0.18 s |
  1425. Client-side nav, expand, collapse: 14/26/14 checks, 0 wrong namespace, 0 undrawn; header 46 px at 390, no sideways
  1426. scroll. Screenshots `/tmp/w047/real/*.png` (looked at). Procedure = ticket #8 below; the session id goes in
  1427. `.scratch/real-sid` (the script reads it); copy + sessions deleted afterwards.
  1428. **Gate** (76 → 101 checks):
  1429. ```
  1430. 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
  1431. 101 passed, 0 failed
  1432. ```
  1433. Screenshots `/tmp/w047/shots/` ({home,show,my-*}-{390,1280}.png, signed-out-modal-1280.png) — looked at.
  1434. Gate note: the follow test re-follows show 1, so the sync's request order between the two shows may flip (the check
  1435. accepts both orders) and `/my/shows` after the sync has show 1 on top.
  1436. **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.
  1437. The unwatched (outline + check) and watched (filled) icons differ only by fill — as asked. No navigation links to
  1438. the `/my/` pages yet. Real-phone check after deploy.
  1439. ## 2026-10-01 — ticket #10: installable app (PWA) + icons
  1440. **Built**: `icons/` (`icon.svg` source → `icon-192.png`, `icon-512.png`, `apple-touch-icon.png` 180; `favicon.svg`,
  1441. `favicon.ico` 16/32/48 — rsvg-convert + magick, commands in README "What it does (step 10)"); `project.hl`:
  1442. `appIcons` (192/512 × any/maskable, like calendar), `appTouchIcon`, `appFavicon`, `appThemeColor = darker.value`,
  1443. `appBackgroundColor = dark.value` (tokens), `offline = [ Home ]`, file routes for the icons, `/favicon.ico` now a
  1444. real file (was `direct = ""`); `components/main.hl` offline note + `netProbe` tick; `styles.hl` `offlineNote`,
  1445. `netProbe`, `@keyframes tracker-net-tick`; gate +12 checks.
  1446. **Decisions**: icon = a TV (antennas) with a check mark on the screen = "watched shows", green `#4ec9b0` on
  1447. rgb(25,30,35), all inside the maskable safe zone so one PNG serves any + maskable. Theme colour = `darker` (the header
  1448. bar, so the phone's status bar blends in), background = `dark`. Offline keeps only the shell/`/`; data pages get
  1449. hl:web's "Unavailable offline". The offline note is driven by `navigator.onLine`, read on a 1 s CSS-animation tick
  1450. (`animationiteration`) — hl:web has no connection-state member and no mount hook (reported as a Hybriel gap).
  1451. **Gate** (64 → 76 checks):
  1452. ```
  1453. 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
  1454. 76 passed, 0 failed
  1455. ```
  1456. Looked at: `/tmp/w045/icon-48.png`, `icon-192.png`, `icon-512.png`, `maskable-circle.png`, `small-on-white.png`
  1457. (favicon 16/32/48), `/tmp/w045/shots/pwa-phone-{online,offline}.png` (390 px) on Loreana.
  1458. Test gotcha: CDP `setOffline` only cuts the TAB — the service worker's own fetches still reach the server, so the
  1459. gate also stops the server before the offline reload.
  1460. **Open**: offline, the header's `<ident-selector>` (script from ident's origin, not cached) shows "choose ident"
  1461. next to "Log out". No note when the device is online but the server is down. Real-phone install test (ticket's
  1462. **Test**) after deploy: open https://tracker.worldapi.org → "Add to home screen".
  1463. ## 2026-10-01 — ticket #9: TMDB sync (new episodes + posters)
  1464. **Built**: `tmdbsync.hl` (`syncShow`: details + ≤ 20 seasons per request via `append_to_response`, add/update
  1465. seasons+episodes, poster w342), `tools/sync-tmdb.hl` (`--limit N`, paced, prints counts), the daily run in
  1466. `project.hl` (`syncTick` on an hl:time `every(1)` clock: at `TRACKER_SYNC_HOUR` UTC (4), once a day, ONE show
  1467. per tick, next show after n × 260 ms), `follows.hl` `allFollowedShowIds`, fake TMDB `tests/faketmdb.mjs`,
  1468. fixture shows got tmdbIds 1001/1002. Details: README "What it does (step 9)".
  1469. **Decisions**: match episode by tmdbId, else by S/E when the local one has no tmdbId (161 migrated); season by
  1470. number. Empty TMDB values never overwrite. Season 0 only if the show already has one (else specials flood
  1471. /my/unwatched); empty TMDB seasons not created. Own 16-hex ids for new rows (hybriel#113). Posters in
  1472. `storage/mpackdb/posters/` (the folder the existing `/posters/:name` route reads — inside `storage/`, so in the
  1473. backup) rather than a new `storage/posters/`. Shows synced = all followed (also Ended ones — updates happen).
  1474. Sync is off without `TMDB_READ_TOKEN` or with `TRACKER_SYNC=0`.
  1475. **Blocking**: `fetch()` blocks the whole hl:web process (measured: requests wait for the running step) — so the
  1476. run is cut into one-show steps. Real data: ~0.7 s per step average, SNL (53 seasons, 4 requests) 1.7 s; persist
  1477. of the 3 tables 36 ms.
  1478. **Real-data proof** (copy of Byrodin's `storage/` in `.scratch/realdata/`, deleted afterwards; procedure as
  1479. ticket #8 below; session file needs `created`/`seen` = now in ms, else the boot sweep deletes it):
  1480. ```
  1481. set -a; . ./.env; set +a
  1482. TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb ./bin/hybriel tools/sync-tmdb.hl --limit 128
  1483. tmdb sync done: shows=126 newSeasons=20 newEpisodes=800 updatedEpisodes=2651 updatedSeasons=174 posters=125 requests=380 errors=0 seconds=185.718
  1484. (again) shows=126 newSeasons=0 newEpisodes=0 updatedEpisodes=0 updatedSeasons=0 posters=0 requests=255 errors=0 seconds=89.888
  1485. ```
  1486. Fresh-process recount: seasons 6430 → 6450, episodes 231584 → 232384 (exactly +20/+800, no row loss), 0
  1487. dangling season/episode refs, 0 duplicate season numbers; 12 duplicate tmdbIds + 1 wrong parent were already in
  1488. the migrated data (same before/after). 126 shows (128 follows), 125 posters (one show has none on TMDB).
  1489. In-app daily run on the copy (TRACKER_SYNC_HOUR = current hour, real TMDB): 126 shows, 255 requests, 136 s,
  1490. 0 errors; `/my/shows` (signed in) polled every 0.5 s meanwhile: 181 answers, avg 0.25 s, max 4.0 s (one), then
  1491. 1.8/1.7/1.4 s. Full page loads after the sync (headless Chrome, load + socket + all images):
  1492. | page | full load | rows |
  1493. |---|---|---|
  1494. | `/my/schedule` | 0.7–0.8 s | 55 (was 0) |
  1495. | `/my/shows` | 1.3–1.55 s (128 real posters now, 1 placeholder) | 128 |
  1496. | `/my/unwatched` | 1.5 s (server 1.3 s) | 1593 (was 831) |
  1497. | `/shows/The-Librarians-The-Next-Chapter` | 0.33 s | poster + new S2 (12 eps) |
  1498. Screenshots + logs: `/tmp/w044-real/` on Loreana (`after-*.png`, `real-sync{1,2}.log`, `real-lat.txt`).
  1499. **Gate** (49 → 64 checks):
  1500. ```
  1501. 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
  1502. (in-app run 4.405 s; 10 pages asked during it, slowest 1371 ms)
  1503. 64 passed, 0 failed
  1504. ```
  1505. Screenshots `/tmp/w044-shots/` (`synced-*.png` looked at: posters, S3, schedule rows fine).
  1506. **Deploy (architect)**: `TMDB_READ_TOKEN` must be in Byrodin's `.env` (env_file) — without it the log says
  1507. `tmdb sync: off (no TMDB_READ_TOKEN)`. First daily run 04:00 UTC after deploy; check `docker logs
  1508. tracker.worldapi.org | grep tmdb`. Or stop the container and run the tool once for an immediate catch-up.
  1509. **Open**: a step blocks requests up to ~2 s (a non-blocking `fetchStart` + `done()` state machine would fix it).
  1510. `/my/unwatched` render grew with the rows (1.3 s server, 1593 rows) — watch it against the 2 s target. A season
  1511. watch record stays solid when the sync adds a new episode to that season (existing season-watch semantics).
  1512. Dev servers get the real token from `.env` automatically → start them with `TRACKER_SYNC=0`. Shows nobody
  1513. follows are never synced (by design).
  1514. ## 2026-09-30 — ticket #8: /my/ pages, S01E01, years, speed
  1515. **Built**: `/my/unwatched`, `/my/schedule` (routes in `project.hl`); old `/unwatched`, `/schedule` →
  1516. **301** (function routes `unwatchedMoved`/`scheduleMoved`). Rows as `S01E02 · <title>` (was
  1517. `Ep 2 · …`), list titles with the year `Doctor Who (2005)`, show page `1 episode` singular.
  1518. New shared helpers in `shows.hl`: `pad2`, `episodeCode`, `showYear`, `titleWithYear`,
  1519. `posterUrlOf`; in `watches.hl`: `watchedSet` (used by `/my/unwatched` and the show page's
  1520. `buildRows`; `isWatched` stays for the single-target toggles).
  1521. **Decisions**: year = `year`, else `release.slice(0,4)`, else none (197 shows lack `year`, some
  1522. have `release = ''`). Year only in LISTS (`/my/*`), not in the show page's `h1`. The 301 drops any
  1523. query (the pages take none). A row whose poster FILE is missing gets `/posters/none` (one cached
  1524. URL) instead of `/posters/<oldId>.jpg`.
  1525. **Why /my/shows was ~25 s live though its data is 0.2 s**: hl:web handles ONE request at a time.
  1526. Measured on the copy: `/my/shows` requested 1 s into a `/unwatched` render took 13.7 s (the
  1527. `/unwatched` render itself 14.5 s). Every request (pages, 128 poster requests) of every visitor
  1528. queued behind a slow `/unwatched`. Plus 128 distinct poster URLs per load, all the placeholder.
  1529. **Real-data check** (copy of Byrodin's `storage/` only, `az5b2` = user `0mujqspn0aqb`, dev
  1530. server :8730, headless Chrome, navigate → load → websocket open, 1280×900):
  1531. | page | before | after |
  1532. |---|---|---|
  1533. | `/my/shows` (128 rows) | 1.2 s (server 0.19 s) | 0.6–0.8 s, 1 poster request |
  1534. | `/unwatched` → `/my/unwatched` (831 rows) | 14.8 s | 0.7–1.2 s (server 0.55–0.65 s) |
  1535. | `/schedule` → `/my/schedule` (0 rows, data not synced since 2025-12) | 0.36 s | 0.3 s |
  1536. | `/shows/The-Librarians-The-Next-Chapter` | 0.34 s | 0.29 s |
  1537. How to repeat (Loreana; never touch Byrodin's folder beyond the tar read):
  1538. ```
  1539. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata # ONLY storage/
  1540. TRACKER_PORT=8730 TRACKER_WATCH=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ ./bin/hybriel project.hl
  1541. # session file: name = sha256(<32-hex id>), JSON {"created":1,"data":{"tag":"t"},"id":"<id>","seen":1,"user":{"id":"0mujqspn0aqb"}}
  1542. curl -s -o /dev/null -w '%{time_total}\n' -b trackersid=<id> http://127.0.0.1:8730/my/unwatched
  1543. # Chrome: tests/cdp.mjs launchBrowser, Network.setCookie trackersid, goto + wait __hl.socket open
  1544. # afterwards: stop the server, rm -rf .scratch/realdata .scratch/realdata-sessions
  1545. ```
  1546. Screenshots: `/tmp/w041-real/{before,after}-{shows,unwatched,schedule,show}.png` (Loreana).
  1547. **Gate** (45 → 49 checks: two 301s, the browser landing on `/my/unwatched`, "1 episode"; the
  1548. existing ones now expect `/my/…`, `S01E02 · Second`, `S02E01 · Season Two Opener`, titles with
  1549. year, `/posters/sh1.png` + `/posters/none`). Fixture: show 1 `image = 'png'` (gate writes a 1×1
  1550. `posters/sh1.png`), show 2 `year = null`, `release = '2025-03-01'`.
  1551. ```
  1552. TRACKER_GATE_SHOTS=/tmp/w041-shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs
  1553. 49 passed, 0 failed
  1554. ```
  1555. Screenshots `/tmp/w041-shots/` (looked at: all fine).
  1556. **Open**: `/my/unwatched` rebuilds all rows per check click (~0.6 s, fine). Still no navigation
  1557. to the `/my/` pages. The single-request server means any future slow page blocks everyone —
  1558. keep every page well under 1 s. `/my/schedule` stays empty until the TMDB sync step.
  1559. ## 2026-09-30 — ticket #7: /my/shows
  1560. Built `/my/shows` (`components/myshows.hl`, route in `project.hl`, styles in `styles.hl`): every
  1561. followed show, newest follow (`follows.at`, epoch ms) first — small poster (2.5rem, `/posters/…` +
  1562. placeholder like the show page), title → `/shows/<slug>`, last watched episode `SxxEyy`.
  1563. **Decisions**: "last episode" = highest (season, episode) among the user's EPISODE watches of that
  1564. show (ticket); a season watch alone doesn't count (the season check watches every episode anyway).
  1565. No watch → no episode element. A show without a poster name → `/posters/none` (the route's own
  1566. placeholder), so every row has a poster. Read-only page, no face (like `/schedule`).
  1567. **Reuse**: new `follows.hl` `followsOfUser` (whole records; `followedShowIds` now builds on it) and
  1568. `shows.hl` `episodeById` (one line); `showById`, `posterName`, `watchesOfUser` unchanged.
  1569. **Performance** (against a COPY of `storage/mpackdb/` in `.scratch/perf-store`, removed after; live
  1570. files md5-verified unchanged): the page touches only the user's follows and episode watches
  1571. (one fetch per watch), never all episodes of a followed show — az5b2's 128 follows → 128 rows (123
  1572. with an episode, e.g. `S36E11`, `S30E91`) built in **160–255 ms**. For comparison **`/unwatched`
  1573. takes ~15 s** on the same copy (805 rows): it fetches every episode of every followed show and
  1574. checks each against the user's 11,692 watches with a linear `isWatched` — not in this ticket's
  1575. scope, flagged below.
  1576. **Gate** (`tests/browser.mjs`, 38 → 45 checks; `tests/seed-show.hl` adds a 2nd show "Unwatched Gate
  1577. Show", no seasons/poster name, followed AFTER the first but stored after it; the first follow is now
  1578. dated a day back): signed out → sign-in message; signed in → newest follow first, title hrefs,
  1579. posters loaded (`naturalWidth > 0`), 40px wide, `S01E02` after /unwatched's click, later `S02E01`
  1580. (watched BEFORE season 1's bulk watch → proves highest, not most recent), none for the other show.
  1581. ```
  1582. TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs
  1583. 45 passed, 0 failed
  1584. ```
  1585. **Open**: `/unwatched` ~15 s on real data (above) — a watched-id map instead of `isWatched` per
  1586. episode would fix it (own ticket). No navigation links to `/my/shows` yet (homepage stays empty).
  1587. ## 2026-09-27 — ticket #6: /schedule
  1588. Built `/schedule` (`components/schedule.hl`): every episode of a show the signed-in user follows
  1589. that is NOT yet released (`e.release > today`), soonest first. Creator: "/schedule has a similar
  1590. list without check icons for the upcoming episodes of shows followed" — same row shape as
  1591. `/unwatched` (`components/unwatched.hl`) minus the check icon.
  1592. **Reuse, not copy**: imports the exact same `followedShowIds` (`follows.hl`), `showById` /
  1593. `seasonsOfShow` / `episodesOfSeason` (`shows.hl`) that `/unwatched` already uses — nothing new
  1594. added to either file. `watches.hl` is not imported at all: a row here is never watched/unwatched,
  1595. there is no check icon and no click, so the page needed no per-user watch state and no `on server`
  1596. face — the whole component is a single read-only server-side render (`rows` computed once from
  1597. `session`, same as every other reactive member here).
  1598. **Order**: soonest first, ascending by `release` (a plain `'YYYY-MM-DD'` string, ordering
  1599. operators confirmed working against this app's own vendored `bin/hybriel`, same as `/unwatched`'s
  1600. STATUS entry) — the ticket named this the architect's own reading ("the creator named the order
  1601. only for /unwatched"), so `insertByReleaseAsc` mirrors `/unwatched`'s `insertByReleaseDesc` with
  1602. the comparison flipped.
  1603. **`decided`**: "upcoming" = `release` strictly after today (not `>=`) — the natural complement of
  1604. `/unwatched`'s own `release <= today`, so every released-or-today episode is unambiguously on one
  1605. list, never both, never neither.
  1606. **Gate** (`tests/browser.mjs`, 35 → 38 checks): reused the same fixture (`tests/seed-show.hl`,
  1607. unchanged) — episode 3 already carried a far-future release (`2099-01-01`) for `/unwatched`'s own
  1608. "excludes the future one" check; `/schedule` now proves the mirror image with the same fixture:
  1609. signed out, no rows, the sign-in message; signed in, only that one future episode shows (the two
  1610. already-released ones excluded), no `button.checkicon` anywhere on the page, unaffected by any of
  1611. the watch toggles the other checks perform on the same fixture (run in any order — `/schedule`
  1612. never reads watch state).
  1613. ```
  1614. node tests/browser.mjs
  1615. 38 passed, 0 failed
  1616. ps -eo pid,args | grep [h]l-browser-tier
  1617. (nothing)
  1618. ```
  1619. Updated `README.md` ("What it does (step 6)", Test, Files) and this file.
  1620. **Open**: nothing shown on the homepage still points to `/schedule` (same as `/unwatched` before
  1621. it — the homepage stays empty per ticket #1's decision).
  1622. ## 2026-09-27 — ticket #5: /unwatched
  1623. Built `/unwatched`: every episode of a show the signed-in user follows that is already released
  1624. (`release` ≤ today, a plain `'YYYY-MM-DD'` string — migrated as-is by `tools/migrate.hl`, never
  1625. run through `dateOf`) and not yet watched, newest release first (creator: "/unwatched lists all
  1626. unwatched episodes of shows followed in release date desc order").
  1627. **Reuse, not copy** (the ticket's own instruction, re "the same check icon and watch logic as the
  1628. show page"): `watchClassOf` (watched → `'checkicon solid'`/`'checkicon regular'`) was a private
  1629. `static` in `components/show.hl`; moved it to `watches.hl` so both pages import the SAME
  1630. definition instead of each declaring their own — `show.hl` now imports it too, nothing duplicated.
  1631. `isWatched`/`setWatched` (already in `watches.hl`) needed no change. New `follows.hl`
  1632. (`followedShowIds`) and `shows.hl` `showById` (one line) round out the read-only data access this
  1633. page and `/schedule` (ticket #6) both need.
  1634. **String comparison** (`e.release <= today`, `row.release >= x.release` for the desc sort):
  1635. hybriel#2 ("no ordering operators on strings") is filed as still-open in this ticket's brief, but
  1636. its own history shows it was fixed and merged to master 2026-09-25 — confirmed directly against
  1637. THIS app's own vendored `bin/hybriel` (`'2020-09-03' <= '2026-09-27'` → `true`), so no numeric
  1638. date workaround was needed.
  1639. **A View text node cannot hold an inline expression** (`"Ep " + row.episodeNumber + " · " +
  1640. row.title` inside a tag body) — same rule as View attributes (components/show.hl's own comment:
  1641. "a View attribute must be a literal, a member or a field path"), just not documented for text
  1642. nodes anywhere yet; it silently rendered `<!-- binary_expression not rendered yet -->` instead of
  1643. erroring. Fixed by resolving the label into a plain field (`row.episodeLabel`) once, server-side,
  1644. in `unwatchedRowsOf` — the same pattern `show.hl`'s `buildRows` already uses for every other
  1645. ternary/concatenation. Not filed as a new Hybriel gap since it's the exact same restriction
  1646. already known and worked around elsewhere in this app, just not yet hit for interpolated text.
  1647. **Empty state**: signed out (no user, so no follows to show) and signed in with nothing unwatched
  1648. both render a message; used `.muted` (colorTextMuted), not `.message` (colorDanger) — the existing
  1649. `.message` class is for real errors (the login-failed banner), and an empty list isn't one (a
  1650. `decided`, small).
  1651. **Gate** (`tests/browser.mjs`, 29 → 35 checks): `tests/seed-show.hl` now takes an optional
  1652. `TRACKER_SEED_IDENTITY` and, when given, seeds a fixed user + a follow of the fixture show for it
  1653. — the only way to give `/unwatched` a known followed show before the browser ever signs in, since
  1654. the real login's `ensureUser` matches an EXISTING `users.db` row by `identity` and reuses its id
  1655. rather than minting a new one. The gate learns alice's per-app identity with one throwaway
  1656. `ident.exchange` call (consuming one code) before writing the seed. Episode 1/2 of the fixture now
  1657. carry a past `release` (`2020-01-01`/`2020-06-15`), episode 3 a far-future one (`2099-01-01`) —
  1658. proving both the release-date filter and the desc order. The new checks run signed out (no rows,
  1659. the sign-in message), then signed in RIGHT AFTER LOGIN, BEFORE the existing show-page checks that
  1660. toggle these same fixture episodes watched (order matters — otherwise `/unwatched` would already
  1661. be empty by the time it's tested): both unwatched episodes list newest-first, excluding the
  1662. future one; a click on the newest (`Second`, ep 2) removes it from the list; a reload proves the
  1663. watch is server-side, leaving only `Pilot` (ep 1).
  1664. ```
  1665. node tests/browser.mjs
  1666. 35 passed, 0 failed
  1667. ps -eo pid,args | grep [h]l-browser-tier
  1668. (nothing)
  1669. ```
  1670. Updated `README.md` ("What it does (step 5)", Test, Files) and this file.
  1671. **Open**: nothing shown on the homepage still points to `/unwatched` — no navigation exists yet
  1672. (the homepage stays empty per ticket #1's decision; a later step, from the creator, presumably
  1673. adds it). `/schedule` (ticket #6) is next and reuses `follows.hl`/`shows.hl`/`watches.hl` the same
  1674. way.
  1675. ## 2026-09-27 — ticket #4: the show page, watch checks (took over a previous half-done session)
  1676. Continued from an earlier session that had built the whole page — header, seasons/episodes,
  1677. the click-handling redesign to a flat `rows` list rebuilt server-side (`components/show.hl`'s
  1678. own header comment explains why: a nested `for` over seasons > episodes risks hybriel#86) — but
  1679. left with the gate red: clicking a check never turned it solid, no console error, and the
  1680. session ran out of budget before finding why.
  1681. **The actual bug**: the show page's own pure helpers (`buildRows`, `genreRowsOf`, `castRowsOf`,
  1682. `watchClassOf`, `initialCollapsed`) were plain instance members (`buildRows = (show, …) => {…}`,
  1683. no `static`). That shape works fine as a member's own top-level initializer (`rows = found ?
  1684. buildRows(showRow, watchRows, collapsed) : []` — this is how the page renders correctly on first
  1685. load) but throws when the SAME function is called from inside an `on server` face
  1686. (`showToggleEpisode` etc.): the server log showed `'buildRows' is not callable — it holds null`.
  1687. Found by running `.scratch/debug-click.mjs` (left by the previous session) with the server's
  1688. stdout/stderr captured — the browser console itself stayed silent (the face's error never
  1689. reaches the client at all, a separate Hybriel gap, hybriel#92 already covers that half).
  1690. Confirmed the fix by grepping other worldapi apps for the same shape:
  1691. `calendar.worldapi.org/components/calendar.hl` has `static soonOf = (u) => { … upcoming(u, …) }`
  1692. (an mpackdb-backed helper), called from its `on server calLoad`/`calSettingsLoad` faces, and
  1693. calendar's own gate is green — so `static` is the proven-safe shape for a component's own helper
  1694. that both (a) touches an mpackdb-backed import and (b) needs calling from more than one place
  1695. (its own initializer AND a later face). Declared all five helpers in `show.hl` `static`; reran
  1696. the gate: 29/29 green, including the season-bulk-toggle and the reload-proves-server-side checks.
  1697. Filed **hybriel#115 was already open** (from the previous session, about the CLIENT bundle
  1698. omitting a case for such a helper) but this session's finding is the SAME root cause reaching
  1699. further than that ticket says: the plain HL interpreter on the SERVER also treats such a helper
  1700. as `null` outside its own initializer, not just the JS-compiled client bundle. Left as one
  1701. `issues` entry pointing at hybriel#115 rather than opening a near-duplicate ticket — the
  1702. repro/observed there already generalizes (a per-instance member function wrapping an
  1703. mpackdb-backed import is unreliable wherever it is called from, not just the browser).
  1704. Ran `node tests/browser.mjs`: 29 passed, 0 failed (was 20 passed / 1 failed — the click timeout
  1705. — before the `static` fix).
  1706. **Rehearsed `tools/relink-episode-seasons.hl` against a full COPY** of the real
  1707. `storage/mpackdb/` (`.scratch/relink-rehearsal/`, since removed) — the tool's own header claimed
  1708. this rehearsal already existed here; it didn't (a leftover claim from the previous session), so
  1709. this session actually ran it:
  1710. ```
  1711. relink: episodes 231584, null season before 36194, linked 36194, still null (no matching season) 0
  1712. relink: episodes.db replaced with the relinked file — re-run to confirm idempotency (0 linked the second time)
  1713. ```
  1714. Ran a second time on the same copy straight after — idempotency confirmed:
  1715. ```
  1716. relink: episodes 231584, null season before 0, linked 0, still null (no matching season) 0
  1717. ```
  1718. All 36,194 pre-existing null-season episodes (ticket #2's count) now link to a real season by
  1719. `(show, seasonNumber)`; none left over. Not run against the LIVE `storage/mpackdb/` — that needs
  1720. the tracker container stopped first (hl:mpackdb does not coordinate two processes on one storage
  1721. directory, hybriel#21), the architect's job per the ticket ("the architect runs it live").
  1722. Updated README.md ("What it does (step 4)", Test, Files) and this file.
  1723. ## 2026-09-27 — ticket #2: old data migrated (took over a previous session's rewritten tool)

Only the first lines are shown.

Branches

Latest commits

  • fbb903cctracker#33/#37 (mission 032): a title without a TMDB poster gets its backdrop (w780, posterFromBackdrop, shown 2:3 centre-cropped); movies store runtime, the page shows release date + runtime; art backfill (public titles without poster file / movies without runtime) and person photo backfill (tmdbProfile / photoCheck) as the last start jobs (TRACKER_ART, TRACKER_PHOTOS; off in every gate start); gates 379/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy: art 4977 titles in 42 min (115 posters, 15 backdrops, 4609 runtimes), The Remaining shows its backdrop + 7 minmre
  • 4ecc67b2tracker: STATUS/LOG for the t38 + t40 merge (gates 374/0, 32/0, 53/0, 229/0, 26/0, check-theme 0; live copy checks)mre
  • e5945d2fMerge t40 (tracker#40 curated franchises, Franchises menu, superseded collections) into main: deploy.sh lists all six gates (browser, kinds, franchises, pager, franchiseseed, check-theme); search.hl keeps #37's personPhotoOf + #40's importWithCredits; pager gate runs with TRACKER_FRANCHISE_SEED=0; gates 374/0, 32/0, 53/0, 229/0, 26/0, check-theme 0mre
  • b638e99dMerge t38 (tracker#38 pagination, #35 Returning/Airing label) into main: LOG/STATUS keep both sides; pager gate follows #37's /people (everyone, last updated first: seed updatedAt); gates pager 229/0, browser 374/0mre
  • f8ffa4dbtracker: report 032 + The Remaining + #39mre
  • 7565a863tracker: LOG timemre
  • b10f00c8tracker#39: double episodes — migrated episodes whose TMDB id TMDB replaced are adopted by their number in the sync (old id -> migratedTmdbId); merge.hl step 3 merges each season's doubles at start (keeper: most watches > synced > first; watches moved/parked; tombstones into mergedEpisodes, nothing deleted); tools/count-duplicate-episodes.hl; gate fixture + paths-m039; live copy 850 -> 0 in 64 s; gates 373/0, 32/0, 52/0mre
  • 8f1d4542tracker#40: superseded collections — a TMDB collection timeline whose titles are all in one curated timeline is hidden (supersededBy; kept: own page + editor finder), set by the collection seed when it makes one and by the curated build (lifted when the cover is gone); partly covered ones join that franchise; no second widget (First Contact: only Star Trek — Prime); gate franchiseseed 26/0, browser 365/0, kinds 32/0, franchises 53/0, check-theme 0; real copy 24 supersededmre
  • 9448d643tracker#35 follow-up (mission 033): TVmaze 'Running' is labelled 'Returning', or 'Airing' while a non-special episode of the two newest seasons is released within today +-7 days (data unchanged); gate fixtures Running/Airing/Aired + a special; browser 366/0, kinds 32/0, franchises 52/0, pager 229/0, check-theme 0mre
  • 7d4b293dtracker#40: "Franchises" in the main menu (desktop header after People, phone sidebar) → /franchises, marked on franchise and timeline pages; gates 365/0, 32/0, 53/0, 24/0, check-theme 0mre
  • 8751adb8tracker: report 032mre
  • 9bce1f65tracker mission 032: STATUS gate files + the hour-boundary flakemre
  • 718bfb89tracker#37 (mission 032): /people = everyone, last updated first (updatedAt stamped by the person fill; view built at boot, touched people first at once), photo + name tiles (person colour) with the /movies pagination, /people/<letter> removed; photo = our file, tmdbProfile, a cast/crew entry's profile (in-memory map at boot), else the new 'no photo' placeholder; new cast/crew/created_by people keep tmdbProfile; search people rows with the photo; /settings = the heading only; util.hl sortDesc starts from sorted runs (same result, 105k: 1.6 s -> 0.15 s); gates 369/0, 32/0, 52/0, check-theme 0; README/STATUS/LOGmre
  • 93dfb0batracker#40 (mission 034): the curated franchises — data/franchises.json (17 franchises, 31 timelines, 285 TMDB titles, movies + series, in-universe/release order, 12 TMDB collections attached); lib/franchiseseed.hl + jobs.hl franchiseSeedTick (last start job, imports missing titles via details.hl importWithCredits = the search's Add, one per step paced, then one build; franchiseseed.db: editor changes win, the creator's same-name franchise adopted / timeline left alone, 404 remembered, resumable, idempotent); timeline heads 'N titles · in-universe order' (orderKind) and wrap on a phone; series pages show the widget; new gate tests/franchiseseed.mjs (5th in deploy.sh), the others run with TRACKER_FRANCHISE_SEED=0; gates 365/0, 32/0, 52/0, 24/0, check-theme 0; real copy 196 imported, 0 failed, 7 min, restart unchanged=31mre
  • 03ec792ftracker#38 (mission 033): pagination goes exactly to the clicked page — tilelist read the clicked button's text after pagination.hl's own handler had rebuilt the buttons (real clicks only); now li.current, else the button's own text; new gate tests/pager.mjs (5 lists x 11 pages, 390/1280, real + script clicks, Back/Forward) 229/0; browser 365/0, kinds 32/0, franchises 52/0, check-theme 0mre
  • 96ba683adeploy.sh: a gate without a 'passed,' line (check-theme) no longer ends the scriptmre
  • eb3b9205tracker: report 031mre
  • 9b5d2e89tracker mission 031: README (What it does, Test: four gates + the #32 checks, Files: theme/, new pages), STATUS (real copy, A/B load, how to repeat, open points), LOGmre
  • 39950e4ctracker#32 (mission 031): the WorldAPI theme (theme/ vendored verbatim from layouts.worldapi.org 85b5654; styles.hl inherits it: accent green-dark, type colours 1-6; own base/header rules, row lines, genre-pill and inverted-button frames removed, the season foldable keeps its line; check-theme 21 -> 0, 4th deploy gate; main actions class primary) and the #32 header (theme AppHeader/MainMenu/UserMenu/Sidebar/ContentFirst: desktop brand, search, Series|Shows|Movies|Genres|People, user icon with Unwatched..Settings, Logout; signed out the ident selector, phone the iD icon dropdown; phone menu in the sidebar overlay; marked entry by :has); /find -> /search/<q>, /genres, /people(/<letter>), /settings; main { ContentFirst { slot } } works around the hl:web one-line slot bug; gates 365/0, 32/0, 52/0, check-theme 0mre
  • a386dc92tracker: reports 029 + 030mre