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

143 lines
9.1 KiB
Markdown

# 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:
```sql
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):**
```js
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).