gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit2fa9d9972fa9d997tracker#9: TMDB sync (followed shows: seasons, episodes, posters), tools/sync-tmdb.hl + daily run 04:00 UTC, fake TMDB in gatemre2fa9d997/STATUS.md

36.3 KB

  1. # STATUS — tracker.worldapi.org
  2. ## 2026-10-01 — ticket #9: TMDB sync (new episodes + posters)
  3. **Built**: `tmdbsync.hl` (`syncShow`: details + ≤ 20 seasons per request via `append_to_response`, add/update
  4. seasons+episodes, poster w342), `tools/sync-tmdb.hl` (`--limit N`, paced, prints counts), the daily run in
  5. `project.hl` (`syncTick` on an hl:time `every(1)` clock: at `TRACKER_SYNC_HOUR` UTC (4), once a day, ONE show
  6. per tick, next show after n × 260 ms), `follows.hl` `allFollowedShowIds`, fake TMDB `tests/faketmdb.mjs`,
  7. fixture shows got tmdbIds 1001/1002. Details: README "What it does (step 9)".
  8. **Decisions**: match episode by tmdbId, else by S/E when the local one has no tmdbId (161 migrated); season by
  9. number. Empty TMDB values never overwrite. Season 0 only if the show already has one (else specials flood
  10. /my/unwatched); empty TMDB seasons not created. Own 16-hex ids for new rows (hybriel#113). Posters in
  11. `storage/mpackdb/posters/` (the folder the existing `/posters/:name` route reads — inside `storage/`, so in the
  12. backup) rather than a new `storage/posters/`. Shows synced = all followed (also Ended ones — updates happen).
  13. Sync is off without `TMDB_READ_TOKEN` or with `TRACKER_SYNC=0`.
  14. **Blocking**: `fetch()` blocks the whole hl:web process (measured: requests wait for the running step) — so the
  15. run is cut into one-show steps. Real data: ~0.7 s per step average, SNL (53 seasons, 4 requests) 1.7 s; persist
  16. of the 3 tables 36 ms.
  17. **Real-data proof** (copy of Byrodin's `storage/` in `.scratch/realdata/`, deleted afterwards; procedure as
  18. ticket #8 below; session file needs `created`/`seen` = now in ms, else the boot sweep deletes it):
  19. ```
  20. set -a; . ./.env; set +a
  21. TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb ./bin/hybriel tools/sync-tmdb.hl --limit 128
  22. tmdb sync done: shows=126 newSeasons=20 newEpisodes=800 updatedEpisodes=2651 updatedSeasons=174 posters=125 requests=380 errors=0 seconds=185.718
  23. (again) shows=126 newSeasons=0 newEpisodes=0 updatedEpisodes=0 updatedSeasons=0 posters=0 requests=255 errors=0 seconds=89.888
  24. ```
  25. Fresh-process recount: seasons 6430 → 6450, episodes 231584 → 232384 (exactly +20/+800, no row loss), 0
  26. dangling season/episode refs, 0 duplicate season numbers; 12 duplicate tmdbIds + 1 wrong parent were already in
  27. the migrated data (same before/after). 126 shows (128 follows), 125 posters (one show has none on TMDB).
  28. In-app daily run on the copy (TRACKER_SYNC_HOUR = current hour, real TMDB): 126 shows, 255 requests, 136 s,
  29. 0 errors; `/my/shows` (signed in) polled every 0.5 s meanwhile: 181 answers, avg 0.25 s, max 4.0 s (one), then
  30. 1.8/1.7/1.4 s. Full page loads after the sync (headless Chrome, load + socket + all images):
  31. | page | full load | rows |
  32. |---|---|---|
  33. | `/my/schedule` | 0.7–0.8 s | 55 (was 0) |
  34. | `/my/shows` | 1.3–1.55 s (128 real posters now, 1 placeholder) | 128 |
  35. | `/my/unwatched` | 1.5 s (server 1.3 s) | 1593 (was 831) |
  36. | `/shows/The-Librarians-The-Next-Chapter` | 0.33 s | poster + new S2 (12 eps) |
  37. Screenshots + logs: `/tmp/w044-real/` on Loreana (`after-*.png`, `real-sync{1,2}.log`, `real-lat.txt`).
  38. **Gate** (49 → 64 checks):
  39. ```
  40. 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
  41. (in-app run 4.405 s; 10 pages asked during it, slowest 1371 ms)
  42. 64 passed, 0 failed
  43. ```
  44. Screenshots `/tmp/w044-shots/` (`synced-*.png` looked at: posters, S3, schedule rows fine).
  45. **Deploy (architect)**: `TMDB_READ_TOKEN` must be in Byrodin's `.env` (env_file) — without it the log says
  46. `tmdb sync: off (no TMDB_READ_TOKEN)`. First daily run 04:00 UTC after deploy; check `docker logs
  47. tracker.worldapi.org | grep tmdb`. Or stop the container and run the tool once for an immediate catch-up.
  48. **Open**: a step blocks requests up to ~2 s (a non-blocking `fetchStart` + `done()` state machine would fix it).
  49. `/my/unwatched` render grew with the rows (1.3 s server, 1593 rows) — watch it against the 2 s target. A season
  50. watch record stays solid when the sync adds a new episode to that season (existing season-watch semantics).
  51. Dev servers get the real token from `.env` automatically → start them with `TRACKER_SYNC=0`. Shows nobody
  52. follows are never synced (by design).
  53. ## 2026-09-30 — ticket #8: /my/ pages, S01E01, years, speed
  54. **Built**: `/my/unwatched`, `/my/schedule` (routes in `project.hl`); old `/unwatched`, `/schedule` →
  55. **301** (function routes `unwatchedMoved`/`scheduleMoved`). Rows as `S01E02 · <title>` (was
  56. `Ep 2 · …`), list titles with the year `Doctor Who (2005)`, show page `1 episode` singular.
  57. New shared helpers in `shows.hl`: `pad2`, `episodeCode`, `showYear`, `titleWithYear`,
  58. `posterUrlOf`; in `watches.hl`: `watchedSet` (used by `/my/unwatched` and the show page's
  59. `buildRows`; `isWatched` stays for the single-target toggles).
  60. **Decisions**: year = `year`, else `release.slice(0,4)`, else none (197 shows lack `year`, some
  61. have `release = ''`). Year only in LISTS (`/my/*`), not in the show page's `h1`. The 301 drops any
  62. query (the pages take none). A row whose poster FILE is missing gets `/posters/none` (one cached
  63. URL) instead of `/posters/<oldId>.jpg`.
  64. **Why /my/shows was ~25 s live though its data is 0.2 s**: hl:web handles ONE request at a time.
  65. Measured on the copy: `/my/shows` requested 1 s into a `/unwatched` render took 13.7 s (the
  66. `/unwatched` render itself 14.5 s). Every request (pages, 128 poster requests) of every visitor
  67. queued behind a slow `/unwatched`. Plus 128 distinct poster URLs per load, all the placeholder.
  68. **Real-data check** (copy of Byrodin's `storage/` only, `az5b2` = user `0mujqspn0aqb`, dev
  69. server :8730, headless Chrome, navigate → load → websocket open, 1280×900):
  70. | page | before | after |
  71. |---|---|---|
  72. | `/my/shows` (128 rows) | 1.2 s (server 0.19 s) | 0.6–0.8 s, 1 poster request |
  73. | `/unwatched` → `/my/unwatched` (831 rows) | 14.8 s | 0.7–1.2 s (server 0.55–0.65 s) |
  74. | `/schedule` → `/my/schedule` (0 rows, data not synced since 2025-12) | 0.36 s | 0.3 s |
  75. | `/shows/The-Librarians-The-Next-Chapter` | 0.34 s | 0.29 s |
  76. How to repeat (Loreana; never touch Byrodin's folder beyond the tar read):
  77. ```
  78. ssh [email protected] 'tar czf - -C /CONTAINERS/projects/tracker.worldapi.org storage' | tar xzf - -C .scratch/realdata # ONLY storage/
  79. TRACKER_PORT=8730 TRACKER_WATCH=0 TRACKER_STORAGE=$PWD/.scratch/realdata/storage/mpackdb TRACKER_SESSIONS=$PWD/.scratch/realdata-sessions/ ./bin/hybriel project.hl
  80. # session file: name = sha256(<32-hex id>), JSON {"created":1,"data":{"tag":"t"},"id":"<id>","seen":1,"user":{"id":"0mujqspn0aqb"}}
  81. curl -s -o /dev/null -w '%{time_total}\n' -b trackersid=<id> http://127.0.0.1:8730/my/unwatched
  82. # Chrome: tests/cdp.mjs launchBrowser, Network.setCookie trackersid, goto + wait __hl.socket open
  83. # afterwards: stop the server, rm -rf .scratch/realdata .scratch/realdata-sessions
  84. ```
  85. Screenshots: `/tmp/w041-real/{before,after}-{shows,unwatched,schedule,show}.png` (Loreana).
  86. **Gate** (45 → 49 checks: two 301s, the browser landing on `/my/unwatched`, "1 episode"; the
  87. existing ones now expect `/my/…`, `S01E02 · Second`, `S02E01 · Season Two Opener`, titles with
  88. year, `/posters/sh1.png` + `/posters/none`). Fixture: show 1 `image = 'png'` (gate writes a 1×1
  89. `posters/sh1.png`), show 2 `year = null`, `release = '2025-03-01'`.
  90. ```
  91. TRACKER_GATE_SHOTS=/tmp/w041-shots TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs
  92. 49 passed, 0 failed
  93. ```
  94. Screenshots `/tmp/w041-shots/` (looked at: all fine).
  95. **Open**: `/my/unwatched` rebuilds all rows per check click (~0.6 s, fine). Still no navigation
  96. to the `/my/` pages. The single-request server means any future slow page blocks everyone —
  97. keep every page well under 1 s. `/my/schedule` stays empty until the TMDB sync step.
  98. ## 2026-09-30 — ticket #7: /my/shows
  99. Built `/my/shows` (`components/myshows.hl`, route in `project.hl`, styles in `styles.hl`): every
  100. followed show, newest follow (`follows.at`, epoch ms) first — small poster (2.5rem, `/posters/…` +
  101. placeholder like the show page), title → `/shows/<slug>`, last watched episode `SxxEyy`.
  102. **Decisions**: "last episode" = highest (season, episode) among the user's EPISODE watches of that
  103. show (ticket); a season watch alone doesn't count (the season check watches every episode anyway).
  104. No watch → no episode element. A show without a poster name → `/posters/none` (the route's own
  105. placeholder), so every row has a poster. Read-only page, no face (like `/schedule`).
  106. **Reuse**: new `follows.hl` `followsOfUser` (whole records; `followedShowIds` now builds on it) and
  107. `shows.hl` `episodeById` (one line); `showById`, `posterName`, `watchesOfUser` unchanged.
  108. **Performance** (against a COPY of `storage/mpackdb/` in `.scratch/perf-store`, removed after; live
  109. files md5-verified unchanged): the page touches only the user's follows and episode watches
  110. (one fetch per watch), never all episodes of a followed show — az5b2's 128 follows → 128 rows (123
  111. with an episode, e.g. `S36E11`, `S30E91`) built in **160–255 ms**. For comparison **`/unwatched`
  112. takes ~15 s** on the same copy (805 rows): it fetches every episode of every followed show and
  113. checks each against the user's 11,692 watches with a linear `isWatched` — not in this ticket's
  114. scope, flagged below.
  115. **Gate** (`tests/browser.mjs`, 38 → 45 checks; `tests/seed-show.hl` adds a 2nd show "Unwatched Gate
  116. Show", no seasons/poster name, followed AFTER the first but stored after it; the first follow is now
  117. dated a day back): signed out → sign-in message; signed in → newest follow first, title hrefs,
  118. posters loaded (`naturalWidth > 0`), 40px wide, `S01E02` after /unwatched's click, later `S02E01`
  119. (watched BEFORE season 1's bulk watch → proves highest, not most recent), none for the other show.
  120. ```
  121. TRACKER_GATE_PORT=8720 TRACKER_GATE_IDENT_PORT=8721 TRACKER_GATE_CHROME=8722-8726 node tests/browser.mjs
  122. 45 passed, 0 failed
  123. ```
  124. **Open**: `/unwatched` ~15 s on real data (above) — a watched-id map instead of `isWatched` per
  125. episode would fix it (own ticket). No navigation links to `/my/shows` yet (homepage stays empty).
  126. ## 2026-09-27 — ticket #6: /schedule
  127. Built `/schedule` (`components/schedule.hl`): every episode of a show the signed-in user follows
  128. that is NOT yet released (`e.release > today`), soonest first. Creator: "/schedule has a similar
  129. list without check icons for the upcoming episodes of shows followed" — same row shape as
  130. `/unwatched` (`components/unwatched.hl`) minus the check icon.
  131. **Reuse, not copy**: imports the exact same `followedShowIds` (`follows.hl`), `showById` /
  132. `seasonsOfShow` / `episodesOfSeason` (`shows.hl`) that `/unwatched` already uses — nothing new
  133. added to either file. `watches.hl` is not imported at all: a row here is never watched/unwatched,
  134. there is no check icon and no click, so the page needed no per-user watch state and no `on server`
  135. face — the whole component is a single read-only server-side render (`rows` computed once from
  136. `session`, same as every other reactive member here).
  137. **Order**: soonest first, ascending by `release` (a plain `'YYYY-MM-DD'` string, ordering
  138. operators confirmed working against this app's own vendored `bin/hybriel`, same as `/unwatched`'s
  139. STATUS entry) — the ticket named this the architect's own reading ("the creator named the order
  140. only for /unwatched"), so `insertByReleaseAsc` mirrors `/unwatched`'s `insertByReleaseDesc` with
  141. the comparison flipped.
  142. **`decided`**: "upcoming" = `release` strictly after today (not `>=`) — the natural complement of
  143. `/unwatched`'s own `release <= today`, so every released-or-today episode is unambiguously on one
  144. list, never both, never neither.
  145. **Gate** (`tests/browser.mjs`, 35 → 38 checks): reused the same fixture (`tests/seed-show.hl`,
  146. unchanged) — episode 3 already carried a far-future release (`2099-01-01`) for `/unwatched`'s own
  147. "excludes the future one" check; `/schedule` now proves the mirror image with the same fixture:
  148. signed out, no rows, the sign-in message; signed in, only that one future episode shows (the two
  149. already-released ones excluded), no `button.checkicon` anywhere on the page, unaffected by any of
  150. the watch toggles the other checks perform on the same fixture (run in any order — `/schedule`
  151. never reads watch state).
  152. ```
  153. node tests/browser.mjs
  154. 38 passed, 0 failed
  155. ps -eo pid,args | grep [h]l-browser-tier
  156. (nothing)
  157. ```
  158. Updated `README.md` ("What it does (step 6)", Test, Files) and this file.
  159. **Open**: nothing shown on the homepage still points to `/schedule` (same as `/unwatched` before
  160. it — the homepage stays empty per ticket #1's decision).
  161. ## 2026-09-27 — ticket #5: /unwatched
  162. Built `/unwatched`: every episode of a show the signed-in user follows that is already released
  163. (`release` ≤ today, a plain `'YYYY-MM-DD'` string — migrated as-is by `tools/migrate.hl`, never
  164. run through `dateOf`) and not yet watched, newest release first (creator: "/unwatched lists all
  165. unwatched episodes of shows followed in release date desc order").
  166. **Reuse, not copy** (the ticket's own instruction, re "the same check icon and watch logic as the
  167. show page"): `watchClassOf` (watched → `'checkicon solid'`/`'checkicon regular'`) was a private
  168. `static` in `components/show.hl`; moved it to `watches.hl` so both pages import the SAME
  169. definition instead of each declaring their own — `show.hl` now imports it too, nothing duplicated.
  170. `isWatched`/`setWatched` (already in `watches.hl`) needed no change. New `follows.hl`
  171. (`followedShowIds`) and `shows.hl` `showById` (one line) round out the read-only data access this
  172. page and `/schedule` (ticket #6) both need.
  173. **String comparison** (`e.release <= today`, `row.release >= x.release` for the desc sort):
  174. hybriel#2 ("no ordering operators on strings") is filed as still-open in this ticket's brief, but
  175. its own history shows it was fixed and merged to master 2026-09-25 — confirmed directly against
  176. THIS app's own vendored `bin/hybriel` (`'2020-09-03' <= '2026-09-27'` → `true`), so no numeric
  177. date workaround was needed.
  178. **A View text node cannot hold an inline expression** (`"Ep " + row.episodeNumber + " · " +
  179. row.title` inside a tag body) — same rule as View attributes (components/show.hl's own comment:
  180. "a View attribute must be a literal, a member or a field path"), just not documented for text
  181. nodes anywhere yet; it silently rendered `<!-- binary_expression not rendered yet -->` instead of
  182. erroring. Fixed by resolving the label into a plain field (`row.episodeLabel`) once, server-side,
  183. in `unwatchedRowsOf` — the same pattern `show.hl`'s `buildRows` already uses for every other
  184. ternary/concatenation. Not filed as a new Hybriel gap since it's the exact same restriction
  185. already known and worked around elsewhere in this app, just not yet hit for interpolated text.
  186. **Empty state**: signed out (no user, so no follows to show) and signed in with nothing unwatched
  187. both render a message; used `.muted` (colorTextMuted), not `.message` (colorDanger) — the existing
  188. `.message` class is for real errors (the login-failed banner), and an empty list isn't one (a
  189. `decided`, small).
  190. **Gate** (`tests/browser.mjs`, 29 → 35 checks): `tests/seed-show.hl` now takes an optional
  191. `TRACKER_SEED_IDENTITY` and, when given, seeds a fixed user + a follow of the fixture show for it
  192. — the only way to give `/unwatched` a known followed show before the browser ever signs in, since
  193. the real login's `ensureUser` matches an EXISTING `users.db` row by `identity` and reuses its id
  194. rather than minting a new one. The gate learns alice's per-app identity with one throwaway
  195. `ident.exchange` call (consuming one code) before writing the seed. Episode 1/2 of the fixture now
  196. carry a past `release` (`2020-01-01`/`2020-06-15`), episode 3 a far-future one (`2099-01-01`) —
  197. proving both the release-date filter and the desc order. The new checks run signed out (no rows,
  198. the sign-in message), then signed in RIGHT AFTER LOGIN, BEFORE the existing show-page checks that
  199. toggle these same fixture episodes watched (order matters — otherwise `/unwatched` would already
  200. be empty by the time it's tested): both unwatched episodes list newest-first, excluding the
  201. future one; a click on the newest (`Second`, ep 2) removes it from the list; a reload proves the
  202. watch is server-side, leaving only `Pilot` (ep 1).
  203. ```
  204. node tests/browser.mjs
  205. 35 passed, 0 failed
  206. ps -eo pid,args | grep [h]l-browser-tier
  207. (nothing)
  208. ```
  209. Updated `README.md` ("What it does (step 5)", Test, Files) and this file.
  210. **Open**: nothing shown on the homepage still points to `/unwatched` — no navigation exists yet
  211. (the homepage stays empty per ticket #1's decision; a later step, from the creator, presumably
  212. adds it). `/schedule` (ticket #6) is next and reuses `follows.hl`/`shows.hl`/`watches.hl` the same
  213. way.
  214. ## 2026-09-27 — ticket #4: the show page, watch checks (took over a previous half-done session)
  215. Continued from an earlier session that had built the whole page — header, seasons/episodes,
  216. the click-handling redesign to a flat `rows` list rebuilt server-side (`components/show.hl`'s
  217. own header comment explains why: a nested `for` over seasons > episodes risks hybriel#86) — but
  218. left with the gate red: clicking a check never turned it solid, no console error, and the
  219. session ran out of budget before finding why.
  220. **The actual bug**: the show page's own pure helpers (`buildRows`, `genreRowsOf`, `castRowsOf`,
  221. `watchClassOf`, `initialCollapsed`) were plain instance members (`buildRows = (show, …) => {…}`,
  222. no `static`). That shape works fine as a member's own top-level initializer (`rows = found ?
  223. buildRows(showRow, watchRows, collapsed) : []` — this is how the page renders correctly on first
  224. load) but throws when the SAME function is called from inside an `on server` face
  225. (`showToggleEpisode` etc.): the server log showed `'buildRows' is not callable — it holds null`.
  226. Found by running `.scratch/debug-click.mjs` (left by the previous session) with the server's
  227. stdout/stderr captured — the browser console itself stayed silent (the face's error never
  228. reaches the client at all, a separate Hybriel gap, hybriel#92 already covers that half).
  229. Confirmed the fix by grepping other worldapi apps for the same shape:
  230. `calendar.worldapi.org/components/calendar.hl` has `static soonOf = (u) => { … upcoming(u, …) }`
  231. (an mpackdb-backed helper), called from its `on server calLoad`/`calSettingsLoad` faces, and
  232. calendar's own gate is green — so `static` is the proven-safe shape for a component's own helper
  233. that both (a) touches an mpackdb-backed import and (b) needs calling from more than one place
  234. (its own initializer AND a later face). Declared all five helpers in `show.hl` `static`; reran
  235. the gate: 29/29 green, including the season-bulk-toggle and the reload-proves-server-side checks.
  236. Filed **hybriel#115 was already open** (from the previous session, about the CLIENT bundle
  237. omitting a case for such a helper) but this session's finding is the SAME root cause reaching
  238. further than that ticket says: the plain HL interpreter on the SERVER also treats such a helper
  239. as `null` outside its own initializer, not just the JS-compiled client bundle. Left as one
  240. `issues` entry pointing at hybriel#115 rather than opening a near-duplicate ticket — the
  241. repro/observed there already generalizes (a per-instance member function wrapping an
  242. mpackdb-backed import is unreliable wherever it is called from, not just the browser).
  243. Ran `node tests/browser.mjs`: 29 passed, 0 failed (was 20 passed / 1 failed — the click timeout
  244. — before the `static` fix).
  245. **Rehearsed `tools/relink-episode-seasons.hl` against a full COPY** of the real
  246. `storage/mpackdb/` (`.scratch/relink-rehearsal/`, since removed) — the tool's own header claimed
  247. this rehearsal already existed here; it didn't (a leftover claim from the previous session), so
  248. this session actually ran it:
  249. ```
  250. relink: episodes 231584, null season before 36194, linked 36194, still null (no matching season) 0
  251. relink: episodes.db replaced with the relinked file — re-run to confirm idempotency (0 linked the second time)
  252. ```
  253. Ran a second time on the same copy straight after — idempotency confirmed:
  254. ```
  255. relink: episodes 231584, null season before 0, linked 0, still null (no matching season) 0
  256. ```
  257. All 36,194 pre-existing null-season episodes (ticket #2's count) now link to a real season by
  258. `(show, seasonNumber)`; none left over. Not run against the LIVE `storage/mpackdb/` — that needs
  259. the tracker container stopped first (hl:mpackdb does not coordinate two processes on one storage
  260. directory, hybriel#21), the architect's job per the ticket ("the architect runs it live").
  261. Updated README.md ("What it does (step 4)", Test, Files) and this file.
  262. ## 2026-09-27 — ticket #2: old data migrated (took over a previous session's rewritten tool)
  263. Continued from an earlier session that had rewritten `tools/migrate.hl` to avoid a real data-loss
  264. bug (see below) but never run it even once, and had left the real `storage/mpackdb/` holding data
  265. from the OLDER, buggy two-pass version of the tool (put a bare record, then `update()` it once the
  266. other side of a circular reference existed). Took over: moved that buggy data aside
  267. (`.scratch/pre-migrate-buggy-backup`, since removed), kept `storage/mpackdb/users.db` (step 1's
  268. login table, untouched by the migration), and ran the already-fixed tool for the first time.
  269. **The fix already on disk (kept as is):** `Show.seasons`/`Season.show` and `Show.cast`/
  270. `Person.shows` are two-way references, so the id on one side must exist before the record on the
  271. other side can be built. The old approach — `put()` a bare record, come back with `update()` once
  272. the far side's id exists — loses rows: reopening a table in a fresh process after an `update()` had
  273. touched it read back FEWER rows than were ever `put()` (seen directly: `persons.db`'s `count()`
  274. dropped 15157 → 15152 after one round of `update()` — a real `hl:mpackdb` bug, reported to Hybriel,
  275. not something an app is allowed to patch around). The fix assigns every new id itself (8 random
  276. bytes, `hl:crypto`) in one pass over each export file BEFORE building any record, so by the time a
  277. record is actually built every reference is already a known id — one `put()` per row, `update()`
  278. never called.
  279. **Run** (`tools/migrate.hl`, `TRACKER_OLD_DATA=.../old-tracker/mongo-export`,
  280. `TRACKER_STORAGE=$PWD/storage/mpackdb`):
  281. ```
  282. migrate: genres 27 new, 0 already there (old 27 -> new 27)
  283. migrate: persons 15157 new, 0 already there (old 15157 -> new 15157), show refs skipped 0
  284. migrate: shows 9453 new, 0 already there (old 9453 -> new 9453), cast refs skipped 0, genre refs skipped 0, season refs skipped 0
  285. migrate: seasons 6430 new, 0 already there (old 6430 -> new 6430), show refs skipped 0, episode refs skipped 0
  286. migrate: episodes 231584 new, 0 already there (old 231584 -> new 231584), show refs skipped 0, season refs left null (pre-existing) 1
  287. migrate: follows 128 new, 0 already there (old 128 -> new 128), show refs skipped 0
  288. migrate: watches 11692 new, 0 already there (old 11692 -> new 11692), target refs left null (pre-existing) 3
  289. migrate: 0 failed
  290. ```
  291. Every count equals the old export's own document count (`old-tracker/README-RECONSTRUCTED.md`:
  292. Show 9453, Season 6430, Episode 231584, Person 15157, Genre 27, Follow 128, Watch 11692). Ran a
  293. second time straight after (idempotency: the tool's `oldId` index finds each row again) — every
  294. table printed `0 new`, all rows `already there`, counts unchanged, 0 failed: the id-loss bug does
  295. not resurface across a real close-and-reopen.
  296. **Proof beyond the tool's own counters** (`tools/verify.hl`, new): the ticket asks for a check that
  297. does not just trust `migrate.hl`'s own "skipped" tallies. It runs against a plain filesystem COPY
  298. of `storage/mpackdb/` (hl:mpackdb rewrites a file's data on open, so the live store is never opened
  299. a second time) and independently re-derives, from the persisted records alone: every table's
  300. `count()` against the export's own document count, and — by building id sets for every table and
  301. walking every reference field (`Show.genres/cast/seasons`, `Season.show/episodes`,
  302. `Episode.show/season`, `Person.shows`, `Follow.show/user`, `Watch.target/user`) — that every
  303. non-null reference resolves to a real id. It also follows one show end to end (`Raised by Wolves`,
  304. 2 seasons, 18 episodes, 1 follow, 2 season-level watches — all cross-checked back to the show).
  305. ```
  306. $ cp -r storage/mpackdb .scratch/verify-copy && TRACKER_VERIFY_COPY=$PWD/.scratch/verify-copy ./bin/hybriel tools/verify.hl
  307. count ok genres: 27 (expected 27)
  308. count ok persons: 15157 (expected 15157)
  309. count ok shows: 9453 (expected 9453)
  310. count ok seasons: 6430 (expected 6430)
  311. count ok episodes: 231584 (expected 231584)
  312. count ok follows: 128 (expected 128)
  313. count ok watches: 11692 (expected 11692)
  314. users: 1 (expected 1, the creator)
  315. 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)
  316. END-TO-END show 105ad9c91b14d663 "Raised by Wolves" seasons=2
  317. season 1 (be2965a5ab7165ca) episodes=10 show-back-ref-ok=true
  318. season 2 (0fe4e53157ed0367) episodes=8 show-back-ref-ok=true
  319. total episodes across seasons: 18
  320. follows on this show: 1, season-level watches touching it: 2
  321. VERIFY PASS
  322. ```
  323. (36194 episodes with no season = 36193 that never had one in the old export, plus the 1 the tool's
  324. own count already named as a pre-existing dangling `season` reference in the source data — neither
  325. is a reference this migration broke; `dangling references: 0` covers exactly that.)
  326. The single old user (`User.jsonl`, `[email protected]`, password not taken over) is this app's
  327. user for ident short id `az5b2` (`storage/mpackdb/users.db`, the same table step 1's login uses) —
  328. confirmed in the copy: one record, `identity=az5b2`.
  329. **Two Hybriel bugs found while building this** (both already filed as issues on this ticket by an
  330. earlier session, not re-filed): `hl:fs readFile` silently truncates at 10 MiB (worked around here
  331. with `split`, no shell, no JS, see `tools/migrate.hl`'s header comment) and `hl:mpackdb` loses rows
  332. across `update()` + reopen (the reason this tool never calls `update()`).
  333. **Open**: nothing shown yet — this ticket is data-only, on purpose (the next step, from the
  334. creator, is showing the data in the UI).
  335. ## 2026-09-27 — ticket #3: no border on the header or filled buttons
  336. Design notes from the first test (architect): "the application-header should have no bottom
  337. border" and "the buttons also no border except they are inverted". Two changes in
  338. `styles.hl`:
  339. - `applicationHeader`: dropped `borderBottom`.
  340. - `ident-selector::part(button)` (the signed-out "choose ident" button): added
  341. `border = 'none'` — the element's own CSS gave it a 1px border the same colour as its
  342. background (`--_accent`), invisible but still a real border in the computed style; this is
  343. the app-side restyling hook ident's README documents (`::part(button)`), not a change to
  344. ident's vendored `selector.js`.
  345. Left unchanged, already correct: the native `button` rule (filled, e.g. no other filled
  346. buttons on this page yet) already has `border = '0'`; `button.quiet` ("Log out") and the
  347. selector's dropdown `[part="identity"]` rows keep their border (transparent background —
  348. inverted style); `a.button` ("Log in with ident") has no border rule and browsers give `<a>`
  349. none by default. The selector's signed-in "logged in with ident" status pill
  350. (`::part(status)`) is not one of the buttons the ticket names and isn't a button element
  351. (a `<span>`) — left with its border.
  352. **Gate** (`tests/browser.mjs`, now 16 checks): added computed-style checks — the header's
  353. `borderBottomWidth` is `0px`; `#loginbutton`'s border is `0px`; the selector's
  354. `[part~="button"]` (inside its shadow root) border is `0px`; `#logout`'s border is NOT `0px`
  355. (still inverted-bordered) once signed in.
  356. ```
  357. node tests/browser.mjs
  358. 16 passed, 0 failed
  359. ```
  360. ## 2026-09-27 — ticket #1 reopened: login redone to match calendar exactly (header selector, empty page)
  361. The architect rejected the first build of ticket #1: the sign-in sat centered on the page
  362. (its own "Sign in with ident" card, identity + sign-out shown in a `homeCard`) instead of
  363. the header identity selector calendar/gitoria use, and there was no `<ident-selector>` at
  364. all. Rebuilt the login by copying calendar.worldapi.org's own files (unchanged in shape, per
  365. the architect's instruction "Copy calendar.worldapi.org's login unchanged"):
  366. - **`users.hl`** (new): the ident exchange + a `usersTable` (`storage/mpackdb/users.db`,
  367. `identity` → this app's user id), copied from calendar's `users.hl` with `TRACKER_KEY` /
  368. `TRACKER_SECRET` / `TRACKER_URL` / `TRACKER_STORAGE` in place of calendar's names (kept
  369. the names already in this app's `docker-compose.yml`/README/DECISIONS rather than
  370. switching to calendar's generic `IDENT_API_KEY`/`IDENT_API_SECRET`).
  371. - **`login.js`** (new): calendar's bridge between `<ident-selector>`'s `ident-login` DOM
  372. event and the hidden `#identcode` input, copied verbatim (creator: "login.js is correct,
  373. as thats for externals in general").
  374. - **`components/main.hl`**: now the header carries `userBox` (`<ident-selector>` + "Log in
  375. with ident" / "Log out"), the `trackerLogin`/`trackerLogOut` faces and the
  376. `trackerSignedIn`/`trackerSignedOut` client push — calendar's shell renamed to this app's
  377. event names.
  378. - **`components/home.hl`**: now truly empty (`View { home {} }`) — no sign-in link, no
  379. identity/sign-out duplicated on the page; that is entirely the header's job now.
  380. - **`components/loginfailed.hl`** (new) + **`project.hl`**: the login routes are now
  381. `/login/callback` (function route, the button's return AND the code exchange) and
  382. `/login/failed`, copied from calendar's `project.hl` (`safePath`, `failed()`,
  383. `loginCallback`) in place of the old `/login` + `/callback` + `/logout` routes; the old
  384. `/logout` POST route is gone (logout is a face, like calendar's).
  385. - **`styles.hl`**: added the header selector styles (`userBox`, `identSelector`,
  386. `ident-selector::part(...)`, sticky header) from calendar's `styles.hl`; dropped the
  387. now-unused `homeCard`/`accountBar`/`userName` rules from the rejected centered sign-in.
  388. **Gate rewritten** (`tests/browser.mjs`, 12 checks): replaced the old gate (which drove
  389. ident's full email-code UI through a vendored dev copy of ident at `.scratch/ident-dev`)
  390. with calendar's own pattern — `tests/identkit.mjs` (copied from calendar unchanged) starts a
  391. throwaway ident (a fresh copy of `/media/STORAGE/projects/ident.worldapi.org`'s code, no
  392. `.env`/storage copied, codes to a mail sink, never the live ident), signs in over its REST
  393. API and registers this app, then the gate proves the header in a real headless Chrome:
  394. signed out → `#selector` + `#loginbutton` in the header, **`main` is empty** → sign in
  395. (`/login/callback?ident_code=`, the same exchange the selector would trigger) → `#selector`
  396. shows `class="in"`, `#logout` appears, **`main` still empty** → sign out → signed out again
  397. → a reload stays signed out. No console errors.
  398. ```
  399. node tests/browser.mjs
  400. 12 passed, 0 failed
  401. ```
  402. `./deploy.sh --dry-run --target .scratch/deploy-preview --url http://127.0.0.1:0/`: gate
  403. passed as step [1/4], rsync preview held none of `storage/`, `.sessions/`, `.env`,
  404. `.scratch/`, `server.*`, logs. Removed the stale `.scratch/ident-dev` (the old gate's vendored
  405. ident copy) and `.scratch/browser-gate` (the old gate's storage) — unused by the new gate.
  406. **Still open**: the app is registered in ident's dev copies only (this gate's own throwaway
  407. ident). The LIVE ident registration (`TRACKER_KEY`/`TRACKER_SECRET` in `.env` next to
  408. `docker-compose.yml`) is the architect's first-deploy step (README "Deploy"), not built here.
  409. ## 2026-09-27 — ticket #2: data migration — blocked twice on sandbox access, nothing done
  410. Two workers in a row (`s-20260927T1029-f57b0f`, then this session) could not start: the
  411. migration source the ticket names, `/media/STORAGE/projects/old-tracker/mongo-export/*.jsonl`
  412. + its `README-RECONSTRUCTED.md`, is not reachable from this worker's sandbox. Verified fresh
  413. this session:
  414. ```
  415. $ ls -la /media/STORAGE/projects/
  416. antcolony-docs hybriel ident.worldapi.org tracker.worldapi.org # no old-tracker
  417. $ mount | grep STORAGE
  418. /dev/nvme1n1p6 on /media/STORAGE/projects/tracker.worldapi.org type btrfs (rw,...)
  419. /dev/nvme1n1p6 on /media/STORAGE/projects/hybriel type btrfs (ro,...)
  420. /dev/nvme1n1p6 on /media/STORAGE/projects/ident.worldapi.org type btrfs (ro,...)
  421. /dev/nvme1n1p6 on /media/STORAGE/projects/antcolony-docs type btrfs (ro,...)
  422. # old-tracker is not among the mounted subvolumes at all
  423. ```
  424. So this is not a data problem (the export is "complete" per the ticket) — it's the worker
  425. sandbox for this ticket that never got `old-tracker` mounted (read-only, like `hybriel` /
  426. `ident.worldapi.org` / `antcolony-docs` above). Nothing was built or changed this session:
  427. no `storage/`, no `tools/`, no id-mapping — writing the migration tool blind, guessing the
  428. JSONL shape from the ticket text alone, risks silently wrong data and was avoided on purpose
  429. (see the project's "Build it properly" rule). Filed as an antcolony issue so the next worker's
  430. sandbox includes `old-tracker` before another attempt.
  431. ## 2026-09-27 — ticket #1: the empty shell, ident login
  432. Built the app from scratch: vendored `bin/hybriel` + `plugins/` + `shared/tokens.hl` from
  433. `ident.worldapi.org` (newest local build, hybriel master 837fe120, no local patch — see
  434. README "Vendored Hybriel"). `project.hl` (routes `/login`, `/callback`, `/logout`, `/`),
  435. `components/home.hl` (the empty homepage), `components/main.hl` (shell), `styles.hl`
  436. (accent green `#4ec9b0`, per `antcolony/README.md` "look and style" / `CONCEPT.md`).
  437. `docker-compose.yml` / `Dockerfile` / `deploy.sh` following ident's own house pattern
  438. (port 45008, container `tracker.worldapi.org`).
  439. **Login**: the ident login BUTTON flow (ident.worldapi.org README "How apps use ident"),
  440. server-side exchange (`POST <ident>/api/exchange`), this app's own framework session
  441. (`session.user = { identity }`, cookie `trackersid`). ident hands over only the identity's
  442. public **short id** — no display name yet (ident#11, properties handover, is on hold) — so
  443. "your name" on the homepage is `Signed in as <shortid>`, the same identifier every other
  444. worldapi app would show; see `decided` in the report.
  445. **Lesson (Hybriel)**: a face that mutates `session.user` (e.g. `signOut`) does NOT
  446. automatically re-render the page — a component's reactive members (`signedIn`, `identity`,
  447. …) are computed once at mount from the framework's read-only session snapshot; a client
  448. handler that calls `emit server X()` must ALSO flip the affected members itself afterwards
  449. (ident's own `components/home.hl` `on doSignOut(e)` does exactly this: `emit server
  450. signOut()` then `signedIn = false` …). Missing that made the sign-out button silently do
  451. nothing client-side (the ack came back `ok:true`, the server-side session really was
  452. cleared — proven by a reload — but the DOM never updated) until copied.
  453. **Gate** (`tests/browser.mjs`, 13 checks): a fresh copy of `ident.worldapi.org`'s code (no
  454. `.env`, no `storage/`, no `.sessions/` — verified empty of secrets/data before first use)
  455. runs as this gate's own ident, on its own storage and its own mail sink (no live ident, no
  456. real mail). The gate registers this app in that dev ident once over the `/__hl/emit` API
  457. (the same one-time step a person does by hand in ident's `/apps` page), then drives the
  458. REAL login-button flow in a real headless Chrome: signed out → click "Sign in with ident" →
  459. ident's email form → the mail-sink code → the first-login optional-names form (Skip) → the
  460. one-identity choice (one click) → back on tracker, "Signed in as `<shortid>`" → "Sign out" →
  461. signed out again → a reload stays signed out. No console errors. Ran twice for stability, 0
  462. failures; no leftover Chrome/hybriel processes after either run.
  463. ```
  464. node tests/browser.mjs
  465. 13 passed, 0 failed
  466. ```
  467. `./deploy.sh --dry-run --target .scratch/deploy-preview --url http://127.0.0.1:0/` (a local
  468. directory, per ident's own convention for testing deploy.sh without touching Byrodin): gate
  469. passed, rsync preview held none of `storage/`, `.sessions/`, `.env`, `.scratch/`, `server.*`,
  470. logs — confirmed by the same grep the script refuses on.
  471. **Open**: the app is not registered with the LIVE ident yet — `TRACKER_KEY`/`TRACKER_SECRET`
  472. are unset until the architect's first deploy does that (README "Deploy"); until then
  473. `/login` on the live site answers a plain error page instead of redirecting (`/` itself
  474. still renders). This is normal for a brand-new app, the same as ident's own SMTP `.env`
  475. before its first deploy — not something this ticket's worker can set (no `.env` access,
  476. and it is a LIVE ident registration).

Branches

Latest commits

  • 2fa9d997tracker#9: TMDB sync (followed shows: seasons, episodes, posters), tools/sync-tmdb.hl + daily run 04:00 UTC, fake TMDB in gatemre
  • 49e1f61edeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre
  • 3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre
  • 44b7d9f9tracker#6: /schedule — upcoming episodes of followed shows, soonest firstmre
  • 91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre
  • a97c0295tracker#4: show page /shows/:slug (header, seasons, episodes, watch checks) + tools/relink-episode-seasons.hlmre
  • 05f407c5tracker: no border on any button except inverted ones (Log out, ident status and identities too); header brand weight 100mre
  • 17375427tracker#2: tools/migrate.hl + tools/verify.hl — old MongoDB data into mpackdb with new idsmre
  • 31aac936tracker#3: no border on the header and on filled buttons; inverted buttons keep theirsmre
  • 2ad9d29ctracker#1: login exactly like calendar (identity selector in the header, empty homepage)mre
  • 3691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre