244 lines
18 KiB
Markdown
244 lines
18 KiB
Markdown
# 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 — 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.
|