diff --git a/.env.example b/.env.example index 83babbe..3d8943d 100644 --- a/.env.example +++ b/.env.example @@ -11,13 +11,18 @@ SCRAPER_TOKEN_PATH=./.secrets/scraper_token # Built-in backups of DATA_PATH (docs/backup.md): an existing folder, ideally on another # disk/NAS, writable by uid 1000. Unset = backups off (./backup is then mounted unused). # BACKUP_PATH=./backup -TZ=Europe/Berlin +TZ=UTC COOKIE_SECURE=0 # Optional server-enforced reading windows. Day keys: 0=Sunday through 6=Saturday. # Ranges may cross midnight. Multiple ranges per day are supported. # READING_WINDOWS_JSON='{"0":[["08:00","01:00"]],"1":[["07:00","24:00"]],"2":[["07:00","24:00"]],"3":[["07:00","24:00"]],"4":[["07:00","24:00"]],"5":[["07:00","24:00"]],"6":[["08:00","01:00"]]}' -READING_TIMEZONE=Europe/Berlin +READING_TIMEZONE=UTC READING_QUIZ_UNLOCK_MINUTES=60 # Optional deployment-managed JSON file under DATA_PATH, e.g. /data/reading-quiz.json. # READING_QUIZ_CATALOG_PATH=/data/reading-quiz.json PROXY_NETWORK=mangacircus-edge +# Extra Compose files, ':'-separated (docs/deployment.md), e.g. HTTPS with the bundled Caddy: +# COMPOSE_FILE=docker-compose.yml:deploy/compose.https.yml +# MANGACIRCUS_DOMAIN=manga.example.org +# PEER_DATA_PATH=./data/peering +# PEER_USAGE_PATH=./data/peering-usage diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0bb097f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Shell scripts and config used inside Linux containers keep LF on every platform +*.sh text eol=lf +deploy/Caddyfile text eol=lf +*.yml text eol=lf diff --git a/.gitignore b/.gitignore index 51cc483..090aef3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ pnpm-debug.log* !.env.example !.env.*.example .secrets/ -/ops/proxmox/environment.json *.pem *.key @@ -52,3 +51,10 @@ $RECYCLE.BIN/ *.swp *.swo *~ + +# --- deploy/circus.sh state --- +.queue-was-paused + +# --- personal agent coordination (synced by a private tool; never published) --- +CLAUDE.local.md +.claude/skills/manga-org/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a138ee7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# Working on MangaCircus + +Guide for contributors and coding agents. Users who only want to run it: [docs/deployment.md](docs/deployment.md). + +## Layout + +| Path | What | +|---|---| +| `server.js`, `library.js` | Reader: HTTP server, accounts, progress, JSON storage; folder scanner and image-size reader | +| `chapter-identity.js`, `chapter-dates.js`, `reading-*.js`, `catalog.js`, `backup.js` | Shared modules (chapter keys, chapter dates, reading clock/limits, collections, built-in backup) | +| `public/` | The web pages: plain HTML, CSS and JavaScript, no build step | +| `scraper/` | Download queue daemon (`service.js`, `daemon.js`), source adapters and parsers, the publisher that writes the library, metadata lookups | +| `peering/` | Optional server-to-server sharing (gateway, client, adapter) | +| `runtime/` | In-container supervisor and configuration | +| `discovery/`, `greatest/` | Discovery Scout and the Greatest of All Time dashboard | +| `tools/` | Demo library, health checks, maintenance tools (dry run by default), UI and Compose test drivers | +| `deploy/` | `circus.sh` operations helper, HTTPS with Caddy | +| `docs/`, `vision/` | Contracts and guides; design notes for planned features | + +## Rules + +- **No runtime dependencies.** Node.js built-ins only. Playwright is an optional dev tool for UI tests. +- **One writer per store.** The reader alone writes `DATA` (users, progress, catalog). The scraper alone + writes the library and its queue. Tools that edit `chapter.json` files are dry runs by default and need + `--apply --writers-stopped`. +- **Library writes go through the publisher** (`scraper/lib/publisher.js`): staging folder, validated + images, one atomic rename. The folder contract is [docs/scraper-guide.md](docs/scraper-guide.md). +- **Sources are polite.** Sequential requests, persisted per-host delays, stop on 401/403/429, never bypass + access controls ([scraper/AGENT_RUNBOOK.md](scraper/AGENT_RUNBOOK.md)). +- **No secrets or personal data in the repository**: no `.env`, `.secrets/`, `data/`, libraries, host + names, IP addresses or local paths. Tests use disposable fixtures and never a real library or user + database. +- Keep the docs current in the same change: a new file format field goes into + [docs/library-format.md](docs/library-format.md), a new setting into the README or + [docs/deployment.md](docs/deployment.md), an API change into [docs/architecture.md](docs/architecture.md). +- Match the surrounding style: small functions, short comments that say why, no framework. + +## Tests + +```bash +npm test # reader, scraper, runtime and peering suites (node --test) +``` + +Without Node on the machine, run them in Docker (the peering test needs `openssl`, present in `node:24`): + +```bash +docker run --rm -v "$PWD:/app" -w /app node:24 npm test +``` + +Browser and Compose acceptance tests (Playwright, disposable data, never the live stack): +`npm run test:ui`, `npm run test:discovery-ui`, `npm run test:direction-ui`, `npm run test:peer-ab`. + +Try changes against demo data: `node tools/make-dummy.js library && npm run start:all`, or +`docker compose -f docker-compose.yml -f compose.dev.yml up -d --build` (mounts the editable source). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..31de3c6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# MangaCircus + +Read and follow @AGENTS.md (layout, rules, tests). Deployment: docs/deployment.md. diff --git a/Dockerfile b/Dockerfile index 374640a..dac2be8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY scraper ./scraper COPY peering ./peering COPY runtime ./runtime RUN mkdir -p /library /data && chown node:node /data /library -ENV PORT=8080 LIBRARY=/library DATA=/data TZ=Europe/Berlin +ENV PORT=8080 LIBRARY=/library DATA=/data TZ=UTC EXPOSE 8080 USER node HEALTHCHECK --interval=30s --timeout=5s --start-period=60s CMD node tools/healthcheck.js reader diff --git a/README.md b/README.md index 4bd8db4..415dfe4 100644 --- a/README.md +++ b/README.md @@ -8,59 +8,53 @@ A no-frills, self-hosted manga / comic / webtoon reader. - Reader with three view modes (**Flow**, **Page**, **Book**), a page zoom that leaves the controls alone, and tap zones for mobile. - A separate **Greatest of All Time** manga-history dashboard, with five-year periods, per-user wish ratings, community ranking, and local-library badges. Its curated titles do not enter the download queue. -## Run it +## Run it with Docker + +The supported way to run MangaCircus is Docker Compose on a Linux host: one image, a **reader** +service (the web app) and a **scraper** service (the managed download queue). Short version: ```bash -node server.js +git clone mangacircus && cd mangacircus +mkdir -p library data/scraper data/peering data/peering-usage backup .secrets +openssl rand -hex 32 > .secrets/scraper_token && chmod 600 .secrets/scraper_token +cp .env.example .env # set LIBRARY_PATH etc.; see the guide +sudo chown -R 1000:1000 library data backup +docker compose build +bash deploy/circus.sh up # reader first, then the scraper ``` -Open http://localhost:8080. The first account you create becomes the admin. Try it with demo content: +Open http://127.0.0.1:8080. The first account you create becomes the admin. The full guide covers +configuration, LAN and HTTPS access (a ready Caddy setup is included), adding manga, updates, backups, +peering and troubleshooting: **[docs/deployment.md](docs/deployment.md)**. + +## Try it without Docker + +Needs Node.js 18 or newer, nothing else: ```bash -node tools/make-dummy.js library +node tools/make-dummy.js library # five demo manga +npm run start:all # reader on :8080 and the scraper daemon on 127.0.0.1:8091 ``` -To run the reader and managed scraper together, use `npm run start:all`. Admins can -then manage the persistent download queue, progress, pauses, retries, and rate limits -under **Settings → Admin → Scraper**. The reader continues working if the scraper is stopped. -**Settings → Admin → Collections & metadata** manages grouped tiles, reading order and relationships; -**Settings → Admin → Service health** shows recovery incidents. See [collections](docs/collections.md) -and [resilience](docs/resilience.md) for the ownership and agent-maintenance contracts. -The current Windows installation uses Docker Compose; its old `MangaCircus Supervisor` -Scheduled Task is disabled. Do not run a native duplicate writer. See -[docs/local-deployment.md](docs/local-deployment.md) for its observed layout, health -checks, restart procedure, and recovery point. -Deployment-controlled reading hours and the optional progress-aware quiz are -documented in [reading limiter](docs/reading-limiter.md). They are disabled -unless configured on the reader container. -See [Greatest of All Time](docs/greatest.md) for catalog ownership, voting, and metadata refresh. - -### Docker - -```bash -node tools/prepare-compose.js -docker compose config -q -docker compose up -d --build -``` - -Stop existing native writers before using their data in containers. `docker-compose.yml` -runs reader and scraper with explicit persistent bind mounts (`library/`, -`data/`), file-backed secrets and only a loopback reader port. No named data volume is -used. See [Compose deployment](docs/compose-deployment.md) for local development, VM -recommendations, cutover, image releases and backup/rollback. The local Docker stack -is verified; VM deployment is still pending. +`npm start` runs the reader alone. Admins manage the persistent download queue, pauses, retries and rate +limits under **Settings → Admin → Scraper**. The reader keeps working when the scraper is stopped. +**Settings → Admin → Collections & metadata** manages grouped tiles, reading order and relationships, and +**Settings → Admin → Service health** shows recovery incidents and backups ([collections](docs/collections.md), +[resilience](docs/resilience.md)). Deployment-controlled reading hours and the optional progress-aware quiz +are described in [reading limiter](docs/reading-limiter.md); they are off unless configured. +See [Greatest of All Time](docs/greatest.md) for that dashboard's catalog and voting. ### Settings workspace -Settings has **User** and **Admin** menu groups. User sections cover Reading, -Reading time & privacy, Notifications and Account. Admin sections cover Scraper, -Server & library, Users & access, Collections & metadata and Service health. -Scraper has local Queue, Automatic updates and Hoster settings tabs. Only admins -see administration; personal settings do not poll scraper services. See -[settings navigation](docs/settings-navigation.md) for routes and verification. +Settings has **User** and **Admin** menu groups. User sections cover Reading, Reading time & privacy, +Notifications and Account. Admin sections cover Scraper, Server & library, Users & access, Collections & +metadata and Service health. Only admins see administration ([settings navigation](docs/settings-navigation.md)). ### Settings (environment variables) +These configure the reader process. In Docker most are set for you by `docker-compose.yml`; the variables +you put in `.env` are listed in [docs/deployment.md](docs/deployment.md#4-configure-env). + | Variable | Default | Meaning | |-----------------|-------------|---------| | `PORT` | `8080` | HTTP port | @@ -76,7 +70,15 @@ see administration; personal settings do not poll scraper services. See | `SCRAPER_TOKEN_FILE` | empty | Alternative secret file (used in Compose) | | `BACKUP` | – | Existing folder for built-in backups of `DATA`; unset = off. Compose: set `BACKUP_PATH`. Schedule and retention under Settings → Service health ([backup](docs/backup.md)) | -Serve it behind a reverse proxy with HTTPS if it is reachable from the internet. All pages use relative URLs, so a sub-path like `/manga/` works. +Serve it behind a reverse proxy with HTTPS if it is reachable from the internet. All pages use relative +URLs, so a sub-path like `/manga/` works. + +### Sources and copyright + +The scraper can download from WeebCentral, MangaDex and MangaPill, and peering can copy chapters between +servers. You are responsible for only downloading and sharing what you are allowed to copy where you live, +and for respecting each site's terms. The scraper is deliberately slow and stops on access errors; it never +bypasses CAPTCHAs, logins or other access controls ([scraper/AGENT_RUNBOOK.md](scraper/AGENT_RUNBOOK.md)). --- @@ -101,23 +103,27 @@ library/ ## Documentation -Unnumbered MangaDex pilots/one-shots use stable `special-mangadex-` -chapter keys and human labels, not fabricated numbers. Existing numeric IDs remain -unchanged. In Settings → Admin → Scraper, **Inspect & queue** shows the source edition -before **Confirm download**. “Job finished” refers to its selected scope, not verified -whole-series completeness. See the scraper service and library format contracts below. - | Document | For | |----------|-----| -| [docs/library-format.md](docs/library-format.md) | The folder format: naming rules, page tags, metadata fields, versions, extras, what survives a rename | -| [docs/import.md](docs/import.md) | How the library is scanned and indexed, caching, change detection and notifications, troubleshooting | -| [docs/architecture.md](docs/architecture.md) | Components, data model, API, security, the reader's layout engine, deployment, design decisions | -| [docs/local-deployment.md](docs/local-deployment.md) | Current local Compose deployment, Tailscale access, persistent state, checks, restart, and recovery | -| [docs/settings-navigation.md](docs/settings-navigation.md) | User/Admin settings sections, scraper tabs, routes and UI checks | -| [docs/backup.md](docs/backup.md) | Built-in scheduled backup of `data/`, what it contains, retention and the restore procedure | -| [docs/reading-insights-concept.md](docs/reading-insights-concept.md) | Active-time presentation, private summaries, deletion/export controls, and the future journal design | +| [docs/deployment.md](docs/deployment.md) | Docker deployment: setup, configuration, HTTPS, operation (`deploy/circus.sh`), updates, backups, troubleshooting | +| [docs/library-format.md](docs/library-format.md) | The folder format: naming rules, page tags, metadata fields, chapter dates, versions, extras, what survives a rename | +| [docs/import.md](docs/import.md) | How the library is scanned and indexed, caching, change detection and notifications | +| [docs/architecture.md](docs/architecture.md) | Components, data model, API, security, the reader's layout engine, design decisions | +| [docs/scraper-service.md](docs/scraper-service.md) | The managed download queue, private API and adapter contract | | [docs/scraper-guide.md](docs/scraper-guide.md) | Rule book (agent-readable) for scrapers and other tools that write into the library | -| [scraper/AGENT_RUNBOOK.md](scraper/AGENT_RUNBOOK.md) | Conservative scraper workflow and host-specific procedures for automated agents | +| [scraper/AGENT_RUNBOOK.md](scraper/AGENT_RUNBOOK.md) | Conservative scraper workflow, rate limits and stop conditions for automated agents | +| [docs/source-modules.md](docs/source-modules.md) | Adding a new source to the scraper | +| [docs/peering.md](docs/peering.md) | Optional server-to-server sharing over mutual TLS | +| [docs/backup.md](docs/backup.md) | Built-in scheduled backup of `data/`, what it contains, retention and restore | +| [docs/settings-navigation.md](docs/settings-navigation.md) | User/Admin settings sections, scraper tabs and routes | +| [docs/reading-insights-concept.md](docs/reading-insights-concept.md) | Active-time presentation, private summaries, export and deletion | +| [docs/resilience.md](docs/resilience.md) | Supervisors, automatic recovery limits and incidents | +| [AGENTS.md](AGENTS.md) | Working on the code: layout, tests, rules for contributors and coding agents | + +Unnumbered MangaDex pilots and one-shots use stable `special-mangadex-` chapter keys and +human labels, not invented numbers. In Settings → Admin → Scraper, **Inspect & queue** shows the source +edition before **Confirm download**. "Job finished" refers to its selected scope, not verified whole-series +completeness. --- @@ -156,5 +162,8 @@ whole-series completeness. See the scraper service and library format contracts server.js HTTP server, accounts, progress, votes, notifications (JSON file storage) library.js folder scanner + image size reader public/ the web pages (library, manga, reader, settings, login) -tools/ demo library generator (pure Node, draws PNGs itself) +scraper/ download queue daemon, source adapters, publisher +peering/ optional server-to-server sharing +deploy/ Docker operations helper (circus.sh) and HTTPS with Caddy +tools/ demo library generator, maintenance and test tools ``` diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..39123d1 --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,5 @@ +# Used by deploy/compose.https.yml. Caddy obtains and renews the certificate on its own. +{$MANGACIRCUS_DOMAIN} { + encode zstd gzip + reverse_proxy reader:8080 +} diff --git a/deploy/circus.sh b/deploy/circus.sh new file mode 100644 index 0000000..f887835 --- /dev/null +++ b/deploy/circus.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# Operate a MangaCircus Docker Compose deployment. Run it from anywhere; it works in the repository +# folder that contains it. Needs Docker with Compose, nothing else on the host (no Node). +# Extra Compose files (e.g. compose.lan.yml) come from COMPOSE_FILE in .env, like plain `docker compose`. +# +# status services, reader health, scraper queue +# up reader first (waits for health), then the scraper; resumes the queue if `down` paused it +# down pause the queue, wait for the running chapter (<= 15 min), stop all services +# restart [service] down + up, or restart one service +# logs [service] [lines] recent logs (default: all services, 100 lines) +# pause | resume pause or resume the scraper queue +# update git pull --ff-only, rebuild the image, recreate safely (queue paused meanwhile) +# rescan make the reader re-read every chapter.json (full reconciliation) without an admin login +# restore-test restore the newest built-in backup into a throwaway reader and check it starts +# +# Safety: never `docker compose down -v`. Only the scraper writes the library; only the reader writes users. +set -euo pipefail +cd "$(dirname "$(readlink -f "$0")")/.." +log() { echo "[circus] $*"; } +[ -f .env ] || { log "no .env here; see docs/deployment.md" >&2; exit 1; } +env_value() { sed -n "s/^$1=//p" .env | tail -n 1 | sed -e 's/^["'\'']//' -e 's/["'\'']$//'; } +running() { [ -n "$(docker compose ps -q --status running "$1" 2>/dev/null)" ]; } + +# Scraper control through the reader container: the token never leaves it and nothing secret is printed. +control() { + docker compose exec -T -e ACTION="$1" reader node -e ' +const {secret}=require("./runtime/config"); +(async()=>{ + const a=process.env.ACTION, state=a==="state"; + const r=await fetch(process.env.SCRAPER_URL+(state?"/api/state":"/api/control"),{method:state?"GET":"POST", + signal:AbortSignal.timeout(20000),headers:{authorization:"Bearer "+secret("SCRAPER_TOKEN"),"content-type":"application/json"}, + ...(state?{}:{body:JSON.stringify({action:a})})}); + if(!r.ok)throw new Error("scraper HTTP "+r.status); + const s=await r.json(); + console.log(JSON.stringify({status:s.status,paused:s.paused,sourcePaused:s.sourcePaused,active:(s.active||[]).length, + queue:(s.queue||[]).length,nextDiscoveryAt:s.nextDiscoveryAt,library:s.libraryStatus&&{state:s.libraryStatus.state,error:s.libraryStatus.error}})); +})().catch(e=>{console.error(e.message);process.exitCode=1;});' +} +field() { sed -n "s/.*\"$1\":\([^,}]*\).*/\1/p"; } +health() { docker compose exec -T reader node -e 'fetch("http://127.0.0.1:8080/health").then(r=>r.text()).then(t=>console.log(t))' 2>/dev/null; } +wait_reader() { + for _ in $(seq 1 60); do health | grep -q '"ok":true' && return 0; sleep 5; done + log "reader not healthy after 5 minutes" >&2; return 1 +} +# Pause the queue and wait for the running chapter; prints whether the queue was paused before. +pause_wait() { + local was=true + if running scraper && running reader; then + was=$(control state | field paused) + [ "$was" = true ] || control pause >/dev/null + for _ in $(seq 1 90); do [ "$(control state | field active)" = 0 ] && break; sleep 10; done + fi + echo "$was" +} +resume_if() { + [ "$1" = false ] || { log "queue stays paused"; return 0; } + for _ in $(seq 1 30); do control resume >/dev/null 2>&1 && { log "queue resumed"; return 0; }; sleep 5; done + log "could not resume the queue" >&2; return 1 +} +start_all() { + docker compose up -d reader + wait_reader + docker compose up -d --remove-orphans + sleep 15 +} + +cmd=${1:-status}; shift || true +case "$cmd" in + status) + docker compose ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}' + running reader && { health || echo "reader: health check failed"; } || echo "reader: not running" + running scraper && running reader && control state || echo "scraper: not running" + ;; + up) + start_all + was=$(cat .queue-was-paused 2>/dev/null || echo true); rm -f .queue-was-paused + resume_if "$was" + docker compose ps --format 'table {{.Service}}\t{{.Status}}' + ;; + down) + was=$(pause_wait); echo "$was" > .queue-was-paused + docker compose stop + log "stopped (queue was paused before: $was)" + ;; + restart) + if [ -n "${1:-}" ]; then docker compose restart "$1"; [ "$1" != reader ] || wait_reader + else "$0" down && "$0" up; fi + ;; + logs) + docker compose logs --no-color --tail "${2:-100}" ${1:+"$1"} + ;; + pause|resume) + control "$cmd" + ;; + update) + git pull --ff-only + VCS_REF=$(git rev-parse --short HEAD) docker compose build + was=$(pause_wait) + start_all # recreates the containers whose image or configuration changed + resume_if "$was" + log "now running $(git log -1 --format='%h %s')" + ;; + rescan) + # The reader re-reads in-place chapter.json edits only on a full reconciliation. Marking the saved + # index as overdue makes it run one within a minute while it keeps serving the current index. + data=$(env_value DATA_PATH); data=${data:-./data} + image=$(docker compose images -q reader | head -n 1) + [ -f "$data/library-index.json" ] || { log "no saved index yet; the reader scans on its own" >&2; exit 1; } + docker compose stop reader + docker run --rm --user 1000:1000 -v "$(readlink -f "$data")":/data "$image" node -e ' +const fs=require("fs"),f="/data/library-index.json",j=JSON.parse(fs.readFileSync(f,"utf8")); +j.fullAt=1; fs.writeFileSync(f+".tmp",JSON.stringify(j)); fs.renameSync(f+".tmp",f);' + docker compose up -d reader + wait_reader + log "full reconciliation scheduled; it logs \"[scan] full: …\" when done (docker compose logs reader)" + ;; + restore-test) + backup=$(env_value BACKUP_PATH); library=$(env_value LIBRARY_PATH); library=${library:-./library} + [ -n "$backup" ] || { log "BACKUP_PATH is not set in .env" >&2; exit 1; } + archive=$(ls -1t "$backup"/circus-*.tar.gz 2>/dev/null | head -n 1) + [ -n "$archive" ] || { log "no circus-*.tar.gz in $backup" >&2; exit 1; } + image=$(docker compose images -q reader | head -n 1) + tmp=$(mktemp -d); mkdir "$tmp/data" + trap 'docker rm -f circus-restore-test >/dev/null 2>&1 || true; rm -rf "$tmp"' EXIT + tar -C "$tmp/data" -xzf "$archive" + chmod -R a+rwX "$tmp/data" + log "archive: $archive" + docker run -d --name circus-restore-test --network none --user 1000:1000 --read-only --tmpfs /tmp \ + -v "$(readlink -f "$library")":/library:ro -v "$tmp/data":/data -e DATA=/data -e LIBRARY=/library \ + -e SIGNUP=closed -e SCAN_INTERVAL=0 "$image" node server.js >/dev/null + out="" + for _ in $(seq 1 30); do + out=$(docker exec circus-restore-test node -e 'fetch("http://127.0.0.1:8080/api/auth").then(r=>r.text()).then(console.log)' 2>/dev/null || true) + [ -n "$out" ] && break; sleep 2 + done + echo "auth: $out" + echo "$out" | grep -q '"firstRun":false' && echo "RESTORE TEST OK" || { echo "RESTORE TEST FAILED" >&2; exit 1; } + ;; + *) + sed -n '2,17p' "$0"; exit 2 + ;; +esac diff --git a/deploy/compose.https.yml b/deploy/compose.https.yml new file mode 100644 index 0000000..abad0b6 --- /dev/null +++ b/deploy/compose.https.yml @@ -0,0 +1,27 @@ +# HTTPS in front of the reader with Caddy and automatic Let's Encrypt certificates. +# Needs a DNS name for this host (MANGACIRCUS_DOMAIN in .env) and ports 80 and 443 reachable +# from the internet. Use it with the main file, e.g. in .env: +# COMPOSE_FILE=docker-compose.yml:deploy/compose.https.yml +# Already running a reverse proxy? Skip this file and see docs/deployment.md, "Your own proxy". +services: + reader: + ports: !reset [] + environment: + COOKIE_SECURE: "1" + + caddy: + image: caddy:2.8.4-alpine + restart: unless-stopped + ports: ["80:80", "443:443", "443:443/udp"] + environment: + MANGACIRCUS_DOMAIN: ${MANGACIRCUS_DOMAIN:?Set MANGACIRCUS_DOMAIN in .env} + volumes: + - ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + networks: [egress] + depends_on: [reader] + +volumes: + caddy-data: {} + caddy-config: {} diff --git a/docker-compose.yml b/docker-compose.yml index dc472eb..3a2cb32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,13 +47,13 @@ services: DATA: /data LIBRARY: /library BACKUP: ${BACKUP_PATH:+/backup} - TZ: ${TZ:-Europe/Berlin} + TZ: ${TZ:-UTC} SIGNUP: closed SCAN_INTERVAL: "10" SCAN_CONCURRENCY: ${SCAN_CONCURRENCY:-8} COOKIE_SECURE: ${COOKIE_SECURE:-0} READING_WINDOWS_JSON: ${READING_WINDOWS_JSON:-} - READING_TIMEZONE: ${READING_TIMEZONE:-Europe/Berlin} + READING_TIMEZONE: ${READING_TIMEZONE:-UTC} READING_QUIZ_UNLOCK_MINUTES: ${READING_QUIZ_UNLOCK_MINUTES:-60} READING_QUIZ_CATALOG_PATH: ${READING_QUIZ_CATALOG_PATH:-} SCRAPER_URL: http://scraper:8091 @@ -87,7 +87,7 @@ services: LIBRARY: /library SCRAPER_DATA: /data/scraper METADATA_CACHE: /data/scraper/metadata/anilist - TZ: ${TZ:-Europe/Berlin} + TZ: ${TZ:-UTC} SCRAPER_HOST: 0.0.0.0 SCRAPER_PORT: "8091" SCRAPER_TOKEN_FILE: /run/secrets/scraper_token @@ -124,7 +124,7 @@ services: PEER_USAGE_FILE: /data/peering-usage/usage.json PEER_HOST: 0.0.0.0 PEER_PORT: "8093" - # Docker Desktop does not publish host ports from an internal-only network. + # Docker (Desktop) does not publish host ports from an internal-only network. networks: [backend, egress] healthcheck: test: [CMD, node, tools/healthcheck-peer.js] diff --git a/docs/architecture.md b/docs/architecture.md index ff9ad62..9d1bb98 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -13,7 +13,7 @@ Related documents: - [scraper-service.md](scraper-service.md): managed queue, private API, and adapter contract. - [collections.md](collections.md): relations, groups, curator UI and agent research. - [resilience.md](resilience.md): supervision, recovery limits and incidents. -- [compose-deployment.md](compose-deployment.md): local/VM deployment and release workflow. +- [deployment.md](deployment.md): Docker deployment, HTTPS, operation, updates and backups. - [settings-navigation.md](settings-navigation.md): User/Admin workspace, scraper tabs and deep links. - [peering.md](peering.md): optional private server-to-server catalog and transfer. - [discovery.md](discovery.md): reader-owned, opt-in deterministic discovery feed and its limits. @@ -325,20 +325,11 @@ mode = adapt && intended ∉ compatible ? compatible[0] : intended ## 8. Deployment -### Current Windows deployment - -The observed local installation uses the Windows Scheduled Task -`\MangaCircus Supervisor`. At logon it runs `tools/run-supervisor.ps1`, which appends -logs under `data/scraper/` and starts one `tools/start-all.js` supervisor. That parent -owns exactly one reader (`server.js`, port 8080) and one scraper daemon -(`scraper/daemon.js`, private port 8091). Starting `npm run start:all` manually at the -same time would create conflicting writers and is not supported. - -The scraper's authoritative `data/scraper/queue.json` is schema version 3. It supports -legacy archive checkpoints and paused conversion of completed legacy work while adding -source-aware lanes, scoped queue jobs, incremental chapter publication, and nightly catalog discovery. See -[local-deployment.md](local-deployment.md) for operational checks and restart/recovery, -and [scraper-service.md](scraper-service.md) for the queue/API contract. +The step-by-step guide is [deployment.md](deployment.md). The scraper's authoritative +`data/scraper/queue.json` is schema version 3. It supports legacy archive checkpoints +and paused conversion of completed legacy work while adding source-aware lanes, scoped +queue jobs, incremental chapter publication, and nightly catalog discovery. See +[scraper-service.md](scraper-service.md) for the queue/API contract. ### Docker or manual deployment @@ -352,7 +343,7 @@ docker compose up -d --build - `/data`: reader state must be writable; the scraper also needs its writable `/data/scraper` state. - `/backup`: reader only, writable target of the built-in backup (`BACKUP_PATH`; off when unset). - **Configuration:** process wiring uses environment variables (`PORT`, `LIBRARY`, `DATA`, `BACKUP`, `SIGNUP`, `SCAN_INTERVAL`, `FULL_SCAN_INTERVAL`, `COOKIE_SECURE`, `SCRAPER_URL`, and `SCRAPER_TOKEN`). Queue pacing and automatic-update schedule are persisted in `data/scraper/queue.json` and edited through the authenticated admin UI. -- **Without Docker:** `npm run start:all` with Node ≥ 18, or supervise `npm start` and `npm run start:scraper` independently. On the current Windows machine that supervision is already owned by `\MangaCircus Supervisor`. +- **Without Docker:** `npm run start:all` with Node ≥ 18 starts one supervisor that owns exactly one reader (`server.js`, port 8080) and one scraper daemon (`scraper/daemon.js`, private port 8091), or supervise `npm start` and `npm run start:scraper` independently. On Windows, `tools/run-supervisor.ps1` runs `start:all` with logs under `data/scraper/`, e.g. from a scheduled task at logon. Never start a second supervisor or a container against the same data. - **Behind a reverse proxy:** terminate TLS, set `COOKIE_SECURE=1`. A sub-path works. - **Upgrades:** replace the code and restart. Reader state has no explicit migration; unknown fields in `db.json` are kept. The scraper normalizes old queue entries into diff --git a/docs/backup.md b/docs/backup.md index 3d9141c..61947f9 100644 --- a/docs/backup.md +++ b/docs/backup.md @@ -112,5 +112,5 @@ and queue changes made after that backup are lost. The scraper token is not in `DATA`; Compose keeps it under `.secrets/` (or the deployment's `SCRAPER_TOKEN_PATH`). Restore tests should unpack into a separate -folder and run an isolated stack (no second writer against the live library), as -a restore-test script does for the host-level archives. +folder and run an isolated reader (no second writer against the live library), as +`deploy/circus.sh restore-test` does. diff --git a/docs/collections.md b/docs/collections.md index d7b9d09..bdbea8f 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -26,7 +26,7 @@ selection and must use **Validate & save JSON**, not an ordinary collection save Metadata imports/research are blocked while drafts exist so they cannot silently replace them. Relationship edits save immediately when there is no collection draft. -Creation works on HTTPS, localhost and ordinary HTTP LAN/Tailscale addresses. The +Creation works on HTTPS, localhost and ordinary HTTP LAN/VPN addresses. The browser uses secure random bytes when its secure-context-only `randomUUID` API is unavailable. This does not change networking or the server's revision/validation rules. @@ -115,9 +115,9 @@ Provider contract: [AniList relationship connections](https://docs.anilist.co/gu ## Repeatable functional acceptance -Run `npm.cmd test`, build `docker compose build reader`, then: +Run `npm test`, build `docker compose build reader`, then: -```powershell +```bash # Playwright is optional developer tooling; not a deployed application dependency. # Set NODE_PATH to its installed package root if it is not on normal resolution paths. node tools/test-compose.js --collections-ui --settings-ui diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..c051986 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,194 @@ +# Deploying MangaCircus with Docker + +This guide sets up MangaCircus on one Linux host with Docker Compose: the reader (web app), the managed +scraper, and optionally HTTPS and server peering. Every step is a command with what you should see. It is +written so that a person or a coding agent can follow it without other context. + +## 1. What runs + +| Service | Port | Role | +|---|---|---| +| `reader` | 8080 (published on `READER_BIND`) | Web app: library, reader, accounts, settings. The only writer of users, progress and the catalog (`DATA_PATH`). Reads the library read-only | +| `scraper` | 8091, internal only | Download queue and source adapters. The only writer of the library (`LIBRARY_PATH`) and of its own state (`SCRAPER_DATA_PATH`). Never publish or proxy this port | +| `peer-gateway` | 8093, optional (`--profile peering`) | Shares chosen manga with other MangaCircus servers over mutual TLS ([peering.md](peering.md)) | + +All services run from one image (`node:24-alpine`, no runtime dependencies) as uid/gid 1000. Each container +has a read-only root file system, no capabilities, rotated logs, a health check and an in-container +supervisor that restarts a crashed process. The reader works when the scraper is stopped. + +Data on the host, all bind mounts (no named volumes): + +| Path (`.env`) | Contents | Back up? | +|---|---|---| +| `LIBRARY_PATH` | The manga: `Manga/Book/Chapter/images` ([library-format.md](library-format.md)) | Yes, it is your collection | +| `DATA_PATH` | `db.json` (users, progress, votes), catalog, library index cache | Yes (built-in backup, section 8) | +| `SCRAPER_DATA_PATH` | Queue, settings, source ledger, metadata cache | Included in the built-in backup | +| `BACKUP_PATH` | Built-in backup archives | Keep it on another disk or NAS | +| `SCRAPER_TOKEN_PATH` | Shared secret between reader and scraper | Regenerate if lost | + +## 2. Requirements + +- Linux x86_64 or arm64 with Docker Engine 24+ and the Compose plugin 2.24.4+ (`docker compose version`). + Docker Desktop on Windows or macOS works for trying it out. Bind mounts are slow there for large libraries. +- 1–2 CPU cores and 1 GB RAM are enough for a few readers. The first scan of a large library is + I/O-bound (about 14 minutes for 300,000 pages on a NAS over NFS). Later starts reuse the index cache. +- Disk: the library plus room for downloads in progress. The image is about 60 MB. +- Git to fetch the code. Node is **not** needed on the host. + +## 3. Get the code and prepare folders + +```bash +git clone mangacircus && cd mangacircus +mkdir -p library data/scraper data/peering data/peering-usage backup .secrets +openssl rand -hex 32 > .secrets/scraper_token && chmod 600 .secrets/scraper_token +cp .env.example .env +sudo chown -R 1000:1000 library data backup +``` + +The containers run as uid 1000: the reader must be able to read the library and write `data/`, and the +scraper must be able to write the library. On an NFS share with squashed ids, map all users to one account +that owns the files. Don't use `chmod 777`. Bind mounts never create missing folders: a typo in a path +fails the start instead of showing an empty library. + +Already have a library? Point `LIBRARY_PATH` at it (folder format in [library-format.md](library-format.md)). +Starting empty? Fill it from the web UI's scraper queue (section 6). To try the reader first, create five +demo manga in an empty library after the build in section 5: +`docker compose run --rm --no-deps scraper node tools/make-dummy.js /library`. + +## 4. Configure `.env` + +| Variable | Default | Meaning | +|---|---|---| +| `LIBRARY_PATH` | `./library` | Library folder (absolute path recommended) | +| `DATA_PATH` | `./data` | Reader data | +| `SCRAPER_DATA_PATH` | `./data/scraper` | Scraper state | +| `PEER_DATA_PATH`, `PEER_USAGE_PATH` | `./data/peering`, `./data/peering-usage` | Peering identity and transfer counters (folders must exist even when peering is off) | +| `SCRAPER_TOKEN_PATH` | `./.secrets/scraper_token` | The secret file from section 3 | +| `BACKUP_PATH` | unset = backups off | Folder for built-in backups (section 8) | +| `READER_BIND` / `READER_PORT` | `127.0.0.1` / `8080` | Where the reader listens on the host. `0.0.0.0` = every interface (plain HTTP, LAN only) | +| `TZ` | `UTC` | Time zone of the containers; affects the nightly update time and backup schedule | +| `COOKIE_SECURE` | `0` | `1` when readers reach the site over HTTPS | +| `READING_WINDOWS_JSON`, `READING_TIMEZONE`, `READING_QUIZ_*` | off | Optional server-enforced reading hours ([reading-limiter.md](reading-limiter.md)) | +| `APP_IMAGE`, `VCS_REF` | `mangacircus:local`, `development` | Image to run; set a pinned registry image to deploy prebuilt releases (section 7) | +| `COMPOSE_FILE` | `docker-compose.yml` | Extra Compose files, `:`-separated (e.g. `docker-compose.yml:deploy/compose.https.yml`) | +| `MANGACIRCUS_DOMAIN` | – | Host name for `deploy/compose.https.yml` | + +Sign-up is closed: the first account becomes the admin, who then adds the other users. Reader settings +not listed here (scan intervals, scan concurrency) are in the README's environment table. + +## 5. Build and start + +```bash +docker compose config -q # no output = the configuration is valid +docker compose build +bash deploy/circus.sh up # reader first, waits for its health check, then the scraper +bash deploy/circus.sh status +``` + +Expected: + +``` +SERVICE STATUS PORTS +reader Up 1 minute (healthy) 127.0.0.1:8080->8080/tcp +scraper Up 40 seconds (healthy) 8080/tcp +{"ok":true,"scan":{"mangas":…,"stale":false,"error":null}, …} +{"status":"idle","paused":false,"active":0,"queue":0, …} +``` + +Open `http://127.0.0.1:8080` (or an SSH tunnel: `ssh -L 8080:127.0.0.1:8080 `) and create the admin +account. The library page fills as soon as the first scan finishes. + +## 6. Access and adding manga + +- **On the LAN:** set `READER_BIND=0.0.0.0` (or the host's LAN address), or use `compose.lan.yml` with + `READER_LAN_BIND`. This is plain HTTP: logins and cookies travel unencrypted, so never forward it to the + internet. +- **HTTPS with the bundled Caddy:** point a DNS name at the host, open ports 80 and 443, set + `MANGACIRCUS_DOMAIN=manga.example.org` and `COMPOSE_FILE=docker-compose.yml:deploy/compose.https.yml`, + then `bash deploy/circus.sh up`. Caddy fetches the certificate. The reader gets no host port and secure + cookies. +- **Your own proxy:** keep `READER_BIND=127.0.0.1` and proxy `http://127.0.0.1:8080`. Alternatively, use + `compose.vm.yml`, which joins the reader to an external Docker network (`PROXY_NETWORK`, default + `mangacircus-edge`) and removes the host port. Set `COOKIE_SECURE=1`. All pages use relative URLs, so a + sub-path such as `/manga/` works. +- **Private access over a VPN** (WireGuard, Tailscale, …): bind the reader to the VPN address, or keep it on + loopback and forward from the VPN client. +- **Adding manga:** admins use **Settings → Admin → Scraper → Inspect & queue** with a series URL from a + supported source (WeebCentral, MangaDex, MangaPill), or copy folders into the library by hand. The reader + notices new folders within `SCAN_INTERVAL` minutes. Rules for tools that write into the library: + [scraper-guide.md](scraper-guide.md). Only download what you are allowed to copy; the scraper keeps + conservative per-host rate limits and stops on 401/403/429 ([scraper/AGENT_RUNBOOK.md](../scraper/AGENT_RUNBOOK.md)). + +## 7. Operating it + +`deploy/circus.sh` wraps the safe sequences (run it from any folder; it finds the repository itself): + +| Task | Command | Notes | +|---|---|---| +| Status | `bash deploy/circus.sh status` | services, reader health, queue | +| Stop / start | `… down` / `… up` | `down` pauses the queue and waits for the running chapter (up to 15 minutes); `up` resumes only what `down` paused | +| Restart | `… restart` or `… restart reader` | | +| Logs | `… logs scraper 200` | | +| Pause / resume downloads | `… pause` / `… resume` | Same as the buttons in Settings → Admin → Scraper | +| Update to the newest code | `… update` | `git pull --ff-only`, rebuild, pause, recreate, resume | +| Re-read edited `chapter.json` files | `… rescan` | Full reconciliation without an admin login; admins can also use Settings → Admin → Rescan | +| Check the newest backup restores | `… restore-test` | Throwaway reader without network; prints `RESTORE TEST OK` | + +**Deploying prebuilt images** (several hosts, or no building on the server): build once, push to your +registry and pin the digest: + +```bash +docker build --build-arg VCS_REF=$(git rev-parse --short HEAD) -t registry.example.org/mangacircus:$(git rev-parse --short HEAD) . +docker push registry.example.org/mangacircus: # note the sha256 digest it prints +# on the server, in .env: APP_IMAGE=registry.example.org/mangacircus@sha256: +docker compose pull && bash deploy/circus.sh restart +``` + +Pinned digests make rollbacks exact: put the previous `APP_IMAGE` back and restart. Data formats only grow +(new fields are ignored by older releases), but a rollback does not undo queue or catalog changes made by +the newer release. + +**Rules for operators and agents:** +- Never run two scrapers, or a container and a native `node` process, against the same library or data + folder. +- Never run `docker compose down -v`. Stop with `deploy/circus.sh down`. +- Never publish port 8091, and never print or commit `.secrets/` or `.env`. +- One-off maintenance tools that write `chapter.json` files are dry runs by default. Run them with + `--apply --writers-stopped` only after `docker compose stop scraper`, then `deploy/circus.sh rescan`. + Examples: `tools/backfill-chapter-dates.js` and `tools/release-dates.js`, run inside the image with + `docker compose run --rm --no-deps scraper node tools/.js /library …`. + +## 8. Backups + +- **Reader and scraper data:** set `BACKUP_PATH` to an existing folder on another disk or NAS (owned by uid + 1000), restart, and choose the schedule and retention under **Settings → Service health → Backups**. A + run pauses the queue, waits for the current chapter, archives the data folder without stopping anything + and resumes. Details, archive contents and the restore procedure: [backup.md](backup.md). Check it now + and then with `deploy/circus.sh restore-test`. +- **The library:** back it up with your usual file backup (it is plain folders and images), including + hidden `.mcscraper-*` folders if downloads are in progress. +- **Restore:** `deploy/circus.sh down`, move the data folder aside, unpack the archive into a fresh data + folder (`tar -xzf circus-….tar.gz -C data`, owned by uid 1000), `deploy/circus.sh up`. Restored archives + have the queue paused; resume it after checking. + +## 9. Optional: server peering + +Share selected manga with friends' MangaCircus servers: enable the `peering` profile +(`docker compose --profile peering up -d peer-gateway`) and follow [peering.md](peering.md). The gateway +needs its own reachable port (`PEER_BIND`, `PEER_PORT`) and uses mutual TLS with pinned certificates. + +## 10. Troubleshooting + +| Symptom | Cause / fix | +|---|---| +| `docker compose up` fails with "bind source path does not exist" | A folder from `.env` is missing; create it (section 3) | +| Reader healthy but the library is empty | First scan still running (`docker compose logs reader` shows `[scan] …` when done), or `LIBRARY_PATH` points at the wrong folder | +| Scraper jobs fail with `EACCES` | The library or `SCRAPER_DATA_PATH` is not writable by uid 1000 | +| Backups fail with `EACCES` on a NAS | Map the share's users to the uid that owns the folder; the backup probes with a real write | +| Settings → Scraper says the service is unavailable | Scraper container stopped or unhealthy: `deploy/circus.sh status`, `logs scraper` | +| Edited `chapter.json` changes don't show | Run `deploy/circus.sh rescan` (in-place edits are picked up by full reconciliations, every 24 hours by default) | +| Logins fail behind HTTPS | `COOKIE_SECURE=1` needs HTTPS end to end at the browser; with plain HTTP keep it `0` | + +More background: [architecture.md](architecture.md) (components, API, security model), +[import.md](import.md) (scanning and caching), [scraper-service.md](scraper-service.md) (queue, adapters, +recovery) and [resilience.md](resilience.md) (supervisors and incidents). diff --git a/docs/discovery.md b/docs/discovery.md index c9ebf56..924a15b 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -49,11 +49,11 @@ No scraper queue edit occurs. From the project directory: -```powershell +```bash node discovery/cli.js run --dry-run --data data --show npm run test:reader docker build -t mangacircus:discovery-test . -$env:APP_IMAGE='mangacircus:discovery-test'; npm run test:discovery-ui +APP_IMAGE=mangacircus:discovery-test npm run test:discovery-ui ``` The dry run reads `db.json`, a saved library index if present, Greatest, and diff --git a/docs/import.md b/docs/import.md index 45e28ba..0fb0de9 100644 --- a/docs/import.md +++ b/docs/import.md @@ -150,7 +150,7 @@ Everything is plain data, and the whole index lives in memory. The reader lays pages out before they load, so it needs every image's size. Instead of decoding images, the scanner reads the **first 32 KB** and parses the header; only if the size isn't there yet does it read on to 256 KB (and, for JPEG, the whole file). Parsing a prefix either finds nothing or the same size as the whole file, so the small first read never changes a result. -Uncached chapter folders and their images are probed **`SCAN_CONCURRENCY` at a time** (default 8, 1–64; chapters of a book in parallel, then assembled in folder order). A first scan over a NAS is bound by per-file latency, not bandwidth: on the server against a Synology (NFS) the old sequential 256 KB probe took about 20 ms per image; see §3.1 for the measurements. `SCAN_CONCURRENCY=1` restores the sequential order. +Uncached chapter folders and their images are probed **`SCAN_CONCURRENCY` at a time** (default 8, 1–64; chapters of a book in parallel, then assembled in folder order). A first scan over a NAS is bound by per-file latency, not bandwidth: on a VM reading a Synology NAS over NFS the old sequential 256 KB probe took about 20 ms per image; see §3.1 for the measurements. `SCAN_CONCURRENCY=1` restores the sequential order. | Format | Where the size is | |--------|-------------------| @@ -163,7 +163,7 @@ Uncached chapter folders and their images are probed **`SCAN_CONCURRENCY` at a t ### 3.1 Measured first-scan speed -the server (Debian VM, 4 vCPU) reading a Synology over NFS 4.0, page cache dropped before each run, production +A Debian VM (4 vCPU) reading a Synology NAS over NFS 4.0, page cache dropped before each run, production running alongside. Each variant scanned a different manga of ~5,000 pages (the NAS caches what it has just read). | Variant | ms per image (round 1 / round 2) | diff --git a/docs/peering.md b/docs/peering.md index 3486fcb..47cd9b7 100644 --- a/docs/peering.md +++ b/docs/peering.md @@ -41,24 +41,24 @@ separate lifetimes. The optional `peer-gateway` Compose profile publishes port 8093 on **127.0.0.1** by default. It uses the same image as reader/scraper, a read-only library mount, a read-only peering policy/identity mount, and its own writable usage-counter mount. -The gateway needs an external-facing Docker network for its host port to work on -Docker Desktop; it also joins the private backend. It has no reader account database, +The gateway needs an external-facing Docker network for its host port to be +published; it also joins the private backend. It has no reader account database, scraper queue, Docker socket, or writable library mount. It must never be scaled past one instance for the same usage-counter directory. 1. Build the image and start the normal stack as documented in - [`compose-deployment.md`](compose-deployment.md). Set the local server name and + [`deployment.md`](deployment.md). Set the local server name and generate its certificate in **Settings → Admin → Peering**. Keep `/data/peering/identity.key` private and backed up. 2. Start the optional service with `docker compose --profile peering up -d peer-gateway`. Verify `docker compose --profile peering ps` shows healthy and `https://127.0.0.1:8093/health` responds. `/health` is deliberately unauthenticated and contains only `{ "ok": true }`. -3. Give the other server a reachable HTTPS endpoint for port 8093. A private - Tailscale TCP forward is recommended for the pilot; the default loopback binding - is not remotely reachable. Do not expose port 8093 on the public internet merely - to make pairing work. Use a stable tailnet DNS name, not a changing IP. Keep - the scraper (8091) private. +3. Give the other server a reachable HTTPS endpoint for port 8093. A private VPN + (WireGuard, Tailscale or similar) with a TCP forward is recommended; the default + loopback binding is not remotely reachable. Do not expose port 8093 on the public + internet merely to make pairing work. Use a stable DNS name, not a changing IP. + Keep the scraper (8091) private. The `PEER_BIND`, `PEER_PORT`, `PEER_DATA_PATH`, and `PEER_USAGE_PATH` Compose variables control the bind and persistent paths. The gateway's internal port remains 8093. @@ -132,9 +132,10 @@ not shared. Catalog and chapter lists are short-lived in-memory caches. Turning serving blocks subsequent requests immediately, though an already streaming image may finish its current chunk/response. -The initial implementation sends original image bytes; it does not transcode them. -See [`vision/server-peering-compression-test.md`](../vision/server-peering-compression-test.md) -for the measured reason. The transport accepts a small amount of controlled page +The initial implementation sends original image bytes; it does not transcode them: +lossless compression of already encoded pages saved only about 6% of the transfer size +in a measurement over 287 pages (webtoon strips about 11%, PNG chapters under 1%); +`node tools/benchmark-peer-transfer.js` repeats it on your own library. The transport accepts a small amount of controlled page parallelism, not unbounded connections. ## Tests and safe operation @@ -149,11 +150,8 @@ checks the queue remains paused, and tears both stacks down. The UI suite checks guided setup, approvals, and non-admin boundary. An isolated single-stack Compose acceptance is: -```powershell -$env:APP_IMAGE = 'mangacircus:local' # replace with the image just built -$env:QA_PORT = '8188' -$env:QA_PEER_PORT = '8193' -node tools/test-compose.js --peer-gateway --peer-ui +```bash +APP_IMAGE=mangacircus:local QA_PORT=8188 QA_PEER_PORT=8193 node tools/test-compose.js --peer-gateway --peer-ui ``` It makes disposable library/data, four containers, a certificate and a browser diff --git a/docs/resilience.md b/docs/resilience.md index c41cfca..28a590b 100644 --- a/docs/resilience.md +++ b/docs/resilience.md @@ -15,7 +15,7 @@ The reader is the only writer of users/progress and the catalog. The scraper is only queue owner and content publisher, and it records its own failures and incidents. Each service's supervisor owns only that child process. The coding agent is an **escalation/maintenance operator**, not a necessary part of normal downloading. -The recurring Codex check is an additional backstop, not the runtime watchdog. +A periodic check by an operator or coding agent is an additional backstop, not the runtime watchdog. ## Automatic recovery implemented @@ -89,8 +89,8 @@ Storage: - `data/scraper/supervisor.lock`, `daemon.lock`: native process ownership; transient. Scraper and supervisor emit structured logs. The queue history retains -200 incidents; the UI shows a bounded subset. Docker logs are rotated. Native Windows -supervisor logs remain append-only: archive them during stopped maintenance if needed. +200 incidents; the UI shows a bounded subset. Docker logs are rotated. Native supervisor logs +(`tools/run-supervisor.ps1`) remain append-only: archive them during stopped maintenance if needed. Never export user databases, tokens, cookies, signed query strings or unredacted environments. ## What self-repair intentionally does not mean diff --git a/docs/scraper-service.md b/docs/scraper-service.md index 0b9604a..3a3828f 100644 --- a/docs/scraper-service.md +++ b/docs/scraper-service.md @@ -31,11 +31,9 @@ npm start # reader only npm run start:scraper # daemon only ``` -These commands are for native development only. The current Windows installation -runs the Docker Compose stack; the old Scheduled Task is disabled. -Never start a native supervisor or daemon against its mounted state. Its observed -ports, logs, persisted settings, restart procedure, and recovery snapshot are recorded in -[`local-deployment.md`](local-deployment.md). +These commands are for native development. Deployments run the Docker Compose stack +([`deployment.md`](deployment.md)); never start a native supervisor or daemon against +the state a container has mounted. | Variable | Default | Purpose | |---|---|---| diff --git a/docs/settings-navigation.md b/docs/settings-navigation.md index 316885e..f55972e 100644 --- a/docs/settings-navigation.md +++ b/docs/settings-navigation.md @@ -73,8 +73,8 @@ not an editor for Docker environment variables or an unrestricted restart consol `npm test` covers route defaults, deep links, ownership and existing service/API behavior. `node tools/test-compose.js --settings-ui` adds optional Playwright headless browser QA against the disposable Compose fixture, never the real user -database or queue. Make Playwright available on the development machine; Windows -uses installed headless Edge by default, overridable with `QA_BROWSER_CHANNEL`. +database or queue. Make Playwright available on the development machine; on Windows it +uses the installed headless Edge by default, overridable with `QA_BROWSER_CHANNEL`. The application itself remains dependency-free. The UI check verifies desktop and 390×844 mobile navigation, form saves, draft diff --git a/scraper/README.md b/scraper/README.md index addc144..ce1a517 100644 --- a/scraper/README.md +++ b/scraper/README.md @@ -4,9 +4,8 @@ The scraper uses small host adapters to create a common manifest, followed by on Start with [AGENT_RUNBOOK.md](AGENT_RUNBOOK.md). It contains the host-specific procedures, safety limits, and stop conditions. `manifest.example.json` documents the interchange format. -The historical human-readable wishlist is [WISHLIST.md](WISHLIST.md). Once the -managed daemon has started, `data/scraper/queue.json` is the authoritative queue and -is edited through the admin UI. +Once the managed daemon has started, `data/scraper/queue.json` is the authoritative +queue and is edited through the admin UI. ## Managed service @@ -32,9 +31,8 @@ Queue schema version 3 supports scoped jobs, multiple jobs for the same upstream series, one sequential worker per source, incremental chapter publication, library duplicate/version checks, and configurable nightly update discovery. See [`docs/scraper-service.md`](../docs/scraper-service.md) for the service contract and -[`docs/local-deployment.md`](../docs/local-deployment.md) for the currently running -Windows Scheduled Task deployment. On that machine the scheduled task owns the single -supervisor; do not run `npm run start:all` beside it. +[`docs/deployment.md`](../docs/deployment.md) for running it in Docker. Run only one +supervisor or container against the same library and data. ```bash npm run test:scraper diff --git a/vision/discovery-scout.md b/vision/discovery-scout.md index ac4790f..3d2a855 100644 --- a/vision/discovery-scout.md +++ b/vision/discovery-scout.md @@ -167,7 +167,7 @@ Keep the first code change small and reviewable: | `public/discover.html`, `discover.js`, `discover.css` (new) | Feed, evidence, feedback, empty/error states, admin run control | | `public/common.js` | One navigation link; no global discovery polling | | `test/discovery*.test.js`, disposable browser test | Identity, consent, isolation, budgets, action permissions and UI behavior | -| `docs/architecture.md`, `docs/compose-deployment.md` | Record ownership, backup, local release and rollback once implemented | +| `docs/architecture.md`, `docs/deployment.md` | Record ownership, backup, local release and rollback once implemented | The exact file split is a proposal, not a requirement to create empty modules. Start with storage and identity tests, then the deterministic pipeline, then API/UI. Do not add model-provider plumbing, a new Compose service, or a nightly timer in the first implementation slice. A candidate must pass schema validation and duplicate/identity checks before it can appear in a published feed. diff --git a/vision/server-peering.md b/vision/server-peering.md index 7bc8a28..3fac8c2 100644 --- a/vision/server-peering.md +++ b/vision/server-peering.md @@ -74,7 +74,7 @@ Catalog snapshots use cursor/revision pagination and bounded page sizes. A revis Input from a peer is untrusted even after pairing: cap manifests, chapter/page sizes and total transfer size; validate schemas and image MIME/content; reject traversal paths, archives/executables, and malformed names; stream rather than buffering a whole manga; verify each page hash before publication. Hashes detect corruption but do not prove editorial correctness. Never overwrite an existing chapter silently. -For transport efficiency, start with original image bytes and page-level resume. A [local compression test](server-peering-compression-test.md) found an average ~6% reduction with zstd-3 but a wide 0.7–10.7% spread by chapter, so codec negotiation is an optional later optimization, not a prerequisite for peering. Measure actual chapter-ready time over a real peer link before adding chapter packing or automatic compression. +For transport efficiency, start with original image bytes and page-level resume. A local compression test (`tools/benchmark-peer-transfer.js`) found an average ~6% reduction with zstd-3 but a wide 0.7–10.7% spread by chapter, so codec negotiation is an optional later optimization, not a prerequisite for peering. Measure actual chapter-ready time over a real peer link before adding chapter packing or automatic compression. ## 5. Reader and queue experience