Files
mangabaka-seeder/docs/DECISIONS.md
T
2026-10-15 11:56:37 +02:00

18 KiB
Raw Blame History

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:images 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.

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.