From 4ee0b0f092d573316ff1f6fe8ad7beaf9f93c1d8 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Tue, 4 Aug 2026 19:51:33 +0700 Subject: [PATCH] docs: split CLAUDE.md into per-directory guidance, add opencode agents Co-Authored-By: Claude Sonnet 5 --- .opencode/agent/implementer.md | 47 ++++++++++++ .opencode/agent/reviewer.md | 69 +++++++++++++++++ CLAUDE.md | 131 +-------------------------------- backend/CLAUDE.md | 81 ++++++++++++++++++++ userscript/CLAUDE.md | 46 ++++++++++++ 5 files changed, 245 insertions(+), 129 deletions(-) create mode 100644 .opencode/agent/implementer.md create mode 100644 .opencode/agent/reviewer.md create mode 100644 backend/CLAUDE.md create mode 100644 userscript/CLAUDE.md diff --git a/.opencode/agent/implementer.md b/.opencode/agent/implementer.md new file mode 100644 index 0000000..e3f79f1 --- /dev/null +++ b/.opencode/agent/implementer.md @@ -0,0 +1,47 @@ +--- +description: Code-writer subagent for subagent-driven development. Fast model (ocg/deepseek-v4-flash) for mechanical, well-specified implementation tasks. Escalates complicated tasks so the controller can re-dispatch on minimax-m3. +mode: subagent +model: 9router/ocg/deepseek-v4-flash +--- + +You are the implementer subagent for Subagent-Driven Development. You implement one task, exactly as specified, and report back with evidence. + +## Before You Begin + +If you have questions about requirements, acceptance criteria, approach, dependencies, or anything unclear in the task description — ask now. Raise concerns before starting work. Don't guess or make assumptions. + +## Your Job + +1. Implement exactly what the task specifies +2. Write tests (follow TDD when the task says to) +3. Verify the implementation works (run the focused test while iterating; run the full suite once before committing) +4. Commit your work +5. Self-review (below) +6. Report back + +Follow existing patterns in the codebase. Don't restructure code outside your task. Don't overbuild — only what was requested (YAGNI). + +## When You're in Over Your Head + +It is always OK to stop and say "this is too hard for me." Bad work is worse than no work. STOP and escalate when the task requires architectural judgment, multi-file integration you can't see clearly through, or you're reading file after file without progress. + +**Report BLOCKED or NEEDS_CONTEXT** with specifics: what you're stuck on, what you tried, what help you need. If the task turns out more complicated than mechanical (design judgment, broad codebase understanding), escalate so the controller can re-dispatch you on the more capable minimax-m3 agent. + +## Self-Review Before Reporting + +- **Completeness:** everything in the spec implemented? edge cases handled? +- **Quality:** names accurate? code clean and maintainable? +- **Discipline:** avoided overbuilding? only what was requested? +- **Testing:** do tests verify real behavior? output pristine (no stray warnings)? +Fix what you find before reporting. + +## Report Format + +Report back with ONLY (under 15 lines): +- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT +- Commits created (short SHA + subject) +- One-line test summary (e.g. "14/14 passing, output pristine") +- Concerns, if any +- Report file path (if the controller gave you one) + +If BLOCKED or NEEDS_CONTEXT, put the specifics in the final message itself — the controller acts on it directly. Use DONE_WITH_CONCERNS if you completed the work but have doubts. Never silently produce work you're unsure about. diff --git a/.opencode/agent/reviewer.md b/.opencode/agent/reviewer.md new file mode 100644 index 0000000..608f09a --- /dev/null +++ b/.opencode/agent/reviewer.md @@ -0,0 +1,69 @@ +--- +description: Reviewer subagent for subagent-driven development. Capable model (ocg/minimax-m3) for task-scoped and whole-branch code review; also the re-dispatch target when implementation tasks are complicated. +mode: subagent +model: 9router/ocg/minimax-m3 +--- + +You are the reviewer subagent for Subagent-Driven Development. You verify one task's implementation matches its requirements (spec compliance) and is well-built (code quality). You may also be dispatched for whole-branch review. + +## Inputs + +- Task brief file (requirements — use exact values verbatim) +- Implementer's report file +- Diff file (commit list, stat summary, full diff with context) + +## Method + +Read the diff file once — it is your view of the change. The context lines ARE the changed files: do not read a changed file separately unless a hunk you must judge is cut off mid-function (say so in your report). Do not re-run git commands. Inspect code outside the diff only to evaluate a concrete risk you can name — one focused check per named risk, and name both the risk and what you checked. + +Your review is read-only. Do not mutate the working tree, index, HEAD, or branch state. + +## Do Not Trust the Report + +Treat the implementer's report as unverified claims. It may be incomplete, inaccurate, or optimistic. Verify against the diff. Design rationales in the report ("kept it per YAGNI") are the implementer grading their own work — a stated rationale never downgrades a finding's severity. + +## Tests + +The implementer already ran the tests and reported results. Do not re-run the suite to confirm. Run a test only when reading the code raises a specific doubt no existing run answers — a focused test, never a package-wide suite. If heavy validation seems warranted, recommend it in your report instead. Warnings or noise in the reported test output are findings — output should be pristine. + +## Part 1: Spec Compliance + +Compare the diff against the brief: +- **Missing:** requirements skipped, missed, or claimed without implementing +- **Extra:** features not requested, over-engineering, nice-to-haves +- **Misunderstood:** right feature built the wrong way, wrong problem solved + +If a requirement can't be verified from this diff alone (lives in unchanged code or spans tasks), report it as a ⚠️ item instead of broadening your search. + +## Part 2: Code Quality + +- Clean separation of concerns? proper error handling? DRY without premature abstraction? edge cases? +- Do new/changed tests verify real behavior, not mocks? edge cases covered? +- Does each file have one clear responsibility? units independently testable? did this change create/significantly grow large files? + +Point at evidence: file:line references for every finding. A tight report that cites lines gives the controller everything it needs. + +## Calibration + +Not everything is Critical. Important = this task can't be trusted until fixed: incorrect or fragile behavior, a missed requirement, maintainability damage you'd block a merge over (verbatim duplication of a logic block, swallowed errors, tests that assert nothing). "Coverage could be broader" and polish suggestions are Minor. If the plan explicitly mandates something this rubric calls a defect, that IS a finding — report Important, labeled plan-mandated. Acknowledge what was done well before listing issues. + +## Output Format + +### Spec Compliance +- ✅ Spec compliant | ❌ Issues found: [what's missing/extra/misunderstood, with file:line] +- ⚠️ Cannot verify from diff: [requirements you couldn't verify, what the controller should check] + +### Strengths +[What's well done? Be specific.] + +### Issues +#### Critical (Must Fix) +#### Important (Should Fix) +#### Minor (Nice to Have) +For each: file:line, what's wrong, why it matters, how to fix (if not obvious). + +### Assessment +**Task quality:** [Approved | Needs fixes] +**Reasoning:** [1-2 sentence technical assessment] + +Your final message is the report itself: begin directly with the spec-compliance verdict. Every line is a verdict, a finding with file:line, or a check you ran — no preamble, no process narration, no closing summary. diff --git a/CLAUDE.md b/CLAUDE.md index 2dc42d4..2363ed0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,10 +2,6 @@ Guidance for Claude Code (claude.ai/code) working in this repo. -## Status - -Greenfield. Only `plans/mangaBookmark.md` exist — no code yet. Plan = spec; read before build. Two deliverables: Go sync backend, single Violentmonkey-compatible userscript. - ## What this is Manga read-progress tracker, user read on **asurascans.com** (current domain; asuracomic.net 301s here) and **demonicscans.org** via **Violentmonkey**. Userscript inject on-page UI (floating button + slide-in panel), sync progress to self-hosted Go backend so bookmarks unify across both sites and devices. @@ -26,132 +22,9 @@ Violentmonkey userscript (isolated world, per-site adapters, localStorage cache) -- fetch() HTTPS --> reverse proxy (TLS + CORS) --> Go net/http --> SQLite (volume) ``` -- **Backend** (`backend/`): stdlib `net/http` (handful routes, no framework) + `modernc.org/sqlite` (pure Go, `CGO_ENABLED=0` -> static binary -> distroless/scratch image). Reverse proxy terminates TLS; Go service listens plain `:8080`. - Single binary, split into packages under `backend/internal/`: `store` - (Bookmark type, SQLite persistence, migrations), `latest` (background - poller, site parsers, TLS fetcher), `session` (cookie signing, login - rate limiter), `httpmw` (Auth/Gzip/CORS middleware), `api` (JSON - bookmark handlers), `userscript` (userscript-serving handler), `web` - (browser UI handler + `templates/` + `static/`, `go:embed`-ed). - `backend/main.go` is the composition root — the only place that wires - packages together into `newRouter`. Root-level `*_test.go` hold - integration tests that exercise the full router; unit tests for a - package live beside it under `internal/`. -- **Single-user store.** One `bookmarks` table keyed `:` (`asura`|`demonic`|`comix`|`kagane`). Sync **last-write-wins**. Schema and endpoint list in plan. -- **Endpoints:** `GET /bookmarks`, `PUT /bookmarks/{key}` (upsert; see `updated_at` rule below), `DELETE /bookmarks/{key}`, `GET /healthz` (no auth). -- **Web UI:** same binary serve password-gated browser UI on second - hostname — `GET /` (list, or login page when no session), - `POST /login`, `POST /logout`, `GET /static/*`, htmx fragment endpoints - under `/ui/*`. Templates + assets `go:embed`-ed under - `backend/internal/web/`, so `backend/Dockerfile` must copy the whole - `internal/` tree, not just `*.go`. Sessions stateless - HMAC cookies keyed off `API_TOKEN`; `WEB_PASSWORD` gates them, and when empty, - web routes not registered at all. UI mutations read-modify-write - through `Store.Get` + `Store.Upsert` so `updated_at` rule stays one - place. See `docs/superpowers/specs/2026-07-25-web-ui-design.md`. - **Design-tool caveat:** templates link `/static/style.css` root-absolutely - (correct — served from `/`), but impeccable detector resolves - stylesheet href with `path.resolve(fileDir, href)`, drops directory - on leading `/` and silently skip file. Relative href don't help - either: template's directory isn't its served path. So - `detect.mjs backend/internal/web/templates` reports **false clean** — - always pass `backend/internal/web/static` too. One finding there, - `overused-font` on "Instrument Serif", deliberate identity choice, not debt. -- **Every action that moves series out of list is confirm-gated.** - Archive, finish, remove each open own `.confirm-row` disclosure - (`toggleConfirmRow(key, kind)` in `filter.js`, `kind` ∈ - `archive|finish|remove`); restore fire instantly since it's the reversal. - Remove's row wear ember wash, two reversible ones wear `.calm` grey. - `--ember` stay reserved for new-chapter signal: busy bar and inline - error use `--mute`. -- **Latest-chapter poller:** ticker goroutine in same binary re-check - each bookmarked series' newest published chapter from backend's own - network access, so `latest_chapter` stay fresh when user not - browsing. Second, parallel signal — userscript keep own - `maybeCaptureLatestOnSeriesPage`/`backgroundRefreshLatest` logic unchanged. - Two independent clocks: per-bookmark cooldown (`latest_checked_at` column, - enforced by `Store.DueForLatestCheck`'s WHERE clause) and wake interval. - Row stamped *before* fetch so broken series wait out full - cooldown instead of retrying every tick, and writes go through - `Store.Get` + `Store.Upsert` so new chapter never reorders list. - Fetches use `bogdanfinn/tls-client` with Chrome profile as defence in depth - against fingerprint-based blocking; any failure log and skip. kagane sits - behind a Cloudflare JavaScript challenge the TLS client can't clear, so it is - browser-only: fetched over CDP via `BROWSER_WS_URL`, and simply not polled - when that's unset. See - `docs/superpowers/specs/2026-07-26-server-latest-chapter-polling-design.md`. - Poller's `Store.Get` + `Store.Upsert` not wrapped in transaction, so - userscript `PUT` that commits between the two can get overwritten by - poller's stale re-read — reverting that read progress and, since stored - value now differs, moving `updated_at` and reordering list. Known, - accepted limitation for single-user deployment, not bug to fix. -- **`updated_at` drives list order, so moves only on real reading progress:** server apply its timestamp when row new or `last_chapter_num` changes, else keep stored value — favouriting series or recording newly published chapter must not reorder list. `PUT` therefore returns row **as stored**, clients must adopt that response rather than own payload. See `plans/2026-07-25-bookmark-list-favorites-design.md` §4. -- **Lifecycle buckets:** `status` on each bookmark is `reading` | `archived` | - `finished`, orthogonal to `favorite`. Archived and finished appear only in - own tab — not in All, Updated, Favourites, or recent strip. Poller keeps - checking archived series and skip finished ones. `finished` settable - only from web UI; `PUT /bookmarks/{key}` reject it with 400. - **Empty incoming status means "keep stored one"** — resolved on the - `VALUES` side of `Store.Upsert`, not conflict clause, since - `excluded.*` is post-evaluation row and default applied there would - wipe bucket on every PUT from client that predates column. See - `docs/superpowers/specs/2026-07-27-status-buckets-design.md`. -- **Config via env:** `API_TOKEN`, `ALLOWED_ORIGINS` (comma list), `DB_PATH` - (default `/data/bookmarks.db`), `PORT` (default `8080`), `WEB_PASSWORD` - (gates browser UI; unset disable it), - `LATEST_CHAPTER_POLL_ENABLED`/`_COOLDOWN`/`_INTERVAL`/`_BATCH`/`_STAGGER` - (background latest-chapter poller; defaults on, `1h`/`10m`/`14`/`20s`). - `USERSCRIPT_PATH` (file served at `/u/{token}/manga-bookmark.user.js`, - default `/userscript/manga-bookmark.user.js`, supplied by bindmount). - `BROWSER_WS_URL` (headless-shell CDP endpoint for kagane; unset disables - browser polling and leaves that site to the userscript alone). +Backend-specific architecture (packages, endpoints, poller, config env vars) lives in `backend/CLAUDE.md`. Userscript-specific structure (adapters, retry queue, UI, live URL shapes) lives in `userscript/CLAUDE.md`. -### 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`/`cover` from **`og:title`/`og:image` meta tags**, not CSS classes. -2. **API client** — `apiGet/apiPut/apiDelete` with bearer header; `localStorage` key `mangabm: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` (`mangabm: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 carries trailing - site-wide build-hash suffix, e.g. `-059befe1`, that **rotates on every - redeploy**), chapter `/comics//chapter/`. `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 may URL-encode punctuation, e.g. `%2527` for `'`), chapter `/title//chapter//` (older `chaptered.php?manga=&chapter=` 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. - -## Commands (once code exists) +## Commands Backend (`cd backend`): - Test all: `go test ./...` diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md new file mode 100644 index 0000000..83d17e8 --- /dev/null +++ b/backend/CLAUDE.md @@ -0,0 +1,81 @@ +Guidance for Claude Code working under `backend/`. See root `CLAUDE.md` for the project-wide architecture diagram, hard constraints, and design system. + +- **Backend** (`backend/`): stdlib `net/http` (handful routes, no framework) + `modernc.org/sqlite` (pure Go, `CGO_ENABLED=0` -> static binary -> distroless/scratch image). Reverse proxy terminates TLS; Go service listens plain `:8080`. + Single binary, split into packages under `backend/internal/`: `store` + (Bookmark type, SQLite persistence, migrations), `latest` (background + poller, site parsers, TLS fetcher), `session` (cookie signing, login + rate limiter), `httpmw` (Auth/Gzip/CORS middleware), `api` (JSON + bookmark handlers), `userscript` (userscript-serving handler), `web` + (browser UI handler + `templates/` + `static/`, `go:embed`-ed). + `backend/main.go` is the composition root — the only place that wires + packages together into `newRouter`. Root-level `*_test.go` hold + integration tests that exercise the full router; unit tests for a + package live beside it under `internal/`. +- **Single-user store.** One `bookmarks` table keyed `:` (`asura`|`demonic`|`comix`|`kagane`). Sync **last-write-wins**. Schema and endpoint list in plan. +- **Endpoints:** `GET /bookmarks`, `PUT /bookmarks/{key}` (upsert; see `updated_at` rule below), `DELETE /bookmarks/{key}`, `GET /healthz` (no auth). +- **Web UI:** same binary serve password-gated browser UI on second + hostname — `GET /` (list, or login page when no session), + `POST /login`, `POST /logout`, `GET /static/*`, htmx fragment endpoints + under `/ui/*`. Templates + assets `go:embed`-ed under + `backend/internal/web/`, so `backend/Dockerfile` must copy the whole + `internal/` tree, not just `*.go`. Sessions stateless + HMAC cookies keyed off `API_TOKEN`; `WEB_PASSWORD` gates them, and when empty, + web routes not registered at all. UI mutations read-modify-write + through `Store.Get` + `Store.Upsert` so `updated_at` rule stays one + place. See `docs/superpowers/specs/2026-07-25-web-ui-design.md`. + **Design-tool caveat:** templates link `/static/style.css` root-absolutely + (correct — served from `/`), but impeccable detector resolves + stylesheet href with `path.resolve(fileDir, href)`, drops directory + on leading `/` and silently skip file. Relative href don't help + either: template's directory isn't its served path. So + `detect.mjs backend/internal/web/templates` reports **false clean** — + always pass `backend/internal/web/static` too. One finding there, + `overused-font` on "Instrument Serif", deliberate identity choice, not debt. +- **Every action that moves series out of list is confirm-gated.** + Archive, finish, remove each open own `.confirm-row` disclosure + (`toggleConfirmRow(key, kind)` in `filter.js`, `kind` ∈ + `archive|finish|remove`); restore fire instantly since it's the reversal. + Remove's row wear ember wash, two reversible ones wear `.calm` grey. + `--ember` stay reserved for new-chapter signal: busy bar and inline + error use `--mute`. +- **Latest-chapter poller:** ticker goroutine in same binary re-check + each bookmarked series' newest published chapter from backend's own + network access, so `latest_chapter` stay fresh when user not + browsing. Second, parallel signal — userscript keep own + `maybeCaptureLatestOnSeriesPage`/`backgroundRefreshLatest` logic unchanged. + Two independent clocks: per-bookmark cooldown (`latest_checked_at` column, + enforced by `Store.DueForLatestCheck`'s WHERE clause) and wake interval. + Row stamped *before* fetch so broken series wait out full + cooldown instead of retrying every tick, and writes go through + `Store.Get` + `Store.Upsert` so new chapter never reorders list. + Fetches use `bogdanfinn/tls-client` with Chrome profile as defence in depth + against fingerprint-based blocking; any failure log and skip. kagane sits + behind a Cloudflare JavaScript challenge the TLS client can't clear, so it is + browser-only: fetched over CDP via `BROWSER_WS_URL`, and simply not polled + when that's unset. See + `docs/superpowers/specs/2026-07-26-server-latest-chapter-polling-design.md`. + Poller's `Store.Get` + `Store.Upsert` not wrapped in transaction, so + userscript `PUT` that commits between the two can get overwritten by + poller's stale re-read — reverting that read progress and, since stored + value now differs, moving `updated_at` and reordering list. Known, + accepted limitation for single-user deployment, not bug to fix. +- **`updated_at` drives list order, so moves only on real reading progress:** server apply its timestamp when row new or `last_chapter_num` changes, else keep stored value — favouriting series or recording newly published chapter must not reorder list. `PUT` therefore returns row **as stored**, clients must adopt that response rather than own payload. See `plans/2026-07-25-bookmark-list-favorites-design.md` §4. +- **Lifecycle buckets:** `status` on each bookmark is `reading` | `archived` | + `finished`, orthogonal to `favorite`. Archived and finished appear only in + own tab — not in All, Updated, Favourites, or recent strip. Poller keeps + checking archived series and skip finished ones. `finished` settable + only from web UI; `PUT /bookmarks/{key}` reject it with 400. + **Empty incoming status means "keep stored one"** — resolved on the + `VALUES` side of `Store.Upsert`, not conflict clause, since + `excluded.*` is post-evaluation row and default applied there would + wipe bucket on every PUT from client that predates column. See + `docs/superpowers/specs/2026-07-27-status-buckets-design.md`. +- **Config via env:** `API_TOKEN`, `ALLOWED_ORIGINS` (comma list), `DB_PATH` + (default `/data/bookmarks.db`), `PORT` (default `8080`), `WEB_PASSWORD` + (gates browser UI; unset disable it), + `LATEST_CHAPTER_POLL_ENABLED`/`_COOLDOWN`/`_INTERVAL`/`_BATCH`/`_STAGGER` + (background latest-chapter poller; defaults on, `1h`/`10m`/`14`/`20s`). + `USERSCRIPT_PATH` (file served at `/u/{token}/manga-bookmark.user.js`, + default `/userscript/manga-bookmark.user.js`, supplied by bindmount). + `BROWSER_WS_URL` (headless-shell CDP endpoint for kagane; unset disables + browser polling and leaves that site to the userscript alone). diff --git a/userscript/CLAUDE.md b/userscript/CLAUDE.md new file mode 100644 index 0000000..9f1c225 --- /dev/null +++ b/userscript/CLAUDE.md @@ -0,0 +1,46 @@ +Guidance for Claude Code working under `userscript/`. See root `CLAUDE.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`/`cover` from **`og:title`/`og:image` meta tags**, not CSS classes. +2. **API client** — `apiGet/apiPut/apiDelete` with bearer header; `localStorage` key `mangabm: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` (`mangabm: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 carries trailing + site-wide build-hash suffix, e.g. `-059befe1`, that **rotates on every + redeploy**), chapter `/comics//chapter/`. `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 may URL-encode punctuation, e.g. `%2527` for `'`), chapter `/title//chapter//` (older `chaptered.php?manga=&chapter=` 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.