refactor: rebrand compose, env and docs to BookmarkManager

This commit is contained in:
2026-08-06 02:55:46 +07:00
parent d41a1d2c0b
commit 44a8df43e1
11 changed files with 81 additions and 81 deletions
+4 -4
View File
@@ -10,7 +10,7 @@ ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicsca
# --- Prod override (Traefik) only --- # --- Prod override (Traefik) only ---
# Subdomain Traefik routes to this service (required by the prod override). # Subdomain Traefik routes to this service (required by the prod override).
# MANGA_API_HOST=manga-api.example.com # BOOKMARK_API_HOST=bookmark-api.example.com
# Traefik's docker network name, if not "proxy". # Traefik's docker network name, if not "proxy".
# PROXY_NETWORK=proxy # PROXY_NETWORK=proxy
# Traefik HTTPS entrypoint + cert resolver names, if yours differ from these. # Traefik HTTPS entrypoint + cert resolver names, if yours differ from these.
@@ -18,7 +18,7 @@ ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicsca
# TRAEFIK_CERTRESOLVER=le # TRAEFIK_CERTRESOLVER=le
# --- Web UI --- # --- Web UI ---
# Password for the browser UI at https://$MANGA_WEB_HOST. Leave unset to # Password for the browser UI at https://$BOOKMARK_WEB_HOST. Leave unset to
# disable the web UI entirely (the routes are not registered at all). # disable the web UI entirely (the routes are not registered at all).
# Generate one: openssl rand -base64 18 # Generate one: openssl rand -base64 18
WEB_PASSWORD= WEB_PASSWORD=
@@ -27,8 +27,8 @@ WEB_PASSWORD=
# whether or not WEB_PASSWORD is set). Left commented on purpose: an example # whether or not WEB_PASSWORD is set). Left commented on purpose: an example
# value here would be a silent wrong-hostname fallback, and Traefik would # value here would be a silent wrong-hostname fallback, and Traefik would
# publish the UI router on a domain you do not own. The same container also # publish the UI router on a domain you do not own. The same container also
# answers on MANGA_API_HOST for the userscript's API. # answers on BOOKMARK_API_HOST for the userscript's API.
# MANGA_WEB_HOST=manga.example.com # BOOKMARK_WEB_HOST=bookmark.example.com
# --- Latest-chapter poller --- # --- Latest-chapter poller ---
# The backend re-checks each bookmarked series' newest published chapter on its # The backend re-checks each bookmarked series' newest published chapter on its
+1 -1
View File
@@ -49,7 +49,7 @@ Smoke test: `curl` endpoints with `Authorization: Bearer <token>`; confirm `OPTI
## Design system ## Design system
Web UI + userscript panel follow **Cinder**, rules in `docs/design-system.md` Web UI + userscript panel follow **Cinder**, rules in `docs/design-system.md`
— source of truth Claude Design project `mangaBookmark Web UI` — source of truth Claude Design project `BookmarkManager Web UI`
(`969ac210-fe02-4c01-ae1b-9a271dcc779a`). Read it before touching (`969ac210-fe02-4c01-ae1b-9a271dcc779a`). Read it before touching
`backend/internal/web/static/style.css`, `backend/internal/web/templates/*`, or userscript `backend/internal/web/static/style.css`, `backend/internal/web/templates/*`, or userscript
`TEMPLATE`/`CSS`. Core law: **ember means new chapter only** — no other `TEMPLATE`/`CSS`. Core law: **ember means new chapter only** — no other
+1 -1
View File
@@ -49,7 +49,7 @@ Smoke test: `curl` endpoints with `Authorization: Bearer <token>`; confirm `OPTI
## Design system ## Design system
Web UI + userscript panel follow **Cinder**, rules in `docs/design-system.md` Web UI + userscript panel follow **Cinder**, rules in `docs/design-system.md`
— source of truth Claude Design project `mangaBookmark Web UI` — source of truth Claude Design project `BookmarkManager Web UI`
(`969ac210-fe02-4c01-ae1b-9a271dcc779a`). Read it before touching (`969ac210-fe02-4c01-ae1b-9a271dcc779a`). Read it before touching
`backend/internal/web/static/style.css`, `backend/internal/web/templates/*`, or userscript `backend/internal/web/static/style.css`, `backend/internal/web/templates/*`, or userscript
`TEMPLATE`/`CSS`. Core law: **ember means new chapter only** — no other `TEMPLATE`/`CSS`. Core law: **ember means new chapter only** — no other
+24 -24
View File
@@ -10,8 +10,8 @@ ACME/cert resolver, and control a domain.
- Docker + Docker Compose on the server. - Docker + Docker Compose on the server.
- A Traefik instance watching a Docker network (default name assumed: `proxy`). - A Traefik instance watching a Docker network (default name assumed: `proxy`).
- DNS: an `A`/`AAAA` record for `manga-api.<yourdomain>` pointing at the server. - DNS: an `A`/`AAAA` record for `bookmark-api.<yourdomain>` pointing at the server.
- The repo copied to the server, e.g. `/opt/mangabm/` (needs `backend/`, - The repo copied to the server, e.g. `/opt/bookmarkmanager/` (needs `backend/`,
`docker-compose.yml`, `docker-compose.prod.yml`, `.env.example`). `docker-compose.yml`, `docker-compose.prod.yml`, `.env.example`).
Confirm the Traefik network exists (create if not): Confirm the Traefik network exists (create if not):
@@ -25,7 +25,7 @@ docker network ls | grep proxy || docker network create proxy
## 1. Configure `.env` ## 1. Configure `.env`
```bash ```bash
cd /opt/mangabm cd /opt/bookmarkmanager
cp .env.example .env cp .env.example .env
``` ```
@@ -39,10 +39,10 @@ API_TOKEN=<paste output of: openssl rand -hex 32>
ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to ALLOWED_ORIGINS=https://asuracomic.net,https://asurascans.com,https://demonicscans.org,https://comix.to,https://kagane.to
# Required for the Traefik override. Both have no fallback — compose refuses # 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 # to start without them. BOOKMARK_WEB_HOST is required even if you never set
# WEB_PASSWORD; see 1b. # WEB_PASSWORD; see 1b.
MANGA_API_HOST=manga-api.violetcrown.my.id BOOKMARK_API_HOST=bookmark-api.violetcrown.my.id
MANGA_WEB_HOST=manga.violetcrown.my.id BOOKMARK_WEB_HOST=bookmark.violetcrown.my.id
# Only if your Traefik setup differs from these defaults: # Only if your Traefik setup differs from these defaults:
# PROXY_NETWORK=proxy # PROXY_NETWORK=proxy
@@ -67,13 +67,13 @@ grep -E '^API_TOKEN=' .env # copy this — the userscript needs the same value
The browser UI is served by the same container on a second hostname. 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 — 1. Add a DNS `A`/`AAAA` record for `bookmark.<yourdomain>` pointing at the server —
the same address as `manga-api.<yourdomain>`. the same address as `bookmark-api.<yourdomain>`.
2. Set both variables in `.env`: 2. Set both variables in `.env`:
```ini ```ini
MANGA_WEB_HOST=manga.violetcrown.my.id BOOKMARK_WEB_HOST=bookmark.violetcrown.my.id
WEB_PASSWORD=<paste output of: openssl rand -base64 18> WEB_PASSWORD=<paste output of: openssl rand -base64 18>
``` ```
@@ -88,16 +88,16 @@ The browser UI is served by the same container on a second hostname.
```bash ```bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build 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/ curl -s -o /dev/null -w '%{http_code}\n' https://bookmark.violetcrown.my.id/
``` ```
Expected `200`, serving the login page. Expected `200`, serving the login page.
Leaving `WEB_PASSWORD` unset is safe: the web routes are not registered and `/` 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. returns 404. The userscript's API on `BOOKMARK_API_HOST` is unaffected either way.
`MANGA_WEB_HOST` itself is required by the prod override regardless — like `BOOKMARK_WEB_HOST` itself is required by the prod override regardless — like
`MANGA_API_HOST`, its Traefik label has no fallback, so `docker compose up` `BOOKMARK_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 refuses to start without it even if `WEB_PASSWORD` is unset and the web UI is
otherwise dormant. otherwise dormant.
@@ -116,9 +116,9 @@ This merges the base file (build/image/env/volume) with the prod override
(no host port, Traefik network + router labels). Always pass **both** `-f` (no host port, Traefik network + router labels). Always pass **both** `-f`
flags — the prod file is not standalone. flags — the prod file is not standalone.
Two services come up: `manga-api` (the backend) and `headless-shell`, a CDP Two services come up: `bookmark-api` (the backend) and `headless-shell`, a CDP
sidecar the poller uses to fetch kagane (behind a Cloudflare JS challenge). sidecar the poller uses to fetch kagane (behind a Cloudflare JS challenge).
It has no published port — only `manga-api` can reach it, over It has no published port — only `bookmark-api` can reach it, over
`BROWSER_WS_URL`. Missing or unreachable, the poller just skips kagane and `BROWSER_WS_URL`. Missing or unreachable, the poller just skips kagane and
logs it; nothing else is affected. logs it; nothing else is affected.
@@ -126,7 +126,7 @@ Check it's up and healthy:
```bash ```bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
docker logs manga-api --tail 20 # expect: "listening on :8080 ..." docker logs bookmark-api --tail 20 # expect: "listening on :8080 ..."
``` ```
--- ---
@@ -137,21 +137,21 @@ Give Traefik a few seconds to issue the cert, then:
```bash ```bash
# Health (no auth) — must be valid TLS, no cert warning. # Health (no auth) — must be valid TLS, no cert warning.
curl -s https://manga-api.violetcrown.my.id/healthz # -> ok curl -s https://bookmark-api.violetcrown.my.id/healthz # -> ok
# Auth enforced. # Auth enforced.
curl -s -o /dev/null -w '%{http_code}\n' \ curl -s -o /dev/null -w '%{http_code}\n' \
https://manga-api.violetcrown.my.id/bookmarks # -> 401 https://bookmark-api.violetcrown.my.id/bookmarks # -> 401
TOKEN=$(grep -E '^API_TOKEN=' .env | cut -d= -f2) TOKEN=$(grep -E '^API_TOKEN=' .env | cut -d= -f2)
curl -s -H "Authorization: Bearer $TOKEN" \ curl -s -H "Authorization: Bearer $TOKEN" \
https://manga-api.violetcrown.my.id/bookmarks # -> [] https://bookmark-api.violetcrown.my.id/bookmarks # -> []
# CORS preflight from a real site origin. # CORS preflight from a real site origin.
curl -s -i -X OPTIONS \ curl -s -i -X OPTIONS \
-H 'Origin: https://asurascans.com' \ -H 'Origin: https://asurascans.com' \
-H 'Access-Control-Request-Method: PUT' \ -H 'Access-Control-Request-Method: PUT' \
https://manga-api.violetcrown.my.id/bookmarks/x | grep -i access-control https://bookmark-api.violetcrown.my.id/bookmarks/x | grep -i access-control
# -> Access-Control-Allow-Origin: https://asurascans.com (+ Methods/Headers) # -> Access-Control-Allow-Origin: https://asurascans.com (+ Methods/Headers)
``` ```
@@ -165,7 +165,7 @@ a bad cert makes the browser block the userscript's `fetch()` (mixed content).
Edit the config block at the top of `userscript/manga-bookmark.user.js`: Edit the config block at the top of `userscript/manga-bookmark.user.js`:
```js ```js
const API_BASE = "https://manga-api.yourdomain.com"; // no trailing slash const API_BASE = "https://bookmark-api.yourdomain.com"; // no trailing slash
const API_TOKEN = "<same token as .env>"; const API_TOKEN = "<same token as .env>";
``` ```
@@ -197,7 +197,7 @@ Tampermonkey/Violentmonkey for quick checks before going mobile.
## 6. Smoke-test the full loop ## 6. Smoke-test the full loop
1. Bookmark a series on Asura. 1. Bookmark a series on Asura.
2. `curl -s -H "Authorization: Bearer $TOKEN" https://manga-api.yourdomain.com/bookmarks` 2. `curl -s -H "Authorization: Bearer $TOKEN" https://bookmark-api.yourdomain.com/bookmarks`
on the server — the series should appear. on the server — the series should appear.
3. Open a chapter of that series — reopen the panel; last-read updates to that 3. Open a chapter of that series — reopen the panel; last-read updates to that
chapter (auto, never regresses on older chapters). chapter (auto, never regresses on older chapters).
@@ -223,7 +223,7 @@ SQLite data persists in the named volume `bookmarks-data` across rebuilds.
| Symptom | Likely cause / fix | | 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>`. | | 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`. | | 404 from Traefik | Service not on the `proxy` network, or `BOOKMARK_API_HOST` mismatch. Confirm `docker network inspect proxy` lists `bookmark-api`. |
| `fetch` fails in the userscript, `curl` works | Origin missing from `ALLOWED_ORIGINS`, or mixed content (backend not HTTPS). | | `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. | | 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. | | Panel button absent | URL didn't match an adapter, or user scripts disabled in Bromite. |
@@ -243,7 +243,7 @@ auto-updates come from it too.
Install once, on the phone (Cromite + Violentmonkey): Install once, on the phone (Cromite + Violentmonkey):
``` ```
https://manga-api.<your-domain>/u/<API_TOKEN>/manga-bookmark.user.js https://bookmark-api.<your-domain>/u/<API_TOKEN>/manga-bookmark.user.js
``` ```
Open that URL in Cromite; Violentmonkey offers to install it. The token is in Open that URL in Cromite; Violentmonkey offers to install it. The token is in
+1 -1
View File
@@ -35,7 +35,7 @@ Not a public reading tracker or social app — a private, self-hosted sync layer
## Brand Commitments ## Brand Commitments
- Name: **mangaBookmark**. - Name: **BookmarkManager**.
- **Dark-first is binding**: current dark-by-default / light-follows-system-preference behavior must be preserved as a design constraint, not just a starting default, because reading happens at night. - **Dark-first is binding**: current dark-by-default / light-follows-system-preference behavior must be preserved as a design constraint, not just a starting default, because reading happens at night.
## Evidence on Hand ## Evidence on Hand
+4 -4
View File
@@ -86,7 +86,7 @@ curl -s -i -X OPTIONS -H 'Origin: https://asurascans.com' \
### Deploy behind your reverse proxy ### Deploy behind your reverse proxy
Route `https://manga-api.<domain>` → the service on `:8080` (TLS at the proxy). Route `https://bookmark-api.<domain>` → the service on `:8080` (TLS at the proxy).
- **Host proxy** (nginx/Caddy on the host): the base compose already binds - **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;`. `127.0.0.1:8080`; point the proxy `proxy_pass http://127.0.0.1:8080;`.
@@ -99,7 +99,7 @@ Route `https://manga-api.<domain>` → the service on `:8080` (TLS at the proxy)
``` ```
Set `PROXY_NETWORK` in `.env` if your network isn't named `proxy`. Set `PROXY_NETWORK` in `.env` if your network isn't named `proxy`.
Verify: `https://manga-api.<domain>/healthz` returns `ok` over valid TLS (no Verify: `https://bookmark-api.<domain>/healthz` returns `ok` over valid TLS (no
mixed-content), and an `OPTIONS` preflight from a real site origin returns the mixed-content), and an `OPTIONS` preflight from a real site origin returns the
CORS headers. CORS headers.
@@ -112,7 +112,7 @@ CORS headers.
Edit the config block at the top of `userscript/manga-bookmark.user.js`: Edit the config block at the top of `userscript/manga-bookmark.user.js`:
```js ```js
const API_BASE = "https://manga-api.<domain>"; // no trailing slash const API_BASE = "https://bookmark-api.<domain>"; // no trailing slash
const API_TOKEN = "<same token as backend>"; const API_TOKEN = "<same token as backend>";
``` ```
@@ -184,7 +184,7 @@ userscript does the looking, from your own browser session:
(`LATEST_CHECK_BATCH` / `LATEST_CHECK_THROTTLE_MS`). Failures are silent and (`LATEST_CHECK_BATCH` / `LATEST_CHECK_THROTTLE_MS`). Failures are silent and
simply retried after the window. simply retried after the window.
Freshness is tracked per device in `localStorage` under `mangabm:lastchecked` Freshness is tracked per device in `localStorage` under `bmgr:manga:lastchecked`
and is deliberately not synced, since each device checks on its own. and is deliberately not synced, since each device checks on its own.
This means a bookmark is as current as its last check — not the moment a This means a bookmark is as current as its last check — not the moment a
+20 -20
View File
@@ -9,16 +9,16 @@ Whole thing is ~5 minutes, most of it waiting on `docker build`. Order matters:
**back up before you pull.** A backup taken after a bad migration is a backup of **back up before you pull.** A backup taken after a bad migration is a backup of
the damage. the damage.
Paths below assume the checkout is at `/opt/mangabm`; substitute your own. The Paths below assume the checkout is at `/opt/bookmarkmanager`; substitute your own. The
one absolute rule about paths: **backups live in `../mangabm-backups/`**, a one absolute rule about paths: **backups live in `../bookmarkmanager-backups/`**, a
sibling of the project directory (`/opt/mangabm-backups`), never inside it. It sibling of the project directory (`/opt/bookmarkmanager-backups`), never inside it. It
sits outside the repo so `git pull`, `git clean -fd` and a bad `rm -rf` inside sits outside the repo so `git pull`, `git clean -fd` and a bad `rm -rf` inside
the checkout cannot take the backups with them. the checkout cannot take the backups with them.
``` ```
/opt/ /opt/
├── mangabm/ <- the checkout (this repo) ├── bookmarkmanager/ <- the checkout (this repo)
└── mangabm-backups/ <- bookmarks-YYYYmmdd-HHMMSS.db └── bookmarkmanager-backups/ <- bookmarks-YYYYmmdd-HHMMSS.db
``` ```
--- ---
@@ -26,12 +26,12 @@ the checkout cannot take the backups with them.
## 0. Preflight ## 0. Preflight
```bash ```bash
cd /opt/mangabm cd /opt/bookmarkmanager
# Both -f flags, every time. The prod override is not standalone. # Both -f flags, every time. The prod override is not standalone.
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml" COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
$COMPOSE ps # manga-api should be Up $COMPOSE ps # bookmark-api should be Up
git status --short # expect empty git status --short # expect empty
git log --oneline -1 # note this hash — it is your rollback target git log --oneline -1 # note this hash — it is your rollback target
df -h /var/lib/docker | tail -1 # a build needs room df -h /var/lib/docker | tail -1 # a build needs room
@@ -44,9 +44,9 @@ dirty tree fails halfway and leaves you in a worse spot than either.
Create the backup directory once, and make sure it is a sibling, not a child: Create the backup directory once, and make sure it is a sibling, not a child:
```bash ```bash
mkdir -p ../mangabm-backups mkdir -p ../bookmarkmanager-backups
BACKUP_DIR="$(cd .. && pwd)/mangabm-backups" # absolute — Docker needs it BACKUP_DIR="$(cd .. && pwd)/bookmarkmanager-backups" # absolute — Docker needs it
echo "$BACKUP_DIR" # -> /opt/mangabm-backups echo "$BACKUP_DIR" # -> /opt/bookmarkmanager-backups
``` ```
--- ---
@@ -59,7 +59,7 @@ prefixes it with the project directory:
```bash ```bash
docker volume ls --filter name=bookmarks-data docker volume ls --filter name=bookmarks-data
# -> local mangabm_bookmarks-data # -> local bookmarkmanager_bookmarks-data
VOL=$(docker volume ls --filter name=bookmarks-data -q | head -1) VOL=$(docker volume ls --filter name=bookmarks-data -q | head -1)
``` ```
@@ -172,11 +172,11 @@ bindmounted read-only and read fresh per request.
```bash ```bash
$COMPOSE ps # Up, and recently (re)created $COMPOSE ps # Up, and recently (re)created
docker logs manga-api --tail 20 # -> "listening on :8080 ..." docker logs bookmark-api --tail 20 # -> "listening on :8080 ..."
``` ```
Nothing in the log about the database or the poller failing. The image is tagged Nothing in the log about the database or the poller failing. The image is tagged
`mangabm-backend:latest`, so the previous image is still on disk untagged — `bookmarkmanager-backend:latest`, so the previous image is still on disk untagged —
that is what makes the rollback in §6 quick. that is what makes the rollback in §6 quick.
--- ---
@@ -186,8 +186,8 @@ that is what makes the rollback in §6 quick.
Same four API checks as `DEPLOY.md` §3, plus the web UI. Set the host names once: Same four API checks as `DEPLOY.md` §3, plus the web UI. Set the host names once:
```bash ```bash
API=https://manga-api.violetcrown.my.id API=https://bookmark-api.violetcrown.my.id
WEB=https://manga.violetcrown.my.id WEB=https://bookmark.violetcrown.my.id
TOKEN=$(grep -E '^API_TOKEN=' .env | cut -d= -f2) TOKEN=$(grep -E '^API_TOKEN=' .env | cut -d= -f2)
curl -s $API/healthz # -> ok curl -s $API/healthz # -> ok
@@ -281,7 +281,7 @@ docker run --rm -v "$VOL":/data -v "$BACKUP_DIR":/backup alpine sh -c '
ls -l /data' ls -l /data'
$COMPOSE start $COMPOSE start
docker logs manga-api --tail 20 docker logs bookmark-api --tail 20
curl -s -H "Authorization: Bearer $TOKEN" $API/bookmarks | head -c 200 curl -s -H "Authorization: Bearer $TOKEN" $API/bookmarks | head -c 200
``` ```
@@ -302,9 +302,9 @@ Two steps here are easy to skip and both bite:
For a routine redeploy where nothing needs deciding: For a routine redeploy where nothing needs deciding:
```bash ```bash
cd /opt/mangabm cd /opt/bookmarkmanager
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml" COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
BACKUP_DIR="$(cd .. && pwd)/mangabm-backups"; mkdir -p "$BACKUP_DIR" BACKUP_DIR="$(cd .. && pwd)/bookmarkmanager-backups"; mkdir -p "$BACKUP_DIR"
VOL=$(docker volume ls --filter name=bookmarks-data -q | head -1) VOL=$(docker volume ls --filter name=bookmarks-data -q | head -1)
STAMP=$(date -u +%Y%m%d-%H%M%S) STAMP=$(date -u +%Y%m%d-%H%M%S)
@@ -314,7 +314,7 @@ docker run --rm -v "$VOL":/data -v "$BACKUP_DIR":/backup alpine sh -c \
git pull --ff-only && git pull --ff-only &&
$COMPOSE up -d --build && $COMPOSE up -d --build &&
sleep 5 && sleep 5 &&
curl -sf https://manga-api.violetcrown.my.id/healthz && echo " deploy ok" curl -sf https://bookmark-api.violetcrown.my.id/healthz && echo " deploy ok"
``` ```
The `&&` chain is deliberate: if the backup or its integrity check fails, The `&&` chain is deliberate: if the backup or its integrity check fails,
@@ -332,7 +332,7 @@ command can tell you the panel works on the phone.
| CSS or template change did not appear | You restarted without `--build`. Assets are `//go:embed`ed. | | CSS or template change did not appear | You restarted without `--build`. Assets are `//go:embed`ed. |
| Font answers `application/octet-stream` | Old binary — the `.woff2` MIME registration is in `web.go`. Rebuild. | | Font answers `application/octet-stream` | Old binary — the `.woff2` MIME registration is in `web.go`. Rebuild. |
| Everyone logged out of the web UI | `API_TOKEN` or `WEB_PASSWORD` changed; sessions are derived from both. Expected, just log in again. | | Everyone logged out of the web UI | `API_TOKEN` or `WEB_PASSWORD` changed; sessions are derived from both. Expected, just log in again. |
| `compose` errors about `MANGA_WEB_HOST` | Run from the directory holding `.env`. Both host vars are required even when the web UI is unused. | | `compose` errors about `BOOKMARK_WEB_HOST` | Run from the directory holding `.env`. Both host vars are required even when the web UI is unused. |
| Userscript did not update on the phone | Violentmonkey polls on its own schedule; force a check. `@version` comes from the file's mtime, so confirm the pull actually touched it. | | Userscript did not update on the phone | Violentmonkey polls on its own schedule; force a check. `@version` comes from the file's mtime, so confirm the pull actually touched it. |
| `apk add sqlite` fails (no network) | Use the cold-copy fallback in §1 — and copy `bookmarks.db-wal` too. | | `apk add sqlite` fails (no network) | Use the cold-copy fallback in §1 — and copy `bookmarks.db-wal` too. |
| Reads work but every write fails after a restore | Restored file is root-owned; the container is uid 65532. `chown 65532:65532` it (§6). | | Reads work but every write fails after a restore | Restored file is root-owned; the container is uid 65532. `chown 65532:65532` it (§6). |
+15 -15
View File
@@ -1,11 +1,11 @@
# Production override: join an existing Traefik network and let Traefik route # Production override: join an existing Traefik network and let Traefik route
# manga-api.<domain> -> this service with TLS. No host port published. # bookmark-api.<domain> -> this service with TLS. No host port published.
# #
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build # docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
# #
# Set in .env: # Set in .env:
# MANGA_API_HOST=manga-api.example.com # your subdomain (required) # BOOKMARK_API_HOST=bookmark-api.example.com # your subdomain (required)
# MANGA_WEB_HOST=manga.example.com # browser UI subdomain, same container (required) # BOOKMARK_WEB_HOST=bookmark.example.com # browser UI subdomain, same container (required)
# PROXY_NETWORK=proxy # Traefik's network name, if not "proxy" # PROXY_NETWORK=proxy # Traefik's network name, if not "proxy"
# TRAEFIK_ENTRYPOINT=websecure # your HTTPS entrypoint name # TRAEFIK_ENTRYPOINT=websecure # your HTTPS entrypoint name
# TRAEFIK_CERTRESOLVER=le # your ACME/cert resolver name # TRAEFIK_CERTRESOLVER=le # your ACME/cert resolver name
@@ -14,7 +14,7 @@
# docker network create proxy # if it doesn't yet # docker network create proxy # if it doesn't yet
services: services:
manga-api: bookmark-api:
# Traffic arrives over the Traefik network, not a published port. # Traffic arrives over the Traefik network, not a published port.
ports: !reset [] ports: !reset []
environment: environment:
@@ -33,19 +33,19 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.docker.network=${PROXY_NETWORK:-proxy}" - "traefik.docker.network=${PROXY_NETWORK:-proxy}"
- "traefik.http.routers.mangabm.rule=Host(`${MANGA_API_HOST:?set MANGA_API_HOST in .env}`)" - "traefik.http.routers.bmapi.rule=Host(`${BOOKMARK_API_HOST:?set BOOKMARK_API_HOST in .env}`)"
- "traefik.http.routers.mangabm.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.bmapi.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
- "traefik.http.routers.mangabm.tls=true" - "traefik.http.routers.bmapi.tls=true"
- "traefik.http.routers.mangabm.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}" - "traefik.http.routers.bmapi.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
- "traefik.http.services.mangabm.loadbalancer.server.port=8080" - "traefik.http.services.bmapi.loadbalancer.server.port=8080"
# Second hostname for the browser UI, same container. Traefik needs the # Second hostname for the browser UI, same container. Traefik needs the
# service named explicitly once more than one router targets it. # service named explicitly once more than one router targets it.
- "traefik.http.routers.mangabm.service=mangabm" - "traefik.http.routers.bmapi.service=bmapi"
- "traefik.http.routers.mangaweb.rule=Host(`${MANGA_WEB_HOST:?set MANGA_WEB_HOST in .env}`)" - "traefik.http.routers.bmweb.rule=Host(`${BOOKMARK_WEB_HOST:?set BOOKMARK_WEB_HOST in .env}`)"
- "traefik.http.routers.mangaweb.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.bmweb.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
- "traefik.http.routers.mangaweb.tls=true" - "traefik.http.routers.bmweb.tls=true"
- "traefik.http.routers.mangaweb.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}" - "traefik.http.routers.bmweb.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
- "traefik.http.routers.mangaweb.service=mangabm" - "traefik.http.routers.bmweb.service=bmapi"
# headless-shell is untouched here: it keeps its `browser` network membership # headless-shell is untouched here: it keeps its `browser` network membership
# from the base file and must never join `proxy` — that network is shared # from the base file and must never join `proxy` — that network is shared
+6 -6
View File
@@ -1,16 +1,16 @@
# Base stack — works standalone for local smoke testing (`docker compose up`). # Base stack — works standalone for local smoke testing (`docker compose up`).
# The service binds 127.0.0.1:8080; a host reverse proxy (nginx/Caddy/Traefik) # The service binds 127.0.0.1:8080; a host reverse proxy (nginx/Caddy/Traefik)
# terminates TLS for manga-api.<domain> and forwards to it. # terminates TLS for bookmark-api.<domain> and forwards to it.
# #
# If your proxy runs in Docker on its own network, use the prod override which # If your proxy runs in Docker on its own network, use the prod override which
# attaches to that network instead of publishing a port: # attaches to that network instead of publishing a port:
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d # docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
services: services:
manga-api: bookmark-api:
build: ./backend build: ./backend
image: mangabm-backend:latest image: bookmarkmanager-backend:latest
container_name: manga-api container_name: bookmark-api
restart: unless-stopped restart: unless-stopped
environment: environment:
# API_TOKEN is required — compose refuses to start without it. # API_TOKEN is required — compose refuses to start without it.
@@ -63,7 +63,7 @@ services:
# container's lifetime. # container's lifetime.
init: true init: true
# Deliberately no `ports:` — an exposed CDP endpoint is remote code # Deliberately no `ports:` — an exposed CDP endpoint is remote code
# execution. Only manga-api, via the `browser` network below, may reach it. # execution. Only bookmark-api, via the `browser` network below, may reach it.
# Don't pass --remote-debugging-address/--remote-debugging-port here: the # Don't pass --remote-debugging-address/--remote-debugging-port here: the
# image's own entrypoint (/headless-shell/run.sh) already starts Chrome on # image's own entrypoint (/headless-shell/run.sh) already starts Chrome on
# 127.0.0.1:9223 and fronts it with a socat proxy listening on 0.0.0.0:9222. # 127.0.0.1:9223 and fronts it with a socat proxy listening on 0.0.0.0:9222.
@@ -86,7 +86,7 @@ volumes:
networks: networks:
# Not `internal: true`: headless Chrome still needs outbound access to reach # Not `internal: true`: headless Chrome still needs outbound access to reach
# kagane.to. Isolation here comes from membership (only manga-api and # kagane.to. Isolation here comes from membership (only bookmark-api and
# headless-shell join it), not from cutting egress. # headless-shell join it), not from cutting egress.
browser: browser:
ipam: ipam:
+3 -3
View File
@@ -1,6 +1,6 @@
# Cinder — mangaBookmark design system # Cinder — BookmarkManager design system
Source of truth: the Claude Design project **mangaBookmark Web UI** Source of truth: the Claude Design project **BookmarkManager Web UI**
(`969ac210-fe02-4c01-ae1b-9a271dcc779a`, `index.html` + siblings (`969ac210-fe02-4c01-ae1b-9a271dcc779a`, `index.html` + siblings
`archived.html`/`fav.html`/`finished.html`/`new.html`/`login.html`/`mobile.html`, `archived.html`/`fav.html`/`finished.html`/`new.html`/`login.html`/`mobile.html`,
`style.css`, `filter.js`). This file records the rules that got implemented so `style.css`, `filter.js`). This file records the rules that got implemented so
@@ -118,7 +118,7 @@ root is at the mercy of the host site's CSP.
Recurring specs (copy these rather than inventing sizes): Recurring specs (copy these rather than inventing sizes):
- Brand: `400 26px/1 display` (`30px` ≥720px), inline SVG mark (§4) + `<em>` in - Brand: `400 26px/1 display` (`30px` ≥720px), inline SVG mark (§4) + `<em>` in
ember italic — `manga<em>Bookmark</em>`. ember italic — `Bookmark<em>Manager</em>`.
- Row title: `400 21px/1.2 display` (`22px` ≥720px). - Row title: `400 21px/1.2 display` (`22px` ≥720px).
- Tab: `400 17px display` (`18px` ≥720px), active gets `border-bottom: 2px` in - Tab: `400 17px display` (`18px` ≥720px), active gets `border-bottom: 2px` in
`--paper` (`--ember` for Updated) plus `margin-bottom: -1px` so it lands on `--paper` (`--ember` for Updated) plus `margin-bottom: -1px` so it lands on
+2 -2
View File
@@ -3,10 +3,10 @@ Guidance for Claude Code working under `userscript/`. See root `CLAUDE.md` for t
### Userscript structure (single IIFE, `manga-bookmark.user.js`) ### Userscript structure (single IIFE, `manga-bookmark.user.js`)
1. **Site adapters** — one per host, `detect(location, document)` return page `type` + IDs. Identify type/IDs from **URL regex** (most stable); pull `title`/`cover` from **`og:title`/`og:image` meta tags**, not CSS classes. 1. **Site adapters** — one per host, `detect(location, document)` return page `type` + IDs. Identify type/IDs from **URL regex** (most stable); pull `title`/`cover` from **`og:title`/`og:image` meta tags**, not CSS classes.
2. **API client** — `apiGet/apiPut/apiDelete` with bearer header; `localStorage` key `mangabm:cache` for instant render + offline fallback. 2. **API client** — `apiGet/apiPut/apiDelete` with bearer header; `localStorage` key `bmgr:manga:cache` for instant render + offline fallback.
3. **Progress logic** — auto-upsert `last_chapter` only when `chapterNum >= stored last_chapter_num` (re-reading old chapters must not regress progress; unparseable -> set current). Manual panel override forces any value. 3. **Progress logic** — auto-upsert `last_chapter` only when `chapterNum >= stored last_chapter_num` (re-reading old chapters must not regress progress; unparseable -> set current). Manual panel override forces any value.
4. **Retry queue** — every write go through `pushBookmark`/`pushDelete`, so 4. **Retry queue** — every write go through `pushBookmark`/`pushDelete`, so
failed mutation park in `localStorage` (`mangabm:queue`) and replayed on failed mutation park in `localStorage` (`bmgr:manga:queue`) and replayed on
next navigation, reconnect, or `refresh()`. Entries are markers next navigation, reconnect, or `refresh()`. Entries are markers
(`{key, op, sendStatus, attempts}`), never payloads — body read from (`{key, op, sendStatus, attempts}`), never payloads — body read from
cache at send time, so one entry per key give ordering and coalescing for cache at send time, so one entry per key give ordering and coalescing for