Two rounds of design-critique fixes on the web UI. Every visual change was verified at 390x844 and 1280x900 in both dark and light with screenshots; `go test ./...` is green throughout; no new dependencies. ## Earlier commits on this branch The two oldest commits predate this session and were never opened as their own PR, so they are under review here too: - Confirm-gate the lifecycle actions, cluster the action strip by consequence. - Fix the accessibility findings from the audit: contrast, focus, reduced motion. ## The rest **Chapter format.** The userscript and the poller both write `"Chapter N"`, and the templates prefixed `Ch ` again, so every real Asura row read `Ch Chapter 250` — while a manual edit stored a bare `250`, leaving two formats in one list. `DisplayChapter`/`DisplayLatest` on `Bookmark` now strip the lead-in and re-add exactly one `Ch `. **Zero-result search.** The client filter only toggled `card.hidden`, so a query matching nothing left a blank list under a fully populated, unfiltered "Continue reading" strip. There is now a no-match state with a Clear-search button, and the strip goes down while a filter is active. **The colour law.** `--ember` is documented as meaning "new chapter" and was spent on eight things, including setting "Nothing new." in the colour reserved for new chapters. Destruction moves to a new `--danger` token; text-input focus follows the searchbar idiom and turns `--paper`. Contrast, both themes: `--danger` on the page 4.82 / 6.65, the solid Remove button 4.94 / 7.30, the confirm question 9.00 / 7.98. **The remove confirm.** Buttons 40px 8px apart became 46px 12px apart, and the question names the series and the loss instead of asking "Remove this?". It opens with **Cancel** focused, not Remove — the two reversible rows still open on their affirmative. **The recent strip.** It was the head of the same `updated_at DESC` list rendered directly below it, on every tab, costing ~240px of the first phone screen. It is now scoped to series with a chapter waiting, and only on All. With nothing new anywhere it does not render — deliberate. **Stale chrome.** The strip and the Updated badge describe the whole library but live outside the swapped `#list`, so archiving a series left it under "Continue reading" with the badge still counting it, and `/?tab=all` reached by htmx differed from the same URL reloaded. Both regions move into `chrome.html` and refresh out of band on every mutation and every tab switch. **Accessibility and touch.** Esc closes any open panel and returns focus to the cell that owns it; opening a confirm moves focus into it; the inline error scrolls into view and no longer self-destructs after 5s; every tab and desktop action cell clears 44px; `role="alert"` on the login error; the card monogram is no longer announced; the busy bar is clipped by its own travel rather than by `overflow: hidden` on the card. **Chapter form label.** The panel's only visible text named the published chapter while the field held your progress. The field gets a real label; "Latest known" moves below it. **gzip.** Nothing was compressed. A stdlib middleware handles the four text types and leaves woff2 alone: style.css 21.8 -> 6.5 KB, htmx 50.9 -> 16.4, filter.js 7.6 -> 2.9. ## Not addressed The `role="status"` error slot is still mutated while hidden and then revealed, which is the non-announcing pattern the confirm rows were fixed for. Delete is still a silent vanish. Both are flagged in the critique snapshot under `.impeccable/critique/`. Design health went 24/36 (66.7%) to 29/40 (72.5%) between snapshots; the two P1s that survived were found and fixed after that run. Reviewed-on: #11 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
14 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 301s here) 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's block on fetching the manga sites is IP-reputation-based, not universal — and not reliably reproducible. Verified 2026-07-26: plain
curlfrom both the CGNAT dev machine and the 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. This contradicts an earlier, untested assumption that the CGNAT dev IP would be blocked; it was not, at least on this date. Treat "does curl work right now" as a live, time-varying fact to re-check, not a fixed property of a given machine — Cloudflare's bot scoring can flip a previously-clean IP without notice. Any backend fetcher still needs a graceful-degrade path for when it does get challenged, and adapters should be verified against live pages (Playwright MCP, on-device devtools, or a direct probe) before finalizing, not assumed from a single earlier test.
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. Design-tool caveat: the templates link/static/style.cssroot-absolutely (correct — they are served from/), but the impeccable detector resolves a stylesheet href withpath.resolve(fileDir, href), which drops the directory on a leading/and silently skips the file. A relative href does not help either: the template's directory is not its served path. Sodetect.mjs backend/templatesreports a false clean — always passbackend/statictoo. Its one finding there,overused-fonton "Instrument Serif", is a deliberate identity choice, not debt. - Every action that moves a series out of the list is confirm-gated.
Archive, finish, and remove each open their own
.confirm-rowdisclosure (toggleConfirmRow(key, kind)infilter.js,kind∈archive|finish|remove); restore fires instantly because it is the reversal. Remove's row wears the ember wash, the two reversible ones wear.calmgrey.--emberstays reserved for the new-chapter signal: busy bar and inline error use--mute. - Latest-chapter poller: a ticker goroutine in the same binary re-checks
each bookmarked series' newest published chapter from the backend's own
network access, so
latest_chapterstays fresh when the user is not browsing. It is a second, parallel signal — the userscript keeps its ownmaybeCaptureLatestOnSeriesPage/backgroundRefreshLatestlogic unchanged. Two independent clocks: a per-bookmark cooldown (latest_checked_atcolumn, enforced byStore.DueForLatestCheck's WHERE clause) and a wake interval. The row is stamped before the fetch so a broken series waits out a full cooldown instead of retrying every tick, and writes go throughStore.Get+Store.Upsertso a new chapter never reorders the list. Fetches usebogdanfinn/tls-clientwith a Chrome profile as defence in depth against fingerprint-based blocking; any failure logs and skips. Seedocs/superpowers/specs/2026-07-26-server-latest-chapter-polling-design.md. The poller'sStore.Get+Store.Upsertis not wrapped in a transaction, so a userscriptPUTthat commits between the two can be overwritten by the poller's stale re-read — reverting that read progress and, since the stored value now differs, movingupdated_atand reordering the list. This is a known, accepted limitation for a single-user deployment, not a bug to fix. 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.- Lifecycle buckets:
statuson each bookmark isreading|archived|finished, orthogonal tofavorite. Archived and finished appear only in their own tab — not in All, Updated, Favourites, or the recent strip. The poller keeps checking archived series and skips finished ones.finishedis settable only from the web UI;PUT /bookmarks/{key}rejects it with 400. An empty incoming status means "keep the stored one" — resolved on theVALUESside ofStore.Upsert, not in the conflict clause, becauseexcluded.*is the post-evaluation row and a default applied there would wipe the bucket on every PUT from a client that predates the column. Seedocs/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(default8080),WEB_PASSWORD(gates the browser UI; unset disables 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 a bindmount).
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. - Retry queue — every write goes through
pushBookmark/pushDelete, so a failed mutation is parked inlocalStorage(mangabm:queue) and replayed on the next navigation, reconnect, orrefresh(). Entries are markers ({key, op, sendStatus, attempts}), never payloads — the body is read from the cache at send time, so one entry per key gives ordering and coalescing for free.sendStatusis sticky: while an archive is pending, later writes to that key keep carrying the bucket, which is what stops a successful in-between write from silently un-archiving the series.refresh()drains before it fetches and overlays anything still pending, so the list never flaps. A 400 drops the entry, a 401 aborts the pass and keeps the queue, and transient failures retry to a cap of 10. Latest-chapter writes deliberately stay out of the queue. Seedocs/superpowers/specs/2026-07-27-offline-retry-queue-design.md. - UI — rendered inside a Shadow DOM root to isolate from site CSS
(critical on mobile). Three tabs (All / Favourites / Archived) and a row of
link chips to the web UI and both manga sites;
WEB_BASEsits in the CONFIG block next toAPI_BASE. The FAB is a7 × 44edge tab whose hit area is widened to28 × 72by an invisible#hitchild;#fabmust keeptouch-action: noneand must not regainoverflow: hidden. Becausetouch-actionis resolved at gesture start, the strip cannot be both browser-scrolled and script-dragged, somakeDraggablesplits by intent: a swipe from#hitscrolls viawindow.scrollBy, a hold ofARM_MSarms a reposition drag, and the visible sliver drags with no hold. Seedocs/superpowers/specs/2026-07-28-edge-tab-hitbox-design.md. - SPA navigation — Asura is Astro, client-routed on the comic/chapter pages: patch
history.pushState/replaceState+ listenpopstate, re-rundetect()on URL change so auto-update fires without reload. Demonic uses classic reloads (initialdocument-idlerun suffices).
Live URL shapes (verified 2026-07-26, may drift — re-check against live pages before trusting)
- asurascans.com: series
/comics/<slug>(slug carries a trailing site-wide build-hash suffix, e.g.-059befe1, that rotates on every redeploy), chapter/comics/<slug>/chapter/<n>.seriesIdmust strip the hash (/-[0-9a-f]{8}$/,stripBuildHashin the userscript,asuraBuildHashin the backend); URLs keep the 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.%2527for'), chapter/title/<slug>/chapter/<n>/<page>(olderchaptered.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) are 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 the endpoints with Authorization: Bearer <token>; confirm OPTIONS preflight returns CORS headers and /healthz returns 200.
Forge: Gitea, not GitHub
origin is a self-hosted Gitea instance (gitea.violetcrown.my.id), so gh does not work here — use tea (Gitea CLI) for anything past plain git. Common ones:
- Open a 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 aGH_TOKENenv var.
tea prints its output as rendered boxes rather than plain text; the PR URL lands on the last line.
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).