# Manga Bookmark Track manga read-progress on **asurascans.com** (a.k.a. asuracomic.net) and **demonicscans.org** from a phone (Bromite / mobile Chromium), synced to a self-hosted Go backend so bookmarks unify across both sites and all devices. Two parts: - **`backend/`** — tiny Go (`net/http` + pure-Go SQLite) sync service. 4 routes, static binary, distroless container. - **`userscript/manga-bookmark.user.js`** — single Bromite-compatible userscript (no `GM_*` APIs) that injects an on-page bookmark UI and syncs via `fetch()`. ``` Bromite userscript (isolated world, Shadow DOM UI, localStorage cache) -- fetch() HTTPS --> reverse proxy (TLS + CORS) --> Go net/http --> SQLite (volume) ``` --- ## 1. Backend ### Config (env) | Var | Default | Notes | |-----|---------|-------| | `API_TOKEN` | *(required)* | Bearer token shared with the userscript. | | `ALLOWED_ORIGINS` | Asura + Demonic origins | Comma-separated CORS allowlist. | | `DB_PATH` | `/data/bookmarks.db` | SQLite file location. | | `PORT` | `8080` | Plain HTTP; TLS terminated by the proxy. | ### Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/bookmarks` | Bearer | All bookmarks (single-user). | | `PUT` | `/bookmarks/{key}` | Bearer | Upsert one series; server sets `updated_at`. | | `DELETE` | `/bookmarks/{key}` | Bearer | Remove one. | | `GET` | `/healthz` | none | `200 ok`. | `key` is `:` — e.g. `asura:trash-of-the-counts-family-f886a8af` or `demonic:Infinite-Level-Up-in-Murim`. Sync is last-write-wins. ### Develop / test ```bash cd backend go test ./... # unit + handler tests CGO_ENABLED=0 go build # static binary ``` ### Run the stack ```bash cp .env.example .env # edit .env: set API_TOKEN (openssl rand -hex 32) docker compose up -d --build # binds 127.0.0.1:8080 ``` Smoke test: ```bash TOKEN=$(grep '^API_TOKEN=' .env | cut -d= -f2) curl -s localhost:8080/healthz # ok curl -s localhost:8080/bookmarks # 401 curl -s -H "Authorization: Bearer $TOKEN" localhost:8080/bookmarks # [] curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"title":"Test","last_chapter":"Chapter 1","last_chapter_num":1}' \ localhost:8080/bookmarks/asura:test-1 curl -s -i -X OPTIONS -H 'Origin: https://asurascans.com' \ -H 'Access-Control-Request-Method: PUT' \ localhost:8080/bookmarks/asura:test-1 | grep -i access-control # 204 + CORS headers ``` ### Deploy behind your reverse proxy Route `https://manga-api.` → the service on `:8080` (TLS at the proxy). - **Host proxy** (nginx/Caddy on the host): the base compose already binds `127.0.0.1:8080`; point the proxy `proxy_pass http://127.0.0.1:8080;`. - **Docker proxy** (Traefik/nginx in a container on its own network): use the override, which drops the published port and joins the shared network: ```bash docker network create proxy # once, if it doesn't exist docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build ``` Set `PROXY_NETWORK` in `.env` if your network isn't named `proxy`. Verify: `https://manga-api./healthz` returns `ok` over valid TLS (no mixed-content), and an `OPTIONS` preflight from a real site origin returns the CORS headers. --- ## 2. Userscript ### Configure Edit the config block at the top of `userscript/manga-bookmark.user.js`: ```js const API_BASE = "https://manga-api."; // no trailing slash const API_TOKEN = ""; ``` The token lives in the userscript's **isolated world** — the manga sites' own JS cannot read it. ### Install on Bromite (mobile) Bromite runs Chromium's native userscript engine (no Tampermonkey needed): 1. Bromite → **Settings → User scripts** → enable user scripts (allow the permission prompt). 2. Save the configured `manga-bookmark.user.js` to the device (or open its raw URL). Bromite detects the `.user.js` and offers to install it. 3. Confirm the install; the `@match` list covers both sites. 4. Open a series on either site — a 📑 button appears bottom-right. > Exact menu wording varies by Bromite build; if "User scripts" is absent, > update Bromite or use a build with userscript support. ### Desktop iteration (optional) The script is `GM_*`-free, so it also runs in Tampermonkey/Violentmonkey on desktop for faster testing — install the same file unchanged. ### Use - **Bookmark**: on a series or chapter page, open the panel → **+ Bookmark this**. - **Auto-progress**: opening a chapter of a bookmarked series records it when the chapter number ≥ the stored one (re-reading older chapters never regresses progress; unparseable numbers set the current chapter). - **Manual override**: panel → **Edit** on any row forces a specific chapter. - **Continue**: jumps to the last-read chapter (or the series page). - Bookmarks made on Asura appear when the panel is opened on Demonic, and vice versa — the backend is the shared store. Offline / backend down: changes are cached in `localStorage` and retried on the next successful load (last-write-wins). --- ## Adapter reference (verified live 2026-07-24) The site adapters key everything off URL regex, with `title`/`cover` from `og:title` / `og:image`. Confirmed against live pages via Playwright: | Site | Series URL | Chapter URL | `series_id` | |------|-----------|-------------|-------------| | **Asura** (`asurascans.com`; `asuracomic.net` 301s here) | `/comics/` | `/comics//chapter/` | `` | | **Demonic** (`demonicscans.org`) | `/manga/` | `/title//chapter//` (`chaptered.php?manga=&chapter=` 301s here) | `` | Notes: - Asura `og:title` carries a `Chapter N - Read Online \| Asura Scans` suffix that the adapter strips; Demonic chapter `og:title` is ` Chapter N`. - Demonic's `<slug>` is identical on `/manga/…` and the canonical `/title/…` reader, so a bookmark set from the series page and the auto-update from the reader resolve to the **same key**. - Asura showed no Next.js markers on the live site, so navigation uses a framework-agnostic watcher (history patch + polling) rather than a Next-only hook — works for client-routed and full-reload sites alike. If either site changes its URL shape, update the regex in the matching adapter in `userscript/manga-bookmark.user.js` and re-verify.