novelfull cover never appears in the userscript list after bookmarking #78

Closed
opened 2026-08-10 19:36:24 +07:00 by sulthan · 0 comments
Owner

Symptom

Bookmarking novelfull Star Odyssey from the userscript: the list row draws the
.cover.ph placeholder, not the cover, and it stays that way.

What is actually happening

Two separate things, both real. The first explains "not instantly" and is certain; the
second explains "never" and depends on what the VPS log says.

1. The userscript can never see a cover acquired after its own PUT

Covers are acquired asynchronously (ADR-0007): Store.OnSeriesCreated -> Acquirer.Acquire
spawns a goroutine (backend/internal/latest/acquire.go:66-89) after the PUT has already
committed and returned. So the PUT response carries cover: "" by construction — the bytes
do not exist yet.

The userscript adopts that response verbatim (pushBookmark -> upsertLocal(saved),
novel-bookmark.user.js:554-557) and persists it under CACHE_KEY. The row is now cached
with an empty cover.

The only thing that ever replaces a cached row with the server's is refresh(), and it runs
in exactly two places:

  • novel-bookmark.user.js:1045 — panel open
  • novel-bookmark.user.js:1338 — script init (page load)

There is no timer, no visibilitychange, no focus listener. The user bookmarks from the
open panel
, so the refresh that would have picked the cover up already happened, seconds
before the cover existed. For the rest of that page's life the row shows a placeholder.

Same gap in manga-bookmark.user.js — same structure, same two refresh sites.

It does heal on the next page load or panel open, so if the cover is genuinely absent after
reopening the panel, it is #2 below, not this.

2. novelfull's series page 403s the plain-TLS fetcher

Measured today from this machine, through the backend's own TLSFetcher (Chrome_133
profile), not curl:

fetchable=true status=403 bytes=5732
body head: <!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>...

That is the Cloudflare interstitial. Acquirer.acquire bails at
acquire.go:118-121 with status 403, before both latestChapterFrom and coverFrom, so
the Series gets neither a chapter nor a cover — and MarkLatestChecked never runs either.
The poll's fillBlankCover heals a blank Cover only from a series page it managed to
fetch, so it needs the same fetch to start succeeding.

fetcherFor prefers the browser for novelfull, so this only bites when BROWSER_WS_URL
is unset or the sidecar is asleep/unreachable (ADR-0005/0006) — which is a supported state,
and per AGENTS.md the challenge is a live time-varying fact, not a fixed property.

Discriminator: grep the VPS log for acquire "novelfull:star-odyssey". A
status 403 line means #2; nothing at all means acquisition succeeded and #1 is the
whole story.

Not the cause — ruled out

  • Cover extraction. The live page carries
    <meta name="image" content="https://novelfull.com/uploads/webp/novel/star-odyssey-8035594265.webp">,
    which is exactly what coverFrom -> metaContent(body, "name", "image") reads
    (sites.go:247-248).
  • Cover byte fetching. That .webp answers a plain, UA-less GET with
    200 image/webp 41706 — no challenge on the uploads path, so TLSCoverFetcher is fine.
  • Wiring. main.go:338-348 passes BrowserFetch, BrowserCoverFetch and Covers to the
    Acquirer.
  • Queue shadowing. overlayPending returns the server list untouched when the queue is
    empty (novel-bookmark.user.js:508-523).

Fix direction

For #1, the lazy version: when an adopted PUT response has cover === "" for a row the
client just created, schedule one delayed refresh() (acquisition is bounded at
acquireTimeout = 45s, and covers usually land in a few seconds — ~10s then, once, no
loop). Both userscripts. Anything push-shaped is far more machinery than this earns.

For #2, nothing to change in code — it is the documented degrade path. Worth confirming the
sidecar is reachable from the VPS before treating it as a defect.

Scope

  • userscript/novel-bookmark.user.js, userscript/manga-bookmark.user.js (syncUpsert /
    pushBookmark, refresh)
  • tests: userscript/test/novel-logic.test.js, userscript/test/logic.test.js
