Scope: `userscript/`. Each entry names the code that holds the truth. The prose is only what the code cannot tell you: rationale, invariants a refactor would break, and dated observations about sites we don't control. ### Structure — single IIFE, `manga-bookmark.user.js` Six parts, in file order: site adapters, API client, progress logic, retry queue, UI, SPA navigation. **Site adapters** — one per host, `detect(location, document)` returning page `type` + IDs. - Identify type and IDs from **URL regex**, which is the most stable surface a site exposes; take `title` from **`og:title`** (or the page heading where a site ships no og: tags), never CSS classes. - **No adapter reads a cover.** The backend acquires, stores and serves every Cover from its own origin, the wire `cover` is already an address there, and `apiPut` strips any `cover` off an outgoing body. **Progress logic** — auto-upsert `last_chapter` only when `chapterNum >= stored last_chapter_num`; unparseable sets the current value. Re-reading an old chapter must not regress progress. A manual panel override forces any value. **Retry queue** — every write goes through `pushBookmark`/`pushDelete`. - Entries are markers (`{key, op, sendStatus, attempts}`), **never payloads**: the body is read from cache at send time, so one entry per key gives ordering and coalescing for free. - `sendStatus` is **sticky** — while an archive is pending, later writes to that key keep carrying the bucket. Without it a successful in-between write silently un-archives the series. - `refresh()` drains before it fetches and overlays anything still pending, so the list never flaps. - 400 drops the entry, 401 aborts the pass and keeps the queue, transient failures retry to a cap. Latest-chapter writes deliberately stay out of the queue. **UI** — rendered inside a **Shadow DOM** root to isolate it from site CSS, which is critical on mobile. - The FAB's *hit* area is widened by an invisible `#hit` child. `#fab` must keep `touch-action: none` and must **not** regain `overflow: hidden`. - `touch-action` is resolved at gesture start, so the strip cannot be both browser-scrolled and script-dragged. `makeDraggable` therefore splits by intent: a swipe from `#hit` scrolls via `window.scrollBy`, a hold of `ARM_MS` arms a reposition drag, and the visible sliver drags with no hold. **SPA navigation** — Asura is Astro and client-routes on comic/chapter pages, so `history.pushState`/`replaceState` are patched and `popstate` listened to, and `detect()` re-runs on URL change. Demonic uses classic reloads, where the initial `document-idle` run suffices. ### Live URL shapes Encoded in the adapters; the notes below are the parts a reader of the regex would get wrong. **Verified 2026-07-26 unless dated otherwise — sites drift, so re-check against a live page before trusting any of it.** - **asurascans.com** — the series slug carries a site-wide build-hash suffix (e.g. `-059befe1`) that **rotates on every redeploy**, so `seriesId` must strip it (`stripBuildHash` here, `asuraBuildHash` in the backend) while URLs keep the full slug — stale-hash URLs 302 to current ones. - **demonicscans.org** — slugs may URL-encode punctuation, and the older `chaptered.php?manga=&chapter=` form still exists as a redirect, which is what series-page chapter-list anchors link through. Encodings (including triple-encoded punctuation like `%25252D`) are identical on `/manga/` and `/title/` pages, so decode-once seriesIds match (verified 2026-07-28). - **comix.to** — only the leading `` is identity; the slug re-renders when a series is renamed (`comixSeriesId`). It is 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 name and after an in-page hop it is the *previous* series'. `document.title` is the one thing client routing updates, hence titles come from there with the chapter page's `" · Ch."` tail stripped. It publishes no `og:image` either, one of the reasons cover acquisition moved to the backend. - **kagane.to** — reader URLs carry no chapter number, so the number comes out of `og:title`. Two shapes exist, `" - Chapter [ - Episode ]"` and `" - Volume Chapter "`; 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 — the panel renders the backend's cover address like every other Site. - **novelfull.com** (novel script) — no `og:*` tags at all, so the title comes from `h3.title` (series) or `a.truyen-title` (chapter). - **lightnovelworld.net** (novel script) — chapter paths are flat at the site root and their slug is a **Chapter Slug, not an identity**: a Series may publish under several. The Series address is read off the page's `a[aria-label='All Chapter']` (fallback: the BreadcrumbList's second crumb), and a chapter page with no pointer resolves to `other` so no Bookmark is offered. The client runs **no latest-chapter scan** for this Site — `computeLatestChapter` yields null and `backgroundRefreshLatest` skips it before any fetch — because the backend Poll's one-hour cooldown dominates the client's four-hour throttle, so a scan would add no freshness while having to truncate at the page's wpdiscuz thread, a public write surface. ### 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, because this script has no previous installation to carry keys over from. Installed alongside the manga script; both write to the same backend, discriminated by `LIBRARY`.