Files
mangaBookmark/plans/mangaBookmark.md
T
2026-07-24 16:23:24 +07:00

9.1 KiB

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.<domain>) 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:

CREATE TABLE IF NOT EXISTS bookmarks (
  key              TEXT PRIMARY KEY,  -- "<site>:<series_id>"
  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 <API_TOKEN> (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.<domain> → 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):

const API_BASE = "https://manga-api.<domain>";
const API_TOKEN = "<paste 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/<slug>-<id>, chapter …/series/<slug>-<id>/chapter/<n> → seriesId = <slug>-<id>, chapterNum = n.
    • Demonic: series …/manga/<slug>, chapter reader …/title/<id>/<chapterId> (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.<domain>/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).