Prepare the repository for a public release
- docs/deployment.md: step-by-step Docker deployment for anyone (setup, configuration, LAN/HTTPS/VPN access, adding manga, operation, prebuilt images, backups, peering, troubleshooting, rules for agents). - deploy/circus.sh: host helper without Node (status, up/down with queue pause, logs, pause/resume, update, rescan, restore-test); deploy/compose.https.yml + Caddyfile for automatic HTTPS. - README rewritten for public readers (Docker first, sources/copyright note, docs index); AGENTS.md + CLAUDE.md for contributors and coding agents. - Removed notes about one particular installation (Windows host, handoffs, audits of a private library, benchmarks with local paths, wishlist, reading map) and the unused ops/proxmox templates; generalized remaining mentions of hosts, VPN and Windows-only commands. - Defaults: TZ and reading time zone UTC; .gitattributes keeps shell/YAML LF. - Personal agent coordination files are git-ignored (CLAUDE.local.md, .claude/skills/manga-org/). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b52da2f9c
commit
f53b92a7dc
24 files changed
+556
-122
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user