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>
84 lines
5.0 KiB
Markdown
84 lines
5.0 KiB
Markdown
# Deployment-controlled reading hours and quiz
|
||
|
||
The reader enforces reading hours on both `GET /api/chapter` and chapter page-image
|
||
requests under `/lib/` (HTTP 423 outside hours). The dashboard, manga metadata,
|
||
covers, progress, reading-time summaries, scraper and administrator pages remain
|
||
available. A reader tab already open when a window ends checks access every 15
|
||
seconds and returns to the gate. Chapter images use `Cache-Control: no-store` when
|
||
the limiter is configured. Already downloaded images in a browser or files available
|
||
directly on disk cannot be recalled; this is a self-imposed server access rule, not
|
||
DRM. There is no schedule-changing API or UI. Signed-in readers and admins can
|
||
view the effective weekly schedule and timezone under **Settings → Reading
|
||
hours**; that page is read-only.
|
||
|
||
## Docker configuration
|
||
|
||
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 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
|
||
valid only as an end. Intervals are start-inclusive and end-exclusive, resolved
|
||
using the IANA timezone, including daylight-saving changes.
|
||
|
||
```dotenv
|
||
READING_WINDOWS_JSON='{"0":[["08:00","01:00"]],"1":[["07:00","12:00"],["13: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_QUIZ_UNLOCK_MINUTES=60
|
||
READING_QUIZ_CATALOG_PATH=/data/reading-quiz.json
|
||
```
|
||
|
||
The Monday split above illustrates multiple on/off periods; replace it with
|
||
`[["07:00","24:00"]]` for an uninterrupted weekday. The Saturday and Sunday
|
||
windows extend to 01:00 on the following mornings. A malformed timezone,
|
||
schedule, unlock length or catalog stops the reader at startup rather than
|
||
silently removing the restriction. `READING_QUIZ_UNLOCK_MINUTES` accepts 1–1440.
|
||
The scraper is independent and must not be restarted for this configuration.
|
||
Until a reviewed catalog is installed at `READING_QUIZ_CATALOG_PATH`, the
|
||
outside-hours gate cannot offer a quiz. It does not relax the schedule.
|
||
|
||
## Question bank and progress watermark
|
||
|
||
The bank is a deployment-managed JSON file, separate from `manga.json` and from
|
||
the scraper. See [`config/reading-quiz.example.json`](../config/reading-quiz.example.json).
|
||
Its `manga` keys are **exact library folder IDs**. Each question has a stable ID,
|
||
prompt, accepted answer variants, an evidence citation, and at least one
|
||
`requires` chapter ID. The chapter IDs are the reader's IDs (for example `1/12`,
|
||
not the on-disk `v01/c0012`). Every required chapter must be marked completed
|
||
for that user before the question enters their pool. This supports a single
|
||
watermark or a set of specific sections. Five eligible questions are needed to
|
||
offer a quiz; an empty bank simply leaves outside-hours reading locked. A
|
||
question's answers and evidence never go to the browser.
|
||
|
||
The challenge randomly samples five progress-eligible questions across *all*
|
||
library manga, not only the one being opened. It asks them sequentially, accepts
|
||
unlimited retries, and has no answer timer. Accepted variants are normalized
|
||
for case, punctuation and accents; one typo is tolerated for answers of five
|
||
or more characters, two for answers of eleven or more. Starting a new visit
|
||
invalidates the previous challenge, and leaving the page abandons it. Passing
|
||
all five grants that user reading access across manga for the configured number
|
||
of minutes, persisted in `db.json` across reader restarts. Questions are not
|
||
shown before their watermark; this depends on the reader's existing
|
||
self-reported completion records.
|
||
|
||
Validate a proposed file with
|
||
`node tools/validate-reading-quiz.js <catalog.json>` before pointing the reader
|
||
at it. For an eventual research agent: write **drafts** with source citations,
|
||
aliases and proposed chapter watermarks, then have a human verify identity,
|
||
answer and spoiler boundary. Only a reviewed catalog should be copied to the
|
||
configured path and loaded by restarting the reader. No wiki or transcript
|
||
fetching is performed by the reader itself.
|
||
|
||
## Testing and rollback
|
||
|
||
`node tools/test-compose.js --reading-limit-ui` creates a disposable Compose
|
||
fixture with two progress-qualified sample manga, tests chapter and image
|
||
denial, quiz flow and the one-hour unlock, then tears it down. Add `--keep`
|
||
to leave an isolated playable instance locked for manual review; its printed
|
||
URL and fixture path identify it. Main data and the running main scraper are
|
||
not mounted. To roll back a future main deployment, restore the previous
|
||
reader image and configuration, recreate only the reader, and preserve current
|
||
`db.json` and library. The additional `readingAccess` field is backward-safe.
|