docs: history keeper in architecture; deferred review minors in TODO; plan ticked

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-15 11:46:05 +02:00
1 parent 0d4a1a61fa
commit 49c9efa704
3 files changed
+34 -28

No files matched your search

+1 -1
View File
@@ -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. |
+6
View File
@@ -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.
+27 -27
View File
@@ -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: <fetchedFrom>` plus `original: <candidate.url>` 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<SearchSummary[]>; get(id: number): Promise<SavedSearch | null>; put(s: SavedSearch): Promise<void>; remove(id: number): Promise<void>; clear(): Promise<void> }`
- `memoryHistory(): HistoryStore`; `saveAndPrune(store, s): Promise<number[]>` (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, `<a download>` 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, `<a download>` 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`; `<p id="found-line">` with `<button id="search-again">` in the review header),
`src/web/app.ts`, `src/web/main.ts`, `src/web/styles.css`; create `src/web/recent.ts`.
- [ ] `AppOptions.history?: HistoryStore | null`. Without it the history elements stay hidden.
- [ ] Refactor `run()`'s display part into `show(r: RunResult, foundAt: string)`; `run()` and `open()` use it.
- [ ] After a run: `prev = await history.get(id)`; if found `carryEdits(prev.cards, session.cards, now)` and
- [x] `AppOptions.history?: HistoryStore | null`. Without it the history elements stay hidden.
- [x] Refactor `run()`'s display part into `show(r: RunResult, foundAt: string)`; `run()` and `open()` use it.
- [x] After a run: `prev = await history.get(id)`; if found `carryEdits(prev.cards, session.cards, now)` and
keep `prev.marks`; save. After edits: debounced save (500 ms). Rehost → `marks.rehosted`; download →
`marks.downloaded`; filed → `marks.filed`; each saves.
- [ ] `open(id)`: `fromSaved` → `session.restore` → fill inputs → `show`. Errors show in the stage line.
- [ ] `recent.ts`: `renderRecent(list, shelf, summaries, now, { open, remove })` — rows with thumbnail (Blob
- [x] `open(id)`: `fromSaved` → `session.restore` → fill inputs → `show`. Errors show in the stage line.
- [x] `recent.ts`: `renderRecent(list, shelf, summaries, now, { open, remove })` — rows with thumbnail (Blob
URL, revoked on re-render), title, relative time, summary line, marks, remove button; shelf of up to 6.
- [ ] Clear history asks `confirm()`.
- [ ] Commit `feat(web): history of searches on the page`.
- [x] Clear history asks `confirm()`.
- [x] Commit `feat(web): history of searches on the page`.
### Task 9: Verify in a browser, docs, finish
- [ ] Headless Chrome (scratch CDP script): run ToG without Wayback; untick one, edit a note; reload; History
- [x] Headless Chrome (scratch CDP script): run ToG without Wayback; untick one, edit a note; reload; History
shows the entry; open it → state restored; Search again → edits carried; download ZIP (page) → `unzip -t`.
- [ ] Userscript panel with the GM shim (scratch `cdp-us.ts`): download ZIP works from the shadow root.
- [ ] Private-window-like failure: stub `indexedDB.open` to error → page still works.
- [ ] Docs: ARCHITECTURE (modules, data flow), DECISIONS (ZIP writer, history storage), README (manual
- [x] Userscript panel with the GM shim (scratch `cdp-us.ts`): download ZIP works from the shadow root.
- [x] Private-window-like failure: stub `indexedDB.open` to error → page still works.
- [x] Docs: ARCHITECTURE (modules, data flow), DECISIONS (ZIP writer, history storage), README (manual
path with download; history), TODO (anything deferred).
- [ ] `git rm` this plan; merge to `main`; push.
- [x] `git rm` this plan; merge to `main`; push.