8e4fa6448e
Closes #136. Spec #136 end to end: `finished` becomes a fact about the Series, written only by the owner, and the reader-facing Lifecycle bucket is gone. ## What landed - **#157** — `series.finished_at bigint NOT NULL DEFAULT 0` plus the migration whose statement order is load-bearing (seed from the buckets, then flip them); both Lane queries lose the `HAVING COUNT(*) FILTER (WHERE b.status <> 'finished')` clause and gate on `finished_at = 0` instead, with the due-query/eligible-count force asymmetry kept deliberate and commented; `StatusFinished`, its API special-case 400, the web tab and the templates' Finished bucket deleted. - **#158** — owner Finish control on the Series detail page: confirm-gated finish, instant un-finish, admin accent (never ember, nothing is destroyed), `Store.SetSeriesFinished`, the two routes behind the owner gate, and the state displayed on the list row without offering the control there. - **#160** — reader side: derived `finished` bool on the flat Bookmark (`s.finished_at > 0`), rendered as a text-only label in both userscripts and on the web card; read-only inbound by omission from `Upsert`'s explicit `series` column list, same mechanism that already protects `cover`. - **#161** — glossary and the stale Reader-count divergence note catch up. - **#159** — `finished` joins the admin filter vocabulary (predicate `finished_at > 0`, label `Finished`, own aggregate count, figure last in the stats block as informational); the four clock-driven hygiene predicates (stale, never-checked, no-cover, no-chapter) exclude finished Series while unpollable, orphan and sighting-raised deliberately do not. ## Verification `go vet ./...` and `go test ./...` green on the merged branch (Docker-backed, throwaway `postgres:17-alpine` per package). Each ticket also passed a two-axis review (spec + standards) on its own branch before merge. Reviewed-on: #163 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
172 lines
8.9 KiB
Markdown
172 lines
8.9 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
|
|
|
|
**Orphan Series**:
|
|
A Series no Reader bookmarks. Removing a Bookmark never removes the Series, so the row
|
|
outlives every relationship to it: nothing reads it, no Poll visits it, and it still owns
|
|
a Cover. A state of the Series, not a Lifecycle bucket — it says how many Readers hold it,
|
|
never anything about a Reader.
|
|
_Avoid_: dangling, unused, dead series, stale
|
|
|
|
**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. 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. Established by a Poll and, between Polls, by a Sighting. 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
|
|
|
|
**Poll Lane**:
|
|
One Site's own stream of Polls, carrying the pace at which that Site is willing to be
|
|
asked. Every Site has exactly one and no Lane can slow, block or borrow from another's;
|
|
a Reader never has one and never influences one.
|
|
_Avoid_: worker, queue, scheduler, batch, wave
|
|
|
|
**Lane Pass**:
|
|
One sweep of a Poll Lane over the Series due on its Site: what it found waiting, how many it
|
|
read, and whether it declined to work at all. A fact about the Lane rather than about any
|
|
Series — a pass that read nothing is still a pass, and one that declined carries the reason it
|
|
declined, since a Lane resting and a Lane stuck look identical from a count alone. Its record
|
|
outlives the process that made it: "the poller has done nothing for six hours" is only
|
|
answerable by something written down.
|
|
_Avoid_: run, cycle, tick, batch, poll history
|
|
|
|
**Forced Poll**:
|
|
A Poll the owner asks for by hand instead of waiting for the Series's turn. It jumps its
|
|
Lane's queue and ignores every waiting rule — the rest between Polls, a Sighting standing
|
|
in for a check, a finished Series — but never overrules a Site that is
|
|
refusing us, the Lane's spacing between fetches, or a Series with no page to fetch. Asked
|
|
for by marking the Series, never by commanding the poller, so it happens on the Lane's
|
|
next pass rather than at the moment of asking.
|
|
It also takes whatever Cover the Site publishes today: asking for one is asking to accept the
|
|
page as it now stands, so it is the only read after Acquisition that can replace a Cover.
|
|
_Avoid_: manual poll, refresh, retry, force refresh
|
|
|
|
**Paused Lane**:
|
|
A Poll Lane the owner has stopped for a bounded time. It makes no Polls until the pause
|
|
expires, so its Series stay due and unstamped exactly as they do when a Site cannot be
|
|
reached. Every pause carries an expiry — a Lane cannot be stopped indefinitely — and it
|
|
outlives a restart, being a fact about the Site rather than about the running process.
|
|
_Avoid_: disabled, off, stopped, suspended, kill switch (that is the deploy-time switch)
|
|
|
|
**Stall**:
|
|
A Poll Lane that owed Polls, made none, and has nothing to say for it. Distinct from the
|
|
two conditions it resembles: a Site that refuses is exercising the pace it is entitled to,
|
|
and a Lane the owner paused was told to stop — a Stall is neither asked for nor explained.
|
|
It is the one fault no Reader surface can show: every Bookmark still opens, Progress still
|
|
syncs, and Latest Chapter is quietly wrong for as long as it lasts.
|
|
_Avoid_: outage, downtime, failure, backlog, lag
|
|
|
|
**Sighting**:
|
|
What a Reader's browser happened to see of a Series's Latest Chapter while that Reader
|
|
was on the page. It reports the same fact as a Poll but carries none of its authority:
|
|
a Poll always overrules it, and only a Sighting on a Series no Reader else holds may
|
|
defer one. A Sighting a later Poll contradicts downwards is a false Sighting, and
|
|
enough of those cost the Reader the right to defer at all.
|
|
_Avoid_: client report, user poll, observation, claim
|
|
|
|
**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 for the Lane's pace. 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 establishes a Cover rather than refreshing
|
|
facts, which no Poll does unless the owner forces one. 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
|
|
|
|
**Correction**:
|
|
A Latest Chapter the owner sets by hand, on a Series no Poll can read. It reports the
|
|
same fact as a Poll and carries even less authority than a Sighting: the next Poll
|
|
overwrites it, so does any Reader's Sighting, and it is never a floor or a pin. It
|
|
exists only because the Site page is unreadable — where a Poll can read the page, the
|
|
Poll is the answer and a Correction is not wanted.
|
|
_Avoid_: override, pin, manual value, fix
|
|
|
|
**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 the two states a Bookmark sits in — reading or archived. A Bookmark is in exactly
|
|
one. Orthogonal to being a favourite. Finished is not a bucket: it is a fact about the
|
|
Series (see `series.finished_at`), owned by the owner and stamped once, and every Bookmark
|
|
on a finished Series is archived.
|
|
_Avoid_: state, status (as a domain word), list
|
|
|
|
**Finished Series**:
|
|
A Series the owner has marked finished, stamped once in `series.finished_at`
|
|
(epoch ms, zero means not finished). The owner is its only writer — no
|
|
adapter, no Reader, no Poll can set it — and a Forced Poll reads a finished
|
|
Series once for that pass and never clears the flag. It is a fact about the
|
|
Series, not a Bookmark bucket: every Bookmark on a finished Series is
|
|
archived, the Lane stops polling it (the due gate reads `finished_at = 0`),
|
|
and Readers see a label and nothing more.
|
|
_Avoid_: completed, done, dropped, shelved (that is Archived), ended
|
|
|
|
**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
|