gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit91c9fc8c91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre91c9fc8c/README.md

15.4 KB

  1. # tracker.worldapi.org
  2. The new tracker: a Hybriel app that will track the TV shows (and later movies) the creator
  3. follows and watches. **`CONCEPT.md` (the creator's) is the source of truth** — read it first;
  4. nothing is built that it does not describe. Built step by step, one ticket per step
  5. (tracker.worldapi.org#1, #2, …).
  6. **Built so far — step 1 (ticket #1)**: the shell. The header carries the ident login (an
  7. identity selector, like calendar/gitoria) and sign-out; the homepage itself stays empty. No
  8. design carried over from the old app.
  9. **Step 2 (ticket #2)**: the old tracker's MongoDB export is now in `storage/mpackdb/` (new
  10. mpackdb ids, every reference re-pointed) — see "Data" and `STATUS.md`. Nothing is shown in the
  11. UI yet; the homepage still renders empty. That is a later step, from the creator.
  12. **Step 4 (ticket #4)**: the show page, `/shows/:slug` (`components/show.hl`) — poster, title,
  13. genre pills, plot, cast (as described in "What it does (step 4)" below), every season with its
  14. episodes, and the watch check (episode and season, season bulk-toggles its episodes).
  15. **Step 5 (ticket #5)**: `/unwatched` (`components/unwatched.hl`) — every already-released,
  16. unwatched episode of a show the signed-in user follows, newest release first (see "What it does
  17. (step 5)" below). The homepage itself is still empty (step 6, `/schedule`, is next).
  18. Written in **Hybriel** on **hl:web**, same stack and conventions as `ident.worldapi.org`
  19. (vendored plugins/binary copied from there, see "Vendored Hybriel").
  20. ## Run (dev, Loreana)
  21. ```bash
  22. cd /media/STORAGE/projects/tracker.worldapi.org
  23. TRACKER_PORT=8700 setsid nohup ./bin/hybriel project.hl > server.log 2>&1 < /dev/null & echo $! > server.pid
  24. # stop: kill $(cat server.pid)
  25. ```
  26. * Config (env; the real environment outranks nothing here — there is no `.env` reader in
  27. project.hl, unlike ident's SMTP settings — set these in the shell or `docker-compose.yml`):
  28. | Variable | Default | |
  29. |---|---|---|
  30. | `TRACKER_PORT` | 45008 | |
  31. | `TRACKER_URL` | `http://127.0.0.1:<port>` | this app's own origin — the ident login button's `return=` is built from it |
  32. | `IDENT_URL` | `https://ident.worldapi.org` | |
  33. | `TRACKER_KEY` / `TRACKER_SECRET` | `''` | this app's API key + secret, from ident's `/apps` (register once, origin = `TRACKER_URL`) — **not set yet on Byrodin**: the first deploy (architect) registers the app in the live ident and puts these in `.env` next to `docker-compose.yml`, exactly like ident's own `.env` holds its SMTP settings |
  34. | `TRACKER_WATCH` | on | `0` = no dev watcher (the container) |
  35. | `TRACKER_STORAGE` | `./storage/mpackdb` | the `usersTable` directory |
  36. | `TRACKER_SESSIONS` | `./.sessions` (hl:web's own default) | this app's own session store |
  37. | `HL_HOST` (or `HOST`) | 0.0.0.0 | interface to bind; `127.0.0.1` on Byrodin |
  38. Without `TRACKER_KEY`/`TRACKER_SECRET`, `/` still renders (signed out, the selector and "Log
  39. in with ident" show in the header); a login attempt answers `'login is not set up on this
  40. server (TRACKER_KEY / TRACKER_SECRET missing)'` instead of exchanging a code — so the app is
  41. never a dead 500 while waiting for that one-time setup.
  42. ## What it does (step 1)
  43. * **Login, copied unchanged from calendar.worldapi.org** (rejected once for a centered
  44. sign-in and no header selector — architect, 2026-09-27): ident only, no own passwords
  45. (ident's login button flow, ident.worldapi.org README "How apps use ident"), the identity
  46. selector (`<ident-selector>`, ident's `/selector.js`) sits in the header
  47. (`components/main.hl`, `userBox`), a plain "Log in with ident" link beside it when signed
  48. out. Choosing an identity in the selector fires `ident-login` (one-time code); `login.js`
  49. (an external-component bridge, allowed per the creator: "login.js is correct, as thats for
  50. externals in general") hands the code to a hidden input, whose `change` calls the server
  51. face `trackerLogin` (`users.hl` `exchangeCode`, `POST <ident>/api/exchange` — nothing else:
  52. ident does not hand over a display name yet, ident#23 done / ident#11 on hold), which makes
  53. or finds this app's own user record (`usersTable`, `storage/mpackdb/users.db`) and sets
  54. `session.user = { id }`. `/login/callback` is the same exchange as a plain redirect (the
  55. login button's return URL) for a client that has no socket yet. "Log out" in the header
  56. calls `trackerLogOut`; ident's own login is untouched. This app's own session cookie is
  57. `trackersid` (cookies ignore ports, an own name keeps it apart from ident's `identsid` on
  58. the same dev host, hybriel#10/#17).
  59. * **The empty homepage** (`/`, `components/home.hl`): no content at all, signed in or out —
  60. the header alone carries the login state. Nothing else.
  61. * **One mpackdb table**: `storage/mpackdb/users.db` (`identity` → this app's user id — no
  62. display name, no other per-user data yet). Step 2 (tracker.worldapi.org#2, open) brings the
  63. old tracker's data across.
  64. ## What it does (step 4)
  65. `/shows/:slug` (`components/show.hl`, slug = the migrated `urlSegment`), read-only data from
  66. `shows.hl`, per-user watch state from `watches.hl` (both reused as-is by `/unwatched` and
  67. `/schedule`, tickets #5/#6 — "reuse, don't copy"):
  68. * **Header**: poster on the left (`/posters/<name>`, `project.hl` `posterRoute` — served from
  69. `storage/mpackdb/posters/`; the real poster files were never in the Mongo backup, see
  70. `DECISIONS.md` "tracker concept" — a placeholder SVG shows until a later TMDB step brings
  71. real ones back), title, genre pills (blue, link to `/genre/<slug>`) and the plot summary to
  72. its right, the cast below as comma-separated red links to `/person/<slug>`.
  73. * **Seasons**, latest first, the latest expanded and every other one collapsed to start; each
  74. row: a round check icon (a disc with a check, drawn in SVG/CSS, no icon font), the season
  75. title/number, its episode count. Its episodes (number + title, and their own check icon)
  76. show once expanded (click the row to toggle).
  77. * **The watch check** (signed-in only — signed out, no check icons show at all): clicking an
  78. episode's check toggles that one watch; clicking a season's check toggles the season's own
  79. watch AND every one of its episodes at once (`watches.hl` `setSeasonWatched`). Every click is
  80. a server round trip (`emit server showToggleEpisode` / `showToggleSeason` / `showRows` for
  81. collapse/expand) that rebuilds the row list server-side and reassigns it whole — a season has
  82. at most a few dozen episodes, cheap. A **Hybriel gap found here** (filed as hybriel#115,
  83. reported after this ticket's first attempt): a plain per-instance member function that calls
  84. an hl:mpackdb-backed import comes back `null` when called from inside an `on server` face,
  85. even though the very same call works as a member's own top-level initializer — the fix is to
  86. declare such helpers `static` (the same shape calendar.worldapi.org's `soonOf` already uses).
  87. `components/show.hl`'s own helpers (`buildRows`, `genreRowsOf`, …) are `static` for exactly
  88. this reason.
  89. * **Data fix**: `tools/relink-episode-seasons.hl`, a one-off idempotent tool that links every
  90. episode whose `season` was still null in the migrated data (36,194 of them, ticket #2) to its
  91. season by matching `(show, seasonNumber)` — never touches an episode whose season is already
  92. set, never runs `update()` on the live table (hybriel#113; it rebuilds a fresh episodes table
  93. and swaps the files over, the same rule `tools/migrate.hl` follows). Not run against the live
  94. data yet — the architect runs it (stop the container first, `storage/mpackdb` is not shared
  95. across processes, hybriel#21).
  96. ## What it does (step 5)
  97. `/unwatched` (`components/unwatched.hl`): every episode of a show the signed-in user follows
  98. (`follows.hl`, new) that is already released (`release` ≤ today) and not yet watched
  99. (`watches.hl` `isWatched`), newest release first. Creator: "/unwatched lists all unwatched
  100. episodes of shows followed in release date desc order"; "check icon is a disc with a check in
  101. it" — the SAME icon/class (`watches.hl` `watchClassOf`, moved there from `components/show.hl` so
  102. both pages share one definition instead of each declaring its own) and the same
  103. `setWatched`/`isWatched` watch logic as the show page (step 4), not copied.
  104. * Each row: check icon, the show's title (linking to `/shows/:slug`), "Ep `<number>` ·
  105. `<title>`", the release date.
  106. * A row only ever shows an unwatched episode, so a click always marks it watched (never toggles
  107. back, unlike the show page's checks) — the row leaves the list. Signed out: no rows, a
  108. "Sign in to see the shows you follow." message instead.
  109. * `follows.hl` (new): read-only per-user follow data (`followedShowIds`), kept apart from
  110. `shows.hl`/`watches.hl` the same way, for `/schedule` (ticket #6) to reuse.
  111. ## Test
  112. ```bash
  113. node tests/browser.mjs # THE GATE: this app's own server + its OWN ident (a copy of
  114. # ident's code without .env, codes to a mail sink — no live
  115. # ident, no real mail; tests/identkit.mjs, same as calendar's
  116. # gate) + a real headless Chrome: signed out (header shows the
  117. # selector + "Log in with ident", the page itself is empty) ->
  118. # sign in -> header shows "Log out", page still empty -> sign
  119. # out -> signed out again -> a reload stays signed out. Also
  120. # checks the header has no bottom border and filled buttons
  121. # (incl. the ident selector's) have none, while inverted ones
  122. # ("Log out") keep theirs.
  123. # tracker.worldapi.org#4: a fixture show (tests/seed-show.hl,
  124. # written into the gate's own storage before the server starts)
  125. # proves /shows/:slug — header renders, no checks signed out;
  126. # signed in, click an episode check (turns solid), click a
  127. # season check (itself AND its episodes turn solid, proven
  128. # after a reload — server-side, not just client state).
  129. # tracker.worldapi.org#5: the same fixture (now followed by the
  130. # test user) proves /unwatched — signed out, no rows; signed in,
  131. # only the two already-released episodes show, newest first, the
  132. # far-future one excluded; click a check, the row disappears
  133. # (proven server-side after a reload).
  134. # 35 checks. Own servers :8700 (this app) / :8701 (ident copy);
  135. # own storage .scratch/gate-store; Chrome on 8702-8709.
  136. ps -eo pid,args | grep [h]l-browser-tier # must print nothing afterwards
  137. ```
  138. ## Deploy (Byrodin)
  139. Target: `/CONTAINERS/projects/tracker.worldapi.org` on Byrodin, container
  140. `tracker.worldapi.org` (`docker-compose.yml`: debian:12-slim, host network,
  141. `HL_HOST=127.0.0.1`, `TRACKER_PORT=45008`, `TRACKER_WATCH=0`, the folder mounted at
  142. `/home/tracker`, `./bin/hybriel project.hl`), public https://tracker.worldapi.org/ via
  143. nginx (TLS ends there; no baseUrl/tls in the app, like ident/notes).
  144. * **First deploy: done by the architect** (folder, nginx vhost with WebSocket Upgrade
  145. headers, cert, DNS, **and registering this app in the live ident** — `/apps` → name +
  146. origin `https://tracker.worldapi.org` → the API key + secret go into `.env` next to
  147. `docker-compose.yml`, `TRACKER_KEY=… TRACKER_SECRET=…`).
  148. * **Later: `./deploy.sh`** on Loreana, in this folder: runs the gate (refuses on a failure;
  149. `--skip-tests` skips it LOUDLY), rsyncs the code to
  150. `[email protected]:/CONTAINERS/projects/tracker.worldapi.org` (never `storage/`,
  151. `.sessions/`, `.env`, `.scratch/`, `server.*`, logs — the preview is checked for them; no
  152. `--delete`), `docker compose up -d && docker compose restart` over `ssh -F /dev/null`,
  153. then waits for https://tracker.worldapi.org/ to answer 200.
  154. * `./deploy.sh --dry-run` = the gate + `rsync -n` + the commands it would run (no restart, no
  155. URL check). `--target DIR|HOST:DIR` and `--url URL` point it elsewhere (tested against a
  156. local directory, see STATUS.md).
  157. ## Data
  158. `storage/mpackdb/`: `users.db` (`identity` → this app's own user id, `users.hl`) plus, since
  159. step 2, the old tracker's data — `genres.db` (27), `persons.db` (15157), `shows.db` (9453),
  160. `seasons.db` (6430), `episodes.db` (231584), `follows.db` (128), `watches.db` (11692), all with
  161. fresh mpackdb ids and every reference re-pointed (`tools/migrate.hl`); the one old user is this
  162. app's user for ident short id `az5b2`. Counts and the referential-integrity proof:
  163. `STATUS.md` "ticket #2". Nothing of this is shown in the UI yet.
  164. ## Files
  165. | File | |
  166. |---|---|
  167. | `CONCEPT.md` | the creator's concept — do not edit |
  168. | `project.hl` | manifest: routes (`/login/callback`, `/login/failed`, `/login.js`, `/` component Home), the app's own session cookie |
  169. | `users.hl` | the ident exchange + the `usersTable`, copied from calendar.worldapi.org's `users.hl` |
  170. | `login.js` | bridge between `<ident-selector>`'s `ident-login` event and the shell, copied verbatim from calendar.worldapi.org |
  171. | `components/home.hl` | `/`: the empty homepage — no content, signed in or out |
  172. | `components/main.hl` | the shell (header: brand, `<ident-selector>`, log in / log out) |
  173. | `components/loginfailed.hl` | `/login/failed`: why a login didn't work |
  174. | `components/show.hl` | `/shows/:slug`: the show page (header, seasons, episodes, watch check) |
  175. | `components/unwatched.hl` | `/unwatched`: unwatched, already-released episodes of followed shows, newest first |
  176. | `shows.hl` | read-only data access (shows/seasons/episodes/genres/persons), reused by `/unwatched`/`/schedule` |
  177. | `follows.hl` | read-only per-user follow data (`followedShowIds`), reused by `/unwatched`/`/schedule` |
  178. | `watches.hl` | per-user watch state (episode/season checks, the shared check-icon class `watchClassOf`), reused by `/unwatched`/`/schedule` |
  179. | `styles.hl` | all CSS (imports the tokens from `shared/tokens.hl`; accent green `#4ec9b0`) |
  180. | `shared/tokens.hl` | the WorldAPI tokens, vendored verbatim from `ident.worldapi.org/shared/tokens.hl` |
  181. | `tests/browser.mjs` | the gate (above) |
  182. | `tests/seed-show.hl` | writes the gate's fixture show (two seasons, three episodes) and, since ticket #5, a follow of it for the test user, before the server starts |
  183. | `tests/identkit.mjs` | starts a throwaway ident copy for the gate, copied from calendar.worldapi.org's `tests/` |
  184. | `tests/cdp.mjs`, `tests/ports.mjs` | the CDP browser driver, copied from `ident.worldapi.org/tests/` |
  185. | `docker-compose.yml`, `Dockerfile`, `deploy.sh` | Byrodin container; the deploy from Loreana (section "Deploy") |
  186. | `tools/migrate.hl` | one-off: old MongoDB export → `storage/mpackdb/` (step 2, idempotent, see `STATUS.md`) |
  187. | `tools/verify.hl` | one-off: proves the migrated data against a COPY of `storage/mpackdb/` (counts, zero dangling references, one show end to end) |
  188. | `tools/relink-episode-seasons.hl` | one-off: links episodes with no `season` (step 2 leftovers) by `(show, seasonNumber)` — not run against the live data yet, see "What it does (step 4)" |
  189. ## Vendored Hybriel
  190. `bin/hybriel` + `plugins/` copied verbatim from `ident.worldapi.org` (2026-09-27), sha256
  191. `9e5e95b33680eb68a016e9e48a76c9f92193fd2ffdbdf437c1aab1bda2731a0d` — hybriel master
  192. 837fe120 (ident's mission 036), the newest vendored+gated build available locally. No local
  193. patch. Re-vendor the same way: copy `bin/hybriel` + `plugins/` from a current worldapi app,
  194. run the gate.

Branches

Latest commits

  • 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