Files
mangaBookmark/CONTEXT.md
T
sulthan 21615be2bd feat: one registry entry per Site, one shared Series-page read (#95)
Closes #94.

## What

Two phases per the spec, in three feature commits plus two review-fix commits:

**Phase one — one registry entry per Site** (`2d134fb`)
The six per-site comparison points that used to live across three files collapse
into one `sites` map in `backend/internal/latest/sites.go`: Latest Chapter parse,
Cover parse, browser-backed list, fetcher route, host pins, and the browser
payload read all become lookups into it. `browserBackedSites()` is derived from
the registry (sorted, deterministic); `fetcherFor` and `fetchableSeriesURL` keep
their signatures and become lookups; `BrowserFetcher.Get` dispatches through the
entries' `Read`/`Done` while the tab lifecycle stays in `BrowserFetcher.run`.

**Phase two — one shared Series-page read** (`f215130`)
`readSeriesPage` (new `read.go`) performs the read the Poll and the Acquisition
have in common: gate, route, fetch, parse Latest Chapter, parse Cover address.
It returns facts only — polling and persistence policies (stamp order, cooldowns,
cover policy) stay with the callers; `acquire.go` gained the comment naming the
deliberate post-fetch stamp order. The poll's legacy cover heal and the
no-chapter byte-count diagnostic were restored after review (`d998f87`) so the
claims "the Poll keeps its own Cover policy" and "pinning is the only
behavioural change" both hold.

## Behaviour

- All six Sites now pin their host exactly; asura/demonic/comix previously
  accepted any https host. For asura this is a strict improvement: its dead old
  domain redirects deep links to the site root and would parse the wrong
  document.
- Everything else is unchanged: existing parse tables, the challenge-body table
  and the gate table pass unmodified except the one deliberate exception — the
  gate table gains the three new pin cases.

## Security invariants preserved

- The address gate is recognisably the same rule, now a single registry lookup:
  `https` + exact hostname match, all callers route through it. No fetch path
  was widened; asura/demonic/comix were narrowed.
- The second host pin inside each browser entry's Read is retained deliberately
  (browser = strong SSRF primitive, `series_url` is client-supplied) and is not
  deduplicated against the shared gate.
- Review hardening: `fetcherFor` now fails closed for unknown site strings
  (previously fell through to the TLS fetcher on an unreachable path), and the
  browser dispatch iterates a sorted list so outcomes cannot depend on map order.
- The security review's log-injection finding was checked against Go's
  `url.Parse` and does not hold: control characters are rejected anywhere in a
  URL, so a client-supplied value in a log line cannot carry a newline.

## Review

Reviewed on three axes (spec, standards, security) by read-only subagents over
`672c16f..f1b26f4`. No blocking findings; all minor/nit findings addressed in
`d998f87` and `700de20`. Verified end to end with `go test ./...` (Docker
Postgres per test package) on every commit.

## Out of scope (tracked separately)

- Dropping asuracomic.net (CORS allowlist, userscript match, API fixtures,
  live env) — separate issue, per spec.

Reviewed-on: #95
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
2026-08-12 05:43:37 +07:00

4.4 KiB

Bookmark Manager

Read-progress tracker for serialised fiction. A reader browses third-party manga and novel sites; userscripts capture where they got to and sync it to a self-hosted backend, so progress survives across sites and devices.

Language

Series: One ongoing work — a manga or a novel — as published by a Site. Identified by the canonical slug the Site itself publishes for it, never by its title and never by a Chapter Slug. A Series exists once and is shared by every Reader who bookmarks it; it owns the facts that are true regardless of who is reading — title, cover, Latest Chapter. A Reader cannot change them; they describe the Series, not anyone's relationship to it. Avoid: manga, title, book, comic

Site: One third-party source a Series is published on. A Series on two Sites is two Series. Avoid: source, host, provider, domain

Chapter Slug: A slug a Site builds its chapter addresses from. Not an identity: one Series may have several, any of them may differ from the slug that identifies the Series, and none is computable from another. Only the Site's own links say which ones a Series uses, so a Chapter Slug is always discovered, never derived. Avoid: series slug, url slug, permalink, chapter path

Cover: The image that stands for a Series wherever it is listed. A fact about the Series like its title — one Cover per Series, shared by every Reader, never per-Reader. Defined by what a Reader's browser can display, not by where the Site keeps the picture: an address no client can load is not a Cover, it is a missing one. Avoid: thumbnail, poster, image URL, artwork

Reader: A person with their own Progress. Exactly one per set of credentials, so there is no separate "account" concept to model — the credential belongs to the Reader. Avoid: user, account, member, subscriber

Bookmark: One Reader's tracked relationship with one Series, holding only what differs between Readers: Progress, Favourite, Lifecycle bucket. Facts about the Series itself belong to the Series, not here. Avoid: entry, item, record, subscription

Library: One of the two halves of the collection — manga or novel — selected by a Bookmark's kind. The web UI and the userscripts each address exactly one Library at a time. Not a per-person concept: "everything one person has bookmarked" is a different idea and must not be called a Library. Avoid: section, tab, category

Progress: The furthest chapter a reader has actually read in a Series. Only a change in Progress is real activity, so only Progress reorders the list. Avoid: position, bookmark (the noun is taken), last read

Latest Chapter: The highest-numbered chapter a Site has published for a Series, discovered without the reader present. The number is what ranks it, never a date and never the Site's own "newest chapter" banner — where a Site disagrees with itself, its list of chapters is the record and its summary of that list is not. Distinct from Progress in every way that matters: it is a fact about the Site, not about the reader, and it must never reorder the list. Avoid: newest, current chapter, update

Poll: The backend's own check of a Site for a Series's Latest Chapter, made without the Reader present. Performed once per Series no matter how many Readers bookmarked it — a Poll is work done on behalf of the Series, never on behalf of a Reader. Avoid: scrape, refresh, check, sync

Acquisition: The single read of a Series page made the moment the Series first exists, giving it both its Latest Chapter and its Cover without waiting out the Poll queue. Distinct from a Poll in the two ways that matter: a Reader is present — it is triggered by their first Bookmark of that Series — and it is the only read that establishes a Cover rather than refreshing facts. It happens once in a Series's life; every later read of the same page is a Poll. Avoid: initial poll, first fetch, prefetch, warm-up

New Chapter: The state where Latest Chapter is ahead of Progress. The single condition the ember accent is permitted to signal. Avoid: unread, update available

Lifecycle bucket: Which of three mutually exclusive states a Bookmark sits in — reading, archived, or finished. A Bookmark is in exactly one. Orthogonal to being a favourite. Avoid: state, status (as a domain word), list

Favourite: A reader's manual pin on a Bookmark. Orthogonal to the Lifecycle bucket, and never a reason to reorder the list. Avoid: starred, pinned, priority