21615be2bd
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>
96 lines
4.4 KiB
Markdown
96 lines
4.4 KiB
Markdown
# 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
|