From 49c9efa704a09d70c24e16e152e08d92ac7eb13a Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 15 Oct 2026 11:46:05 +0200 Subject: [PATCH] docs: history keeper in architecture; deferred review minors in TODO; plan ticked Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 2 +- docs/TODO.md | 6 +++ docs/plans/002-download-and-history.md | 54 +++++++++++++------------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a08f932..aae6a87 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -15,7 +15,7 @@ self-hosted Bun server (primary), a userscript, and later a Bun CLI. | `src/core/session.ts` | Review state shared by the page and the userscript: cards, main cover, rehosting (cards remember host, cache key and expiry), manual text, the download bundle, `restore` from history, token flow (preview → confirm → save for review). | | `src/core/zip.ts`, `download.ts` | Store-only ZIP writer (CRC-32, UTF-8 names); the download bundle: file names `NN_role_lang[_YYYY]_WxH.ext` and `covers.txt` (spec 002 §2). | | `src/core/saved.ts` | History model (spec 002 §3): `SavedSearch` (plain data, source by id), `toSaved`/`fromSaved` (drops expired links), `carryEdits` (search again), `pruneIds` (30 entries, 300 MB), `HistoryStore` interface and an in-memory one. | -| `src/web/` | Built. The page ("Obi Shelf" design, see `DESIGN.md`): `template.ts` markup shared with the userscript panel, `app.ts` wiring (`mountApp`), `render.ts` shelf and timeline views, `view-model.ts` pure helpers, `transport.ts` (direct to MangaBaka/litterbox, everything else via `/api/fetch`), `styles.css` scoped for a shadow root, `save-file.ts` (Blob download, works in the panel's shadow root), `history-db.ts` (IndexedDB `HistoryStore`; null when storage is refused), `recent.ts` (History list and the start screen's recent shelf; the page only), `dark-reader.ts` (switches to the page's own dark theme and locks Dark Reader out when that extension is darkening the page). | +| `src/web/` | Built. The page ("Obi Shelf" design, see `DESIGN.md`): `template.ts` markup shared with the userscript panel, `app.ts` wiring (`mountApp`), `render.ts` shelf and timeline views, `view-model.ts` pure helpers, `transport.ts` (direct to MangaBaka/litterbox, everything else via `/api/fetch`), `styles.css` scoped for a shadow root, `save-file.ts` (Blob download, works in the panel's shadow root), `history-db.ts` (IndexedDB `HistoryStore`; null when storage is refused), `history-keeper.ts` (when the series on screen is saved: after a search, debounced edits, marks; flushes before a new search, an open and page hide; removal/clear stop tracking), `recent.ts` (History list and the start screen's recent shelf; the page only), `dark-reader.ts` (switches to the page's own dark theme and locks Dark Reader out when that extension is darkening the page). | | `src/server/` | Built. `app.ts`: web-standard handler (`createApp(env)`) — static assets, `/api/config`, `/api/fetch` allowlisted proxy (redirects re-checked, 20 MB cap), `/api/upload` (store or catbox; takes a source URL, never bytes), `/covers/*`. `storage.ts` (interface, in-memory) and `storage-dir.ts` (folder, size cap, 30-day sweep). `config.ts` (env, client IP), `rate-limit.ts`, `assets-path.ts`. `bun.ts`: thin `Bun.serve` entry. Gate and Workers entry deferred. | | `src/userscript/` | Built. `gm.ts`: `GM_xmlhttpRequest` transport (no proxy; honest User-Agent for Wayback) and GM storage. `main.ts`: a "Seed covers" launcher on MangaBaka series pages (follows client-side navigation) opening the page's app in a shadow-root panel; optional server URL in its settings. Built by `scripts/build-userscript.ts` into `dist/web/mbseed.user.js`, served at `/mbseed.user.js`. | | `src/cli/` | Later: Bun CLI. | diff --git a/docs/TODO.md b/docs/TODO.md index 07d8935..63e3a0d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -38,6 +38,12 @@ Design: [specs/001-webtoons-covers.md](specs/001-webtoons-covers.md). - [ ] Where are personal access tokens created in the MangaBaka UI? Add it to the README. - [ ] Does the MangaBaka cover form accept fields filled by a userscript? +- [ ] Small fixes from the spec 002 review (deferred as minor): Search again should use the stored inputs, not the + fields; a prune failure after a good save reports "couldn't be saved"; ZIP times are UTC, not local; accented + titles lose letters in the ZIP name (Pokémon → pok-mon); covers.txt shows the date, not the time; the + "rehosted" mark says "rehosted on direct" for no rehosting and outlives expired links; `openHistory` reads + `globalThis.indexedDB` outside its `try`. + ## Someday - [ ] S3-compatible bucket as a `Storage` option, if a single server's disk stops being enough. - [ ] Password gate (spec §4.11) — only if the open site gets abused or traffic grows too large. diff --git a/docs/plans/002-download-and-history.md b/docs/plans/002-download-and-history.md index c3d5c1e..9c0a575 100644 --- a/docs/plans/002-download-and-history.md +++ b/docs/plans/002-download-and-history.md @@ -52,18 +52,18 @@ checked in headless Chrome. The userscript only gains the shared download button **Produces:** `interface ZipEntry { name: string; bytes: Uint8Array; modified: Date }`, `crc32(bytes: Uint8Array): number`, `zipStore(entries: readonly ZipEntry[]): Uint8Array`, `class ZipError`. -- [ ] Failing tests: +- [x] Failing tests: - `crc32(new TextEncoder().encode("123456789")) === 0xcbf43926`; `crc32(new Uint8Array()) === 0`. - Two entries (one UTF-8 name `03_poster_zh-Hant_480x623.jpg`, one `covers.txt`): write to a temp file, `unzip -t` exits 0, `unzip -p file name` returns the original bytes; general-purpose flag bit 11 set (bytes 6–7 of the local header). - `zipStore` throws `ZipError` when total size would exceed 0xFFFFFFFF (pass an entry whose `bytes` is a fake object with `length` 2**32, checked before any copy). -- [ ] Run, see them fail. -- [ ] Implement: table-driven CRC-32 (poly 0xEDB88320); per entry a local header (sig 0x04034b50, version 20, +- [x] Run, see them fail. +- [x] Implement: table-driven CRC-32 (poly 0xEDB88320); per entry a local header (sig 0x04034b50, version 20, flags 0x0800, method 0, DOS time/date from UTC fields, crc, sizes, name length), name, data; then central directory entries (sig 0x02014b50, offsets) and the end record (sig 0x06054b50). Little-endian via DataView. -- [ ] Run, see them pass. Commit `feat(core): store-only ZIP writer`. +- [x] Run, see them pass. Commit `feat(core): store-only ZIP writer`. ### Task 2: Download bundle (`src/core/download.ts`) and `Session.bundle` @@ -77,13 +77,13 @@ checked in headless Chrome. The userscript only gains the shared download button - `downloadBundle(input: BundleInput): { zipName: string; files: ZipEntry[] }` — `files` = covers then `covers.txt`. - `Session.input: RunInput | null` (set by `run`, by `restore`), `Session.bundle(now = new Date())`. -- [ ] Failing tests: names (`01_poster_en_480x623.jpg`; historical `seenAt` 2019… → `_2019_`; `zh-Hant` +- [x] Failing tests: names (`01_poster_en_480x623.jpg`; historical `seenAt` 2019… → `_2019_`; `zh-Hant` kept; 100 cards → `001_`); `slug("Tower of God")` = `tower-of-god`, `slug("나 혼자만 레벨업")` = `series`; zip name `tower-of-god-9999-covers.zip`; `covers.txt` contains the header lines, each file name, `type: volume · language: en · rating: safe`, `MAIN COVER` only for the main one, the note, and `source: ` plus `original: ` when it differs; `Session.bundle()` includes only selected cards in shelf order and marks the main cover (via `defaultChoice()` kind `new`). -- [ ] Run, fail. Implement. Run, pass. Commit `feat(core): download bundle of the selected covers`. +- [x] Run, fail. Implement. Run, pass. Commit `feat(core): download bundle of the selected covers`. ### Task 3: Link expiry and hosting on cards @@ -97,10 +97,10 @@ checked in headless Chrome. The userscript only gains the shared download button - `Session.restore(result: RunResult, cards: ReviewCard[], makeDefault: boolean, input: RunInput | null)`: sets state, resets conflicts, puts each card's `hosting.key → hostedUrl` back in the rehost cache. -- [ ] Failing tests: `linkExpiry` per host; after `rehost("litterbox", {litterboxTime: "1h"})` with the fake +- [x] Failing tests: `linkExpiry` per host; after `rehost("litterbox", {litterboxTime: "1h"})` with the fake transport, a card has `hosting.by === "litterbox"` and `until` ≈ now + 1 h; after `restore` a second litterbox rehost of the same card makes no upload call. -- [ ] Run, fail. Implement. Run, pass. Commit `feat(core): remember where and until when a cover is hosted`. +- [x] Run, fail. Implement. Run, pass. Commit `feat(core): remember where and until when a cover is hosted`. ### Task 4: Saved searches (`src/core/saved.ts`) @@ -119,11 +119,11 @@ checked in headless Chrome. The userscript only gains the shared download button - `interface HistoryStore { list(): Promise; get(id: number): Promise; put(s: SavedSearch): Promise; remove(id: number): Promise; clear(): Promise }` - `memoryHistory(): HistoryStore`; `saveAndPrune(store, s): Promise` (removed ids). -- [ ] Failing tests: round trip keeps edits, main-cover flag, input, bytes identity of covers; unknown +- [x] Failing tests: round trip keeps edits, main-cover flag, input, bytes identity of covers; unknown source → `SavedSearchError`; drops expired litterbox, keeps catbox; carries edits by hash (unticked stays unticked, note kept; new cover keeps defaults; expired link not carried); prune by count (31 → oldest out) and by bytes (newest kept even when alone over the cap); `memoryHistory` lists newest first; `saveAndPrune`. -- [ ] Run, fail. Implement. Run, pass. Commit `feat(core): saved searches for the history`. +- [x] Run, fail. Implement. Run, pass. Commit `feat(core): saved searches for the history`. ### Task 5: View helpers @@ -133,7 +133,7 @@ checked in headless Chrome. The userscript only gains the shared download button "yesterday", "4 days ago", else `YYYY-MM-DD`); `markText(m: Marks): string[]` (["downloaded", "rehosted on litterbox", "filed #123"]); `summaryLine(s: SearchSummary): string` ("14 new · 3 on MangaBaka"). -- [ ] Failing tests, implement, pass, commit `feat(web): history view helpers`. +- [x] Failing tests, implement, pass, commit `feat(web): history view helpers`. ### Task 6: Download button (page and panel) @@ -141,10 +141,10 @@ checked in headless Chrome. The userscript only gains the shared download button `src/web/styles.css` if needed; create `src/web/save-file.ts` (`saveFile(bytes, name, type, root)`: Blob URL, `` appended to `root`'s body or shadow root, click, revoke after 30 s). -- [ ] `draw()` sets `ui.download.disabled = selected === 0` and its label "Download N covers (ZIP)". -- [ ] Click: `const b = session.bundle(); saveFile(zipStore(b.files), b.zipName, "application/zip")`; +- [x] `draw()` sets `ui.download.disabled = selected === 0` and its label "Download N covers (ZIP)". +- [x] Click: `const b = session.bundle(); saveFile(zipStore(b.files), b.zipName, "application/zip")`; then `onDownloaded` hook (Task 8 uses it for the mark). -- [ ] Typecheck, tests, build. Browser check (Task 9). Commit `feat(web): download the selected covers as a ZIP`. +- [x] Typecheck, tests, build. Browser check (Task 9). Commit `feat(web): download the selected covers as a ZIP`. ### Task 7: IndexedDB history store (`src/web/history-db.ts`) @@ -153,7 +153,7 @@ Blob URL, `` appended to `root`'s body or shadow root, click, revoke sorts by `savedAt` desc; every request wrapped so errors reject the promise. If `indexedDB` is missing or `open` fails, `openHistory` resolves to `null` and the page runs without history. -- [ ] Implement (thin; covered by the browser check). Commit with Task 8. +- [x] Implement (thin; covered by the browser check). Commit with Task 8. ### Task 8: History UI (impeccable `craft`, Obi Shelf) @@ -162,23 +162,23 @@ sorts by `savedAt` desc; every request wrapped so errors reject the promise. If in `.mbseed-empty`; `

` with `