# CLAUDE.md 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. ## Hard constraints (drive design — don't violate) Userscript targets **Violentmonkey**, so `GM_*` APIs available, but stay GM-free where plain web APIs suffice — keeps portability across engines: - **Avoid `GM_*` unless needed.** Prefer page `localStorage` over `GM_setValue`/`GM_getValue`, on-page UI over `GM_registerMenuCommand`, plain `fetch()` over `GM_xmlhttpRequest` for cross-origin. - Cross-origin `fetch()` work **only** against CORS-enabled backend. Manga sites `https://`, so backend **must be HTTPS** (else mixed-content block). - Asura and Demonic are **separate origins with separate `localStorage`** — shared remote store only way to unify bookmarks. Cloud sync required, not optional. - Userscript run in **isolated world**, so embedded API token safe from site's JS. - Cloudflare's block on manga sites **IP-reputation-based, not universal — and not reliably reproducible.** Verified 2026-07-26: plain `curl` from both CGNAT dev machine *and* deployed VPS got clean 200s with real HTML on both asurascans.com and demonicscans.org (homepage, series, chapter pages) — no interactive Turnstile challenge from either IP at test time. Contradicts earlier untested assumption CGNAT dev IP blocked; wasn't, at least this date. Treat "does curl work right now" as live, time-varying fact to re-check, not fixed property of machine — Cloudflare's bot scoring can flip previously-clean IP without notice. Backend fetcher still needs graceful-degrade path for when challenged, and adapters should be **verified against live pages** (Playwright MCP, on-device devtools, or direct probe) before finalize, not assumed from single earlier test. ## Architecture ``` 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). ### 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) Backend (`cd backend`): - Test all: `go test ./...` - Single test: `go test -run TestName ./...` - Build static binary: `CGO_ENABLED=0 go build` Local stack: `docker compose up` (named volume mounted at `/data`, `restart: unless-stopped`). Smoke test: `curl` endpoints with `Authorization: Bearer `; confirm `OPTIONS` preflight return CORS headers and `/healthz` return 200. ## Forge: Gitea, not GitHub `origin` is self-hosted Gitea instance (`gitea.violetcrown.my.id`), so **`gh` don't work here — use `tea` (Gitea CLI) for anything past plain git.** Common ones: - Open PR: `tea pr create --head --base main --title "..." --description "..."` - List / view / check out: `tea pr list`, `tea pr `, `tea pr checkout ` - Issues: `tea issue create`, `tea issue list` - Auth lives in `tea login`, not `GH_TOKEN` env var. `tea` print output as rendered boxes rather than plain text; PR URL lands on last line. ## Design system Web UI + userscript panel follow **Cinder**, rules in `docs/design-system.md` — source of truth Claude Design project `mangaBookmark Web UI` (`969ac210-fe02-4c01-ae1b-9a271dcc779a`). Read it before touching `backend/internal/web/static/style.css`, `backend/internal/web/templates/*`, or userscript `TEMPLATE`/`CSS`. Core law: **ember means new chapter only** — no other state (busy, error, destruction) may use `--ember`; destruction gets `--danger`. No cards/corners/shadows, one `--measure: 760px` column, tokens only (never hardcode hex outside `:root`), both colour branches touched together. Any move that pulls series out of list (archive/finish/remove) must be confirm-gated via its own `.confirm-row`; only restore fires instantly. ## Security invariants - Auth on `/bookmarks*`: require `Authorization: Bearer `, **constant-time compare**, 401 otherwise. - CORS: reflect `Origin` only when in `ALLOWED_ORIGINS`; allow `GET,PUT,DELETE,OPTIONS` + headers `Authorization,Content-Type`; answer preflight `OPTIONS` with `204`. ## Comments Comment only if code alone can't carry info. Cost per read — must earn spot. Write for: - Why not what. Tradeoffs, non-obvious decisions. - Load-bearing detail looking incidental — say so if "simplify" breaks it. - Non-local consequence, invisible from function alone. - Wire format / encoding / interface contract — save callers re-deriving. - Gotcha/workaround, with ref if exists. - Domain/business rule not derivable from code. Skip: - Restating code (no `// increment i` above `i++`). - Trivial getter/setter/pass-through. - Banners, dividers, `// helpers`. - Change narration (`// fix bug`, `// as requested`, `// new impl`) — git's job. - Commented-out code — delete. - TODO without concrete action. Style: one dense comment over function beats one per line inside. Tight, no worked example unless bug subtle. Wrong comment worse than none — update/delete on change. Default fewer — sparse+high-signal beats comprehensive. Test: "competent reader get this from code in few sec?" Yes → skip. Needs detour through another file/spec/git-blame → write it. ## Relevant skills `multi-stage-dockerfile` and `docker-compose-orchestration` for container work (referenced in plan). `golang-code-style`, `golang-error-handling`, `golang-performance`, `golang-testing` for backend Go work. ## graphify Project has knowledge graph at graphify-out/ with god nodes, community structure, cross-file relationships. Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. Return scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - If graphify-out/wiki/index.md exists, use for broad navigation instead of raw source browsing. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain don't surface enough context. - After modifying code, run `graphify update .` to keep graph current (AST-only, no API cost).