Files
mangaBookmark/CLAUDE.md
T
sulthan eeb601cbe2 Split backend into internal packages by responsibility
All Go files lived flat in backend/ as one package main. Move store,
latest-chapter polling, sessions, HTTP middleware, the JSON API, the
userscript handler, and the web UI (with its templates/static assets)
into backend/internal/{store,latest,session,httpmw,api,userscript,web},
each with an exported API. main.go becomes the composition root wiring
them into newRouter; root-level tests cover the assembled router while
package-local tests cover unit behavior. Update Dockerfile/.dockerignore
for the new internal/ tree and CLAUDE.md to describe the layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 19:41:38 +07:00

16 KiB
Raw Blame History

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 <site>:<series_id> (asura|demonic). 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. 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).

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> (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.

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 <token>; 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 <branch> --base main --title "..." --description "..."
  • List / view / check out: tea pr list, tea pr <n>, tea pr checkout <n>
  • 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 <API_TOKEN>, 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 "<question>" when graphify-out/graph.json exists. Use graphify path "<A>" "<B>" for relationships and graphify explain "<concept>" 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).