Covers render in the userscript panel, from a public route (#60) #69

Merged
sulthan merged 2 commits from feat/60-public-cover-route into main 2026-08-10 08:52:58 +07:00
Owner

Closes #60.

Spec: #55. Originating bug: #47. Architecture: docs/adr/0007-backend-hosts-cover-bytes.md. Neither #47 nor #55 is closed from here.

What this branch does

The panel now renders Covers from the deployment's own origin, and both userscripts stop having an opinion about where a Cover lives.

The public route was already in place. GET /covers/{address} landed with #59 (92eba07) and is registered on the bare mux, outside httpmw.Auth and outside the web UI's Discord session — backend/main.go:210-214, handler backend/internal/api/handlers.go:142-158. It reads no cookie and no header, answers 404 for an address that was never stored (and for a row whose file has gone missing — recorded-but-gone is not-found, never a fabricated body), refuses anything that is not ^[0-9a-f]{64}$ before the value becomes a path, and sets Cache-Control: public, max-age=604800, immutable. Those four properties are asserted by backend/cover_test.go:231-278. This branch re-verified them rather than re-implementing them; the only backend line it touches is a comment.

Both userscripts lose cover scraping entirely. Every adapter's cover: field is gone, along with the two helpers that fed them: the manga script's coverFromPage() (the img[alt] DOM scan comix needed, because comix publishes no og:image) and the novel script's metaName() plus the now-callerless module-level meta(). Nothing under userscript/ reads og:image, meta[name=image], or img[alt] any more.

Nothing sends a cover either. delete body.cover sits in apiPut — manga-bookmark.user.js:486, novel-bookmark.user.js:275 — which is the single chokepoint every write passes through (pushBookmark, the retry-queue flush, toggleFavorite, toggleArchive). It operates on the Object.assign copy, so the in-memory row keeps the cover it renders with. This matters beyond tidiness: a Reader upgrading from an older copy has localStorage rows carrying third-party scraped URLs, and without the strip those would ride back up on the next write. The handler discards the field regardless (handlers.go:53-59) — it is permanently inert, not pending removal.

Failed loads get the designed empty state, not the broken-image glyph. onerror: (e) => e.target.replaceWith(el("div", { class: "cover ph" })) on the cover <img> in both card renderers (manga:1380-1390, novel:1134-1144). The replacement is byte-identical to the existing no-cover branch on the very next line, so it picks up the .cover.ph styling already in the panel CSS — no new tokens, no new rule. el() routes any on* prop through addEventListener, so this is a listener, not an inline attribute string, and the swap is a createElement + DOM call with no markup parsing anywhere near it. This is the half of #47 that was visible on kagane.

The deleted scraping's tests went with it: the two comix cover cases, the pageImages and namedMetas fixtures, the img[alt] and meta[name=...] stub branches, the now-dead querySelectorAll stub member, and every stale og:image fixture and p.cover assertion across both suites. The export lists needed no change and that was checked, not assumed — coverFromPage and metaName were module-private on origin/main and no cover symbol ever appeared in module.exports.

Docs that described the deleted behaviour were corrected in the same breath, because leaving them would instruct the next agent to put the scraping back: userscript/AGENTS.md (adapter contract + the per-site notes for comix, kagane and novelfull), the README's adapter reference, and the userscript testing skill's stub table.

Verification

  • go test -count=1 ./... — green across all nine packages (backend 29.8s, latest, store, session, token, userscript, web).
  • node --check clean on both userscripts; node --test on both logic suites — 46 tests, 46 pass.
  • gofmt -l clean; go build ./... clean.
  • The onerror swap is DOM behaviour and deliberately has no coverage in the Node harness — that harness stubs a browser precisely so it never needs a DOM, and #60 says not to invent coverage for it. It was instead exercised for real: the el() helper and the exact render expression were loaded into a headless Chromium with a deliberately unloadable src, and the resulting DOM was <div class="cover ph"></div>. Ad hoc, not committed.
  • Not done, needs you: the on-device criterion — a comix Series bookmarked mid-chapter showing its Cover in the panel. That needs a real install against the deployment and is the one box left unticked on #60.

Reviewed

Both /code-review axes ran against cc0fa92. Spec found no missed requirement and no scope creep; standards found the diff clean on the four areas it scrutinised (the delete body.cover placement, the onerror handler's DOM safety, comment quality, dead-code removal). Their combined findings — the dead querySelectorAll stub, the stale README and skill text, and the handler comment whose premise this change invalidates — are fixed in 8b58019.

Out of scope, deliberately

The kagane-specific cover proxy still exists and still carries its session gate (#63 deletes it). The poll's blank-Cover fill (#61) and browser-backed Sites joining the pipeline (#62) are untouched.

Closes #60. Spec: #55. Originating bug: #47. Architecture: `docs/adr/0007-backend-hosts-cover-bytes.md`. Neither #47 nor #55 is closed from here. ## What this branch does The panel now renders Covers from the deployment's own origin, and both userscripts stop having an opinion about where a Cover lives. **The public route was already in place.** `GET /covers/{address}` landed with #59 (`92eba07`) and is registered on the bare mux, outside `httpmw.Auth` and outside the web UI's Discord session — `backend/main.go:210-214`, handler `backend/internal/api/handlers.go:142-158`. It reads no cookie and no header, answers `404` for an address that was never stored (and for a row whose file has gone missing — recorded-but-gone is not-found, never a fabricated body), refuses anything that is not `^[0-9a-f]{64}$` *before* the value becomes a path, and sets `Cache-Control: public, max-age=604800, immutable`. Those four properties are asserted by `backend/cover_test.go:231-278`. This branch re-verified them rather than re-implementing them; the only backend line it touches is a comment. **Both userscripts lose cover scraping entirely.** Every adapter's `cover:` field is gone, along with the two helpers that fed them: the manga script's `coverFromPage()` (the `img[alt]` DOM scan comix needed, because comix publishes no `og:image`) and the novel script's `metaName()` plus the now-callerless module-level `meta()`. Nothing under `userscript/` reads `og:image`, `meta[name=image]`, or `img[alt]` any more. **Nothing sends a cover either.** `delete body.cover` sits in `apiPut` — `manga-bookmark.user.js:486`, `novel-bookmark.user.js:275` — which is the single chokepoint every write passes through (`pushBookmark`, the retry-queue flush, `toggleFavorite`, `toggleArchive`). It operates on the `Object.assign` copy, so the in-memory row keeps the cover it renders with. This matters beyond tidiness: a Reader upgrading from an older copy has `localStorage` rows carrying third-party scraped URLs, and without the strip those would ride back up on the next write. The handler discards the field regardless (`handlers.go:53-59`) — it is permanently inert, not pending removal. **Failed loads get the designed empty state, not the broken-image glyph.** `onerror: (e) => e.target.replaceWith(el("div", { class: "cover ph" }))` on the cover `<img>` in both card renderers (`manga:1380-1390`, `novel:1134-1144`). The replacement is byte-identical to the existing no-cover branch on the very next line, so it picks up the `.cover.ph` styling already in the panel CSS — no new tokens, no new rule. `el()` routes any `on*` prop through `addEventListener`, so this is a listener, not an inline attribute string, and the swap is a `createElement` + DOM call with no markup parsing anywhere near it. This is the half of #47 that was visible on kagane. **The deleted scraping's tests went with it**: the two comix cover cases, the `pageImages` and `namedMetas` fixtures, the `img[alt]` and `meta[name=...]` stub branches, the now-dead `querySelectorAll` stub member, and every stale `og:image` fixture and `p.cover` assertion across both suites. The export lists needed no change and that was checked, not assumed — `coverFromPage` and `metaName` were module-private on `origin/main` and no cover symbol ever appeared in `module.exports`. Docs that described the deleted behaviour were corrected in the same breath, because leaving them would instruct the next agent to put the scraping back: `userscript/AGENTS.md` (adapter contract + the per-site notes for comix, kagane and novelfull), the README's adapter reference, and the userscript testing skill's stub table. ## Verification - `go test -count=1 ./...` — green across all nine packages (`backend` 29.8s, `latest`, `store`, `session`, `token`, `userscript`, `web`). - `node --check` clean on both userscripts; `node --test` on both logic suites — 46 tests, 46 pass. - `gofmt -l` clean; `go build ./...` clean. - The `onerror` swap is DOM behaviour and deliberately has no coverage in the Node harness — that harness stubs a browser precisely so it never needs a DOM, and #60 says not to invent coverage for it. It was instead exercised for real: the `el()` helper and the exact render expression were loaded into a headless Chromium with a deliberately unloadable `src`, and the resulting DOM was `<div class="cover ph"></div>`. Ad hoc, not committed. - **Not done, needs you:** the on-device criterion — a comix Series bookmarked mid-chapter showing its Cover in the panel. That needs a real install against the deployment and is the one box left unticked on #60. ## Reviewed Both `/code-review` axes ran against `cc0fa92`. Spec found no missed requirement and no scope creep; standards found the diff clean on the four areas it scrutinised (the `delete body.cover` placement, the `onerror` handler's DOM safety, comment quality, dead-code removal). Their combined findings — the dead `querySelectorAll` stub, the stale README and skill text, and the handler comment whose premise this change invalidates — are fixed in `8b58019`. ## Out of scope, deliberately The kagane-specific cover proxy still exists and still carries its session gate (#63 deletes it). The poll's blank-Cover fill (#61) and browser-backed Sites joining the pipeline (#62) are untouched.
sulthan added 2 commits 2026-08-10 04:28:15 +07:00
Both userscripts stop scraping Covers and stop putting one on the wire.
A scraped address has no rendering path left now that the backend
acquires, stores and serves every Cover from its own origin (ADR-0007),
and keeping one would reintroduce third-party URLs into exactly the
place #47 came from.

- Adapters no longer read og:image / meta[name=image], and comix's
  img[alt] cover scan and novelfull's metaName helper are deleted.
- apiPut strips `cover` off every outgoing body, so a cached row's
  address (already ours) never travels back either. The server ignores
  the field regardless.
- Both card renderers swap in the existing `.cover.ph` placeholder when
  the image fails to load, so a failure looks designed rather than
  broken - the other half of what #47 reported.
- The deleted scraping's test cases go with it: the comix cover cases,
  the img[alt] and meta[name] stub branches, and the stale og:image
  fixtures. Export lists are unchanged; nothing cover-specific was
  exported.

The cover route itself is already public and uncredentialed, with the
immutable cache directive and 404-for-unknown covered by the tests that
landed with #59.
Review follow-ups. The README's adapter reference and the userscript
testing skill still described a scrape that no longer exists, and the
manga test stub kept a querySelectorAll whose only caller was the
deleted coverFromPage. The handler comment's premise ("every installed
userscript still sends one") stops being true the moment a Reader
reinstalls, so it now says older copies may.
sulthan merged commit e2c054e7ce into main 2026-08-10 08:52:58 +07:00
Sign in to join this conversation.