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:
MaashkinoozheandClaude Opus 5.5 committed 2026-10-22 22:58:03 +02:00
1 parent 88555d4780
commit b2bb34174c
37 files changed
+163 -311

No files matched your search

+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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).