142 lines
7.6 KiB
Markdown
142 lines
7.6 KiB
Markdown
# 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 Sentinel.
|
|
|
|
## 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.
|