tracker
All repositories: gitoria
95.3 KB
# STATUS — tracker.worldapi.org## 2026-10-01 — ticket #19 (mission 066): franchises and timelines (branch t19 from fa1f9df, NOT merged, NOT deployed)**Built** (details [docs/franchises.md](docs/franchises.md)): tables `franchises.db`, `timelines.db`, `collectionchecks.db`(`franchises.hl`); `/franchises`, `/franchises/<slug>` (each timeline: Timeline | Release toggle, a series by its LAST releasedepisode), `/timelines/<slug>`; the widget `‹ Prequel | Timeline | Sequel ›` with the franchise above on every show/movie page(one per timeline); the creator's editor `/franchises/edit` + `/timelines/<slug>/edit` (`TRACKER_EDITORS`, default az5b2,checked in every face); the TMDB collection seed in the app (own clock, one movie per step, 260 ms per request, waits for thedaily sync and the adult backfill, resumes).**Decisions**: a collection becomes a timeline only with ≥ 2 of our non-adult movies (a 1-title widget says nothing); seededtimelines get no franchise (franchises are hand-made); pages/widget skip non-public titles (adult != false), the editor showsall; sort toggle client-side; styles in styles.hl (own section), the widget a composed component so show.hl changes by 4 lines.**Merge notes** (existing files touched): `components/show.hl` (1 import, 1 static, 1 member, 1 View line before`showSeasons`), `project.hl` (4 component imports, 1 franchises import, 5 routes after `/genre/:genre`, `franchisesReady` atboot, the seed clock block before `on Error`), `styles.hl` (a section before "/ (tracker.worldapi.org#13)" + 2 lines in themedia block), `tests/browser.mjs` (`TRACKER_COLLECTIONS: '0'` in startServer), `tests/faketmdb.mjs` (`collections`,`belongs_to_collection`, `at` on requests), `README.md`, `STATUS.md`. New: franchises.hl, components/{franchises,franchise,franchisewidget,franchiseedit,timelineedit}.hl, tests/{franchises.mjs,seed-franchises.hl,realdata-066.mjs}, docs/franchises.md.**Gates** (both must be green):```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.mjs48 passed, 0 failedTRACKER_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.mjs266 passed, 0 failed```Shots looked at: Loreana /tmp/w066/shots/{franchises,franchise,timeline,widget-*,franchise-edit,timeline-edit*}-{390,1280}.png.**Real data** (copy of Byrodin `storage/` 2026-10-01 ~22:47, :8738, real TMDB): seed **4,774 movies** checked in **2,459 s**(~41 min), 4,957 TMDB requests, **110 timelines / 272 titles**, 235 collections with < 2 of our movies, 4,143 movies without acollection, 124 TMDB 404, 0 retries, slowest step 860 ms. Pages during the seed (curl ×40): `/` avg 0.08 s (one 2.2 s outlier),`/movies` 0.01, `/shows/Foundation` 0.18, `/franchises` 0.11. No Star Wars / Alien MOVIES in our data (only series) → checkedStar Trek: The Next Generation (4), Harry Potter (7), Lord of the Rings (3): order right, widget right on the middle title(e.g. First Contact: ‹ Generations | TNG Collection | Insurrection ›). Full loads: `/franchises` 74–109 ms (83 KB, 110timelines), timeline pages 49–213 ms, movie pages with widget 403–464 ms (curl: 0.38 s vs 0.34 s for a movie without atimeline). Creator (az5b2): Edit link + editor forms present. 0 console problems. Shots /tmp/w066/real/*.png (looked at).How to repeat:```tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage | ssh loreana 'tar xzf - -C <wt>/.scratch/realdata' # from Byrodinln -s ../tracker.worldapi.org/.env .env # the real TMDB token for the worktree (never read it); rm afterwardsTRACKER_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 &grep '^collections' /tmp/w066/real/server.log # wait for "collections done"# session file for 0mujqspn0aqb (name sha256(id)) in .scratch/realdata-sessions/, id → .scratch/real-sidREAL_MATCH='Star Trek|Harry Potter|Lord of the Rings' REAL_PORT=8738 REAL_CHROME=8739 REAL_OUT=/tmp/w066/real node tests/realdata-066.mjskill <pid>; rm -f .env; rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid```**Open**: (1) deploy: the first start runs the seed ~40 min (TMDB pace), after the adult backfill. (2) Not built: rename /delete franchises and timelines, edit a franchise summary, franchise `shortId` (field null). (3) New parts of a collection thatalready has a timeline are not added by a later seed; titles imported later are not seeded unless their check row isremoved. (4) The widget sits under the whole header — on a phone after the cast (the creator may want it higher). (5) Atimeline page shows its name twice (page title + block head). (6) A show page costs ~40 ms more with a widget.## 2026-10-01 — ticket #29 (mission 063): unwatched checks looked "active" (NOT deployed)**Cause**: NOT a code regression of #23 — on real data (as az5b2) all 1,287 `/my/unwatched` checks are `checkicon regular`(fill transparent), fresh load and after a client-side navigation; live CSS = HEAD. The look itself was the problem: #11's"inverted check" (outline circle + outline check) was drawn in the ACCENT colour, so a page full of them read as ticked(creator: "in /my/unwatched all checkboxes are active"). **Fix** (`styles.hl`): the unwatched outline + check are muted grey(`colorTextMuted`, rgb(145,150,155)), accent only on hover; watched unchanged (solid accent disc, dark check). Keeps #11'sshape (not a bare empty circle). Applies everywhere the check is used: /my/unwatched, show seasons + episodes, movie page./my/schedule has no checks; /my/movies only marks watched movies (#24).**Gate** (259 → 266): `checkLook(sel)` in tests/browser.mjs measures a REAL check on the page (no hidden copy any more):computed circle fill/stroke + check stroke, topmost element, and the SCREENSHOT pixel inside the disc (svg 12,6) →`lookEmpty` / `lookSolid`. On /my/unwatched (both rows), show page (unwatched episode + season, watched episode, #23 mixedstate), movie page (both states), /my/movies (watched mark). Against the old styles.hl it fails 4 checks (stroke accent).```mkdir -p /tmp/w063/shots # the gate needs the shots dir to existTRACKER_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.mjs266 passed, 0 failed```**Real data** (copy of Byrodin `storage/` 2026-10-01 ~19:30; procedure as mission 060 — the inner ssh from Loreana toByrodin was refused once, so the tar was piped from Byrodin into Loreana instead; server :8730 also needs`TRACKER_OLD_SHORT_IDS=$PWD/data/old-short-ids.json`): `REAL_OUT=/tmp/w063/real node tests/realdata-063.mjs` → /my/unwatched1,287 checks all regular/muted/unfilled (390: 1913 ms, 1280: 1050 ms full load), Foundation 11 muted + 2 solid, Star Trek:First Contact movie check muted, /my/movies none (creator has no watched movie); one click 1287 → 1286 rows in 1.5 s, goneafter a reload; 0 console problems. Shots /tmp/w063/real/{my-unwatched,show,my-movies}-{390,1280}.png (looked at).**Open**: deploy; creator to confirm the muted look (if they want a bare empty circle that reverses #11).## 2026-10-01 — ticket #27 (mission 060): short ids for titles and persons (NOT deployed)**Built** (details README "What it does (mission 060)"): `shortId` (5 × `[a-z0-9]`) on shows (movies + series) and persons, uniqueacross both through one in-memory map (`shortids.hl`). The 702 old ids come back from `data/old-short-ids.json` (made once by`tools/old-short-ids.hl` from the old export); they are reserved for new ids. New records get one at creation (search import, theperson fill's titles). A background backfill (own 0.1 s clock, 40 ms batches, resumes) gives everyone else one. `#shortid` underthe poster / photo (no photo: under the facts), small, muted, monospace. `/<shortId>` → 301 to the page, else 404.**Decisions**: backfill in the background, not at start (all 25k at once ≥ 16 s blocked; batches measured ≤ 62 ms). Uniqueness viathe in-memory map, not an mpackdb `!shortId` index (per table only; adding an index to the live tables is untested, hybriel#21).Old-id file in the code (`data/`, deployed), not in `storage/`. Upper case in `/<id>` is read as lower case. A record shows no iduntil the backfill reaches it (first ~5 min after the deploy).**Gate** (238 → 259):```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.mjs259 passed, 0 failed```The gate gets its own old-id file (`TRACKER_OLD_SHORT_IDS`: sh1 → 100jh). Shots looked at: Loreana /tmp/w060/shots/shortid-*.**Real data** (copy of Byrodin `storage/` taken 2026-10-01 ~14:09 Loreana time: 10,010 shows, 15,157 persons; :8730):- 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.- `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,**duplicates 0**; `tools/count-duplicate-ids.hl`: 0 duplicate @ids in all tables.- Pages by curl, back to back, during the backfill (avg / max / answers > 1 s of ~520 each): `/shows/Foundation` 0.234 / 1.56 / 9,`/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):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 wasbusy with other workers); the backfill adds ~0.1 s on average.- Browser (`node tests/realdata-060.mjs`, signed in as the creator, full loads ms 390 | 1280): Raised by Wolves (via `/100jh`)1372/297/310 | 335/266/301, Foundation (`100sp`, an old id too) 302/306/250 | 1246/205/195, Star Trek: First Contact (`aafif`)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–27ms 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).How to repeat (ONLY `storage/`; inner ssh with `< /dev/null`):```ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdataTRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_OLD_SHORT_IDS=$PWD/data/old-short-ids.json ./bin/hybriel tools/count-short-ids.hlOUT=/tmp/w060 tests/realdata-060-backfill.sh 1 450 "" # starts :8730 on the copy, curl-times 4 pages until "short ids done"# (3rd arg `full` = keep timing until MAX — the baseline once nothing is left). Then, server still up: session file for user# 0mujqspn0aqb (name sha256(<32-hex id>), JSON {"created":<now ms>,"data":{"tag":"t"},"id":"<id>","seen":<now ms>,"user":{"id":"0mujqspn0aqb"}}# in .scratch/realdata-sessions/), id → .scratch/real-sidREAL_OUT=/tmp/w060/real node tests/realdata-060.mjs # REAL_SERIES, REAL_MOVIE, REAL_PERSON, REAL_OLD, REAL_REPEATkill $(cat /tmp/w060/real.pid); TRACKER_STORAGE=… tools/count-short-ids.hl again; rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid```**Open**: (1) deploy — the first start runs the backfill (~5 min, pages ~0.1 s slower meanwhile; ids appear as it goes).(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 fieldwas copied on every access — fixed by keeping the queue a plain `static` list). (3) Nothing creates persons in the app today; if alater step does, it must call `claimShortId('person', …)`. (4) `/<shortId>` of an adult title redirects; its page says "Not found"to non-followers as before.## 2026-10-01 — tickets #22–#25 (mission 058): air dates, season check, movie watched check, genre pages (NOT deployed)**Built** (details README "What it does (mission 058)"): #22 every episode row shows its air date (server rows + openedseasons). #23 a season's check is DERIVED = all its episodes watched (live with the face's answer; a season click on a partlywatched season watches the rest). #24 a movie's page has `#moviewatch` (watch `targetType 'movie'`, face `showToggleMovie`;signed out → modal), `/my/movies` says "N movies · M watched" + a check per watched row. #25 `/genres/<genre>` (+ `/page/<n>`,`components/genre.hl`, catalog.hl `index.genres`): movies AND series, newest first, 24 per page, adult hidden; pills link there,`/genre/<x>` → 301.**Decisions**: the season record is still written by a season click but no longer read for the check. A series' place in agenre list = its newest released episode date (else its release), a movie's = its release (one comparable date). Genre by`urlSegment` (case-insensitive fallback). TMDB's TV and movie genres stay separate (see Open).**Gate** (214 → 238):```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.mjs238 passed, 0 failed```One old check changed: "an unwatched check is the inverted icon" now styles a detached copy classed `regular` (with #23 everycheck on that page is solid at that point). Fixture: genre Action (`g0000000000000g2`) on Pager Movie 01–30, Pager Future Movie,Pager Older Series + the adult `Hidden Action Film`. Shots looked at (Loreana /tmp/w058/shots/: show-open-*, movie-watched-*,genre-action-*, my-movies-*).**Real data** (copy of Byrodin `storage/` 2026-10-01 12:03 UTC, :8730, creator 0mujqspn0aqb, `node tests/realdata-058.mjs`, fullload goto → hydrated, 3 repeats, ms 390 | 1280): `/shows/Foundation` 549/277/221 | 224/228/225 (10 open rows all dated, top seasoncheck == all its episodes); `/shows/Star-Trek-First-Contact` (movie) 234/250/250 | 237/238/239; `/genres/Action` 118/991/42 |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`66/63/66 | 68/63/63. Movie check on the copy: click 361 ms → reload solid → click back → reload regular. Genre kinds page 1:Action 24 movies/0 series, Drama 5/19, Action-Adventure 0/24. 0 console problems. Screenshots /tmp/w058/real/ on Loreana (looked at).How to repeat (ONLY `storage/`; inner ssh with `< /dev/null`):```ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdataTRACKER_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# session file for 0mujqspn0aqb (name sha256(id), created/seen = now ms), id → .scratch/real-sidREAL_OUT=/tmp/w058/real node tests/realdata-058.mjs # REAL_SHOW, REAL_MOVIE, REAL_GENRE, REAL_REPEAT# afterwards rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid```**Open**: (1) deploy. (2) TMDB's TV genres ("Action & Adventure", "Sci-Fi & Fantasy", "War & Politics") are separate genres fromthe movie ones, so `/genres/Action` has no series on real data — merge them? (creator). (3) A series tile shows its start yearwhile it is placed by its newest episode (same as /shows). (4) The season's own watch record can now disagree with its episodes(not read anywhere for the check).## 2026-10-01 — ticket #17 (mission 057): season caret up/down, skeleton rows, faster opening (NOT deployed)**Built** (`components/show.hl`, `styles.hl`, gate): caret DOWN closed / UP open. Opening a season: caret up + min(episodes, 8)shimmering skeleton rows at once (client-side), then the sessionless face `showSeasonEpisodes(seasonId)` → `filledRows`replaces them. Closing: client-only, no request. Watch state from member `watchedEpisodes` (per season `{ all, except }`).Adult title pages (followers only) keep the session face `showRows(slug, collapsed)` (signature changed: no toggle inside).Details: README "What it does (ticket #17)".**Why it was slow** (measured): server work per open was small (probe on the copy: showBySlug 60–78 ms linear scan,watchesOfUser 20 ms, watchedSet 6, episodesOfSeason 6) — but the face took `session`, so hl:web RE-MOUNTED the page on theserver and shipped every session-derived member (`rows` with the initial seasons!) as `sync`, which the client repainted,then the handler repainted again. TRAP for future faces: a face with `session` = a full page re-render server-side + sync.**Real data** (copy of Byrodin `storage/` 2026-10-01 12:25, :8730, creator az5b2, `node tests/realdata-057.mjs`, ms in thepage from click; 3 repeats; 390 and 1280 alike):| | Daily Show S30 (142 ep) | Daily Show S28 (123) | LWT S12 (30) | LWT S10 (21) ||---|---|---|---|---|| before: episodes visible | 380–450 | 355–385 | 265–1093 | 235–285 || after: skeleton visible | 77–106 | 76–90 | 25–54 | 24–38 || after: episodes visible | 233–288 | 226–268 | 58–102 | 54–74 || close before → after | 420–1388 → 135–175 | 420–500 → 135–165 | 285–390 → 28–45 | 260–370 → 26–30 |Skeleton rows 8 (cap) on all; faces sent: only showSeasonEpisodes, never showRows; client-filled watch state == the server'srebuild (Daily Show 296 checks/121 solid, LWT 74/29). Page load unchanged (Daily Show ~310–360 ms warm both; HTML 157 → 167 kBfrom `watchedEpisodes` — a plain id map was 274 kB, hence `{ all, except }`). Adult path checked once on a 2nd copy with LWTset `adult = true` (:8728): signed out "Not found", creator opens seasons via showRows (skeleton 8, rows 300–400 ms, no dup).The Daily Show's skeleton needs ~80 ms because hl:web re-renders the whole flat row list (~264 rows) on every `rows` write.How to repeat (ONLY `storage/`; inner ssh with `< /dev/null`):```ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdataTRACKER_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# session file for user 0mujqspn0aqb (mission 056 entry: name sha256(id), created/seen = now ms), id → .scratch/real-sidREAL_OUT=/tmp/w057/real-after node tests/realdata-057.mjs # REAL_SHOWS, REAL_SEASONS (indexes, default 1,3), REAL_REPEAT# afterwards rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid```**Gate** (208 → 214):```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.mjs214 passed, 0 failed```Skeleton state tested with a FAKE DELAY: an init script holds the websocket frame naming `showSeasonEpisodes` back 1.5 swhile `window.__delayFace` is set. Shots `show-skeleton/open/closed-{390,1280}.png` looked at (Loreana /tmp/w057/shots/).**Open**: (1) deploy. (2) Episode/season check clicks still use session faces that rebuild all rows (+ the re-mount sync) —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).(4) The adult fallback is not in the gate (no fixture series is adult; changing seed-adult shifts many counts).## 2026-10-01 — mission 056: #16 merged into main, 3 fixes, Hybriel master 8590df63 (NOT deployed)**Commits** (main, not pushed): 2c53d5e merge t16-person (worktree tracker-wt16 + branch removed; its `.scratch/w055` moved to`.scratch/w055-wt16`), 2b7fdd6 header + backfill posters, f40c250 Hybriel + adult show page, + this docs commit.- **Merge**: all conflicts additive. Filmography (people.hl `filmographyOf`) lists only `isPublicTitle` (`adult == false`)titles whose credit is not adult — unknown hidden like the public lists.- **Header** (signed out, phone): ident's "choose ident" + "Log in with ident" wrapped (73 px). Now nowrap, the phone says"Log in" (`span.wide` " with ident" from 40rem): 46 px at 320/390, 54 px at 1280. TRAP: a style key twice in one blockREPLACES the first (a 2nd `'ident-selector::part(button)'` dropped its padding/border) — merge into one line.- **Backfill**: an adult title's poster is not downloaded (flag + ids stored). Real data: posters 5,433 (was 8,740), dir269 MB (was 459 MB) → ~190 MB saved.- **Hybriel** master 8590df63 (≥ 73267707: #122 18cf2fb9, #121 426944c7), sha256 `ca830ef8…`; old in `.scratch/pre-056/`.Duplicate @ids on live copy: 0 in all 8 tables (episodes 232,384, shows 9,453, watches 11,663, …). Workarounds: none removed(static helpers kept). SVG namespace check already in the gate (047).- **Adult show page** (components/show.hl `visibleShow`/`adultHiddenFor`): `adult == true` → "Not found" for non-followers,signed out too; no member carries the row (`showRow` itself is null — every member is shipped to the browser). A followersees it; Unfollow on the page hides it at once (session sync, #122; old build: only after reload). Faces showRows/showToggle* refuse it. TRAP (why 054's try failed): `collapsed` derived from `found`/`showRow` (session-derived) getsre-derived on the server with MOUNT state after each session face → seasons snap back, 2nd click dead. `collapsed`now from the slug (`initialCollapsedOf`). Unknown (`null`) pages are not hidden.- **Gate fixes**: the backfill "start" line comes with the clock's first tick, after the server answers → wait for it(was a deterministic fail after the merge); every `stopServer()` first sends the tab to about:blank (websocket reconnecterrors failed the console check intermittently).**Gate** (203 after merge → 208):```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.mjs208 passed, 0 failed```Proven failing without the fix: poster check (forced download → FAIL), Unfollow-hides check on the old Hybriel (FAIL, pagestayed until reload). Shots `header-signed-out-{320,390,1280}.png`, `person*.png` looked at.**Real data** (copy of Byrodin `storage/` 2026-10-01 09:47, :8730, real TMDB, backfill run on the copy 78 min, copy deleted):```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````node tests/realdata-056.mjs` (full load ms 390/1280): `/` 426/91, `/movies` 92/90, `/movies/page/2` 93/37, `/shows` 287/41,`/shows/page/2` 92/42, `/search/the` 336/126, `/search/sex` 442/91, `/person/Jared-Harris` 242/102 (first visit filled 97credits, 80 added, 487 ms), `/shows/Foundation` 189/145; signed in (az5b2): `/my/shows` 650/219, `/my/movies` 41/38,`/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`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"signed out (slug not in the HTML) and for az5b2 (follows no adult title). Every link of all 251 list pages, `/`, 6 searches(the, sex, porn, xxx, girl, love) and the person: 5,914 slugs → `tools/check-public-slugs.hl`: notAdult=5914, adult=0,unknown=0. 0 console problems. Screenshots `/tmp/w056/real/{out,in}-*-{390,1280}.png` on Loreana (looked at).How to repeat (ports 8730/8731; ONLY `storage/`; inner ssh with `< /dev/null`):```ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdataTRACKER_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# wait for 'adult backfill done' (~80 min); session file for user 0mujqspn0aqb as in ticket #8 (created/seen = now ms), id → .scratch/real-sidREAL_PERSON=Jared-Harris REAL_SHOW=/shows/Foundation REAL_ADULT=This-Aint-Star-Trek-XXX node tests/realdata-056.mjs# stop the server, then: TRACKER_STORAGE=… SLUGS=/tmp/w056/real/slugs.txt USER_ID=0mujqspn0aqb ./bin/hybriel tools/check-public-slugs.hl# afterwards rm -rf .scratch/realdata .scratch/realdata-sessions .scratch/real-sid```**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 starget (1,595 rows). (3) hl:fetch blocks the server during a slow TMDB answer (max step 5 s in this run). (4) Unfollowing anadult title on its page turns the page into "Not found" at once (wanted: hidden for non-followers). (5) 137 TMDB-404 titlesstay hidden; adult people are not hidden; people are never refreshed.## 2026-10-01 — ticket #16 (mission 055, branch t16-person, worktree tracker-wt16): person pages**Built**: `/person/<slug>` (`components/person.hl`, `people.hl`, route + `/profiles/:name` in `project.hl`, styles block, search.hl`titleIdByTmdb`): photo, facts, short bio, filmography (poster, title, year, type, role; newest first; adult hidden). Incompleteperson → "Loading filmography…" and a step-by-step TMDB fill without reload (1 TMDB request + photo, then 40 titles per facecall), missing titles added as minimal records with TMDB's `adult`, person marked `filmographyAt`. Details: README "What it does(ticket #16)".**Decisions**: the fill is driven by the page (face `personFill` per step; first step from an invisible CSS-animation tick — hl:webhas no mount hook), so it needs no server timer and other requests are served between steps. No sign-in needed. Credits stored inthe migrated `person.shows` (+ `posterPath`, `adult`); migrated credits TMDB no longer lists are kept. Rows without our poster fileshow TMDB's w92 thumb (like search's web rows) — posters of added titles otherwise come later (sync/backfill). Crew credits count(role = job). New titles get this person in `cast` (other existing titles' `cast` is not touched). Bio clamped to 4 lines, click =all. Hidden: `show.adult == true` OR the credit's TMDB `adult`.**Real data** (copy of Byrodin `storage/` 2026-10-01 ~10:00, deleted afterwards; dev server :8777, signed out, `TRACKER_SYNC=0`,real TMDB; `node tests/realdata-person.mjs`): cast click on the show page → person page shown / filled (ms), titles added, server log:| person (show) | shown | filled | credits | added | rows | steps | TMDB request | other requests waited max ||---|---|---|---|---|---|---|---|---|| Tyler Hoechlin (Superman & Lois, 33 migrated credits) | 131 | 643 | 36 | 7 | 38 | 2 | 249 ms | 678 ms || Grant Gustin (The Flash) | 68 | 593 | 33 | 21 | 33 | 2 | 242 ms | 203 ms || Jared Harris (Foundation) | 85 | 869 | 97 | 80 | 97 | 4 | 287 ms | 220 ms || Niamh Algar (Raised by Wolves) | 76 | 584 | 39 | 20 | 40 | 2 | 250 ms | 209 ms || Eric Roberts (stress, direct load) | — | 3.8 s fill | 758 | 712 | 758 | 20 | 646 ms | 612 ms |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;0 console problems, no sideways scroll at 390. Screenshots `/tmp/w055/real/*.png` on Loreana (looked at).How to repeat (ports 8777–8779; the fill WRITES — use a copy):```mkdir -p .scratch/realdata && ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdataTRACKER_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.hlnode tests/realdata-person.mjs # REAL_CASES='/shows/X:Person-Slug,…' for others; grep 'person fill' in the server log# afterwards: stop the server, rm -rf .scratch/realdata .scratch/realdata-sessions```**Gate** (167 → 186 checks):```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.mjs186 passed, 0 failed```Screenshots `/tmp/w055/shots/person{,-complete}-{390,1280}.png` (looked at).**Merge notes**: shared files touched additively — `project.hl` (1 import, `profilesDir`/`profileRoute` before the "old addresses"block, 2 routes after `/search/:q?`), `search.hl` (`titleIdByTmdb` before "the query as it comes in the address"), `styles.hl` (oneblock before `@media` + 2 lines at the end of it), `tests/faketmdb.mjs` (`persons`, `personRequests`, `personDelayMs`), `tests/seed-show.hl`(Test Actor gets `tmdbId = 3002`, genre Drama `tmdbId = 18`, two persons), `tests/browser.mjs` (`gatePersons` fixture, a blockbefore "#10 THE INSTALLABLE APP"), README/STATUS. Mission 054 (`adult`): people.hl already reads `show.adult == true` and writes `adult`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.**Open**: the TMDB step blocks the server for its request (~0.25–0.65 s; hl:fetch is blocking). People are never refreshed aftercomplete (the ticket's "daily sync may refresh followed people later" — not built). Adult PEOPLE are not hidden (only titles). Addedtitles have no poster file / seasons until synced. Signed-out header at 390 wraps "choose ident" / "Log in with ident" to two lines(not from #16 — seen in the real-data shots, every page).## 2026-10-01 — mission 054: adult titles hidden from the public lists + the adult-flag backfill (NOT deployed)**Built**: `shows.adult` true/false/null(unknown). Public lists (catalog.hl: homepage rows, `/movies`, `/shows`) and thesearch (search.hl `titleOk`; "Fetch from web": `include_adult=false` + only results with `adult: false`) show ONLY`adult == false` (shows.hl `isPublicTitle`) — unknown is hidden, so the first deploy is safe. `/my/*` and the show pageare not filtered (a followed adult title stays reachable). The daily sync, the search import and the backfill storeTMDB's flag. **Backfill** in the app (project.hl `backfillTick` → tmdbsync.hl `backfillTitle`): one title per step on a0.1 s clock shared with the daily sync (which wins), `GET /movie|tv/<id>?append_to_response=external_ids` + the posterwhen missing, 260 ms pause per API request; 404 → `adultCheck='TMDB 404'` (hidden, never asked again); no answer/429/5xx→ retried (≤ 2 per start); resumes after a restart (queue = what is still unknown); log every 500 + done line. Env`TRACKER_BACKFILL=0` off, `TRACKER_BACKFILL_LOG_EVERY`. Details: README "What it does (mission 054)".**Decision / Hybriel finding**: hiding the SHOW PAGE from non-followers was built and dropped. `found = showRow != null &&!adultHiddenFor(showRow, me)` (a static reaching mpackdb, reading `me`) → after any session face on the page(`showToggleEpisode`) the browser re-derives `found` and then `rows`, whose `buildRows` reaches mpackdb → console`'watchesOfUser' is a static of 'Watches' whose initializer reaches 'watchesTable', a name the realm 'client' … cannot bind`,the click does nothing (gate: "ep3 solid" timeout). A plain copy in between (`hideAdult = hiddenAdult`) did not stop it.So the show page of an adult title opens for anyone who has the link (it is in no list and no search).**Gate** (167 → 184 checks):```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(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)184 passed, 0 failed```New block (end of the gate, before the PWA part): `tests/seed-adult.hl` (app stopped) = 1 adult movie alice follows +14 unknown (fake TMDB: 1 adult, 1 404). Backfill off: none on `/`, `/movies` (2 pages), `/shows`, search; the adult one onalice's `/my/movies` and its page; web search drops TMDB's adult result. Backfill on with 300 ms per fake answer: stoppedafter 6 → restart asks only the other 8 (resume; mpackdb `update` survives SIGTERM without `persist`), pages < 1.2 smeanwhile (slowest 634 ms), lists + search updated at once, poster served, 3rd start "nothing to do". All other fixturesare `adult = false` (seed-show.hl); `startServer()` runs with `TRACKER_BACKFILL=0` unless a block switches it on; on athrow the gate now prints the browser console + server log tail. Screenshot `adult-home-1280.png` (looked at).**Real data** (copy of Byrodin `storage/` 2026-10-01 07:37, real TMDB, :8730, `TRACKER_SYNC=0`, deleted afterwards):```adult backfill: start, 9453 titles without an adult flag (65 ms to find them)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```- 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 →`adultCheck`), errors 140 = 137 × 404 + 2 poster 404 + 1 TLS "no answer" (retried OK). Posters: 126 → 8,866 files,459 MB (adult titles' posters included). A restart afterwards: `start, 0 titles` / `nothing to do`.- Lists after: `/movies` 4,242 movies (177 pages), `/shows` 1,624 series (68 pages). EVERY slug on `/`, all 245 listpages and 10 searches (the, sex, porn, slut, milf, girl, xxx, anal, teen, hot): 6,008 links → all `adult == false`(0 others). TMDB-non-adult titles like "Porn King: The Rise & Fall of Ron Jeremy" (2022 doc) and "Slut." (2004) stay.- Page latency DURING the run (curl every ~5 s, 1,155 samples each): `/` avg 0.156 s max 4.87 s, `/movies` avg 0.059max 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(one 5.8 s) — all a slow TMDB/image answer (movies, no catalog work); hl:fetch blocks the one request loop.- After: full load (Chrome, signed out) 390/1280: `/` 0.57 (cold)/0.09 s, `/movies` 0.07/0.04, `/shows` 0.04/0.04,`/search/the` 0.46/0.08 s, 0 console problems. Screenshots `/tmp/w054/real/{home,movies,shows,search-the}{,-bottom}-{390,1280}.png`on Loreana — looked at: no adult title or poster. Logs `/tmp/w054/real-backfill.log`, `real-latency.log`, `real-restart.log`.How to repeat (ports 8730/8731; ONLY `storage/`; note the inner ssh needs `< /dev/null` inside `ssh loreana bash -s`):```ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' < /dev/null | tar xzf - -C .scratch/realdataTRACKER_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# progress: grep 'adult backfill' in its log; afterwards stop it, rm -rf the copy + sessions```**Open**: (1) deploy: the live app starts the backfill at once (~100 min, ~450 MB of posters into `storage/` → thenightly backup); until it reaches a title the lists are emptier (all unknown at the first start). (2) A slow TMDB answerblocks pages for its duration (max seen 4.9 s) — needs a non-blocking fetch in Hybriel or shorter timeouts (20 s now).(3) Posters of adult titles are downloaded too (served only by their file name; could be skipped to save ~170 MB).(4) The show page of an adult title is not hidden (Hybriel limit above). (5) 137 titles TMDB no longer has stay hidden.(6) Seen in passing: signed out at 390 px the header buttons wrap to two lines (pre-existing).## 2026-10-01 — mission 053: #15 (TVmaze) and #14 (search) merged into main (with #13)**Merges** (on main, never pushed): `git merge t15-tvmaze` → `03edc78`, then `git merge t14-search` → `dcc5eec`.Branches t14-search / t15-tvmaze and worktrees tracker-wt14 / tracker-wt15 removed afterwards.**Conflicts** (both sides kept): README (intro steps 13/14/15, "What it does" sections, Test block, Files rows merged),STATUS (all entries, newest on top), `tests/realdata-check.mjs` (#13's page list + #15's env REAL_PORT/REAL_CHROME/REAL_OUT/REAL_SHOW; REAL_SHOTS still works), `project.hl` (imports of both), `styles.hl` (both blocks),`tests/browser.mjs` (fixtures: movie 1005 + TVmaze episode lists + thetvdb 4004→504; screenshot list = all pages).**Behaviour fixes from combining**: (1) #13's 32 extra fixture titles were named "Gate …" and flooded #14's "gate"search checks → renamed "Pager Movie NN / Pager Future Movie / Pager Undated Movie / Pager Older Series"(`tests/seed-show.hl`). (2) `search.hl` `importTitle` now calls `catalog.hl` `refreshCatalogShow` → an imported title ison /movies, /shows and the home rows at once (+1 gate check; the offline check's home Shows row is 4 tiles now).**Gate**:```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.mjs167 passed, 0 failed```**Real data** (copy of Byrodin `storage/` 2026-10-01, :8730, `az5b2`, `TRACKER_SYNC=0`, deleted afterwards; procedure =ticket #8; session file `created`/`seen` = now ms): search index 0.58 s at boot. Server (curl): `/` 6 ms, `/movies` 11,`/shows` 8, `/search/doctor%20who` 14, `/search/the` 56, `/my/shows` 178, `/my/unwatched` 1204, `/my/schedule` 252,`/my/movies` 6 ms. Full load in Chrome (`REAL_OUT=/tmp/w053/real node tests/realdata-check.mjs` + a search timing),390 / 1280: `/` 1.32 (cold) / 0.08 s, `/movies` 0.11 / 0.04, `/shows` 0.10 / 0.09, `/search/doctor%20who` 1.04 (cold) /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 /0.27, show page 0.11 / 0.11 s; 0 console problems, no sideways scroll. Screenshots `/tmp/w053/real/` + gate shots`/tmp/w053/shots/` on Loreana (looked at).**Open**: `/my/unwatched` (1594 rows) is still the slowest page (server 1.2 s); not deployed; open points of #13/#14/#15below stay (adult titles, imported titles not followed → no daily sync, …).## 2026-10-01 — ticket #14 (mission 052, branch t14-search, worktree tracker-wt14): search**Built**: header magnifier → `/search` / `/search/<text>` (`components/search.hl`), `search.hl` (in-memory word-prefixindex over 9453 titles + 15157 people, built at boot; TMDB `search/multi`; `importTitle` = new show record + the dailysync's `syncShow`). Signed out: search + web work, "Add" opens the sign-in modal. Details: README "What it does (ticket #14)".**Decisions**: address `/search/<text>` instead of `/search?q=` (hl:web gives a page no query string; `/search?q=x` opensthe empty page). Min 2 letters. ≤ 30 titles + ≤ 10 people. One TMDB request per "Fetch from web" (button, not perkeystroke). Web list leaves out what we have by TMDB id + kind. Import is not auto-follow (so the daily sync won'trefresh it later — architect's call). Genres matched to ours by name; cast not imported. People link to `/person/<slug>`(no page yet, like the show page's cast).**Measured on Hybriel**: a module `static x = f()` is evaluated LAZILY (never, if nothing reads `x`) — the index is builtby `ensureIndex()` called from a `project.hl` member. Static hashes/lists in a module can be mutated in place(`buckets[k].push(i)` works, 200k pushes 174 ms). The server percent-decodes the path BEFORE routing (`%2F` → 404, invalidUTF-8 passes through raw); a client-side navigation hands the param still encoded. A record literal entry `id = …`shadows a local `id` for later entries (bit me: `tmdbId = id` took the new 16-hex id).**Real data** (copy of Byrodin `storage/` 2026-10-01 ~07:00, deleted afterwards; dev server :8770, az5b2, real TMDB):index build 0.60 s at boot (log `search index: 9453 titles, 15157 people, 4180 keys, 612 ms`); `dbSearch` 0–1 ms for"doctor who"/"star trek"/"sopranos", 38–44 ms for "the"/"th" (2000+ candidates); server render `/search/doctor%20who`9 ms, `/search/the` 56 ms; browser: per keystroke → results + address 56–76 ms (incl. CDP), `/search` cold 0.85 s / warm0.04 s; Fetch from web "pluribus" 0.12–0.23 s (real TMDB); Add "Pluribus (2025)" tv 225171 → `/shows/Pluribus` in0.77–1.1 s: Season 1 9 episodes, poster, TMDB/IMDb/TVDB/TVmaze, genres Drama + Sci-Fi & Fantasy; afterwards found in ourdatabase and gone from the web list; 0 console problems. Screenshots `/tmp/w052-t14/real/*.png` on Loreana (looked at).How to repeat (ports 8770–8772; ONLY `storage/` is copied):```ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdatacp -r .scratch/realdata/storage .scratch/probe-store # the import WRITES — work on a second copyTRACKER_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# AFTER the server is up (it sweeps old session files at boot): session file name = sha256(<32-hex id>), JSON# {"created":<now ms>,"data":{"tag":"t"},"id":"<id>","seen":<now ms>,"user":{"id":"0mujqspn0aqb"}}; the id into .scratch/real-sidTRACKER_REAL_WEB=pluribus node tests/realdata-search.mjs # TRACKER_REAL_IMPORT=0: no import# afterwards: stop the server, rm -rf .scratch/realdata .scratch/probe-store .scratch/realdata-sessions .scratch/real-sid```**Gate** (111 → 136 checks):```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(in-app run 5.413 s; 12 pages asked during it, slowest 1301 ms)136 passed, 0 failed```Fake TMDB: `/3/search/multi` (→ `fake.searchRequests`, w92 thumbs → `fake.thumbRequests`, both kept out of`fake.requests` so the sync checks are untouched); tv 1004 + movie 1005 importable, tv 1006 in the search but 404 (thefailed import = the hybriel#121/#122 check), TVmaze thetvdb 4004 → 504. Screenshots `/tmp/w052-t14/shots/search*.png`(+ the existing ones; header still one row at 390 with the magnifier) — looked at.**Merge notes** (#13 likely touches the same): `project.hl` (imports, one route, `searchIndexReady` before `server`),`components/main.hl` (the magnifier inside `userBox`), `styles.hl` (one block before the `@media` block),`tests/browser.mjs` (fixtures at the top, a signed-out block before the "#3 borders" checks, `search` in the 390/1280 pagelist, a signed-in block before "#10 THE INSTALLABLE APP"), `tests/faketmdb.mjs`, README (intro, a section, Test, Files).**Open**: no person page (`/person/<slug>` 404s, like the cast links). Imported titles aren't followed → not re-synced.Web search is open to signed-out visitors (one TMDB request per click; no rate limit of ours). The field's autofocus is provenon a full load only (real-data run: `focused: searchinput`); after the client-side click on the magnifier it is not checked,and not on a real phone.## 2026-10-01 — ticket #15 (mission 052, worktree tracker-wt15, branch t15-tvmaze): TVmaze merge in the sync**Built**: `tmdbsync.hl` `mergeTvmaze` (after the TMDB step, series with a tvmzId): `GET /shows/<tvmzId>/episodes`, byS/E number: ADD missing episodes/seasons, FILL empty/placeholder titles + empty air dates; never overwrites a TMDBvalue; TMDB's own placeholder ("Episode 4") no longer overwrites a real title; TMDB later adopts TVmaze rows bynumber. `numberingAgrees` skips shows TVmaze numbers differently (≥ 80 % of shared S/E within 1 day or same title).Details: README "What it does (step 15)". Tool line + totals: `tvmazeEpisodes tvmazeSeasons tvmazeFilled tvmazeSkipped`.**Real data** (copy of Byrodin `storage/` 2026-10-01 ~07:00, deleted afterwards; procedure = ticket #8 below, serveron :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`):```run 1: shows=126 newEpisodes=0 updatedEpisodes=0 requests=255 tvmazeRequests=126 errors=0 tvmazeEpisodes=0 tvmazeFilled=21 tvmazeSkipped=7 seconds=178.9run 2: updatedEpisodes=8 (Greek Salad "Épisode n" → "Episode n", after "Épisode" became a placeholder) tvmazeFilled=0 tvmazeSkipped=6 seconds=174.9run 3: all 0, tvmazeSkipped=6, seconds=169.9 (idempotent)```Filled: Rings of Power S3E1–8 real titles (TMDB "Episode n", airs 2026-11), Last Week Tonight 8x26–30. No episodewas missing on TMDB that day (tvmazeEpisodes=0) — the add path is proven by the gate only. Skipped (checked by hand,correct): Daily Show (TVmaze seasons by year), Star Trek: Prodigy + Blood & Treasure (shifted numbering), Alex Rider,Intergalactic, The Rising (other premiere dates + placeholder titles). Loads (390 / 1280): /my/shows 1.51 / 0.26 s,/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.Screenshots `/tmp/w052-tvmaze/real/*.png` on Loreana (looked at), logs `sync{1,2,3}.log`, `skipped*.txt`, `filled.txt`.**Gate** (111 → 117 checks):```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.mjs117 passed, 0 failed```Screenshots `tvmaze-{schedule,show}-{390,1280}.png` looked at.**Open**: adds a new TVmaze-only episode on real data not yet seen live; the daily run takes ~70 s longer (1 TVmazerequest/series). Shows skipped by the numbering check get no TVmaze help (by design).## 2026-10-01 — ticket #13 (mission 050): homepage, /movies, /shows, /my/movies**Built**: `/` (`components/home.hl`): signed in 4 icon tiles → `/my/unwatched` `/my/schedule` `/my/shows` `/my/movies`;for everyone the text, "Movies" (→ `/movies`, 10 latest RELEASED movies) and "Shows" (→ `/shows`, 10 series by newestreleased episode) as poster rows (phone: sideways scroll inside the row; ≥ 40rem grid of 5). `/movies`, `/shows`(`components/movies.hl`, `allshows.hl` → shared `components/tilelist.hl`): all, 24 per page, pagination =components.hybriel `pagination.hl` copied verbatim (4b4dabe). `/my/movies` (`components/mymovies.hl`); `/my/shows` isseries only now. `catalog.hl`: lists built once at start, `refreshCatalogShow` after each synced show (`project.hl``syncTick`), day roll on first read of a new day. `follows.hl` `insertByFollowDesc` (moved from myshows.hl),`shows.hl` `allShows`/`allEpisodes`.**Decisions**: page in the PATH (`/movies/page/2`), not `?page=2` as the mission said — a component can't read thequery (hybriel#11) and Back drops it (hl:web `popstate` uses `location.pathname`), so `?page` would mean full reloads +Back to page 1. Pagination click → host: the copied component has no data out (hybriel#87 ruling: a passed function),so a `pager-box { on click }` around it reads the button text — no edit of the copy. One page → no pagination./movies = ALL movies incl. future-dated (first) and undated (last); the homepage row only released ones. Series withouta released episode come after the others, by their own `release`. Home text says episodes come from TMDB (TVmaze isonly used for ids — the mission's "TMDB/TVmaze" would be wrong). Sign-in hint only when signed out.**Real data** (copy of Byrodin `storage/` 2026-10-01 ~06:45, deleted; `az5b2`, :8730, procedure = ticket #8 below; thesession file needs `created`/`seen` = now in ms, `1` is swept at boot): boot → first answer 1.47 s (catalog build).Server time (curl) 5–7 ms for `/`, `/movies`, `/movies/page/200`, `/shows`, `/shows/page/30`, `/my/movies`. Full loadin a FRESH Chrome per page (document + modules + socket + every poster in view), signed in / out:| page | 390 in / out | 1280 in / out ||---|---|---|| `/` | 1.11 / 1.34 s | 1.13 / 1.29 s || `/movies` | 0.30 / 0.91 s | 0.57 / 0.57 s || `/movies/page/2` | 0.53 / 0.63 s | 1.31 / 0.50 s || `/shows` (24 real posters) | 1.07 / 1.36 s | 1.37 / 1.11 s || `/my/movies` | 0.80 / 0.66 s | 0.83 / 0.58 s |Same Chrome (`node tests/realdata-check.mjs`): `/movies`, `/shows`, `/my/movies` 0.04–0.45 s; a page change by thepagination 60–75 ms (client-side); Back works; no sideways scroll; 0 console problems. `refreshCatalogShow` for all126 followed shows: 0.31 s (SNL 23 ms). Screenshots `/tmp/w050/real/*-{390,1280}.png` (looked at). Not mine but seen:`/my/unwatched` 1.47–1.64 s (1594 rows), `/my/shows` 1.25 s cold at 390.**Gate** (111 → 135 checks):```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(in-app run 5.415 s; 11 pages asked during it, slowest 1433 ms)135 passed, 0 failed```**Session-sync double rows** (team lead: since hybriel 64527baa every face taking `session` also syncs the members derivedfrom session; a handler that ALSO appends the returned row shows it twice): checked — no tracker handler appends, everyone assigns the whole list (`rows = r.rows`) or a flag; the new #13 pages have no faces. 4 gate checks "row appearsonce" (exact count + unique button values, 400 ms after the face for a late sync): /my/unwatched click, show pageexpand, episode click, Follow toggles.Changed existing checks: the three "page stays empty" (now home content), the in-app run expects `newEpisodes=2` (thefake TMDB adds a released episode to show 2 too, to prove the in-process re-sort). Screenshots `/tmp/w050/shots/`(home, movies, movies-page-2, shows-list, my-movies at 390/1280 — looked at).**Open**: (1) **adult titles** — the migrated movie table has many porn titles (e.g. on /movies page 1 and 4); theyare on the public homepage and /movies. No `adult` flag stored; the creator/architect decides (filter via TMDB `adult`,hide unfollowed movies, …). (2) Movie posters: only followed shows/movies have real posters → nearly every movie tile isthe placeholder until the sync covers more. (3) If `?page=N` links are wanted anyway, that needs hybriel#11's queryhalf. (4) Real-phone check after deploy.## 2026-10-01 — ticket #12 (mission 049): link icons to TMDB, IMDb, TheTVDB, TVmaze**Built**: show page header badges `TMDB` `IMDb` `TVDB` `TVmaze` (`components/show.hl` `linkRows`, `shows.hl``externalLinksOf`, `styles.hl` `showLinks`/`a.extlink`: CSS-only monochrome badges, accent on hover), only for ids theshow has, `target=_blank rel=noopener`; a movie: TMDB (`/movie/<id>`) + IMDb. Phone: between title row and poster;desktop: under the title. Sync (`tmdbsync.hl`): details request now `?append_to_response=external_ids` (no extrarequest) → fills a MISSING imdbId/tvdbId; still no tvmzId → TVmaze `lookup/shows?imdb=`, on 404 `?thetvdb=` (550 mspause each, `pauseMsAfter` in both runners); never overwrites. Totals: `ids=`, `tvmazeRequests=`. New`tools/count-external-ids.hl` (copy only).**Bug found + fixed**: movies (`type = 'movie'`, 7814 rows, 1 followed: Star Trek: First Contact, tmdbId 199) weresynced as `/tv/199` — another title. Now `/movie/<id>?append_to_response=external_ids`: imdbId + poster only.**Live data still carries the damage**: that record has `status = 'Ended'` and `tmdbSummary` = the overview of TVshow 199 ("Kabuhayang Swak na Swak…") — not visible (the page shows `summary`), the new sync leaves both alone; aone-off fix (set `tmdbSummary = null`, `status` back) is the architect's call.**Decisions**: TheTVDB link = `https://thetvdb.com/dereferrer/series/<id>` (it and `?tab=series&id=` both 301 to theslug page, checked 2026-10-01); IMDb `/title/<tt>/`; TVmaze `www.tvmaze.com/shows/<id>`. Badges with text, notdrawn pictograms (no logos copied; legible at 0.75rem). An imdb id is only taken as `tt`+digits, a tvdb/TVmaze idonly as a number > 0. A show TVmaze doesn't know is asked again each daily run (2 lookups/day on real data).**Real data** (copy of Byrodin `storage/` 2026-10-01 ~06:20, deleted afterwards; `tools/count-external-ids.hl`):| | shows | tmdbId | imdbId | tvdbId | tvmzId ||---|---|---|---|---|---|| followed series, before → after 1 sync | 125 | 125 | 125 | 125 | 123 → **124** || followed movie | 1 | 1 | 1 | – | – || all series (only followed are synced) | 1639 | 1639 | 907 | 910 | 845 → 846 || all movies | 7814 | 7814 | 379 | – | – |Sync: `shows=126 newSeasons=0 newEpisodes=0 updatedEpisodes=0 updatedSeasons=0 posters=1 ids=1 requests=256tvmazeRequests=4 errors=0 seconds=105.6` (poster = the movie's real one; id = Greek Salad tvmz 60613; Mapping theWorld: TVmaze knows neither tt0395050 nor tvdb 109331). TMDB's external ids match ALL 125 stored imdb/tvdb ids (0differ). Show pages (az5b2, :8730): Librarians 1.08 s cold / 0.13 s, Greek Salad 0.18 s, Star Trek: First Contact0.13 s (TMDB movie/199 + IMDb, real poster now); no sideways scroll, 0 console problems. Screenshots`/tmp/w049/real/*-{390,1280}.png` (looked at). Procedure = ticket #8 below (sync tool with `.env` sourced).**Gate** (101 → 111 checks):```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(in-app run 5.416 s; 12 pages asked during it, slowest 1323 ms)111 passed, 0 failed```Fixture: show 1 imdb `tt0000001` only (TMDB says tt9999999 + tvdb 4001), show 2 tvdb `4002` only (TMDB: tt0000002 +4999), movie `gate-test-movie` (tmdb 1003, followed by a 2nd user so alice's lists are unchanged → the sync now has3 shows). Fake TVmaze: imdb tt0000001 → 501, thetvdb 4002 → 502. Screenshots `/tmp/w049/shots/` (show-*, synced-show{,-390}, synced-movie{,-390} looked at). The new-tab check closes the tab at once (it would load the real site).**Open**: real-phone tap test after deploy (new tab). Star Trek: First Contact's wrong `tmdbSummary`/`status`(above). Unfollowed shows never get ids filled (sync covers followed only, by design).## 2026-10-01 — mission 047: re-vendor Hybriel (invisible checks), mobile first, show page additions**Bug** (live, the creator: "i dont see the check marks"): after a CLIENT-SIDE navigation (and after expand/collapseor a check click) the check `<svg>` was built in the XHTML namespace (circle width 0). Fixed by **re-vendoring Hybrielmaster ff51cf46** (bin sha256 `55a724a0…c5`, README "Vendored Hybriel"; old bin/plugins in `.scratch/pre-047/`).Proof the new check fails on the old build: `/tmp/w047/old-build-gate.log` (`ns: http://www.w3.org/1999/xhtml,circleWidth: 0`), passes on the new one.**Built**: `styles.hl` mobile first (phone base rules, one `@media (min-width: 40rem)` block; header one row at390 px; /my/unwatched + /my/schedule rows two-line on a phone). Show page: phone order title (+ Follow) → full-widthposter → genres/plot/cast (desktop unchanged); season caret (down/right); episodes not indented; artists orange`#ce9178`; unwatched check = accent outline circle + accent check; **Follow toggle** (face `showToggleFollow`,`follows.hl` `isFollowing`/`setFollowed`: new record `at = now`, unfollow deletes); signed out checks + Follow visible,a click opens the sign-in modal (`components/modal.hl`, copied verbatim from components.hybriel becbd59), nothing sent.Session-sync workaround in show/myshows/unwatched/schedule: `rows` reads `me` (a plain copy of `userId`), and`following` goes through a LOCAL static `followingOf` — an imported mpackdb helper called directly in a memberinitializer came back false after a client-side navigation (gate caught it; not reduced to a minimal repro yet).**Duplicate @ids** (hybriel#113) on a copy of live storage (2026-10-01 04:13), `tools/count-duplicate-ids.hl` + the`*.id.txt` indexes: **0 in every table** (episodes 232384, follows 128, genres 27, persons 15157, seasons 6450, shows9453, users 1, watches 11663 rows).**Real data** (copy, `az5b2`, :8730, `node tests/realdata-check.mjs`, full load incl. socket + posters):| page | 390 px | 1280 px ||---|---|---|| `/my/shows` (128) | 1.29 s (cold posters) | 0.24 s || `/my/unwatched` (1594) | 1.51 s | 1.46 s || `/my/schedule` (54) | 0.35 s | 0.32 s || `/shows/The-Librarians-The-Next-Chapter` direct / client-side | 0.14 / 0.19 s | 0.13 / 0.18 s |Client-side nav, expand, collapse: 14/26/14 checks, 0 wrong namespace, 0 undrawn; header 46 px at 390, no sidewaysscroll. Screenshots `/tmp/w047/real/*.png` (looked at). Procedure = ticket #8 below; the session id goes in`.scratch/real-sid` (the script reads it); copy + sessions deleted afterwards.**Gate** (76 → 101 checks):```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.mjs101 passed, 0 failed```Screenshots `/tmp/w047/shots/` ({home,show,my-*}-{390,1280}.png, signed-out-modal-1280.png) — looked at.Gate note: the follow test re-follows show 1, so the sync's request order between the two shows may flip (the checkaccepts both orders) and `/my/shows` after the sync has show 1 on top.**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.The unwatched (outline + check) and watched (filled) icons differ only by fill — as asked. No navigation links tothe `/my/` pages yet. Real-phone check after deploy.## 2026-10-01 — ticket #10: installable app (PWA) + icons**Built**: `icons/` (`icon.svg` source → `icon-192.png`, `icon-512.png`, `apple-touch-icon.png` 180; `favicon.svg`,`favicon.ico` 16/32/48 — rsvg-convert + magick, commands in README "What it does (step 10)"); `project.hl`:`appIcons` (192/512 × any/maskable, like calendar), `appTouchIcon`, `appFavicon`, `appThemeColor = darker.value`,`appBackgroundColor = dark.value` (tokens), `offline = [ Home ]`, file routes for the icons, `/favicon.ico` now areal file (was `direct = ""`); `components/main.hl` offline note + `netProbe` tick; `styles.hl` `offlineNote`,`netProbe`, `@keyframes tracker-net-tick`; gate +12 checks.**Decisions**: icon = a TV (antennas) with a check mark on the screen = "watched shows", green `#4ec9b0` onrgb(25,30,35), all inside the maskable safe zone so one PNG serves any + maskable. Theme colour = `darker` (the headerbar, so the phone's status bar blends in), background = `dark`. Offline keeps only the shell/`/`; data pages gethl:web's "Unavailable offline". The offline note is driven by `navigator.onLine`, read on a 1 s CSS-animation tick(`animationiteration`) — hl:web has no connection-state member and no mount hook (reported as a Hybriel gap).**Gate** (64 → 76 checks):```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.mjs76 passed, 0 failed```Looked at: `/tmp/w045/icon-48.png`, `icon-192.png`, `icon-512.png`, `maskable-circle.png`, `small-on-white.png`(favicon 16/32/48), `/tmp/w045/shots/pwa-phone-{online,offline}.png` (390 px) on Loreana.Test gotcha: CDP `setOffline` only cuts the TAB — the service worker's own fetches still reach the server, so thegate also stops the server before the offline reload.**Open**: offline, the header's `<ident-selector>` (script from ident's origin, not cached) shows "choose ident"next to "Log out". No note when the device is online but the server is down. Real-phone install test (ticket's**Test**) after deploy: open https://tracker.worldapi.org → "Add to home screen".## 2026-10-01 — ticket #9: TMDB sync (new episodes + posters)**Built**: `tmdbsync.hl` (`syncShow`: details + ≤ 20 seasons per request via `append_to_response`, add/updateseasons+episodes, poster w342), `tools/sync-tmdb.hl` (`--limit N`, paced, prints counts), the daily run in`project.hl` (`syncTick` on an hl:time `every(1)` clock: at `TRACKER_SYNC_HOUR` UTC (4), once a day, ONE showper tick, next show after n × 260 ms), `follows.hl` `allFollowedShowIds`, fake TMDB `tests/faketmdb.mjs`,fixture shows got tmdbIds 1001/1002. Details: README "What it does (step 9)".**Decisions**: match episode by tmdbId, else by S/E when the local one has no tmdbId (161 migrated); season bynumber. Empty TMDB values never overwrite. Season 0 only if the show already has one (else specials flood/my/unwatched); empty TMDB seasons not created. Own 16-hex ids for new rows (hybriel#113). Posters in`storage/mpackdb/posters/` (the folder the existing `/posters/:name` route reads — inside `storage/`, so in thebackup) rather than a new `storage/posters/`. Shows synced = all followed (also Ended ones — updates happen).Sync is off without `TMDB_READ_TOKEN` or with `TRACKER_SYNC=0`.**Blocking**: `fetch()` blocks the whole hl:web process (measured: requests wait for the running step) — so therun is cut into one-show steps. Real data: ~0.7 s per step average, SNL (53 seasons, 4 requests) 1.7 s; persistof the 3 tables 36 ms.**Real-data proof** (copy of Byrodin's `storage/` in `.scratch/realdata/`, deleted afterwards; procedure asticket #8 below; session file needs `created`/`seen` = now in ms, else the boot sweep deletes it):```set -a; . ./.env; set +aTRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb ./bin/hybriel tools/sync-tmdb.hl --limit 128tmdb sync done: shows=126 newSeasons=20 newEpisodes=800 updatedEpisodes=2651 updatedSeasons=174 posters=125 requests=380 errors=0 seconds=185.718(again) shows=126 newSeasons=0 newEpisodes=0 updatedEpisodes=0 updatedSeasons=0 posters=0 requests=255 errors=0 seconds=89.888```Fresh-process recount: seasons 6430 → 6450, episodes 231584 → 232384 (exactly +20/+800, no row loss), 0dangling season/episode refs, 0 duplicate season numbers; 12 duplicate tmdbIds + 1 wrong parent were already inthe migrated data (same before/after). 126 shows (128 follows), 125 posters (one show has none on TMDB).In-app daily run on the copy (TRACKER_SYNC_HOUR = current hour, real TMDB): 126 shows, 255 requests, 136 s,0 errors; `/my/shows` (signed in) polled every 0.5 s meanwhile: 181 answers, avg 0.25 s, max 4.0 s (one), then1.8/1.7/1.4 s. Full page loads after the sync (headless Chrome, load + socket + all images):| page | full load | rows ||---|---|---|| `/my/schedule` | 0.7–0.8 s | 55 (was 0) || `/my/shows` | 1.3–1.55 s (128 real posters now, 1 placeholder) | 128 || `/my/unwatched` | 1.5 s (server 1.3 s) | 1593 (was 831) || `/shows/The-Librarians-The-Next-Chapter` | 0.33 s | poster + new S2 (12 eps) |Screenshots + logs: `/tmp/w044-real/` on Loreana (`after-*.png`, `real-sync{1,2}.log`, `real-lat.txt`).**Gate** (49 → 64 checks):```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(in-app run 4.405 s; 10 pages asked during it, slowest 1371 ms)64 passed, 0 failed```Screenshots `/tmp/w044-shots/` (`synced-*.png` looked at: posters, S3, schedule rows fine).**Deploy (architect)**: `TMDB_READ_TOKEN` must be in Byrodin's `.env` (env_file) — without it the log says`tmdb sync: off (no TMDB_READ_TOKEN)`. First daily run 04:00 UTC after deploy; check `docker logstracker.worldapi.org | grep tmdb`. Or stop the container and run the tool once for an immediate catch-up.**Open**: a step blocks requests up to ~2 s (a non-blocking `fetchStart` + `done()` state machine would fix it).`/my/unwatched` render grew with the rows (1.3 s server, 1593 rows) — watch it against the 2 s target. A seasonwatch record stays solid when the sync adds a new episode to that season (existing season-watch semantics).Dev servers get the real token from `.env` automatically → start them with `TRACKER_SYNC=0`. Shows nobodyfollows are never synced (by design).## 2026-09-30 — ticket #8: /my/ pages, S01E01, years, speed**Built**: `/my/unwatched`, `/my/schedule` (routes in `project.hl`); old `/unwatched`, `/schedule` →**301** (function routes `unwatchedMoved`/`scheduleMoved`). Rows as `S01E02 · <title>` (was`Ep 2 · …`), list titles with the year `Doctor Who (2005)`, show page `1 episode` singular.New shared helpers in `shows.hl`: `pad2`, `episodeCode`, `showYear`, `titleWithYear`,`posterUrlOf`; in `watches.hl`: `watchedSet` (used by `/my/unwatched` and the show page's`buildRows`; `isWatched` stays for the single-target toggles).**Decisions**: year = `year`, else `release.slice(0,4)`, else none (197 shows lack `year`, somehave `release = ''`). Year only in LISTS (`/my/*`), not in the show page's `h1`. The 301 drops anyquery (the pages take none). A row whose poster FILE is missing gets `/posters/none` (one cachedURL) instead of `/posters/<oldId>.jpg`.**Why /my/shows was ~25 s live though its data is 0.2 s**: hl:web handles ONE request at a time.Measured on the copy: `/my/shows` requested 1 s into a `/unwatched` render took 13.7 s (the`/unwatched` render itself 14.5 s). Every request (pages, 128 poster requests) of every visitorqueued behind a slow `/unwatched`. Plus 128 distinct poster URLs per load, all the placeholder.**Real-data check** (copy of Byrodin's `storage/` only, `az5b2` = user `0mujqspn0aqb`, devserver :8730, headless Chrome, navigate → load → websocket open, 1280×900):| page | before | after ||---|---|---|| `/my/shows` (128 rows) | 1.2 s (server 0.19 s) | 0.6–0.8 s, 1 poster request || `/unwatched` → `/my/unwatched` (831 rows) | 14.8 s | 0.7–1.2 s (server 0.55–0.65 s) || `/schedule` → `/my/schedule` (0 rows, data not synced since 2025-12) | 0.36 s | 0.3 s || `/shows/The-Librarians-The-Next-Chapter` | 0.34 s | 0.29 s |How to repeat (Loreana; never touch Byrodin's folder beyond the tar read):```ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata # ONLY storage/TRACKER_PORT=8730 TRACKER_WATCH=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ ./bin/hybriel project.hl# session file: name = sha256(<32-hex id>), JSON {"created":1,"data":{"tag":"t"},"id":"<id>","seen":1,"user":{"id":"0mujqspn0aqb"}}curl -s -o /dev/null -w '%{time_total}\n' -b trackersid=<id> http://127.0.0.1:8730/my/unwatched# Chrome: tests/cdp.mjs launchBrowser, Network.setCookie trackersid, goto + wait __hl.socket open# afterwards: stop the server, rm -rf .scratch/realdata .scratch/realdata-sessions```Screenshots: `/tmp/w041-real/{before,after}-{shows,unwatched,schedule,show}.png` (Loreana).**Gate** (45 → 49 checks: two 301s, the browser landing on `/my/unwatched`, "1 episode"; theexisting ones now expect `/my/…`, `S01E02 · Second`, `S02E01 · Season Two Opener`, titles withyear, `/posters/sh1.png` + `/posters/none`). Fixture: show 1 `image = 'png'` (gate writes a 1×1`posters/sh1.png`), show 2 `year = null`, `release = '2025-03-01'`.```TRACKER_GATE_SHOTS=/tmp/w041-shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs49 passed, 0 failed```Screenshots `/tmp/w041-shots/` (looked at: all fine).**Open**: `/my/unwatched` rebuilds all rows per check click (~0.6 s, fine). Still no navigationto the `/my/` pages. The single-request server means any future slow page blocks everyone —keep every page well under 1 s. `/my/schedule` stays empty until the TMDB sync step.## 2026-09-30 — ticket #7: /my/showsBuilt `/my/shows` (`components/myshows.hl`, route in `project.hl`, styles in `styles.hl`): everyfollowed show, newest follow (`follows.at`, epoch ms) first — small poster (2.5rem, `/posters/…` +placeholder like the show page), title → `/shows/<slug>`, last watched episode `SxxEyy`.**Decisions**: "last episode" = highest (season, episode) among the user's EPISODE watches of thatshow (ticket); a season watch alone doesn't count (the season check watches every episode anyway).No watch → no episode element. A show without a poster name → `/posters/none` (the route's ownplaceholder), so every row has a poster. Read-only page, no face (like `/schedule`).**Reuse**: new `follows.hl` `followsOfUser` (whole records; `followedShowIds` now builds on it) and`shows.hl` `episodeById` (one line); `showById`, `posterName`, `watchesOfUser` unchanged.**Performance** (against a COPY of `storage/mpackdb/` in `.scratch/perf-store`, removed after; livefiles md5-verified unchanged): the page touches only the user's follows and episode watches(one fetch per watch), never all episodes of a followed show — az5b2's 128 follows → 128 rows (123with an episode, e.g. `S36E11`, `S30E91`) built in **160–255 ms**. For comparison **`/unwatched`takes ~15 s** on the same copy (805 rows): it fetches every episode of every followed show andchecks each against the user's 11,692 watches with a linear `isWatched` — not in this ticket'sscope, flagged below.**Gate** (`tests/browser.mjs`, 38 → 45 checks; `tests/seed-show.hl` adds a 2nd show "Unwatched GateShow", no seasons/poster name, followed AFTER the first but stored after it; the first follow is nowdated a day back): signed out → sign-in message; signed in → newest follow first, title hrefs,posters loaded (`naturalWidth > 0`), 40px wide, `S01E02` after /unwatched's click, later `S02E01`(watched BEFORE season 1's bulk watch → proves highest, not most recent), none for the other show.```TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs45 passed, 0 failed```**Open**: `/unwatched` ~15 s on real data (above) — a watched-id map instead of `isWatched` perepisode would fix it (own ticket). No navigation links to `/my/shows` yet (homepage stays empty).## 2026-09-27 — ticket #6: /scheduleBuilt `/schedule` (`components/schedule.hl`): every episode of a show the signed-in user followsthat is NOT yet released (`e.release > today`), soonest first. Creator: "/schedule has a similarlist without check icons for the upcoming episodes of shows followed" — same row shape as`/unwatched` (`components/unwatched.hl`) minus the check icon.**Reuse, not copy**: imports the exact same `followedShowIds` (`follows.hl`), `showById` /`seasonsOfShow` / `episodesOfSeason` (`shows.hl`) that `/unwatched` already uses — nothing newadded to either file. `watches.hl` is not imported at all: a row here is never watched/unwatched,there is no check icon and no click, so the page needed no per-user watch state and no `on server`face — the whole component is a single read-only server-side render (`rows` computed once from`session`, same as every other reactive member here).**Order**: soonest first, ascending by `release` (a plain `'YYYY-MM-DD'` string, orderingoperators confirmed working against this app's own vendored `bin/hybriel`, same as `/unwatched`'sSTATUS entry) — the ticket named this the architect's own reading ("the creator named the orderonly for /unwatched"), so `insertByReleaseAsc` mirrors `/unwatched`'s `insertByReleaseDesc` withthe comparison flipped.**`decided`**: "upcoming" = `release` strictly after today (not `>=`) — the natural complement of`/unwatched`'s own `release <= today`, so every released-or-today episode is unambiguously on onelist, never both, never neither.**Gate** (`tests/browser.mjs`, 35 → 38 checks): reused the same fixture (`tests/seed-show.hl`,unchanged) — episode 3 already carried a far-future release (`2099-01-01`) for `/unwatched`'s own"excludes the future one" check; `/schedule` now proves the mirror image with the same fixture:signed out, no rows, the sign-in message; signed in, only that one future episode shows (the twoalready-released ones excluded), no `button.checkicon` anywhere on the page, unaffected by any ofthe watch toggles the other checks perform on the same fixture (run in any order — `/schedule`never reads watch state).```node tests/browser.mjs38 passed, 0 failedps -eo pid,args | grep [h]l-browser-tier(nothing)```Updated `README.md` ("What it does (step 6)", Test, Files) and this file.**Open**: nothing shown on the homepage still points to `/schedule` (same as `/unwatched` beforeit — the homepage stays empty per ticket #1's decision).## 2026-09-27 — ticket #5: /unwatchedBuilt `/unwatched`: every episode of a show the signed-in user follows that is already released(`release` ≤ today, a plain `'YYYY-MM-DD'` string — migrated as-is by `tools/migrate.hl`, neverrun through `dateOf`) and not yet watched, newest release first (creator: "/unwatched lists allunwatched episodes of shows followed in release date desc order").**Reuse, not copy** (the ticket's own instruction, re "the same check icon and watch logic as theshow page"): `watchClassOf` (watched → `'checkicon solid'`/`'checkicon regular'`) was a private`static` in `components/show.hl`; moved it to `watches.hl` so both pages import the SAMEdefinition instead of each declaring their own — `show.hl` now imports it too, nothing duplicated.`isWatched`/`setWatched` (already in `watches.hl`) needed no change. New `follows.hl`(`followedShowIds`) and `shows.hl` `showById` (one line) round out the read-only data access thispage and `/schedule` (ticket #6) both need.**String comparison** (`e.release <= today`, `row.release >= x.release` for the desc sort):hybriel#2 ("no ordering operators on strings") is filed as still-open in this ticket's brief, butits own history shows it was fixed and merged to master 2026-09-25 — confirmed directly againstTHIS app's own vendored `bin/hybriel` (`'2020-09-03' <= '2026-09-27'` → `true`), so no numericdate workaround was needed.**A View text node cannot hold an inline expression** (`"Ep " + row.episodeNumber + " · " +row.title` inside a tag body) — same rule as View attributes (components/show.hl's own comment:"a View attribute must be a literal, a member or a field path"), just not documented for textnodes anywhere yet; it silently rendered `<!-- binary_expression not rendered yet -->` instead oferroring. Fixed by resolving the label into a plain field (`row.episodeLabel`) once, server-side,in `unwatchedRowsOf` — the same pattern `show.hl`'s `buildRows` already uses for every otherternary/concatenation. Not filed as a new Hybriel gap since it's the exact same restrictionalready known and worked around elsewhere in this app, just not yet hit for interpolated text.**Empty state**: signed out (no user, so no follows to show) and signed in with nothing unwatchedboth render a message; used `.muted` (colorTextMuted), not `.message` (colorDanger) — the existing`.message` class is for real errors (the login-failed banner), and an empty list isn't one (a`decided`, small).**Gate** (`tests/browser.mjs`, 29 → 35 checks): `tests/seed-show.hl` now takes an optional`TRACKER_SEED_IDENTITY` and, when given, seeds a fixed user + a follow of the fixture show for it— the only way to give `/unwatched` a known followed show before the browser ever signs in, sincethe real login's `ensureUser` matches an EXISTING `users.db` row by `identity` and reuses its idrather than minting a new one. The gate learns alice's per-app identity with one throwaway`ident.exchange` call (consuming one code) before writing the seed. Episode 1/2 of the fixture nowcarry a past `release` (`2020-01-01`/`2020-06-15`), episode 3 a far-future one (`2099-01-01`) —proving both the release-date filter and the desc order. The new checks run signed out (no rows,the sign-in message), then signed in RIGHT AFTER LOGIN, BEFORE the existing show-page checks thattoggle these same fixture episodes watched (order matters — otherwise `/unwatched` would alreadybe empty by the time it's tested): both unwatched episodes list newest-first, excluding thefuture one; a click on the newest (`Second`, ep 2) removes it from the list; a reload proves thewatch is server-side, leaving only `Pilot` (ep 1).```node tests/browser.mjs35 passed, 0 failedps -eo pid,args | grep [h]l-browser-tier(nothing)```Updated `README.md` ("What it does (step 5)", Test, Files) and this file.**Open**: nothing shown on the homepage still points to `/unwatched` — no navigation exists yet(the homepage stays empty per ticket #1's decision; a later step, from the creator, presumablyadds it). `/schedule` (ticket #6) is next and reuses `follows.hl`/`shows.hl`/`watches.hl` the sameway.## 2026-09-27 — ticket #4: the show page, watch checks (took over a previous half-done session)Continued from an earlier session that had built the whole page — header, seasons/episodes,the click-handling redesign to a flat `rows` list rebuilt server-side (`components/show.hl`'sown header comment explains why: a nested `for` over seasons > episodes risks hybriel#86) — butleft with the gate red: clicking a check never turned it solid, no console error, and thesession ran out of budget before finding why.**The actual bug**: the show page's own pure helpers (`buildRows`, `genreRowsOf`, `castRowsOf`,`watchClassOf`, `initialCollapsed`) were plain instance members (`buildRows = (show, …) => {…}`,no `static`). That shape works fine as a member's own top-level initializer (`rows = found ?buildRows(showRow, watchRows, collapsed) : []` — this is how the page renders correctly on firstload) but throws when the SAME function is called from inside an `on server` face(`showToggleEpisode` etc.): the server log showed `'buildRows' is not callable — it holds null`.Found by running `.scratch/debug-click.mjs` (left by the previous session) with the server'sstdout/stderr captured — the browser console itself stayed silent (the face's error neverreaches the client at all, a separate Hybriel gap, hybriel#92 already covers that half).Confirmed the fix by grepping other worldapi apps for the same shape:`calendar.worldapi.org/components/calendar.hl` has `static soonOf = (u) => { … upcoming(u, …) }`(an mpackdb-backed helper), called from its `on server calLoad`/`calSettingsLoad` faces, andcalendar's own gate is green — so `static` is the proven-safe shape for a component's own helperthat both (a) touches an mpackdb-backed import and (b) needs calling from more than one place(its own initializer AND a later face). Declared all five helpers in `show.hl` `static`; reranthe gate: 29/29 green, including the season-bulk-toggle and the reload-proves-server-side checks.Filed **hybriel#115 was already open** (from the previous session, about the CLIENT bundleomitting a case for such a helper) but this session's finding is the SAME root cause reachingfurther than that ticket says: the plain HL interpreter on the SERVER also treats such a helperas `null` outside its own initializer, not just the JS-compiled client bundle. Left as one`issues` entry pointing at hybriel#115 rather than opening a near-duplicate ticket — therepro/observed there already generalizes (a per-instance member function wrapping anmpackdb-backed import is unreliable wherever it is called from, not just the browser).Ran `node tests/browser.mjs`: 29 passed, 0 failed (was 20 passed / 1 failed — the click timeout— before the `static` fix).**Rehearsed `tools/relink-episode-seasons.hl` against a full COPY** of the real`storage/mpackdb/` (`.scratch/relink-rehearsal/`, since removed) — the tool's own header claimedthis rehearsal already existed here; it didn't (a leftover claim from the previous session), sothis session actually ran it:```relink: episodes 231584, null season before 36194, linked 36194, still null (no matching season) 0relink: episodes.db replaced with the relinked file — re-run to confirm idempotency (0 linked the second time)```Ran a second time on the same copy straight after — idempotency confirmed:```relink: episodes 231584, null season before 0, linked 0, still null (no matching season) 0```All 36,194 pre-existing null-season episodes (ticket #2's count) now link to a real season by`(show, seasonNumber)`; none left over. Not run against the LIVE `storage/mpackdb/` — that needsthe tracker container stopped first (hl:mpackdb does not coordinate two processes on one storagedirectory, hybriel#21), the architect's job per the ticket ("the architect runs it live").Updated README.md ("What it does (step 4)", Test, Files) and this file.## 2026-09-27 — ticket #2: old data migrated (took over a previous session's rewritten tool)Continued from an earlier session that had rewritten `tools/migrate.hl` to avoid a real data-lossbug (see below) but never run it even once, and had left the real `storage/mpackdb/` holding datafrom the OLDER, buggy two-pass version of the tool (put a bare record, then `update()` it once theother side of a circular reference existed). Took over: moved that buggy data aside(`.scratch/pre-migrate-buggy-backup`, since removed), kept `storage/mpackdb/users.db` (step 1'slogin table, untouched by the migration), and ran the already-fixed tool for the first time.**The fix already on disk (kept as is):** `Show.seasons`/`Season.show` and `Show.cast`/`Person.shows` are two-way references, so the id on one side must exist before the record on theother side can be built. The old approach — `put()` a bare record, come back with `update()` oncethe far side's id exists — loses rows: reopening a table in a fresh process after an `update()` hadtouched it read back FEWER rows than were ever `put()` (seen directly: `persons.db`'s `count()`dropped 15157 → 15152 after one round of `update()` — a real `hl:mpackdb` bug, reported to Hybriel,not something an app is allowed to patch around). The fix assigns every new id itself (8 randombytes, `hl:crypto`) in one pass over each export file BEFORE building any record, so by the time arecord is actually built every reference is already a known id — one `put()` per row, `update()`never called.**Run** (`tools/migrate.hl`, `TRACKER_OLD_DATA=.../old-tracker/mongo-export`,`TRACKER_STORAGE=$PWD/storage/mpackdb`):```migrate: genres 27 new, 0 already there (old 27 -> new 27)migrate: persons 15157 new, 0 already there (old 15157 -> new 15157), show refs skipped 0migrate: shows 9453 new, 0 already there (old 9453 -> new 9453), cast refs skipped 0, genre refs skipped 0, season refs skipped 0migrate: seasons 6430 new, 0 already there (old 6430 -> new 6430), show refs skipped 0, episode refs skipped 0migrate: episodes 231584 new, 0 already there (old 231584 -> new 231584), show refs skipped 0, season refs left null (pre-existing) 1migrate: follows 128 new, 0 already there (old 128 -> new 128), show refs skipped 0migrate: watches 11692 new, 0 already there (old 11692 -> new 11692), target refs left null (pre-existing) 3migrate: 0 failed```Every count equals the old export's own document count (`old-tracker/README-RECONSTRUCTED.md`:Show 9453, Season 6430, Episode 231584, Person 15157, Genre 27, Follow 128, Watch 11692). Ran asecond time straight after (idempotency: the tool's `oldId` index finds each row again) — everytable printed `0 new`, all rows `already there`, counts unchanged, 0 failed: the id-loss bug doesnot resurface across a real close-and-reopen.**Proof beyond the tool's own counters** (`tools/verify.hl`, new): the ticket asks for a check thatdoes not just trust `migrate.hl`'s own "skipped" tallies. It runs against a plain filesystem COPYof `storage/mpackdb/` (hl:mpackdb rewrites a file's data on open, so the live store is never openeda second time) and independently re-derives, from the persisted records alone: every table's`count()` against the export's own document count, and — by building id sets for every table andwalking every reference field (`Show.genres/cast/seasons`, `Season.show/episodes`,`Episode.show/season`, `Person.shows`, `Follow.show/user`, `Watch.target/user`) — that everynon-null reference resolves to a real id. It also follows one show end to end (`Raised by Wolves`,2 seasons, 18 episodes, 1 follow, 2 season-level watches — all cross-checked back to the show).```$ cp -r storage/mpackdb .scratch/verify-copy && TRACKER_VERIFY_COPY=$PWD/.scratch/verify-copy ./bin/hybriel tools/verify.hlcount ok genres: 27 (expected 27)count ok persons: 15157 (expected 15157)count ok shows: 9453 (expected 9453)count ok seasons: 6430 (expected 6430)count ok episodes: 231584 (expected 231584)count ok follows: 128 (expected 128)count ok watches: 11692 (expected 11692)users: 1 (expected 1, the creator)dangling references: 0 (episodes with a pre-existing null season: 36194, watches with a pre-existing null target: 3 — not dangling, the export already had no target)END-TO-END show 105ad9c91b14d663 "Raised by Wolves" seasons=2season 1 (be2965a5ab7165ca) episodes=10 show-back-ref-ok=trueseason 2 (0fe4e53157ed0367) episodes=8 show-back-ref-ok=truetotal episodes across seasons: 18follows on this show: 1, season-level watches touching it: 2VERIFY PASS```(36194 episodes with no season = 36193 that never had one in the old export, plus the 1 the tool'sown count already named as a pre-existing dangling `season` reference in the source data — neitheris a reference this migration broke; `dangling references: 0` covers exactly that.)The single old user (`User.jsonl`, `[email protected]`, password not taken over) is this app'suser for ident short id `az5b2` (`storage/mpackdb/users.db`, the same table step 1's login uses) —confirmed in the copy: one record, `identity=az5b2`.**Two Hybriel bugs found while building this** (both already filed as issues on this ticket by anearlier session, not re-filed): `hl:fs readFile` silently truncates at 10 MiB (worked around herewith `split`, no shell, no JS, see `tools/migrate.hl`'s header comment) and `hl:mpackdb` loses rowsacross `update()` + reopen (the reason this tool never calls `update()`).**Open**: nothing shown yet — this ticket is data-only, on purpose (the next step, from thecreator, is showing the data in the UI).## 2026-09-27 — ticket #3: no border on the header or filled buttonsDesign notes from the first test (architect): "the application-header should have no bottomborder" and "the buttons also no border except they are inverted". Two changes in`styles.hl`:- `applicationHeader`: dropped `borderBottom`.- `ident-selector::part(button)` (the signed-out "choose ident" button): added`border = 'none'` — the element's own CSS gave it a 1px border the same colour as itsbackground (`--_accent`), invisible but still a real border in the computed style; this isthe app-side restyling hook ident's README documents (`::part(button)`), not a change toident's vendored `selector.js`.Left unchanged, already correct: the native `button` rule (filled, e.g. no other filledbuttons on this page yet) already has `border = '0'`; `button.quiet` ("Log out") and theselector's dropdown `[part="identity"]` rows keep their border (transparent background —inverted style); `a.button` ("Log in with ident") has no border rule and browsers give `<a>`none by default. The selector's signed-in "logged in with ident" status pill(`::part(status)`) is not one of the buttons the ticket names and isn't a button element(a `<span>`) — left with its border.**Gate** (`tests/browser.mjs`, now 16 checks): added computed-style checks — the header's`borderBottomWidth` is `0px`; `#loginbutton`'s border is `0px`; the selector's`[part~="button"]` (inside its shadow root) border is `0px`; `#logout`'s border is NOT `0px`(still inverted-bordered) once signed in.```node tests/browser.mjs16 passed, 0 failed```## 2026-09-27 — ticket #1 reopened: login redone to match calendar exactly (header selector, empty page)The architect rejected the first build of ticket #1: the sign-in sat centered on the page(its own "Sign in with ident" card, identity + sign-out shown in a `homeCard`) instead ofthe header identity selector calendar/gitoria use, and there was no `<ident-selector>` atall. Rebuilt the login by copying calendar.worldapi.org's own files (unchanged in shape, perthe architect's instruction "Copy calendar.worldapi.org's login unchanged"):- **`users.hl`** (new): the ident exchange + a `usersTable` (`storage/mpackdb/users.db`,`identity` → this app's user id), copied from calendar's `users.hl` with `TRACKER_KEY` /`TRACKER_SECRET` / `TRACKER_URL` / `TRACKER_STORAGE` in place of calendar's names (keptthe names already in this app's `docker-compose.yml`/README/DECISIONS rather thanswitching to calendar's generic `IDENT_API_KEY`/`IDENT_API_SECRET`).- **`login.js`** (new): calendar's bridge between `<ident-selector>`'s `ident-login` DOMevent and the hidden `#identcode` input, copied verbatim (creator: "login.js is correct,as thats for externals in general").- **`components/main.hl`**: now the header carries `userBox` (`<ident-selector>` + "Log inwith ident" / "Log out"), the `trackerLogin`/`trackerLogOut` faces and the`trackerSignedIn`/`trackerSignedOut` client push — calendar's shell renamed to this app'sevent names.- **`components/home.hl`**: now truly empty (`View { home {} }`) — no sign-in link, noidentity/sign-out duplicated on the page; that is entirely the header's job now.- **`components/loginfailed.hl`** (new) + **`project.hl`**: the login routes are now`/login/callback` (function route, the button's return AND the code exchange) and`/login/failed`, copied from calendar's `project.hl` (`safePath`, `failed()`,`loginCallback`) in place of the old `/login` + `/callback` + `/logout` routes; the old`/logout` POST route is gone (logout is a face, like calendar's).- **`styles.hl`**: added the header selector styles (`userBox`, `identSelector`,`ident-selector::part(...)`, sticky header) from calendar's `styles.hl`; dropped thenow-unused `homeCard`/`accountBar`/`userName` rules from the rejected centered sign-in.**Gate rewritten** (`tests/browser.mjs`, 12 checks): replaced the old gate (which droveident's full email-code UI through a vendored dev copy of ident at `.scratch/ident-dev`)with calendar's own pattern — `tests/identkit.mjs` (copied from calendar unchanged) starts athrowaway ident (a fresh copy of `/media/STORAGE/projects/ident.worldapi.org`'s code, no`.env`/storage copied, codes to a mail sink, never the live ident), signs in over its RESTAPI and registers this app, then the gate proves the header in a real headless Chrome:signed out → `#selector` + `#loginbutton` in the header, **`main` is empty** → sign in(`/login/callback?ident_code=`, the same exchange the selector would trigger) → `#selector`shows `class="in"`, `#logout` appears, **`main` still empty** → sign out → signed out again→ a reload stays signed out. No console errors.```node tests/browser.mjs12 passed, 0 failed````./deploy.sh --dry-run --target .scratch/deploy-preview --url http://127.0.0.1:0/`: gatepassed as step [1/4], rsync preview held none of `storage/`, `.sessions/`, `.env`,`.scratch/`, `server.*`, logs. Removed the stale `.scratch/ident-dev` (the old gate's vendoredident copy) and `.scratch/browser-gate` (the old gate's storage) — unused by the new gate.**Still open**: the app is registered in ident's dev copies only (this gate's own throwawayident). The LIVE ident registration (`TRACKER_KEY`/`TRACKER_SECRET` in `.env` next to`docker-compose.yml`) is the architect's first-deploy step (README "Deploy"), not built here.## 2026-09-27 — ticket #2: data migration — blocked twice on sandbox access, nothing doneTwo workers in a row (`s-20260927T1029-f57b0f`, then this session) could not start: themigration source the ticket names, `/media/STORAGE/projects/old-tracker/mongo-export/*.jsonl`+ its `README-RECONSTRUCTED.md`, is not reachable from this worker's sandbox. Verified freshthis session:```$ ls -la /media/STORAGE/projects/antcolony-docs hybriel ident.worldapi.org tracker.worldapi.org # no old-tracker$ mount | grep STORAGE/dev/nvme1n1p6 on /media/STORAGE/projects/tracker.worldapi.org type btrfs (rw,...)/dev/nvme1n1p6 on /media/STORAGE/projects/hybriel type btrfs (ro,...)/dev/nvme1n1p6 on /media/STORAGE/projects/ident.worldapi.org type btrfs (ro,...)/dev/nvme1n1p6 on /media/STORAGE/projects/antcolony-docs type btrfs (ro,...)# old-tracker is not among the mounted subvolumes at all```So this is not a data problem (the export is "complete" per the ticket) — it's the workersandbox for this ticket that never got `old-tracker` mounted (read-only, like `hybriel` /`ident.worldapi.org` / `antcolony-docs` above). Nothing was built or changed this session:no `storage/`, no `tools/`, no id-mapping — writing the migration tool blind, guessing theJSONL shape from the ticket text alone, risks silently wrong data and was avoided on purpose(see the project's "Build it properly" rule). Filed as an antcolony issue so the next worker'ssandbox includes `old-tracker` before another attempt.## 2026-09-27 — ticket #1: the empty shell, ident loginBuilt the app from scratch: vendored `bin/hybriel` + `plugins/` + `shared/tokens.hl` from`ident.worldapi.org` (newest local build, hybriel master 837fe120, no local patch — seeREADME "Vendored Hybriel"). `project.hl` (routes `/login`, `/callback`, `/logout`, `/`),`components/home.hl` (the empty homepage), `components/main.hl` (shell), `styles.hl`(accent green `#4ec9b0`, per `antcolony/README.md` "look and style" / `CONCEPT.md`).`docker-compose.yml` / `Dockerfile` / `deploy.sh` following ident's own house pattern(port 45008, container `tracker.worldapi.org`).**Login**: the ident login BUTTON flow (ident.worldapi.org README "How apps use ident"),server-side exchange (`POST <ident>/api/exchange`), this app's own framework session(`session.user = { identity }`, cookie `trackersid`). ident hands over only the identity'spublic **short id** — no display name yet (ident#11, properties handover, is on hold) — so"your name" on the homepage is `Signed in as <shortid>`, the same identifier every otherworldapi app would show; see `decided` in the report.**Lesson (Hybriel)**: a face that mutates `session.user` (e.g. `signOut`) does NOTautomatically re-render the page — a component's reactive members (`signedIn`, `identity`,…) are computed once at mount from the framework's read-only session snapshot; a clienthandler that calls `emit server X()` must ALSO flip the affected members itself afterwards(ident's own `components/home.hl` `on doSignOut(e)` does exactly this: `emit serversignOut()` then `signedIn = false` …). Missing that made the sign-out button silently donothing client-side (the ack came back `ok:true`, the server-side session really wascleared — proven by a reload — but the DOM never updated) until copied.**Gate** (`tests/browser.mjs`, 13 checks): a fresh copy of `ident.worldapi.org`'s code (no`.env`, no `storage/`, no `.sessions/` — verified empty of secrets/data before first use)runs as this gate's own ident, on its own storage and its own mail sink (no live ident, noreal mail). The gate registers this app in that dev ident once over the `/__hl/emit` API(the same one-time step a person does by hand in ident's `/apps` page), then drives theREAL login-button flow in a real headless Chrome: signed out → click "Sign in with ident" →ident's email form → the mail-sink code → the first-login optional-names form (Skip) → theone-identity choice (one click) → back on tracker, "Signed in as `<shortid>`" → "Sign out" →signed out again → a reload stays signed out. No console errors. Ran twice for stability, 0failures; no leftover Chrome/hybriel processes after either run.```node tests/browser.mjs13 passed, 0 failed````./deploy.sh --dry-run --target .scratch/deploy-preview --url http://127.0.0.1:0/` (a localdirectory, per ident's own convention for testing deploy.sh without touching Byrodin): gatepassed, rsync preview held none of `storage/`, `.sessions/`, `.env`, `.scratch/`, `server.*`,logs — confirmed by the same grep the script refuses on.**Open**: the app is not registered with the LIVE ident yet — `TRACKER_KEY`/`TRACKER_SECRET`are unset until the architect's first deploy does that (README "Deploy"); until then`/login` on the live site answers a plain error page instead of redirecting (`/` itselfstill renders). This is normal for a brand-new app, the same as ident's own SMTP `.env`before its first deploy — not something this ticket's worker can set (no `.env` access,and it is a LIVE ident registration).
Branches
- mainmain branch
Latest commits
- 0553b51ftracker#19 (mission 066): franchises and timelines — tables, /franchises, /franchises/<slug>, /timelines/<slug> (Timeline | Release sort, series by last episode), the Prequel | Timeline | Sequel widget with the franchise above, the creator's editor, TMDB collection seed in the app (resumes, paced); gate tests/franchises.mjs 48/0 + browser.mjs 266/0, tests/realdata-066.mjs, docs/franchises.md, README + STATUSmre
- fa1f9dfatracker#29 (mission 063): unwatched check muted grey outline + check (accent only on hover), watched stays solid — no code regression, the accent outline read as ticked; gate checks real checks visibly (computed style + screenshot pixel) on /my/unwatched, show, movie, /my/movies; gate 266, tests/realdata-063.mjs, README + STATUSmre
- d8b12d67tracker#27 (mission 060): short ids for movies, series and persons — old 702 kept (data/old-short-ids.json), new random [a-z0-9]{5} unique across both, claimed at creation, background backfill (resumes), shown under poster/photo, /<shortId> → 301; gate 259, tests/realdata-060*, README + STATUSmre
- f14db671tracker#22-#25 (mission 058): episode air dates, season check = all episodes watched, movie watched check (+ /my/movies count), /genres/<genre> pages (movies + series, newest first, paginated); gate 238, tests/realdata-058.mjs, README + STATUSmre
- 1704ec45tracker#17 (mission 057): season caret down/up, skeleton rows while a season loads, sessionless showSeasonEpisodes face (no page re-mount), client-only close; gate 214, tests/realdata-057.mjs, README + STATUSmre
- f2fe3e36mission 056: README + STATUS (merge, fixes, Hybriel 8590df63, real-data check), tests/realdata-056.mjs, tools/check-public-slugs.hlmre
- f40c250emission 056: re-vendor hybriel master 8590df63 (#121, #122); an adult title's page is Not found for non-followers; gate: leave the page before stopping the servermre
- 2b7fdd6cmission 056: signed-out header one row on phones ("Log in", nowrap), backfill skips adult titles' posters, gate checksmre
- 2c53d5efMerge branch 't16-person' (tracker#16 person pages) into main; filmography shows only public titles (054 adult flag), gate race fix (backfill start line)mre
- c171227emission 054: hide adult/unknown titles from the public lists and the search; in-app adult-flag backfill (TMDB details + poster per title, resumes), gate + real-data proofmre
- 139fafd8tracker#16: short bio (4 lines, click = all), real-data check script, README + STATUSmre
- 93be9476tracker#16: person pages /person/<slug> with the filmography fetched from TMDB on the first visit (step by step), gatemre
- 47a3cae6STATUS: mission 053 merge commit idsmre
- dcc5eecaMerge branch 't14-search'mre
- 03edc783Merge branch 't15-tvmaze'mre
- 71b46345tracker#15: numbering check by date or title, placeholder titles in other languages, docs + real-data proofmre
- 6bb2daf1tracker#13: homepage (tiles, intro, latest movies/shows), /shows, /movies/page/N, /my/movies; lists cached in memorymre
- b8bd1157tracker#14: README + STATUS (search, real-data numbers, gate, merge notes)mre
- 65c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre
- 34f2c15btracker#15: TVmaze merge in the sync (gaps only: new episodes/seasons, empty titles/air dates; numbering check), fake TVmaze episodes + gatemre