# 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 current `revision`. - `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): ```json { "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](https://docs.anilist.co/guide/graphql/connections). ## Repeatable functional acceptance Run `npm.cmd test`, build `docker compose build reader`, then: ```powershell # 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.