tracker
All repositories: gitoria
10.8 KB
# Franchises and timelines (tracker.worldapi.org#19, mission 020 (old 066))Creator: "we have franchises, and timelines, and some franchises have multiple timelines ... make them sortable by timeline orrelease date at shows the release date of the last episode counts. in a show or movie page we show a franchise widget with< Prequel | TimelineName | Sequel > and if the timeline is part of a franchise that franchise is above it and when you click onit you get the franchise index." Names: "Franchise", "Timeline"; navigation `/franchises`.## Data (`franchises.hl`, new tables — nothing in `shows.db` changes)| Table | Fields ||---|---|| `franchises.db` (`!slug`) | `name`, `slug`, `shortId` (always null for now), `summary`, `created` || `timelines.db` (`!slug`) | `name`, `slug`, `franchise` (@id or null), `order` [show @id …] (in-world order), `source` (`tmdb`/`editor`/`seed`), `tmdbCollection` (TMDB id or null), `orderKind` (`in-universe`/`release`/none — #40; shown as "· in-universe order" beside the count), `supersededBy` (#40: @id of the curated timeline that holds ALL its titles → hidden), `created` || `franchiseseed.db` | #40: what the curated seed made — `kind` (`franchise`/`timeline`/`collection`/`missing`), `key`, `ref` (@id), `order` + `franchise` (what it last wrote), `created`, `why`, `at` || `collectionchecks.db` | pk = the movie's show @id; `collection` (TMDB id or null), `why` (`collection`, `collection known`, `no collection`, `TMDB 404`, `collection 404`), `at` — the seed's progress |* Slugs like the shows' urlSegment: `Star Trek — Prime` → `Star-Trek-Prime`; taken → `-2`, `-3`; never `edit`.* A title may be in several timelines → one widget per timeline. `byShow` (show @id → [timeline @id]) is built at start(`ensureFranchises`, one scan) and kept current by every edit.* Inspect: the pages (`/franchises` lists every timeline with its count), or a small .hl script opening`new MPackDB(file = '<copy>/timelines.db', primaryKey = '@id', indexes = ['!slug'])` + `find(null, null)` — ONLY on a copyor with the app stopped (opening a table rewrites its files, hybriel#40).## Ordering* **Timeline** = `order` as edited (the seed: TMDB collection parts by `release_date`).* **Release** = a movie's `release`; a SERIES' LAST episode: the newest one already out (≤ today, UTC), else the newestannounced one, else the series' own `release`. No date → last. Equal dates keep the timeline order. Shown per row as`1996-11-18` / `last episode 2020-06-15`.* Franchise index: its timelines oldest first (by their earliest release).* Public like all lists (mission 010 (old 054)): pages and prequel/sequel links skip titles that are not `adult == false`; the editorshows every title.## Pages| Route | Component | ||---|---|---|| `/franchises` | `components/franchises.hl` | franchises (→ index, n timelines), then the timelines without one (TMDB collections) with n titles; creator: "Edit" || `/franchises/<slug>` | `components/franchise.hl` | the index: name, summary, each timeline a header (name → its page, count, `Timeline | Release` toggle) + its rows || `/timelines/<slug>` | `components/franchise.hl` | one timeline, its franchise above it; creator: "Edit" || `/franchises/edit` | `components/franchiseedit.hl` | creator: new franchise (name, summary), new timeline (name → its editor), find a timeline by name || `/timelines/<slug>/edit` | `components/timelineedit.hl` | creator: franchise chips (None + all; a tap attaches), rows ↑ ↓ ✕, "Add titles" (search.hl index) || widget on `/shows/<slug>` | `components/franchisewidget.hl` | composed by `components/show.hl` under the header: franchise above, `‹ Prequel | name | Sequel ›`, the title under each side, a missing side greyed out (`span.off`) |The sort toggle is client-only (`flatRows` is pure; both orders come with the page). One flat row list (`head`/`item`rows) instead of a nested for (hybriel#86).**Editor = the creator only**: `isEditorSession` = the signed-in user's ident id is in `TRACKER_EDITORS` (default `az5b2`).Every editing face (`fedCreateFranchise`, `fedCreateTimeline`, `tedSetFranchise`, `tedMove`, `tedRemove`, `tedAdd`) checks itagain server-side; `fedFindTimelines` / `tedSearch` are public reads. Not built: rename/delete of franchises or timelines,editing a franchise's summary (open point).## The seed (TMDB movie collections)In the app, own `every(0.1)` clock (`project.hl collectionTick` → `franchises.hl seedStep`), ONE movie per step:1. `GET /movie/<tmdbId>` → `belongs_to_collection` (none → check row `no collection`; 404 → `TMDB 404`; other failure →back of the queue, ≤ 2 retries per start).2. A collection seen before (`collectionsDone`, from check rows + timelines) → just a check row, no further request.3. Else `GET /collection/<id>` → parts sorted by `release_date`, mapped to OUR non-adult movies by tmdbId. **≥ 2 → onetimeline** (`source = 'tmdb'`, no franchise); < 2 → none (`small`). Every matched part gets a check row at once, so itsown details request is never made.Pause after a step: 260 ms × requests made (≤ 40 / 10 s, like the adult backfill). Waits while the daily sync or the adultbackfill runs. Queue at start = every movie with a tmdbId, `adult != true`, without a check row → resumes after a restart;nothing to do → one log line. Persisted every `TRACKER_COLLECTIONS_PERSIST_EVERY` (25) steps and at the end. A hand-editedtimeline is never touched. Log: `collections: start, N movies to check`, every `TRACKER_COLLECTIONS_LOG_EVERY` (500)`collections: pos/N movies, checked= timelines= titles= small= noCollection= notFound= requests= errors= retries= seconds=`,`collections done: …`. `TRACKER_COLLECTIONS=0` switches it off (off too without `TMDB_READ_TOKEN`).Re-seed (e.g. after many imports): delete `collectionchecks.db*` with the app stopped — existing collection timelines areskipped (their `tmdbCollection` is known); NEW parts of an existing collection are not added (open point).Real data: STATUS.md, ticket #19.## Test* Gate: `tests/franchises.mjs` (own storage `.scratch/franchise-store`, fixture `tests/seed-franchises.hl` on top of`tests/seed-show.hl`, fake TMDB `collections`): seed + resume + pacing, widget (ends, client-side Sequel click, franchiseabove), `/franchises`, index sort both ways (series by last episode), editor only for `TRACKER_EDITORS`, 390/1280 nosideways scroll, screenshots with `TRACKER_GATE_SHOTS`.* `tests/browser.mjs` runs with `TRACKER_COLLECTIONS=0` (its fake TMDB request counts must not see the seed).* Real data: `tests/realdata-066.mjs` (procedure in STATUS.md).## Since the merge (mission 023 (old 070))* Headings are #20's typed ones: "Franchises" (blueLight), "Franchise | <name>", "Timeline | <name>" (greenLight), also on thetimeline editor. Title links use `shows.hl titlePath` (`/movies/…`, `/series/…`, `/shows/…`).* The seed waits for the daily sync, the adult backfill, the details repair, the kind backfill AND the credits job (one TMDBpacing at a time): on the first start after the deploy it runs last (~32–37 min after the kind backfill's ~16–21 min).## The curated seed (tracker#40, mission 034)Creator: "you never build timlines and franchises". Architect: a curated seed for the big franchises from a hand-written order filein the repo (data, not code), movies AND series, in-universe order where known, else release; missing titles fetched via the searchimport; a resumable job on deploy; the editor stays for corrections.* **Data**: `data/franchises.json` (`TRACKER_FRANCHISE_FILE`). Per franchise `name`, `summary`, `collections` (TMDB collection idswhose collection-seed timelines join it), `timelines` [{ `name`, `order` (`in-universe`|`release`), `titles` [[`tv`|`movie`, TMDBid, "title (year)"]] }]. The ids were checked against themoviedb.org (title + year) on 2026-10-04. A title may sit in severaltimelines (Terminator's alternate lines all start with T1 + T2; Peacemaker in DCEU and DCU).* **Job** (`lib/jobs.hl franchiseSeedTick` → `lib/franchiseseed.hl`): waits for the merge, sync, backfills, repair, kinds, creditsAND the collection seed; prepare = the file's titles we lack (`search.hl titleIdByTmdb`, a tombstone → its keeper) minus theremembered 404s; one import per step (`details.hl importWithCredits` — the search's "Add": complete with seasons, poster, ids,TVmaze, cast + crew), pause `tmdb.hl pauseMsAfter`; queue done → ONE build step (no TMDB): franchise (seed row → else thecreator's of that name, adopted → else new), collections attached (only a timeline without franchise, once), each timeline:no row → the creator's of that name? kept : new (`source = 'seed'`, needs ≥ 1 title, else `empty`); row → order/franchise asthe seed last wrote? update to the file : `kept`.* **Editor changes win**: comparing the timeline with the seed's own record — any ↑ ↓ ✕, added title or franchise chip by thecreator makes it "edited", never written again (also a merge.hl id swap counts as an edit — rare). The creator can renamenothing yet (open point of #19), so a renamed file timeline would be made anew.* **Log**: `franchise seed: start, 17 franchises, 31 timelines, 285 titles in the file — N we have, M to import, K TMDB lacks`,each import's `search import: …`, every 25 imports a progress line, `franchise seed done: franchises=… (created= adopted=)timelines=… (created= updated= unchanged= kept= empty=) collections= titles= had= imported= failed= missing= placed= requests=tvmazeRequests= seconds=`.* **Re-run after editing the file**: just restart — new titles are imported, unedited seeded timelines follow the file.To make the seed forget everything: delete `franchiseseed.db*` with the app stopped (it then ADOPTS its own franchises by nameand keeps its own timelines as "the creator's" — so delete those too if a clean re-seed is wanted).* **Test**: gate `tests/franchiseseed.mjs` (24 checks); real copy `tests/realdata-m034.sh` (STATUS.md mission 034).* **Superseded collections** (team lead: no second widget, e.g. First Contact = TNG Collection + Prime): a TMDB collectiontimeline whose titles are ALL in one curated (`source = 'seed'`) timeline gets `supersededBy` = that timeline — no widget, not on/franchises nor a franchise index, not counted; NOT deleted: `/timelines/<slug>` still answers and the editor's finder lists itas "… · hidden: all in <name>". Decided by `franchises.hl seededCoverOf`; set by the collection seed when it MAKES a timeline(`collections done: … superseded=N`) and by the curated build for existing ones (`superseded=`, `unsuperseded=` when the coveris gone again). A collection only PARTLY in a curated timeline stays shown and joins that franchise (`joined=`). Real copy2026-10-04: 24 superseded (STATUS mission 034), First Contact shows ONE widget (Star Trek — Prime, STAR TREK above).
Branches
- mainmain branch
Latest commits
- e5945d2fMerge t40 (tracker#40 curated franchises, Franchises menu, superseded collections) into main: deploy.sh lists all six gates (browser, kinds, franchises, pager, franchiseseed, check-theme); search.hl keeps #37's personPhotoOf + #40's importWithCredits; pager gate runs with TRACKER_FRANCHISE_SEED=0; gates 374/0, 32/0, 53/0, 229/0, 26/0, check-theme 0mre
- b638e99dMerge t38 (tracker#38 pagination, #35 Returning/Airing label) into main: LOG/STATUS keep both sides; pager gate follows #37's /people (everyone, last updated first: seed updatedAt); gates pager 229/0, browser 374/0mre
- f8ffa4dbtracker: report 032 + The Remaining + #39mre
- 7565a863tracker: LOG timemre
- b10f00c8tracker#39: double episodes — migrated episodes whose TMDB id TMDB replaced are adopted by their number in the sync (old id -> migratedTmdbId); merge.hl step 3 merges each season's doubles at start (keeper: most watches > synced > first; watches moved/parked; tombstones into mergedEpisodes, nothing deleted); tools/count-duplicate-episodes.hl; gate fixture + paths-m039; live copy 850 -> 0 in 64 s; gates 373/0, 32/0, 52/0mre
- 8f1d4542tracker#40: superseded collections — a TMDB collection timeline whose titles are all in one curated timeline is hidden (supersededBy; kept: own page + editor finder), set by the collection seed when it makes one and by the curated build (lifted when the cover is gone); partly covered ones join that franchise; no second widget (First Contact: only Star Trek — Prime); gate franchiseseed 26/0, browser 365/0, kinds 32/0, franchises 53/0, check-theme 0; real copy 24 supersededmre
- 9448d643tracker#35 follow-up (mission 033): TVmaze 'Running' is labelled 'Returning', or 'Airing' while a non-special episode of the two newest seasons is released within today +-7 days (data unchanged); gate fixtures Running/Airing/Aired + a special; browser 366/0, kinds 32/0, franchises 52/0, pager 229/0, check-theme 0mre
- 7d4b293dtracker#40: "Franchises" in the main menu (desktop header after People, phone sidebar) → /franchises, marked on franchise and timeline pages; gates 365/0, 32/0, 53/0, 24/0, check-theme 0mre
- 8751adb8tracker: report 032mre
- 9bce1f65tracker mission 032: STATUS gate files + the hour-boundary flakemre
- 718bfb89tracker#37 (mission 032): /people = everyone, last updated first (updatedAt stamped by the person fill; view built at boot, touched people first at once), photo + name tiles (person colour) with the /movies pagination, /people/<letter> removed; photo = our file, tmdbProfile, a cast/crew entry's profile (in-memory map at boot), else the new 'no photo' placeholder; new cast/crew/created_by people keep tmdbProfile; search people rows with the photo; /settings = the heading only; util.hl sortDesc starts from sorted runs (same result, 105k: 1.6 s -> 0.15 s); gates 369/0, 32/0, 52/0, check-theme 0; README/STATUS/LOGmre
- 93dfb0batracker#40 (mission 034): the curated franchises — data/franchises.json (17 franchises, 31 timelines, 285 TMDB titles, movies + series, in-universe/release order, 12 TMDB collections attached); lib/franchiseseed.hl + jobs.hl franchiseSeedTick (last start job, imports missing titles via details.hl importWithCredits = the search's Add, one per step paced, then one build; franchiseseed.db: editor changes win, the creator's same-name franchise adopted / timeline left alone, 404 remembered, resumable, idempotent); timeline heads 'N titles · in-universe order' (orderKind) and wrap on a phone; series pages show the widget; new gate tests/franchiseseed.mjs (5th in deploy.sh), the others run with TRACKER_FRANCHISE_SEED=0; gates 365/0, 32/0, 52/0, 24/0, check-theme 0; real copy 196 imported, 0 failed, 7 min, restart unchanged=31mre
- 03ec792ftracker#38 (mission 033): pagination goes exactly to the clicked page — tilelist read the clicked button's text after pagination.hl's own handler had rebuilt the buttons (real clicks only); now li.current, else the button's own text; new gate tests/pager.mjs (5 lists x 11 pages, 390/1280, real + script clicks, Back/Forward) 229/0; browser 365/0, kinds 32/0, franchises 52/0, check-theme 0mre
- 96ba683adeploy.sh: a gate without a 'passed,' line (check-theme) no longer ends the scriptmre
- eb3b9205tracker: report 031mre
- 9b5d2e89tracker mission 031: README (What it does, Test: four gates + the #32 checks, Files: theme/, new pages), STATUS (real copy, A/B load, how to repeat, open points), LOGmre
- 39950e4ctracker#32 (mission 031): the WorldAPI theme (theme/ vendored verbatim from layouts.worldapi.org 85b5654; styles.hl inherits it: accent green-dark, type colours 1-6; own base/header rules, row lines, genre-pill and inverted-button frames removed, the season foldable keeps its line; check-theme 21 -> 0, 4th deploy gate; main actions class primary) and the #32 header (theme AppHeader/MainMenu/UserMenu/Sidebar/ContentFirst: desktop brand, search, Series|Shows|Movies|Genres|People, user icon with Unwatched..Settings, Logout; signed out the ident selector, phone the iD icon dropdown; phone menu in the sidebar overlay; marked entry by :has); /find -> /search/<q>, /genres, /people(/<letter>), /settings; main { ContentFirst { slot } } works around the hl:web one-line slot bug; gates 365/0, 32/0, 52/0, check-theme 0mre
- a386dc92tracker: reports 029 + 030mre
- 71e0fd7dtracker missions 029 + 030: README (What it does, Files, gate count), STATUS (real-copy numbers, how to repeat, open points), LOGmre
- d36ea6eatracker#34 + #35 (mission 030): Follow directly under the poster, as wide as the poster (show.hl, styles.hl); the status pill next to a series' title — TVmaze's status (new tvmazeStatus, stored by the sync's TVmaze merge) else TMDB's, TVmaze Ended + TMDB Canceled = Canceled, inverted (filled, dark text, no border), green running / yellow pending / red canceled / muted ended (shows.hl statusOf); the daily delta asks TVmaze's status of an unfollowed series TVmaze's change list names (dailysync.hl syncRunStep, sync.hl syncTvmazeStatus); the status backfill after the details repair (backfill.hl, jobs.hl statusTick; resumable, 550 ms per TVmaze request); gates 354/0, 32/0, 52/0mre