Files
mangaBookmark/DEPLOY.md
T
sulthan 0416354c06 Serve the userscript from the backend; card + loading fixes (#8)
Serves the userscript from the backend so Violentmonkey auto-updates it, plus two panel fixes.

## Backend: `GET /u/{token}/manga-bookmark.user.js`

The script is read off disk per request from `USERSCRIPT_PATH` and streamed back with its `@version` line rewritten.

- **Token in the path, not a header.** Violentmonkey's update poll sends no `Authorization` header, and the script embeds `API_TOKEN` in plain text — an open URL would hand that token to anyone who guessed it. Compare is constant-time.
- **404, never 401**, for both a wrong token and a missing file: a prober learns nothing about whether the route exists.
- Registered outside `withAuth` and outside the `WEB_PASSWORD` gate, so the script is installable on a deployment that never enabled the web UI.
- Stdlib only (`crypto/subtle`, `os`, `regexp`) — no new Go dependencies.

**The served `@version` is derived from the file's mtime** (`YYYY.MM.DD.HHMM`, UTC), discarding whatever the file body says. Violentmonkey only updates when the served version sorts higher than the installed one, so a body-derived version means one typo or accidental downgrade freezes updates forever. An mtime-derived version is monotonic by construction. A file with no `@version` line is served byte-identical. `os.Stat` runs before `os.ReadFile`, so a concurrent edit can only serve new content under an old stamp — which self-heals on the next poll — never the reverse.

## Bindmount

`./userscript` is bindmounted read-only at `/userscript`. The script is deliberately **not** copied into the image: the build context stays `./backend`, and widening it would churn every `COPY` path for a file the mount always supplies. Editing the file on the VPS is live on the next poll — no rebuild, no restart. `git pull` restores the committed version, so a redeploy always ships the repo's script; checkout sets mtime to now, so even a rollback serves a *higher* version and is adopted. Without the mount the endpoint 404s and logs it; bookmark sync is unaffected.

`@downloadURL` / `@updateURL` are literal URLs in the metadata block — it is parsed before any JS runs, so `API_BASE`/`API_TOKEN` cannot be interpolated. The token was already committed in this file, so this adds no new exposure.

## Userscript UI

- **Card actions moved under the subtitle.** Only the cover and the title continue reading now; the subtitle and the action row are inert siblings in the text column. A thumb that misses ★ lands on nothing, and Remove is never inside a link.
- **Loading spinner** while the first fetch is in flight — the panel used to read as frozen on the first open after a cold start. It draws only when there is nothing cached to draw instead, so a populated list never flaps.

## Verification

- `go test -count=1 ./...` — ok, 7.070s
- `node --check` clean; `node --test userscript/test/logic.test.js` — 14/14
- Live `docker compose` smoke: `/healthz` 200, wrong token 404, script served with a stamped `@version 2026.07.28.1057` and both metadata URLs present; `touch`ing the file advanced the served version to `2026.07.28.1100` with no restart.

Layout and spinner are verified on-device — there is deliberately no DOM test harness.

Reviewed-on: #8
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
2026-07-28 18:28:27 +07:00

8.9 KiB

Deployment

Step-by-step for the backend (Docker + Traefik) and the Bromite userscript. Assumes you already run Traefik in Docker with a working HTTPS entrypoint and an ACME/cert resolver, and control a domain.


0. Prerequisites

  • Docker + Docker Compose on the server.
  • A Traefik instance watching a Docker network (default name assumed: proxy).
  • DNS: an A/AAAA record for manga-api.<yourdomain> pointing at the server.
  • The repo copied to the server, e.g. /opt/mangabm/ (needs backend/, docker-compose.yml, docker-compose.prod.yml, .env.example).

Confirm the Traefik network exists (create if not):

docker network ls | grep proxy || docker network create proxy

1. Configure .env

cd /opt/mangabm
cp .env.example .env

Edit .env:

# Required — long random secret, also goes in the userscript.
API_TOKEN=<paste output of: openssl rand -hex 32>

# CORS allowlist — leave as-is unless a site changes hostname.
ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org

# Required for the Traefik override. Both have no fallback — compose refuses
# to start without them. MANGA_WEB_HOST is required even if you never set
# WEB_PASSWORD; see 1b.
MANGA_API_HOST=manga-api.violetcrown.my.id
MANGA_WEB_HOST=manga.violetcrown.my.id

# Only if your Traefik setup differs from these defaults:
# PROXY_NETWORK=proxy
# TRAEFIK_ENTRYPOINT=websecure
# TRAEFIK_CERTRESOLVER=le

Generate + insert the token in one line:

sed -i "s|^API_TOKEN=.*|API_TOKEN=$(openssl rand -hex 32)|" .env
grep -E '^API_TOKEN=' .env   # copy this — the userscript needs the same value

Match TRAEFIK_ENTRYPOINT / TRAEFIK_CERTRESOLVER to your Traefik's actual names (check your Traefik static config — common alternatives: https, myresolver, cloudflare). Wrong names = no certificate issued.


1b. Web UI

The browser UI is served by the same container on a second hostname.

  1. Add a DNS A/AAAA record for manga.<yourdomain> pointing at the server — the same address as manga-api.<yourdomain>.

  2. Set both variables in .env:

    MANGA_WEB_HOST=manga.violetcrown.my.id
    WEB_PASSWORD=<paste output of: openssl rand -base64 18>
    

    Generate and insert in one line:

    sed -i "s|^WEB_PASSWORD=.*|WEB_PASSWORD=$(openssl rand -base64 18)|" .env
    grep -E '^WEB_PASSWORD=' .env   # this is what you type into the site
    
  3. Redeploy and check:

    docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
    curl -s -o /dev/null -w '%{http_code}\n' https://manga.violetcrown.my.id/
    

    Expected 200, serving the login page.

Leaving WEB_PASSWORD unset is safe: the web routes are not registered and / returns 404. The userscript's API on MANGA_API_HOST is unaffected either way.

MANGA_WEB_HOST itself is required by the prod override regardless — like MANGA_API_HOST, its Traefik label has no fallback, so docker compose up refuses to start without it even if WEB_PASSWORD is unset and the web UI is otherwise dormant.

Sessions are signed with a key derived from API_TOKEN and WEB_PASSWORD, so rotating either one logs every browser out. The session cookie lasts 60 days.


2. Build + start

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build

This merges the base file (build/image/env/volume) with the prod override (no host port, Traefik network + router labels). Always pass both -f flags — the prod file is not standalone.

Check it's up and healthy:

docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
docker logs manga-api --tail 20   # expect: "listening on :8080 ..."

3. Verify over HTTPS

Give Traefik a few seconds to issue the cert, then:

# Health (no auth) — must be valid TLS, no cert warning.
curl -s https://manga-api.violetcrown.my.id/healthz          # -> ok

# Auth enforced.
curl -s -o /dev/null -w '%{http_code}\n' \
  https://manga-api.violetcrown.my.id/bookmarks              # -> 401

TOKEN=$(grep -E '^API_TOKEN=' .env | cut -d= -f2)
curl -s -H "Authorization: Bearer $TOKEN" \
  https://manga-api.violetcrown.my.id/bookmarks              # -> []

# CORS preflight from a real site origin.
curl -s -i -X OPTIONS \
  -H 'Origin: https://asurascans.com' \
  -H 'Access-Control-Request-Method: PUT' \
  https://manga-api.violetcrown.my.id/bookmarks/x | grep -i access-control
# -> Access-Control-Allow-Origin: https://asurascans.com  (+ Methods/Headers)

All four must pass. Valid TLS is non-negotiable — the manga sites are HTTPS, so a bad cert makes the browser block the userscript's fetch() (mixed content).


4. Configure the userscript

Edit the config block at the top of userscript/manga-bookmark.user.js:

const API_BASE = "https://manga-api.yourdomain.com"; // no trailing slash
const API_TOKEN = "<same token as .env>";

The token sits in the userscript's isolated world — the manga sites' JS can't read it.

Also edit the @downloadURL/@updateURL metadata lines near the top of the file — they ship hardcoded to this deployment's domain and token, so a deployer who skips them ends up auto-updating from someone else's backend. See "Installing / updating the userscript" below for how those two lines are used.


5. Install on Bromite

  1. Bromite → Settings → User scripts → enable (accept the permission prompt).
  2. Put the edited manga-bookmark.user.js on the device (save the file, or open its raw URL). Bromite detects .user.js and offers to install.
  3. Confirm install — the @match list covers both sites.
  4. Open a series on asurascans.com or demonicscans.org → a 📑 button appears bottom-right → tap → + Bookmark this.

Optional desktop test: the script is GM_*-free, so the same file installs in Tampermonkey/Violentmonkey for quick checks before going mobile.


6. Smoke-test the full loop

  1. Bookmark a series on Asura.
  2. curl -s -H "Authorization: Bearer $TOKEN" https://manga-api.yourdomain.com/bookmarks on the server — the series should appear.
  3. Open a chapter of that series — reopen the panel; last-read updates to that chapter (auto, never regresses on older chapters).
  4. Open Demonic, open the panel — the Asura bookmark shows there too (shared store, cross-site unified list).

Updating

Pull new code, then rebuild:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build

SQLite data persists in the named volume bookmarks-data across rebuilds.


Troubleshooting

Symptom Likely cause / fix
No cert / TLS error at the domain TRAEFIK_ENTRYPOINT or TRAEFIK_CERTRESOLVER name wrong; or DNS not resolving yet. Check docker logs <traefik>.
404 from Traefik Service not on the proxy network, or MANGA_API_HOST mismatch. Confirm docker network inspect proxy lists manga-api.
fetch fails in the userscript, curl works Origin missing from ALLOWED_ORIGINS, or mixed content (backend not HTTPS).
401 with the right token Trailing space/newline in API_TOKEN; regenerate and restart.
Panel button absent URL didn't match an adapter, or user scripts disabled in Bromite.
compose ... config errors about API_TOKEN Run compose from the dir with .env, or export the vars.

Backend config reference and endpoint list: see README.md.


Installing / updating the userscript

The backend serves the script itself, so Violentmonkey can auto-update it. Complements §4 above — that step points API_BASE/API_TOKEN at your backend; this one points @downloadURL/@updateURL at the same place so auto-updates come from it too.

Install once, on the phone (Cromite + Violentmonkey):

https://manga-api.<your-domain>/u/<API_TOKEN>/manga-bookmark.user.js

Open that URL in Cromite; Violentmonkey offers to install it. The token is in the path because Violentmonkey's update poll sends no Authorization header, and the script embeds API_TOKEN in plain text — an open URL would leak it. A wrong token answers 404.

Updating, without a redeploy:

vi userscript/manga-bookmark.user.js   # on the VPS, in this checkout

./userscript is bindmounted read-only into the container and read fresh on every request, so the edit is live immediately. The served @version is derived from the file's mtime (YYYY.MM.DD.HHMM, UTC), not from the @version in the file, so any edit outranks the installed copy and Violentmonkey pulls it on its next check. The @version in the repo is a human marker only.

Updating via redeploy: git pull overwrites the file with the committed version, which is the intended behaviour — a deploy always ships the repo's script. Note that git pull sets mtime to checkout time, so even a rollback serves a higher version and is adopted.

If the mount is missing, the endpoint answers 404 and logs it; bookmark sync is unaffected.