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>
7.7 KiB
Collections and relationships
Collections are a presentation layer, not a filesystem migration. Creating, reordering, or removing one never renames manga/chapter folders or changes reading IDs.
Using the UI
Open Settings → Admin → Collections & metadata (or Manage collections on the dashboard). Create a collection, add library manga, choose each member's role and reading order, and save. Choose a member cover, a mosaic of up to four covers, or upload a custom image under 600 KB. Dashboard display can be grouped, expanded, or individual; the choice is remembered per browser. A collection page shows aggregate progress and continues the first unfinished member in the chosen order.
New collection starts an unsaved draft using an ungrouped library manga. Rename it, add members and choose Save collection (or Save all changes). Each manga can belong to one collection. If all manga are grouped, remove a member from another collection first; a collection's final member is retained until you delete the collection. Deletion never removes manga or reading progress.
Save feedback and errors appear above the editor. Controls are locked during a save; failed saves retain drafts. If another admin has changed the catalog, the stale write is rejected. Reload catalog fetches the latest revision, with confirmation before discarding edits; copy any wanted changes first. Unsaved JSON survives collection 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
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.
Tag combinations match a single member, not tags scattered across unrelated members. Reading-time totals only include the current account's visible Reading Story. Collection publication status is ongoing if any member is ongoing, completed if all are completed, otherwise mixed. Collections do not control scraping or merge editions.
Three distinct concepts
| Concept | Meaning | Owner |
|---|---|---|
| Relationship | Directed fact: target is a prequel/sequel/adaptation/etc. of source | Reader's Catalog Curator |
| Collection | Which library manga share a dashboard tile | Admin decision |
| Reading order | Recommended order within that collection | Admin decision, independent of chronology |
One manga belongs to at most one collection, preventing duplicate dashboard tiles and double-counted progress. Relationships can connect any number of works across groups. Only confirmed relationships appear on reader manga pages. Suggestions, rejections and conflicts remain visible to the admin. Removing a collection leaves its manga, reading history and relationships intact.
Metadata and agent responsibilities
The metadata provider, not the image scraper, supplies relationships. AniList
metadata now retains manga relationship edges in manga.json → x-anilist.relations.
Existing metadata will not magically gain these fields: use Research relationships
for a manga with a verified AniList ID, or import already-cached suggestions.
Research uses one public metadata lookup with a 30-day cache, never image-host traffic.
Only relationships whose two exact AniList identities already exist in the library
become suggestions. This does not auto-enqueue missing manga or guess title matches.
An agent is a catalog researcher/curator assistant. It can read the admin JSON export, research public evidence, identify conflicting claims and propose relationships or 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 the scraper's stall watchdog.
Persistence and API contract
data/catalog.json is reader-owned, version 1, atomically replaced on save. Its
revision supports optimistic concurrency; stale writes return 409. Unknown members,
duplicate membership, invalid relationships and cover paths are rejected. Existing
members absent from a temporary mount are retained rather than erased.
Custom covers are content-addressed in data/collection-covers/. Include both in backups.
Authenticated reader APIs:
GET /api/collections,GET /api/collection?id=…— account-specific public views.GET /api/admin/catalog— admin export plus available manga, roles and relation types.POST /api/admin/catalog— validated full replacement, with currentrevision.POST /api/admin/catalog/suggest— import cached edges, with current revision.POST /api/admin/catalog/research—{m: libraryMangaId, revision}; verified metadata lookup.POST /api/admin/collection-cover—{id, revision, base64}; validated image upload.
Example proposal (use real IDs and the latest revision from the export):
{
"version": 1,
"revision": 0,
"collections": [{
"id": "moon-garden",
"title": "Moon Garden Universe",
"description": "Main story, then the prequel.",
"grouped": true,
"coverMode": "mosaic",
"coverMangaId": "Moon Garden",
"members": [
{"mangaId": "Moon Garden", "role": "main", "readingOrder": 1},
{"mangaId": "Before the Rain", "role": "prequel", "readingOrder": 2}
]
}],
"relations": [{
"from": "Moon Garden", "to": "Before the Rain", "type": "prequel",
"state": "suggested", "method": "agent-research", "confidence": 0.8,
"explanation": "Verify the publisher's chronology before accepting.",
"evidence": [{"provider": "Publisher", "url": "https://example.com/work", "note": "Chronology"}]
}]
}
Provider contract: AniList relationship connections.
Repeatable functional acceptance
Run npm.cmd test, build docker compose build reader, then:
# 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
The runner creates an isolated Compose project on loopback 8180 with three synthetic
manga, disposable accounts, paused scraper and disabled discovery. It never mounts
the real library, DB or queue. Headless Edge (override QA_BROWSER_CHANNEL if needed)
proxies a synthetic http://manga-qa.test:8180 origin to that fixture, reproducing the
browser restrictions of a non-localhost HTTP address without using a real tailnet
account. Screenshots: data/qa/collections/desktop-editor.png and mobile-editor.png.
Coverage: creation, validation, persistence after reload, member add/remove/role/order, selected/mosaic/custom covers and upload limits, JSON round-trip/draft protection, relationship creation/rejection/duplicates/removal, cached import, missing verified metadata identity, desktop/mobile edits, dashboard grouping and collection reading order, delete/cancel, overlapping saves, transient save failure/retry, concurrent-admin revision conflicts, and ordinary-reader read/write denial. No external manga or metadata research is performed; provider lookup success remains a separate test. The earlier settings test checked collections navigation only; this suite exercises the actual collection workflow and the insecure-origin failure it missed.