Files
mangabaka-seeder/docs/DECISIONS.md
T

254 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Decisions
Append-only. Never edit or delete an entry — supersede it with a new one that links back.
One entry per decision a future maintainer would otherwise have to reverse-engineer.
No index: `grep -n '^## ' docs/DECISIONS.md` lists every entry (date, title, line number).
## 2026-10-14 — Adopt AGENTS.md as single source of truth
**Status:** accepted
**Context:** Agents had no durable project context and no consistent workflow between sessions.
The design context lived in `HANDOVER.md`, which is now split into this file, the spec and `docs/TODO.md`.
**Decision:** `AGENTS.md` is canonical; `CLAUDE.md`/`GEMINI.md` are pointers. Specs are permanent in
`docs/specs/`. Plans live in `docs/plans/` only while work is in progress and are removed before the
merge request. Both override the superpowers default paths.
**Alternatives:** Per-tool rule files (drift, synchronisation debt); no rules file (agents re-explore
the repo every session — measurably slower and more tokens).
**Consequences:** Rule changes happen in one file; pointer files must stay thin. Plans are working
artefacts: kept after merge they go stale and contradict the code, so the spec is the record.
## 2026-10-15 — Page views via our Shynet instance
**Status:** accepted
**Context:** The user asked for analytics from their self-hosted Shynet.
**Decision:** Shynet's snippet (deferred script plus `<noscript>` pixel) at the end of the page's `<body>`
(`src/web/index.html`). The userscript panel is not tracked: mangabaka.org isn't ours to measure.
**Alternatives:** none considered; the user named the tool.
**Consequences:** A third-party script runs on the page's origin, so it could read `localStorage`, including a
MangaBaka token saved with "Remember on this device". It is the user's own instance; anyone pointing the page at
another analytics host must keep that in mind.
## 2026-10-15 — Tapas source (spec 003)
**Status:** accepted
**Context:** The user asked for tapas.io next; comics and novels (chosen in chat).
**Decision:** A second `Source` beside Webtoons. The series is read from `/series/<slug>/info` (the plain
`/series/<slug>` is the episode reader); id and episode links resolve to it. Covers: the `.thumb-wrapper`
portrait with its size suffix removed (960×1440 original) and the `og:image` banner. Image URLs are normalised
to `us-a.tapas.io` (old pages name a CloudFront host that serves the same paths). Mature series sit behind an
age check that carries no data: they resolve from the check's `from` link as mature with no current covers and
a warning; we don't fake an age or a session. In the userscript the user's own Tapas cookies go along, so a
signed-in adult gets the real page. The same-series check compares the exact slug or id, which also keeps
`<slug>-novel` captures out of the Wayback history.
**Alternatives:** Tapas' episode JSON endpoint (no series images); getting past the age check (not ours to do).
**Consequences:** On the page, mature Tapas series only get archived covers.
## 2026-10-15 — Download as a ZIP, and a history in IndexedDB (spec 002)
**Status:** accepted
**Context:** First user review: contributors without a token want the cover files to upload by hand, and a
way back to series they searched before. Design approved in chat (instant restore; history on the page only;
one ZIP).
**Decision:**
- Our own store-only ZIP writer (`core/zip.ts`, ~100 lines): covers don't compress, and it avoids a
dependency. Downloads go through a Blob URL and `<a download>`; this works in the userscript's shadow root
on mangabaka.org (checked in Chrome under the site's CSP), so `GM_download` isn't needed.
- History lives in IndexedDB on the page's origin (`summaries` for the list, `searches` with the bytes),
one entry per MangaBaka series, 30 entries / 300 MB, newest always kept. Stored as plain data with the
source by id (and its name for the list). Expired litterbox/server links are dropped on restore; cards
record `hosting` (host, cache key, expiry) when rehosted.
- Searching a saved series again carries the user's edits over by SHA-256, and keeps the entry's marks.
**Alternatives:** A ZIP library (fflate) — a new dependency for ~100 lines; history in localStorage (5 MB,
strings) or re-running the search on open (minutes with Wayback); history on our server (privacy, accounts).
**Consequences:** History is per browser and lost with site data. Old entries can show covers MangaBaka has
since gained; filing always fetches a fresh snapshot, and the "Found …" line shows the age.
## 2026-10-15 — Wayback history starts off
**Status:** accepted (amends spec 001 §4.9 "include history (on)")
**Context:** First user review: a Wayback run takes minutes at the 10 s pace, while the current covers
come back in seconds.
**Decision:** "Old covers from the Wayback Machine" is unticked by default; the granularity stays 6 months.
**Alternatives:** Keep it on and rely on "Skip the rest of the history".
**Consequences:** Historical covers need one extra click.
## 2026-10-15 — Use our own dark theme when Dark Reader is on
**Status:** accepted
**Context:** With a light system theme the page is light, so the Dark Reader extension restyled it with
its generated colours instead of the designed umber theme (the user reported Dark Reader did not see it as
a dark site). Dark Reader marks `<html data-darkreader-scheme="dark">` while active and removes its styles
when `<meta name="darkreader-lock">` appears, even after load (read in its source, 2026-10-15).
**Decision:** `src/web/dark-reader.ts` watches for that mark; on `dark` it sets `<html data-theme="dark">`
(which turns on a copy of the dark tokens in `styles.css`) and adds the lock. One-way for the page's
lifetime; the `dimmed` (light) scheme is ignored. A test keeps the two dark token blocks identical.
The userscript panel is untouched: the rule cannot match inside its shadow root.
**Alternatives:** A static `darkreader-lock` (Dark Reader users on a light system would get the light
page); a manual theme toggle (not asked for).
**Consequences:** The dark tokens exist twice in `styles.css`. Turning Dark Reader off keeps the page
dark until reload. Relies on Dark Reader's attribute name, which is not a documented API.
## 2026-10-15 — Docker image built by Gitea CI
**Status:** accepted
**Context:** The user wants a Dockerfile that Gitea builds automatically, plus a docker-compose example,
mirroring the working workflow in their `budget2.0` repository.
**Decision:** Two-stage image on `oven/bun:1.3.14-slim` (no runtime dependencies, so only `src`, `scripts`
and the built page are copied). It runs as `bun`, listens on `0.0.0.0:8787`, and has a healthcheck on
`/api/config`. Storage is off by default; the compose example turns it on in a named volume at `/data/covers`.
The userscript is built when the container starts, not in the image, because its `@connect` list depends on
`MBSEED_PUBLIC_URL`, which is only known at run time. `bun.ts` handles SIGTERM/SIGINT: without it Bun as
PID 1 ignored `docker stop` and was killed after 10 s. The workflow copies budget2.0: tests first, then
Buildx, push to the Gitea registry with a `REGISTRY_TOKEN` secret (a package-write token; Gitea's job
token cannot publish packages), and a read-only smoke test of the pushed image.
**Alternatives:** Building the userscript in the image (wrong `@connect` for every deployment);
a distroless or compiled-binary image (smaller, but harder to debug, and Bun's slim image is enough).
**Consequences:** The `REGISTRY_TOKEN` secret must exist in Gitea before the image job can push. Starting
the container takes about a second longer for the userscript build.
## 2026-10-15 — Harden the server proxy and uploads (final review)
**Status:** accepted (amends "Implementation details: server API" below)
**Context:** The whole-branch review found that `/api/fetch` passed the upstream content type through. Anyone can
archive an HTML page on Wayback and link a user to our `/api/fetch?url=…` for it: the script would run on our origin
and could read a MangaBaka token saved with "remember on this device". `/api/upload` also accepted any Wayback URL
and any bytes starting with `GIF`, so our domain could be used as a 30-day file drop and the store filled.
**Decision:**
- Proxied bodies are always `application/octet-stream` with `nosniff`, `Content-Security-Policy: sandbox;
default-src 'none'` and `Content-Disposition: attachment`. Nothing in our clients reads the content type.
- `/api/fetch` and `/api/upload` require the header `x-mbseed: 1`, which our page and userscript send and a link or
form cannot.
- `/api/upload` rehosts only from a source's image hosts (`Source.imageHosts`) or Wayback's copy of one, and only
images 1–10 000 px a side.
- The origin check compares hosts, so `https://host` passes behind a TLS proxy that Bun sees as `http://host`.
**Alternatives:** A session cookie or the password gate (deferred, spec §4.11); serving the page and the proxy on
different origins (more deployment work).
**Consequences:** A new source must list its image hosts. Clients other than ours must send `x-mbseed: 1`.
## 2026-10-14 — Implementation details: server API, Webtoons fetch forms
**Status:** accepted
**Context:** Decisions made while implementing plan 001 that the spec leaves open.
**Decision:**
- `createApp(env)` returns the handler (spec §4.10 says `handle(req, env)`) so rate-limit state is bound once.
- `/api/*` accepts `Origin: https://mangabaka.org` besides our own, for the userscript.
- Behind the proxy, the rate limiter keys on the **last** `X-Forwarded-For` entry; earlier ones come from the client.
- `/api/fetch` buffers responses up to 20 MB instead of streaming: a simple cap; pages and covers are far smaller.
- Webtoons is fetched as `/-/-/-/list?title_no=N` (official) or `/-/canvas/-/list?title_no=N` (canvas); the wrong form is a 404, so the other is tried. A made-up slug made Webtoons hang, so the user's own path is never fetched.
- Test fixtures are marked `-text` in `.gitattributes` to stay byte-exact.
**Alternatives:** Stream `/api/fetch` (needs a counting stream for the cap); trust the first `X-Forwarded-For` entry (spoofable).
**Consequences:** Reverse proxies must append to `X-Forwarded-For` (see `docs/ARCHITECTURE.md`).
## 2026-10-14 — Spec amendments from testing the real services
**Status:** accepted (amends spec 001 §2.2–§4.5)
**Context:** Before planning, we parsed real pages and downloaded every cover for Tower of God
(current + Wayback every 6 months, 2015–2026) and the canvas example; the user reviewed them in
`samples/` (gitignored). Findings: Wayback answered `429` "suspected abusive bot traffic" to a fake
Chrome User-Agent but nothing at all to an honest one at one request per 10 s; most historical
`og:image`s are character or landing-page art, not covers; MangaBaka requires exactly one
`is_default` per series; the public images endpoint is paginated and lacks `is_default`.
**Decision:**
- History granularity (every 1 / 3 / 6 / 12 months, default 6) over the span of captures, one
snapshot per bucket — the user's idea, replacing "at most 60 snapshots".
- Wayback gets `mangabaka-seeder/<version> (+repo URL)` as User-Agent and one request per 10 s; a
snapshot that fails after retries stops the history and keeps partial results.
- Images taller than 1.6 : 1 are "landing-page art": type `other`, not selected by default.
- "Make this the series' main cover" is a checkbox, on by default, warning that it replaces the
main cover for every language.
- Retries only for `GET`/`HEAD`, never `POST`.
**Alternatives:** A fixed snapshot limit (arbitrary; either misses years or wastes requests);
spoofing a browser more convincingly (dishonest, and the honest UA simply works); dropping tall
art entirely (it is genuine Webtoons art some editors may want).
**Consequences:** A default run costs ~4–5 minutes of Wayback time, so the UI needs progress and
cancel. Our server makes every user's Wayback requests from one IP — if that gets blocked, the
userscript (user's own IP) still works.
## 2026-10-14 — Size cap on the covers folder; refuse when full, never evict
**Status:** accepted (extends "Server storage is a plain folder" below)
**Context:** The user asked for storage limits on the local folder. The site is open (no gate), so
uploads must not be able to fill the server's disk.
**Decision:** `MBSEED_STORAGE_MAX_BYTES` (default 1 GiB) caps the folder. Uploads past it get 507 and
the page offers litterbox/catbox instead. Files are never deleted before their 30 days.
**Alternatives:** Evict the oldest files (rejected: their links may be in requests still awaiting
review, which would break them silently); per-IP upload quotas (deferred until abuse shows up).
**Consequences:** Under heavy use the server host becomes unavailable until the sweep frees space.
## 2026-10-14 — Server storage is a plain folder on our server
**Status:** accepted (replaces the S3/R2 storage in the entries below)
**Context:** Self-hosted, a bucket is extra infrastructure. The user chose: "For now, store images in
a plain folder on the server."
**Decision:** `MBSEED_STORAGE_DIR` holds `<sha256>.<ext>`; our server serves it at `/covers/…`
under `MBSEED_PUBLIC_URL`. `bun.ts` deletes files older than 30 days (by mtime) at startup and every
24 hours. Writes are temp file + rename. It sits behind the `Storage` interface.
**Alternatives:** S3-compatible bucket (MinIO, R2, B2) with a lifecycle rule — kept as a later option.
**Consequences:** The server must be publicly reachable for MangaBaka to fetch covers, and its
uptime now matters until each review is done. Folder access needs Bun APIs, so it lives in
`storage-dir.ts`, not in the web-standard `app.ts`.
## 2026-10-14 — Spec signed off; self-hosted only; password gate deferred
**Status:** accepted (narrows the entry below)
**Context:** The user signed off spec 001 and said the app will run self-hosted, and that
password-gating should wait: "if it gets abused or we see too big amounts of traffic, we will deal
with it then."
**Decision:** First version runs `server/app.ts` under `Bun.serve` on our own machine behind our
reverse proxy. No gate, no Cloudflare entry; both stay designed in the spec (§4.10, §4.11) and are
listed under Someday in `docs/TODO.md`. The handler stays web-standard so either can be added later.
**Alternatives:** Build the gate now (cost without a demonstrated need).
**Consequences:** The site is open: the host allowlist, the 20 MB cap and the per-IP rate limit are
the only protections. The rate limit needs the real client IP from our reverse proxy
(`MBSEED_TRUST_PROXY`).
## 2026-10-14 — Also support catbox/litterbox; optional password gate; self-hostable server
**Status:** accepted (extends the two entries below)
**Context:** The user asked for catbox/litterbox support as well, and for a way to password-gate the
static site "such that we could lock the service behind our own server".
**Decision:** Image hosts are `server` (our storage, 30 days, default when configured), `litterbox`
(≤ 72h; uploaded straight from the browser, which works because litterbox sends CORS headers),
`catbox` (permanent; through our server for the page, direct for the userscript) and `direct`.
The server is one web-standard `fetch` handler with two thin entries: Cloudflare Workers (R2
binding) and `Bun.serve` (S3-compatible bucket) for our own machine. An optional shared password
(`MBSEED_PASSWORD`) gates every route, static assets included, with an HMAC-signed session cookie,
plus `Authorization: Bearer` for the userscript. Self-hosted instances may use the reverse proxy's
auth instead. "Behind our own server" was read as covering both the built-in gate and self-hosting.
**Alternatives:** Cloudflare Access in front of the Worker (free for small teams, but
Cloudflare-only and no help when self-hosting); per-user accounts (not needed for a small group);
Basic auth (can't log out, and the browser prompt is clumsy for the userscript).
**Consequences:** On Workers, assets must run through the Worker (`run_worker_first`) or the gate
leaks the page. The userscript works without any server when using litterbox or catbox. Litterbox
links may die before a slow review; the UI warns about it.
## 2026-10-14 — Temporary image hosting: Cloudflare R2 with a 30-day expiry
**Status:** accepted (supersedes "Rehosting on catbox.moe" below)
**Context:** A static page can't upload to catbox.moe (no CORS). The user said temporary hosting is
fine with an expiry of at most a month: if moderators take longer, the user reruns.
**Decision:** The Worker rehosts into R2 at `covers/<sha256>.<ext>`, with a bucket lifecycle rule
deleting objects after 30 days. `direct` (no rehost) stays as an option.
**Alternatives:** litterbox (CORS works, but 72h maximum — likely shorter than a review);
0x0.st (CORS and MangaBaka acceptance unverified); catbox (no CORS, permanent).
**Consequences:** Needs a Cloudflare account with a card on file (R2). Expected cost is $0 on
free tiers; abuse of the Worker is the cost risk, hence the allowlist and the per-IP rate limit.
## 2026-10-14 — Static page + Cloudflare Worker, and a userscript; Bun; links given directly
**Status:** accepted (supersedes "Stack: TypeScript / Node CLI" and "Automatic series matching" below)
**Context:** The user wants a static web page first, with the CLI as secondary, Bun instead of Node,
and to pass both the MangaBaka and Webtoons links. Checked 2026-10-14: `api.mangabaka.org` sends
`access-control-allow-origin: *` (submissions too); Webtoons HTML, pstatic images and Wayback do not.
**Decision:** One pure core with an injected `Transport`. The page uses a Worker proxy (allowlisted
hosts) for Webtoons/Wayback and calls MangaBaka directly. The userscript uses `GM_xmlhttpRequest`
and needs no proxy for fetching. The user chose to pursue both front ends. Tooling is Bun
(`bun test`, `bun build`). No automatic MangaBaka search; a `title_no` link check only warns.
**Alternatives:** Local Bun server only (not a hosted page); userscript only.
**Consequences:** Two front ends to keep in step; the Worker is public infrastructure to protect.
Cloudflare IPs may be throttled by Webtoons/Wayback — the userscript doesn't have that risk.
## 2026-10-14 — Original design decisions (from the design conversation)
**Status:** partly superseded — see the two entries above
**Context:** Recorded in the former `HANDOVER.md`.
**Decision:**
- Stack: TypeScript, chosen over Go (yambs is Go) to keep a browser extension or userscript possible.
*Runtime superseded: Bun, not Node.*
- Submission path: the API when a token is available, otherwise help with manual entry.
- Which images: all — poster, square thumbnail, banner/background and foreground art.
- Historical covers through the Wayback Machine.
- Main cover: the current poster (square for canvas) gets `is_default: true`, existing covers `false`.
- Rehosting on catbox.moe by default. *Superseded: R2, 30 days.*
- Content rating chosen by the user, default `safe`; mature titles show a warning.
- Image bytes kept in memory; sizes up to ~650 KB seen; 20 MB cap.
- Extensibility: new sources and image hosts are one module plus one registry line.
- Safety: nothing uploaded or sent without explicit user action; `save_mode: "review"`.
- Not now: fields other than images; following language editions.
- Automatic series matching via `/v2/series/match`. *Superseded: links given directly.*
**Alternatives:** not recorded
**Consequences:** See the spec.