Fold the sentinel into the scraper and drop the sentinel service (BL-0046)
The stall watchdog now runs inside the scraper (ScraperService.checkStalls, every 30 s from the work loop): same rule as before, one checkpoint-safe restart, quarantine on the second stall, missing checkpoint is no evidence, and paused time no longer counts. Stalls are recorded as scraper incidents (failure.kind 'stall'). The private /api/recovery endpoint is gone. Dropped with the service: reader/scraper-unreachable incidents (supervisor and Docker handle crashes; the health page shows "Scraper: unreachable"), duplicate job-failure incidents and the unused /metrics endpoint. The stack is now reader + scraper (+ optional peer-gateway). The supervisor test moved to runtime/test (npm run test:runtime stays). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88555d4780
commit
b2bb34174c
37 files changed
+163
-311
No files matched your search
@@ -2,8 +2,8 @@
|
||||
|
||||
MangaCircus is a self-hosted reader for manga, comics and webtoons. The reader owns
|
||||
accounts/progress and a separate collection catalog. The scraper owns the persistent
|
||||
queue and chapter publication. Sentinel observes health/progress and requests bounded
|
||||
checkpoint recovery. Separate supervisors isolate service failures. All content remains
|
||||
queue and chapter publication, and its built-in stall watchdog requeues a stuck job
|
||||
once at a checkpoint. Separate supervisors isolate service failures. All content remains
|
||||
a plain folder tree; there is no database server or embedded autonomous LLM.
|
||||
|
||||
Related documents:
|
||||
@@ -48,12 +48,12 @@ Related documents:
|
||||
|
||||
## 3. Components
|
||||
|
||||
Runtime topology (2026-10-05): native `tools/start-all.js` launches reader, scraper and
|
||||
Sentinel independently. In Compose, each container runs `runtime/launch.js <service>`
|
||||
Runtime topology (2026-10-22): native `tools/start-all.js` launches reader and scraper
|
||||
independently. In Compose, each container runs `runtime/launch.js <service>`
|
||||
and exactly one service child. A failure does not stop unrelated services. Exactly one
|
||||
reader and queue-owning scraper may write each shared state tree; do not scale replicas.
|
||||
The catalog and relation store is reader-owned `data/catalog.json`. Sentinel writes only
|
||||
`data/sentinel/`; it has no Docker socket or arbitrary code-repair authority.
|
||||
The catalog and relation store is reader-owned `data/catalog.json`. No service has a
|
||||
Docker socket or arbitrary code-repair authority.
|
||||
|
||||
The admin browser never talks to the scraper daemon directly. Admin-only reader
|
||||
routes proxy commands to the daemon's private API, so its shared token stays on the
|
||||
@@ -375,7 +375,7 @@ docker compose up -d --build
|
||||
|----------|--------------|-----|
|
||||
| Index the folder tree in place | Import into a DB / managed storage | Stays readable by any viewer and writable by any tool. Nothing to keep in sync |
|
||||
| No dependencies, no framework, no build | Express, React, bundler | Minimal code, maximum longevity, runs anywhere Node runs |
|
||||
| Built-in backup in the reader, queue paused, no container stop | Host job that stops the stack; backup in Sentinel | Only the reader sees all of `/data` and the in-memory `db.json`; the scraper's pause API gives a consistent point without downtime |
|
||||
| Built-in backup in the reader, queue paused, no container stop | Host job that stops the stack; backup in a separate service | Only the reader sees all of `/data` and the in-memory `db.json`; the scraper's pause API gives a consistent point without downtime |
|
||||
| JSON file for user data | SQLite (`node:sqlite` is still experimental in Node 22/24), Postgres | Zero dependencies, trivial backup, enough for the target size. Easy to swap: all access goes through the `db` object |
|
||||
| Page roles as file-name tags | Page lists in JSON | Visible in any file browser, survives copying, no second source of truth. Unknown tags are harmless |
|
||||
| Image sizes read on the server | Measure in the browser after load | Layout before load: exact scroll restore, zoom anchoring, auto-detected double pages, no layout jumps |
|
||||
|
||||
+11
-11
@@ -28,11 +28,10 @@ target private.
|
||||
### Why the reader owns it
|
||||
|
||||
The reader is the only service that mounts all of `/data` (the scraper sees only
|
||||
`/data/scraper`, Sentinel only `/data/sentinel`), it holds `db.json` in memory and owns
|
||||
its atomic writer, it already talks to the scraper's private control API with the
|
||||
shared token, and the admin settings UI lives there. Running it in Sentinel would need
|
||||
a full read-write `/data` mount there and break its "writes only `data/sentinel/`"
|
||||
contract (see [architecture.md §3](architecture.md#3-components)).
|
||||
`/data/scraper`), it holds `db.json` in memory and owns its atomic writer, it already
|
||||
talks to the scraper's private control API with the shared token, and the admin
|
||||
settings UI lives there. Running it in the scraper would need a full read-write `/data`
|
||||
mount there (see [architecture.md §3](architecture.md#3-components)).
|
||||
|
||||
## Settings and status
|
||||
|
||||
@@ -67,9 +66,10 @@ POST /api/admin/backup {command:"run"} -> 409 when off or already running
|
||||
- `db.json` is serialized from the reader's in-memory state at that moment.
|
||||
- `scraper/queue.json` is stored with `paused: true` (status `paused`), so a
|
||||
restored copy never starts downloading by itself. The live file is not touched.
|
||||
- `scan-cache.json`, `library-index.json`, `catalog.json`, peering, Sentinel state,
|
||||
logs and everything else under `DATA` is copied as found; all of them are
|
||||
written by atomic rename, so each file is complete.
|
||||
- `scan-cache.json`, `library-index.json`, `catalog.json`, peering, logs and
|
||||
everything else under `DATA` is copied as found; all of them are written by
|
||||
atomic rename, so each file is complete. A leftover `sentinel/` folder from
|
||||
before 2026-10-22 is copied too; it is unused and can be deleted.
|
||||
- Left out: `runtime*.json`, `*.lock` (`daemon.lock`, `supervisor.lock`), `*.pid`,
|
||||
`*.tmp`, symlinks, and the `BACKUP` folder itself if it lies inside `DATA`.
|
||||
- `.circus-backup.json` at the archive root records when and how it was made.
|
||||
@@ -93,8 +93,8 @@ plus those seconds.
|
||||
Restoring replaces the user data with the state of the archive. Reading progress
|
||||
and queue changes made after that backup are lost.
|
||||
|
||||
1. Stop the stack (or at least reader, scraper and Sentinel):
|
||||
`docker compose stop reader scraper sentinel`.
|
||||
1. Stop the stack (or at least reader and scraper):
|
||||
`docker compose stop reader scraper`.
|
||||
2. Move the current data folder aside instead of deleting it:
|
||||
`mv /srv/mangacircus/data /srv/mangacircus/data.before-restore`.
|
||||
3. Unpack the archive into an empty data folder owned by uid 1000:
|
||||
@@ -107,7 +107,7 @@ and queue changes made after that backup are lost.
|
||||
4. Start the reader first: `docker compose up -d --no-build reader`. Check `/health`,
|
||||
log in, check progress and library. The reader rebuilds a missing or stale library
|
||||
index by itself.
|
||||
5. Start `scraper sentinel`. The restored queue is paused: check it under
|
||||
5. Start `scraper`. The restored queue is paused: check it under
|
||||
Settings → Scraper and resume it when it looks right.
|
||||
|
||||
The scraper token is not in `DATA`; Compose keeps it under `.secrets/` (or the
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ membership edits. It should preserve rejected decisions and report uncertainty.
|
||||
It must not turn a shared-universe link into an automatic collection or an unquestioned
|
||||
reading order. No embedded LLM, API credentials, automatic research loop, or autonomous
|
||||
catalog approval is enabled in this release. That optional capability belongs behind
|
||||
this same evidence/review boundary, not inside adapters or Sentinel.
|
||||
this same evidence/review boundary, not inside adapters or the scraper's stall watchdog.
|
||||
|
||||
## Persistence and API contract
|
||||
|
||||
|
||||
+1
-1
@@ -29,6 +29,6 @@ Public catalog chapter counts are often absent; the UI says “Chapters unknown
|
||||
|
||||
## Deployment and QA
|
||||
|
||||
The Docker image now copies `greatest/` as app code; no new Compose service, volume, port, secret, or migration is needed. Rebuild/recreate **reader only** to pick up catalog/UI changes. The scraper and sentinel do not need restarting. Existing `data/db.json` accepts the new `greatestRatings` object without migration.
|
||||
The Docker image now copies `greatest/` as app code; no new Compose service, volume, port, secret, or migration is needed. Rebuild/recreate **reader only** to pick up catalog/UI changes. The scraper does not need restarting. Existing `data/db.json` accepts the new `greatestRatings` object without migration.
|
||||
|
||||
Run `npm run test:reader` for catalog/API tests and `node tools/test-greatest-ui.js` for disposable headless-browser QA. The UI fixture blocks remote covers and never contacts the scraper. Manual catalog edits should preserve stable IDs once anyone has voted; changing a title's ID strands its votes.
|
||||
+1
-1
@@ -58,7 +58,7 @@ past one instance for the same usage-counter directory.
|
||||
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
|
||||
scraper (8091) and Sentinel (8092) private.
|
||||
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.
|
||||
|
||||
@@ -16,7 +16,7 @@ hours**; that page is read-only.
|
||||
Set these deployment variables in the Compose `.env` file next to
|
||||
`docker-compose.yml`, then recreate **only the reader**. The `reader` service
|
||||
passes them through to the container. `READER_IMAGE` selects a reader-only
|
||||
release while `APP_IMAGE` continues to select the scraper and Sentinel image.
|
||||
release while `APP_IMAGE` continues to select the scraper image.
|
||||
Leaving `READING_WINDOWS_JSON` empty disables the limiter. Day keys
|
||||
are `0` Sunday through `6` Saturday. Each day has any number of `[start,end]`
|
||||
intervals; an end earlier than its start carries into the next day. `24:00` is
|
||||
|
||||
+29
-29
@@ -4,16 +4,15 @@
|
||||
|
||||
```text
|
||||
Browser → Reader ──private API──→ Scraper → library (atomic chapter publication)
|
||||
│ ↑
|
||||
└──health──→ Sentinel ───┘ bounded checkpoint recovery only
|
||||
└─ stall watchdog: bounded checkpoint recovery only
|
||||
|
||||
Supervisor(reader) Supervisor(scraper) Supervisor(sentinel)
|
||||
Supervisor(reader) Supervisor(scraper)
|
||||
↑
|
||||
Coding agent: inspect incidents → reproduce → patch → test → release
|
||||
```
|
||||
|
||||
The reader is the only writer of users/progress and the catalog. The scraper is the
|
||||
only queue owner and content publisher. Sentinel owns only observations and incidents.
|
||||
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.
|
||||
@@ -28,18 +27,18 @@ The recurring Codex check is an additional backstop, not the runtime watchdog.
|
||||
| Parser/local adapter failure | Quarantine job, keep partial data, continue other titles | Two different affected jobs in an hour pause source as systemic failure |
|
||||
| Storage/permission failure | Pause all downloads and report | No automatic retry |
|
||||
| HTTP 401/403/429 or access challenge | Pause affected source lane | No automatic retry/bypass |
|
||||
| Active job with no progress for 15 minutes | Sentinel requests cooperative checkpoint-safe retry | One stall recovery; next stall quarantined |
|
||||
| Active job with no progress for 15 minutes | Scraper stall watchdog stops it at its next checkpoint and requeues it | One stall recovery; next stall quarantined |
|
||||
|
||||
Sentinel only treats an active job as stalled when its checkpoint can be read.
|
||||
The scraper resolves source-specific work folders (including MangaPill's
|
||||
`mangapill-` prefix) before reporting progress; a missing checkpoint is not
|
||||
evidence that downloads stopped. An operator retry resets the job's stall
|
||||
recovery count without deleting its partial download.
|
||||
|
||||
The daemon remains authoritative: a recovery request includes the observed progress
|
||||
fingerprint and is rejected if the job has moved, paused, or is no longer stale.
|
||||
Sentinel also observes completed/page counts; a deliberately paused queue is not a
|
||||
stall. Timeouts bound source fetches to 60 seconds and metadata calls to 30 seconds.
|
||||
The stall watchdog runs inside the scraper (`ScraperService.checkStalls`, every 30 s
|
||||
from the work loop). A job is stalled when its checkpoint fingerprint (completed
|
||||
chapters/pages) has not moved for 15 minutes, or for one chapter delay plus three
|
||||
minutes if that is longer. Time spent paused does not count, and a deliberately
|
||||
paused queue is not a stall. It only treats a job as stalled when its checkpoint can
|
||||
be read: the scraper resolves source-specific work folders (including MangaPill's
|
||||
`mangapill-` prefix), and a missing checkpoint is not evidence that downloads stopped.
|
||||
Each stall records an incident with `failure.kind: 'stall'` and an `events.log` entry.
|
||||
An operator retry resets the job's stall recovery count without deleting its partial
|
||||
download. Timeouts bound source fetches to 60 seconds and metadata calls to 30 seconds.
|
||||
Publication remains one complete chapter at a time, on the same filesystem.
|
||||
No recovery action raises pacing, creates another worker for the same source,
|
||||
changes identities or deletes partial images.
|
||||
@@ -62,14 +61,14 @@ use an explicit operator retry after fixing their cause.
|
||||
|
||||
## Observability
|
||||
|
||||
**Settings → Service health** shows Sentinel status, active checkpoints, failed jobs
|
||||
and incident history. **Settings → Scraper** retains queue, pacing and discovery controls.
|
||||
**Settings → Service health** shows scraper reachability ("Scraper: unreachable" when
|
||||
the reader cannot reach it), active checkpoints, failed jobs and the scraper's incident
|
||||
history. Process crashes are handled by the supervisor and Docker restart policy, not
|
||||
recorded as incidents. **Settings → Scraper** retains queue, pacing and discovery controls.
|
||||
Service errors are classified rather than buried in a single historical `lastError`.
|
||||
`npm run check` performs read-only checks. Sentinel's private, token-protected `/metrics`
|
||||
exports reader-up, scraper-up and open-incident gauges for a future Prometheus collector.
|
||||
It is not a public endpoint.
|
||||
`npm run check` performs read-only checks of reader and scraper.
|
||||
|
||||
Scraper status is independent of expensive library scans. Concurrent admin/Sentinel
|
||||
Scraper status is independent of expensive library scans. Concurrent admin/health
|
||||
probes share one background inventory refresh and receive the previous summary
|
||||
immediately. The dashboard summary refreshes at most once per minute unless a
|
||||
publication or metadata change invalidates it; duplicate-sensitive decisions still
|
||||
@@ -82,22 +81,23 @@ may temporarily show no library targets, but queue and rate controls remain usab
|
||||
Storage:
|
||||
|
||||
- `data/catalog.json`, `data/collection-covers/`: reader catalog and covers.
|
||||
- `data/scraper/queue.json`, `events.log`: queue, circuits, failures and incidents.
|
||||
- `data/sentinel/state.json`: Sentinel incidents and last observation.
|
||||
- `data/runtime-reader.json`, `data/scraper/runtime.json`, `data/sentinel/runtime.json`:
|
||||
supervisor status and persisted crash budget.
|
||||
- `data/scraper/queue.json`, `events.log`: queue, circuits, failures and incidents
|
||||
(including stalls).
|
||||
- `data/runtime-reader.json`, `data/scraper/runtime.json`: supervisor status and
|
||||
persisted crash budget.
|
||||
- `data/sentinel/`: only on installs from before 2026-10-22; unused, safe to delete.
|
||||
- `data/scraper/supervisor.lock`, `daemon.lock`: native process ownership; transient.
|
||||
|
||||
Scraper/Sentinel/supervisor emit structured logs. Queue and Sentinel histories retain
|
||||
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.
|
||||
Never export user databases, tokens, cookies, signed query strings or unredacted environments.
|
||||
|
||||
## What self-repair intentionally does not mean
|
||||
|
||||
Sentinel cannot execute a shell, patch adapters, query image sources, access a Docker
|
||||
socket, or issue unrestricted restart commands. It cannot repair broken markup or
|
||||
solve source challenges. The agent handles those escalations in the repository:
|
||||
The stall watchdog cannot execute a shell, patch adapters, access a Docker socket,
|
||||
or restart processes; it only requeues or quarantines a job. Nothing at runtime
|
||||
repairs broken markup or solves source challenges. The agent handles those escalations in the repository:
|
||||
|
||||
1. Inspect one bounded incident and relevant checkpoint; preserve the active lane.
|
||||
2. Add a fixture reproducing the failure, patch the smallest adapter/logic surface.
|
||||
|
||||
@@ -32,7 +32,7 @@ npm run start:scraper # daemon only
|
||||
```
|
||||
|
||||
These commands are for native development only. The current Windows installation
|
||||
runs the three-service Docker Compose stack; the old Scheduled Task is disabled.
|
||||
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).
|
||||
|
||||
Reference in new issue
Block a user