A newly bookmarked Series acquires its Cover at creation #59
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
Spec: #55. Originating bug: #47. Architecture and rejected alternatives:
docs/adr/0007-backend-hosts-cover-bytes.md. Domain vocabulary:CONTEXT.md.Do not close #47 or #55 from this ticket.
What to build
This is the ticket that fixes the reported bug. A Reader bookmarks a Series from the middle of a chapter on comix — the exact case in #47 — and within seconds the Cover appears in the web UI, on that device and every other.
When a Bookmark creates a Series nobody has bookmarked before, the backend fetches that Series' page and extracts both the Latest Chapter and the Cover from the same response, then fetches and stores the cover bytes through the gated fetcher. This runs at creation rather than waiting for the Poll queue, and both halves of that sentence matter. A newly created Series sits at the back of a queue ordered by how many Readers hold it, so a Series with a single Reader waits behind every popular one — the Reader who just bookmarked it would otherwise see neither a Cover nor a Latest Chapter for an unbounded stretch. And it is the same fetch, not a second one, which is also the reason no client-supplied hint is worth accepting: the page must be fetched anyway for the chapter signal, so a hint would save no request while adding a client-controlled input to a server-side fetch.
The acquisition is asynchronous with respect to the write. A Reader's bookmark action must not block on a third-party Site's latency and must not fail because that Site is down. A Cover that cannot be produced leaves the Bookmark, its Progress and its Latest Chapter completely intact.
The wire changes with it.
coverin the bookmark list becomes an absolute URL on the deployment's public origin, built from newly required configuration — absolute because the userscript renders on third-party origins where a relative path resolves against the Site rather than against us, and because the server owns the wire shape rather than exporting a join to every client (ADR-0004). Until the bytes exist,coveris the empty string, not an address that will 404: "no Cover yet" and "Cover is broken" must stay distinguishable in the UI and in the logs, and the client-side fallback must remain a genuine last resort rather than a routine occurrence.The request side keeps accepting a
coverfield and ignores it. Rejecting it would break every installed userscript the moment the server deployed, and the flat wire contract depends on old scripts continuing to work. This extends the existing "ignored after creation" rule to "ignored always". The field is permanently inert rather than pending removal, and the place it is decoded must say so in a comment — a bare TODO would be a promise nobody intends to keep, which the repo's comment rules forbid.At this point the four plain-TLS Sites all light up. Browser-backed Sites follow in their own ticket.
Acceptance criteria
covervalue is accepted and the value ignored, both at creation and afterwardsgo test ./...is greenBlocked by
Implemented in PR #68 (branch
feat/59-cover-at-creation), which closes this issue on merge.Every acceptance criterion is ticked except the manual one, which is left honest: I exercised a live backend, not a browser on a chapter page. What was actually run — a real Go binary against a real Postgres, bookmarking
comix:n8we-dungeons-and-crayonsoverPUT /bookmarks/{key}— returned"cover": "http://127.0.0.1:8099/covers/8ce74d80…"and"latest_chapter": "Chapter 81"seconds after the write, andcurlon that address answered200,Content-Type: image/jpeg,Cache-Control: public, max-age=604800, immutable, and a 280x420 JPEG. The web UI half is pinned by a test (TestListRendersAcquiredCover) asserting the card's<img src>carries that public address, not eyes on the rendered page. Someone should do the on-device pass before ticking the last box.That live run is also what found the one thing reading could not: comix answers
Content-Type: image/jpg, which is not a registered media type, so the fetcher rejected the bytes.store.CoverContentTypenow canonicalises it toimage/jpegat the boundary, so one image cannot land under two spellings.Notes for the siblings:
GET /covers/{address}, 404 for an unknown address, malformed/traversal rejection, and the immutable cache directive. #59's own manual criterion cannot pass without a route serving the URL #59 puts on the wire. #60 is now client-side work; re-point those criteria at PR #68 rather than re-implementing them.prefetchCoverreturns early on an emptyseries.cover, so the poll never extracts a cover from the page it just fetched. The acquirer's doc comment names #61 for that reason.Upsertno longer persists the userscript-scraped address. Stated boundary of #59, but a user-visible gap on two Sites in the meantime.Deploy note:
PUBLIC_BASE_URLis new and required. The API refuses to start without it, anddocker composerefuses too.