cc0fa92a1a
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.
6.3 KiB
6.3 KiB
Guidance for OpenCode (and Claude Code) working under userscript/. See root AGENTS.md for the project-wide architecture diagram, hard constraints, and design system.
Userscript structure (single IIFE, manga-bookmark.user.js)
- Site adapters — one per host,
detect(location, document)return pagetype+ IDs. Identify type/IDs from URL regex (most stable); pulltitlefromog:title(or the page heading where a site ships no og: tags), not CSS classes. No adapter reads a cover: the backend acquires, stores and serves every Cover from its own origin (ADR-0007), the wire'scoveris already an address on our origin, andapiPutstrips anycoveroff an outgoing body. - API client —
apiGet/apiPut/apiDeletewith bearer header;localStoragekeybmgr:manga:cachefor instant render + offline fallback. - Progress logic — auto-upsert
last_chapteronly whenchapterNum >= stored last_chapter_num(re-reading old chapters must not regress progress; unparseable -> set current). Manual panel override forces any value. - Retry queue — every write go through
pushBookmark/pushDelete, so failed mutation park inlocalStorage(bmgr:manga:queue) and replayed on next navigation, reconnect, orrefresh(). Entries are markers ({key, op, sendStatus, attempts}), never payloads — body read from cache at send time, so one entry per key give ordering and coalescing for free.sendStatusis sticky: while archive pending, later writes to that key keep carrying bucket, which stop successful in-between write from silently un-archiving series.refresh()drains before it fetches and overlays anything still pending, so list never flaps. 400 drops entry, 401 abort pass and keep queue, and transient failures retry to cap of 10. Latest-chapter writes deliberately stay out of queue. Seedocs/superpowers/specs/2026-07-27-offline-retry-queue-design.md. - UI — rendered inside Shadow DOM root to isolate from site CSS
(critical on mobile). Three tabs (All / Favourites / Archived) and row of
link chips to web UI and both manga sites;
WEB_BASEsits in CONFIG block next toAPI_BASE. FAB is7 × 44edge tab whose hit area widened to28 × 72by invisible#hitchild;#fabmust keeptouch-action: noneand must not regainoverflow: hidden. Sincetouch-actionresolved at gesture start, strip can't be both browser-scrolled and script-dragged, somakeDraggablesplits by intent: swipe from#hitscrolls viawindow.scrollBy, hold ofARM_MSarms reposition drag, visible sliver drags with no hold. Seedocs/superpowers/specs/2026-07-28-edge-tab-hitbox-design.md. - SPA navigation — Asura is Astro, client-routed on comic/chapter pages: patch
history.pushState/replaceState+ listenpopstate, re-rundetect()on URL change so auto-update fire without reload. Demonic uses classic reloads (initialdocument-idlerun suffice).
Live URL shapes (verified 2026-07-26, may drift — re-check against live pages before trust)
- asurascans.com: series
/comics/<slug>(slug carries trailing site-wide build-hash suffix, e.g.-059befe1, that rotates on every redeploy), chapter/comics/<slug>/chapter/<n>.seriesIdmust strip hash (/-[0-9a-f]{8}$/,stripBuildHashin userscript,asuraBuildHashin backend); URLs keep full slug — stale-hash URLs 302 to current ones. Astro-rendered; chapter links present in raw server HTML. - demonicscans.org: series
/manga/<slug>(slug may URL-encode punctuation, e.g.%2527for'), chapter/title/<slug>/chapter/<n>/<page>(olderchaptered.php?manga=<id>&chapter=<n>form still exists as redirect, what series-page chapter-list anchors link through). Encodings (incl. triple-encoded punctuation like%25252D) identical on /manga/ and /title/ pages, so decode-once seriesIds match — verified 2026-07-28. - comix.to: series
/title/<id>-<slug>, chapter/title/<id>-<slug>/<uploadId>-chapter-<n>. Only the leading<id>is identity — the slug re-renders when a series is renamed (comixSeriesId). An SPA that never rewritesog:title: the server-rendered head keeps whatever document loaded first, so on a cold loadog:titleis the homepage's "Comix — Read Comics online for free" and after an in-page hop it is the previous series' name.document.titleis the one thing client routing does update, so titles come from there, with the chapter page's" · Ch.<n>"tail stripped. It publishes noog:imageeither, which is one of the reasons cover acquisition moved to the backend. - kagane.to: series
/series/<uuid>, reader/series/<uuid>/reader/<bookUuid>. Reader URLs carry no chapter number, so the number comes out ofog:title. Two shapes exist:"<Series> - Chapter <n>[ - Episode <n>]"and, for volume-numbered series,"<Series> - Volume <v> Chapter <n>"with no episode name — both must yield a bare series title, or the volume tail lands in the bookmark's title. Its covers are challenge- and CORP-protected, so nothing outside kagane.to can load one directly; the panel renders the backend's own cover address like every other Site. Behind a Cloudflare JS challenge, so the backend polls it through the headless browser. - novelfull.com (novel script): series
/<slug>.html, chapter/<slug>/chapter-<n>[-<title-slug>].html. Noog:*tags at all — title fromh3.title(series) ora.truyen-title(chapter); the script reads no cover. Behind a Cloudflare JS challenge no TLS fingerprint clears, so the backend polls it through the headless browser. - lightnovelworld.net (novel script): series
/novel/<slug>/, chapter/<slug>-chapter-<n>/— flat, at the site root.h1.entry-titleis the clean title on a series page and<Title> Chapter <n>on a chapter page. Its series page lists every chapter with an absolute href, so the backend polls it with the plain TLS client.
Second script: novel-bookmark.user.js
A copy of the manga script with two adapters, LIBRARY = "novel" and
STORE_PREFIX = "bmgr:novel:". No migration loop (this script has no previous
installation to carry keys over from). Installed alongside the manga script;
both write to the same backend with the same LIBRARY column discriminating
them.