Files
mangaBookmark/userscript/AGENTS.md
T
sulthan 672c16ffbf Remove the client latest-chapter scan for lightnovelworld (#91) (#93)
Follows the spec published on #91: remove the client-side latest-chapter scan for lightnovelworld rather than porting #87's truncation into a second codebase.

- lightnovelworld.latestChapterFromAnchors deleted, not stubbed: absence is what the background-fetch guard keys off.
- computeLatestChapter tolerates an adapter with no scanner (yields null) and is exported as the test seam.
- backgroundRefreshLatest skips a scanner-less Site before the due filter: no Series page fetched, no freshness timestamp recorded, no batch slot consumed. The on-page path (maybeCaptureLatestOnSeriesPage) routes through the same null-tolerant computation.
- novelfull's scanner, the shared max-chapter helper and all four manga Sites untouched.
- userscript/AGENTS.md records the Poll-only contract for this Site and why.

All seven acceptance criteria from the spec met. node --check clean; novel suite 30/30 (the regression pin fails if a lnw scan is reintroduced, scoped or not); manga suite 35/35, manga userscript byte-for-byte unchanged.

Two-axis code review: no hard standard violations, spec-clean; one follow-up commit matching the sibling adapter guard from the manga script.

Reviewed-on: #93
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
2026-08-11 20:58:18 +07:00

7.0 KiB
Raw Blame History

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)

  1. Site adapters — one per host, detect(location, document) return page type + IDs. Identify type/IDs from URL regex (most stable); pull title from og: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's cover is already an address on our origin, and apiPut strips any cover off an outgoing body.
  2. API client — apiGet/apiPut/apiDelete with bearer header; localStorage key bmgr:manga:cache for instant render + offline fallback.
  3. Progress logic — auto-upsert last_chapter only when chapterNum >= stored last_chapter_num (re-reading old chapters must not regress progress; unparseable -> set current). Manual panel override forces any value.
  4. Retry queue — every write go through pushBookmark/pushDelete, so failed mutation park in localStorage (bmgr:manga:queue) and replayed on next navigation, reconnect, or refresh(). 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. sendStatus is 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. See docs/superpowers/specs/2026-07-27-offline-retry-queue-design.md.
  5. 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_BASE sits in CONFIG block next to API_BASE. FAB is 7 × 44 edge tab whose hit area widened to 28 × 72 by invisible #hit child; #fab must keep touch-action: none and must not regain overflow: hidden. Since touch-action resolved at gesture start, strip can't be both browser-scrolled and script-dragged, so makeDraggable splits by intent: swipe from #hit scrolls via window.scrollBy, hold of ARM_MS arms reposition drag, visible sliver drags with no hold. See docs/superpowers/specs/2026-07-28-edge-tab-hitbox-design.md.
  6. SPA navigation — Asura is Astro, client-routed on comic/chapter pages: patch history.pushState/replaceState + listen popstate, re-run detect() on URL change so auto-update fire without reload. Demonic uses classic reloads (initial document-idle run 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>. seriesId must strip hash (/-[0-9a-f]{8}$/, stripBuildHash in userscript, asuraBuildHash in 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. %2527 for '), chapter /title/<slug>/chapter/<n>/<page> (older chaptered.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 rewrites og:title: the server-rendered head keeps whatever document loaded first, so on a cold load og:title is the homepage's "Comix — Read Comics online for free" and after an in-page hop it is the previous series' name. document.title is the one thing client routing does update, so titles come from there, with the chapter page's " · Ch.<n>" tail stripped. It publishes no og:image either, 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 of og: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. No og:* tags at all — title from h3.title (series) or a.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. The chapter path's slug is a Chapter Slug, not an identity: the Series address is read off the page's a[aria-label='All Chapter'] (fallback: the BreadcrumbList's second crumb), and a Series may publish under several Chapter Slugs. A chapter page with no pointer resolves to other, so no Bookmark is offered. h1.entry-title is 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. The client performs no latest-chapter scan for this Site: the Poll's one-hour cooldown dominates the client's four-hour throttle, so a scan would add no freshness, and the page's wpdiscuz thread is a public write surface a scan would have to truncate at. computeLatestChapter yields null here and backgroundRefreshLatest skips the Site before any fetch.

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.