Serve the userscript from the backend; card + loading fixes #8

Merged
sulthan merged 6 commits from userscript-download-url into main 2026-07-28 18:28:28 +07:00
Owner

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; touching 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.

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.
sulthan added 5 commits 2026-07-28 18:07:00 +07:00
Reads the file per request from USERSCRIPT_PATH and rewrites its @version
to an mtime-derived value, so Violentmonkey always sees a higher version
after an edit regardless of what the file body claims. Also guards against
ServeMux's own path-cleaning redirect turning an empty {token} segment into
a 307 instead of the required 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only the cover and title continue reading now; the action row is a sibling
of the subtitle inside the text column instead of a full-width row below
the whole card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sulthan added 1 commit 2026-07-28 18:13:16 +07:00
Documents the Node stub harness in userscript/test/logic.test.js: the four
globals it installs, why document.body is left undefined, the module.exports
test hook, and what is deliberately not testable (no DOM harness).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sulthan merged commit 0416354c06 into main 2026-07-28 18:28:28 +07:00
sulthan deleted branch userscript-download-url 2026-07-31 01:02:01 +07:00
Sign in to join this conversation.