## Symptom Bookmarking novelfull **Star Odyssey** from the userscript: the list row draws the `.cover.ph` placeholder, not the cover, and it stays that way. ## What is actually happening Two separate things, both real. The first explains "not instantly" and is certain; the second explains "never" and depends on what the VPS log says. ### 1. The userscript can never see a cover acquired after its own PUT Covers are acquired asynchronously (ADR-0007): `Store.OnSeriesCreated` -> `Acquirer.Acquire` spawns a goroutine (`backend/internal/latest/acquire.go:66-89`) *after* the PUT has already committed and returned. So the PUT response carries `cover: ""` by construction — the bytes do not exist yet. The userscript adopts that response verbatim (`pushBookmark` -> `upsertLocal(saved)`, `novel-bookmark.user.js:554-557`) and persists it under `CACHE_KEY`. The row is now cached with an empty cover. The only thing that ever replaces a cached row with the server's is `refresh()`, and it runs in exactly two places: - `novel-bookmark.user.js:1045` — panel open - `novel-bookmark.user.js:1338` — script init (page load) There is no timer, no `visibilitychange`, no `focus` listener. The user bookmarks *from the open panel*, so the refresh that would have picked the cover up already happened, seconds before the cover existed. For the rest of that page's life the row shows a placeholder. Same gap in `manga-bookmark.user.js` — same structure, same two refresh sites. It does heal on the next page load or panel open, so if the cover is genuinely absent after reopening the panel, it is #2 below, not this. ### 2. novelfull's series page 403s the plain-TLS fetcher Measured today from this machine, through the backend's own `TLSFetcher` (Chrome_133 profile), not curl: ``` fetchable=true status=403 bytes=5732 body head: <!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>... ``` That is the Cloudflare interstitial. `Acquirer.acquire` bails at `acquire.go:118-121` with `status 403`, before both `latestChapterFrom` and `coverFrom`, so the Series gets neither a chapter nor a cover — and `MarkLatestChecked` never runs either. The poll's `fillBlankCover` heals a blank Cover only from a series page it managed to fetch, so it needs the same fetch to start succeeding. `fetcherFor` prefers the browser for novelfull, so this only bites when `BROWSER_WS_URL` is unset or the sidecar is asleep/unreachable (ADR-0005/0006) — which is a supported state, and per AGENTS.md the challenge is a live time-varying fact, not a fixed property. **Discriminator:** grep the VPS log for `acquire "novelfull:star-odyssey"`. A `status 403` line means #2; nothing at all means acquisition succeeded and #1 is the whole story. ### Not the cause — ruled out - Cover extraction. The live page carries `<meta name="image" content="https://novelfull.com/uploads/webp/novel/star-odyssey-8035594265.webp">`, which is exactly what `coverFrom` -> `metaContent(body, "name", "image")` reads (`sites.go:247-248`). - Cover byte fetching. That `.webp` answers a plain, UA-less GET with `200 image/webp 41706` — no challenge on the uploads path, so `TLSCoverFetcher` is fine. - Wiring. `main.go:338-348` passes `BrowserFetch`, `BrowserCoverFetch` and `Covers` to the Acquirer. - Queue shadowing. `overlayPending` returns the server list untouched when the queue is empty (`novel-bookmark.user.js:508-523`). ## Fix direction For #1, the lazy version: when an adopted PUT response has `cover === ""` for a row the client just created, schedule one delayed `refresh()` (acquisition is bounded at `acquireTimeout = 45s`, and covers usually land in a few seconds — ~10s then, once, no loop). Both userscripts. Anything push-shaped is far more machinery than this earns. For #2, nothing to change in code — it is the documented degrade path. Worth confirming the sidecar is reachable from the VPS before treating it as a defect. ## Scope - `userscript/novel-bookmark.user.js`, `userscript/manga-bookmark.user.js` (`syncUpsert` / `pushBookmark`, `refresh`) - tests: `userscript/test/novel-logic.test.js`, `userscript/test/logic.test.js`
sulthan added the needs-infobug labels 2026-08-10 19:36:24 +07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#78