# Manga Bookmark — Userscript + Self-Hosted Sync Backend ## Context The user reads manga on **asurascans.com** (now serves from `asuracomic.net`) and **demonicscans.org**, on a **mobile browser (Bromite)**. They want to bookmark a series and auto-record the latest chapter they've read, with a UI reachable while those sites are open on the phone. Progress must sync across devices, so it lives in a backend on the user's own server. ### Hard constraints (drive the whole design) Bromite uses Chromium's native userscript engine — **not** Tampermonkey. Per Bromite's wiki / Chromium docs: - **No `GM_setValue` / `GM_getValue`** → persistence must use page `localStorage`. - **No `GM_registerMenuCommand`** → UI must be injected on-page (floating button + panel). - **`GM_xmlhttpRequest` is same-origin only** → cross-origin calls use plain `fetch()`, which works only against a **CORS-enabled** backend. - Userscripts run in an **isolated world** → the manga site's JS cannot read our embedded API token (safe to embed). - `asurascans.com` and `demonicscans.org` are **separate origins** with **separate `localStorage`** → the only way to unify bookmarks across both sites is a **shared remote store**. Cloud sync is therefore required, not a nice-to-have. - The manga sites are `https://`, so the backend **must be HTTPS** (mixed-content block otherwise). User already runs a reverse proxy + domain, so a subdomain (e.g. `manga-api.`) fronts the service. ### Decisions locked with user - Backend: **self-hosted, Go** (resource-friendly), Docker Compose, behind existing reverse proxy (TLS handled there). - Scope: **track read progress only** — no "new chapters available" detection (YAGNI for v1). - Record last-read: **auto on chapter open + manual override** in the panel. - UI: **floating button + slide-in panel**. --- ## Architecture ``` Bromite (mobile) userscript (isolated world, per-site adapters) localStorage cache <--> fetch() over HTTPS | reverse proxy (TLS, CORS origin) | Go service (net/http) -> SQLite file (volume) ``` Two deliverables in this repo: ``` mangaBookmark/ backend/ main.go # server bootstrap, config from env, router handlers.go # GET/PUT/DELETE /bookmarks, /healthz store.go # SQLite open + queries (modernc.org/sqlite, CGO_ENABLED=0) middleware.go # bearer-token auth + CORS/preflight store_test.go # handler + store tests (httptest + temp sqlite) go.mod Dockerfile # multi-stage: golang:alpine build -> scratch/distroless docker-compose.yml # service + named volume for the sqlite file userscript/ manga-bookmark.user.js README.md # deploy steps + Bromite install steps + config ``` --- ## Backend (Go) **Stack:** stdlib `net/http` (no framework needed for 3 routes) + `modernc.org/sqlite` (pure Go → static binary, `scratch` image). Rust/axum + `rusqlite` is a drop-in alternative if preferred later. **Data model** — one table, `key` unique across both sites: ```sql CREATE TABLE IF NOT EXISTS bookmarks ( key TEXT PRIMARY KEY, -- ":" site TEXT NOT NULL, -- "asura" | "demonic" series_id TEXT NOT NULL, title TEXT, series_url TEXT, cover TEXT, last_chapter TEXT, -- label as shown, e.g. "Chapter 123" last_chapter_num REAL, -- parsed for max() comparison last_chapter_url TEXT, updated_at INTEGER NOT NULL -- unix ms ); ``` **Endpoints** (JSON): - `GET /bookmarks` → array of all bookmarks (single-user store). - `PUT /bookmarks/{key}` → upsert one series (body = bookmark object). Server sets `updated_at`. - `DELETE /bookmarks/{key}` → remove one. - `GET /healthz` → `200 ok` (no auth). **Middleware:** - **Auth:** require `Authorization: Bearer ` (env) on `/bookmarks*`; 401 otherwise. Constant-time compare. - **CORS:** reflect `Origin` when it's in `ALLOWED_ORIGINS` (env, comma list: `https://asuracomic.net,https://asurascans.com,https://demonicscans.org`). Allow methods `GET,PUT,DELETE,OPTIONS`, headers `Authorization,Content-Type`. Answer preflight `OPTIONS` with `204`. **Config via env:** `API_TOKEN`, `ALLOWED_ORIGINS`, `DB_PATH` (default `/data/bookmarks.db`), `PORT` (default `8080`). **Dockerfile:** multi-stage — `golang:1.23-alpine` build with `CGO_ENABLED=0 go build`, final stage `gcr.io/distroless/static` (or `scratch`) copying the binary; `/data` volume for the SQLite file. (See `multi-stage-dockerfile` skill.) **docker-compose.yml:** one service, named volume mounted at `/data`, env vars, `restart: unless-stopped`. Attach to the existing reverse-proxy network (or expose a local port the proxy targets) — the proxy terminates TLS and routes `manga-api.` → service `:8080`. (See `docker-compose-orchestration` skill.) --- ## Userscript (`manga-bookmark.user.js`) Single Bromite-compatible file. **No `GM_*` calls anywhere** (so it also runs in desktop Tampermonkey/Violentmonkey for faster iteration). Wrapped in an IIFE. **Metadata header:** `@match https://asuracomic.net/*`, `https://asurascans.com/*`, `https://demonicscans.org/*`; `@run-at document-idle`; `@name`, `@version`. **Config block (top of file, user fills in):** ```js const API_BASE = "https://manga-api."; const API_TOKEN = ""; ``` **Modules inside the IIFE:** 1. **Site adapters** — one per host, each exposing `detect(location, document)` → `{ type: 'series'|'chapter'|'other', site, seriesId, title, cover, seriesUrl, chapterLabel, chapterNum, chapterUrl }`. - Identify page **type + IDs from URL regex** (most stable); pull `title`/`cover` from **`og:title` / `og:image` meta tags** (present and stable on both sites, avoids brittle CSS classes). - Asura: series `…/series/-`, chapter `…/series/-/chapter/` → `seriesId = -`, `chapterNum = n`. - Demonic: series `…/manga/`, chapter reader `…/title//` (exact reader path to be confirmed against live DOM). - **URL patterns + selectors get verified against live pages during implementation** (Cloudflare blocks server-side fetch; confirm via Playwright MCP or on-device devtools before finalizing). 2. **API client** — `apiGet()`, `apiPut(key, obj)`, `apiDelete(key)` via `fetch` with the bearer header. `localStorage` key `mangabm:cache` holds the last-known bookmark list for instant render + offline fallback. 3. **Progress logic** — on a chapter page of a **bookmarked** series, auto-upsert `last_chapter` when `chapterNum >= stored last_chapter_num` (so re-reading old chapters doesn't regress progress; unparseable → set current). Manual override in the panel forces any value. Bookmarking a new series is available from both series and chapter pages. 4. **Floating UI** — rendered inside a **Shadow DOM** root (isolates from site CSS; important on mobile). Fixed circular button bottom-right (respects safe-area insets, high `z-index`); tap toggles a slide-in panel: - Header: context of current page — "+ Bookmark this" if unbookmarked, else current progress + "Update to this chapter". - List: bookmarks sorted by `updated_at` desc — title, last chapter, **Continue** link (→ `last_chapter_url` or `series_url`), edit-chapter input, remove. - Toasts for sync success/failure. 5. **SPA navigation** — Asura is a Next.js client-routed app (no full reload on chapter change). Patch `history.pushState`/`replaceState` + listen for `popstate`, re-run `detect()` on URL change so auto-update fires without reload. Demonic (classic reloads) works via the initial `document-idle` run. **Sync strategy:** last-write-wins (single user). On load: `apiGet()` → render → cache. On mutation: optimistic cache+UI update, then `PUT`/`DELETE`; on failure show a toast, keep local, retry on next load. --- ## Verification **Backend** - `go test ./...` — auth (401 without/with bad token), CORS preflight headers + origin reflection, upsert→get→delete round-trip against a temp SQLite file. - Local smoke: `docker compose up`, then `curl` `GET/PUT/DELETE` with `Authorization` header; confirm `OPTIONS` preflight returns the CORS headers. - Deployed: hit `https://manga-api./healthz`; confirm valid TLS (no mixed-content) and preflight from a real site origin. **Userscript** - Confirm adapter URL regex + `og:` extraction on live Asura + Demonic pages (Playwright MCP or on-device devtools) before finalizing selectors. - Desktop dry-run in Tampermonkey/Violentmonkey (same file, no GM APIs): bookmark a series, open a chapter, verify panel updates and `curl GET /bookmarks` on the server reflects it. - On Bromite: install per README, repeat the flow on both sites; verify auto-update on chapter open, manual override, Continue link, and cross-site unified list (bookmark on Asura shows when panel opened on Demonic). **Open items to confirm during build:** exact Demonic reader URL path + chapter-number source; Asura's live series/chapter path (post-`asuracomic.net` migration); Bromite's current userscript-install steps (documented in README, verified on-device).