Final-review fix wave over the web UI branch. - sessionKey now derives from API_TOKEN and WEB_PASSWORD with a \x00 separator, so rotating the password logs every browser out too. - uiChapter only clears last_chapter_url when the number actually changes. The form is pre-filled, so a bare tap of Save resubmits the same value; that used to destroy the chapter URL silently while updated_at stayed put, degrading Continue to the series index page. - MANGA_WEB_HOST is now required by the prod override rather than falling back to manga.example.com, matching MANGA_API_HOST. - Comment fixes: static cache rationale, pruneLocked aliasing invariant, and the stale "3 routes" line in CLAUDE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.4 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Status
Greenfield. Only plans/mangaBookmark.md exists — no code yet. That plan is the spec; read it before building. Two deliverables: a Go sync backend and a single Bromite-compatible userscript.
What this is
A manga read-progress tracker for a user reading on asurascans.com (the current domain; asuracomic.net is the older one) and demonicscans.org from Bromite (mobile Chromium). A userscript injects on-page UI (floating button + slide-in panel) and syncs progress to a self-hosted Go backend so bookmarks unify across both sites and across devices.
Hard constraints (these drive the design — do not violate)
Bromite uses Chromium's native userscript engine, not Tampermonkey:
- No
GM_*APIs anywhere. NoGM_setValue/GM_getValue(use pagelocalStorage), noGM_registerMenuCommand(inject on-page UI), noGM_xmlhttpRequestfor cross-origin (use plainfetch()). Keeping the script GM-free also lets it run in desktop Tampermonkey/Violentmonkey for faster iteration. - Cross-origin
fetch()works only against a CORS-enabled backend. Manga sites arehttps://, so backend must be HTTPS (mixed-content block otherwise). - Asura and Demonic are separate origins with separate
localStorage— a shared remote store is the only way to unify bookmarks. Cloud sync is required, not optional. - Userscript runs in an isolated world, so the embedded API token is safe from the site's JS.
- Cloudflare blocks server-side fetch of the manga sites — verify URL regex +
og:selectors against live pages (Playwright MCP or on-device devtools) before finalizing adapters, not by curling.
Architecture
Bromite userscript (isolated world, per-site adapters, localStorage cache)
-- fetch() HTTPS --> reverse proxy (TLS + CORS) --> Go net/http --> SQLite (volume)
- Backend (
backend/): stdlibnet/http(a handful of routes, no framework) +modernc.org/sqlite(pure Go,CGO_ENABLED=0-> static binary -> distroless/scratch image). The reverse proxy terminates TLS; the Go service listens plain:8080. - Single-user store. One
bookmarkstable keyed<site>:<series_id>(asura|demonic). Sync is last-write-wins. Schema and endpoint list are in the plan. - Endpoints:
GET /bookmarks,PUT /bookmarks/{key}(upsert; seeupdated_atrule below),DELETE /bookmarks/{key},GET /healthz(no auth). - Web UI: the same binary serves a password-gated browser UI on a second
hostname —
GET /(list, or login page when there is no session),POST /login,POST /logout,GET /static/*, and htmx fragment endpoints under/ui/*. Templates and assets arego:embed-ed, sobackend/Dockerfilemust copytemplates/andstatic/as well as*.go. Sessions are stateless HMAC cookies keyed offAPI_TOKEN;WEB_PASSWORDgates them and, when empty, the web routes are not registered at all. UI mutations read-modify-write throughStore.Get+Store.Upsertso theupdated_atrule stays in one place. Seedocs/superpowers/specs/2026-07-25-web-ui-design.md. updated_atdrives list order, so it moves only on real reading progress: the server applies its timestamp when the row is new orlast_chapter_numchanges, and otherwise keeps the stored value — favouriting a series or recording a newly published chapter must not reorder the list.PUTtherefore returns the row as stored, and clients must adopt that response rather than their own payload. Seeplans/2026-07-25-bookmark-list-favorites-design.md§4.- Config via env:
API_TOKEN,ALLOWED_ORIGINS(comma list),DB_PATH(default/data/bookmarks.db),PORT(default8080),WEB_PASSWORD(gates the browser UI; unset disables it).
Userscript structure (single IIFE, manga-bookmark.user.js)
- Site adapters — one per host,
detect(location, document)returns pagetype+ IDs. Identify type/IDs from URL regex (most stable); pulltitle/coverfromog:title/og:imagemeta tags, not CSS classes. - API client —
apiGet/apiPut/apiDeletewith bearer header;localStoragekeymangabm:cachefor instant render + offline fallback. - Progress logic — auto-upsert
last_chapteronly whenchapterNum >= stored last_chapter_num(re-reading old chapters must not regress progress; unparseable -> set current). Manual panel override forces any value. - UI — rendered inside a Shadow DOM root to isolate from site CSS (critical on mobile).
- SPA navigation — Asura is Next.js client-routed: patch
history.pushState/replaceState+ listenpopstate, re-rundetect()on URL change so auto-update fires without reload. Demonic uses classic reloads (initialdocument-idlerun suffices).
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 the endpoints with Authorization: Bearer <token>; confirm OPTIONS preflight returns CORS headers and /healthz returns 200.
Security invariants
- Auth on
/bookmarks*: requireAuthorization: Bearer <API_TOKEN>, constant-time compare, 401 otherwise. - CORS: reflect
Originonly when inALLOWED_ORIGINS; allowGET,PUT,DELETE,OPTIONS+ headersAuthorization,Content-Type; answer preflightOPTIONSwith204.
Relevant skills
multi-stage-dockerfile and docker-compose-orchestration for the container work (referenced in the plan).
graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
Rules:
- For codebase questions, first run
graphify query "<question>"when graphify-out/graph.json exists. Usegraphify path "<A>" "<B>"for relationships andgraphify explain "<concept>"for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run
graphify update .to keep the graph current (AST-only, no API cost).