Discover page: refresh, harvest admin and seed reasons; docs (BL-0051)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f87fbe948
commit
613db96355
10 files changed
+265
-58
No files matched your search
@@ -210,7 +210,7 @@ All requests and responses are JSON. POSTs require `Content-Type: application/js
|
||||
| `GET /api/discovery` | user | The caller's discovery feed, settings, feedback and `refresh: { state: idle\|queued\|running, position?, nextAllowedAt?, blockedReason? }` (§6.6) |
|
||||
| `POST /api/discovery/feedback` `{candidateId, value}` | user | The caller's choice for a candidate; requires opt-in |
|
||||
| `POST /api/discovery/refresh` | user | Queue a per-user AniList harvest for the caller (opt-in required, else `403`). `{ harvestId, position }`; `409` while one is queued or running for the caller; `429 { nextAllowedAt }` within an hour of the last accepted refresh; `503 { blockedReason }` after AniList pushed back, until a nightly or admin harvest |
|
||||
| `GET /api/admin/discovery` | admin | Last scoring run, opted-in users, candidate count, plus `harvest` (last run record), `currentHarvest`, `cache: { files, bytes }`, `settings: { harvestEnabled, harvestAt }` and `nextNightlyAt` (`null` when off) |
|
||||
| `GET /api/admin/discovery` | admin | Last scoring run, `lastError` (a failed admin run or a failed re-score after a harvest), opted-in users, candidate count, plus `harvest` (last run record), `currentHarvest`, `cache: { files, bytes }`, `settings: { harvestEnabled, harvestAt }` and `nextNightlyAt` (`null` when off) |
|
||||
| `POST /api/admin/discovery/run` | admin | Re-score every user from local data; `409` while a run is active |
|
||||
| `POST /api/admin/discovery/harvest` | admin | Start a full harvest for all opted-in users: `{ harvestId, position }`, `409` while any harvest is queued or running |
|
||||
| `POST /api/admin/discovery/settings` `{harvestEnabled, harvestAt}` | admin | Exactly a boolean and an `HH:MM` time, anything else is `400`; stored in `discovery.json` `settings` |
|
||||
|
||||
+3
-1
@@ -21,7 +21,7 @@ Data on the host, all bind mounts (no named volumes):
|
||||
| Path (`.env`) | Contents | Back up? |
|
||||
|---|---|---|
|
||||
| `LIBRARY_PATH` | The manga: `Manga/Book/Chapter/images` ([library-format.md](library-format.md)) | Yes, it is your collection |
|
||||
| `DATA_PATH` | `db.json` (users, progress, votes), catalog, library index cache | Yes (built-in backup, section 8) |
|
||||
| `DATA_PATH` | `db.json` (users, progress, votes), catalog, discovery feeds, library index cache | Yes (built-in backup, section 8). `discovery-cache/` inside it is safe to delete and may be left out of backups: the next harvest rebuilds it |
|
||||
| `SCRAPER_DATA_PATH` | Queue, settings, source ledger, metadata cache | Included in the built-in backup |
|
||||
| `BACKUP_PATH` | Built-in backup archives | Keep it on another disk or NAS |
|
||||
| `SCRAPER_TOKEN_PATH` | Shared secret between reader and scraper | Regenerate if lost |
|
||||
@@ -69,6 +69,8 @@ demo manga in an empty library after the build in section 5:
|
||||
| `TZ` | `UTC` | Time zone of the containers; affects the nightly update time and backup schedule |
|
||||
| `COOKIE_SECURE` | `0` | `1` when readers reach the site over HTTPS |
|
||||
| `READING_WINDOWS_JSON`, `READING_TIMEZONE`, `READING_QUIZ_*` | off | Optional server-enforced reading hours ([reading-limiter.md](reading-limiter.md)) |
|
||||
| `DISCOVERY_HARVEST_AT` | `03:30` | Daily time (`HH:MM`) of the nightly Discover harvest from AniList, in the container's local time zone (`TZ`). Only users who opted in are harvested; the admin can change the time or switch it off on the Discover page ([discovery.md](discovery.md)) |
|
||||
| `DISCOVERY_ANILIST_URL` | AniList's public API | Tests only: points the discovery harvest at a fixture server. Leave unset in production |
|
||||
| `APP_IMAGE`, `VCS_REF` | `mangacircus:local`, `development` | Image to run; set a pinned registry image to deploy prebuilt releases (section 7) |
|
||||
| `COMPOSE_FILE` | `docker-compose.yml` | Extra Compose files, `:`-separated (e.g. `docker-compose.yml:deploy/compose.https.yml`) |
|
||||
| `MANGACIRCUS_DOMAIN` | – | Host name for `deploy/compose.https.yml` |
|
||||
|
||||
+85
-29
@@ -1,23 +1,28 @@
|
||||
# Discovery Scout (deterministic first release)
|
||||
# Discovery Scout
|
||||
|
||||
The reader now has a separate **Discover** page. This first slice deliberately uses
|
||||
the curated Greatest catalog, already-cached AniList identities/relations, library
|
||||
metadata, each user's Greatest votes, and explicit Discover feedback. It does not
|
||||
call an LLM, fetch new metadata, inspect chapter/image hosters, edit a collection,
|
||||
The reader has a separate **Discover** page. Suggestions come from the curated
|
||||
Greatest catalog, cached AniList relations of Greatest and library titles, and a
|
||||
wider pool that the reader harvests from AniList's public API for the users who
|
||||
opted in (design: [the wider-pool spec](superpowers/specs/2026-10-25-discovery-wider-pool-design.md)).
|
||||
Discovery does not call an LLM, inspect chapter/image hosters, edit a collection,
|
||||
or enqueue downloads. The reader remains the only writer of account and discovery
|
||||
state; no fourth Compose service, credential, or database is required.
|
||||
state; no further Compose service, credential, or database is required.
|
||||
|
||||
## Use
|
||||
|
||||
1. Open `discover.html`. Each account independently enables **Personalized
|
||||
discovery** (off by default). Reading activity is a separate opt-in. The
|
||||
mature-title switch only applies to candidates with a known mature tag; it is
|
||||
not a content-safety guarantee.
|
||||
2. An admin presses **Run discovery now** on the same page. Existing suggestions
|
||||
remain visible until the new feed is published. Runs are bounded to 50
|
||||
candidates and 20 suggestions per opted-in user, with no more than four from
|
||||
one period. A second concurrent run is rejected.
|
||||
3. A reader can open **Why this is here · evidence** and record Interested, Maybe,
|
||||
mature-title switch only affects titles AniList marks as adult or with a known
|
||||
mature tag; it is not a content-safety guarantee.
|
||||
2. **Refresh my suggestions** queues a harvest for your own account (at most once an
|
||||
hour). The button shows the queue position, `Fetching…` while it runs, and when
|
||||
the next refresh is possible. Your existing suggestions stay visible until the
|
||||
new feed is published.
|
||||
3. An admin can press **Run discovery now** (re-score from local data, no network)
|
||||
or **Harvest now** (fetch from AniList for every opted-in user, then re-score).
|
||||
A second concurrent run or harvest is rejected. A nightly harvest runs on its
|
||||
own; the admin panel can switch it off or change its time.
|
||||
4. A reader can open **Why this is here · evidence** and record Interested, Maybe,
|
||||
No thanks, or Hide. Feedback is private to the account. Dismissed/hidden
|
||||
titles disappear at once; other feedback influences the next run. The user
|
||||
can undo a choice by clicking its active button again.
|
||||
@@ -28,17 +33,70 @@ the existing source inspection and confirmation process to download anything.
|
||||
The title/year/format matching is intentionally conservative but not a verified
|
||||
identity merge. Missing year, cover, or status remains missing.
|
||||
|
||||
## Seeds from AniList
|
||||
|
||||
A harvest starts from each opted-in user's **anchors**: titles they voted
|
||||
`flame`/`good` in Greatest, marked Interested/Maybe in Discover, read (only with
|
||||
reading activity opted in), or own with an AniList ID. The 15 strongest anchors
|
||||
count. It then asks AniList for four seed kinds:
|
||||
|
||||
| Seed | What | Reason shown on the card |
|
||||
|---|---|---|
|
||||
| Community recommendations | "readers who liked X recommend Y" for each anchor | AniList readers who liked {anchor} recommend this ({rating}). |
|
||||
| Same creator | other works by the story/art authors of strong anchors | Also by {author} — you read/liked {anchor}. |
|
||||
| Top in your tags | well-rated titles in the user's three strongest tags | Top-rated in {tag}, one of your most-liked tags. |
|
||||
| Fresh | recently started, releasing and trending titles | New: started {year}, ongoing and trending. |
|
||||
|
||||
A title found by several seeds keeps every reason and evidence item; the card
|
||||
shows the strongest, the details list all of them, each with the date it was
|
||||
fetched. A feed holds at most 20 suggestions, at most 8 from one seed kind, up to
|
||||
4 fresh titles, and some from outside your strongest tags.
|
||||
|
||||
- **Budgets:** a nightly or admin harvest makes at most 300 requests, a
|
||||
per-user refresh 60. Requests are sequential and limited to 25 a minute. When
|
||||
the budget is spent the harvest stops; what was fetched is still used.
|
||||
- **Cache:** `data/discovery-cache/`, one file per response. Titles,
|
||||
recommendations and creators stay fresh for 30 days, tag lists and the fresh
|
||||
list for 7 days; at most 5,000 files, oldest pruned first. Scoring reads only
|
||||
this cache and never the network, so a failed harvest means older candidates,
|
||||
never an empty feed.
|
||||
- **Nightly time:** `03:30` by default, or `DISCOVERY_HARVEST_AT`, in the
|
||||
container's local time zone (`TZ`). The admin panel overrides both and can turn
|
||||
the nightly harvest off. A night missed while the reader was down runs once
|
||||
after startup.
|
||||
- **Privacy:** for opted-in users only, the server sends AniList the IDs of
|
||||
their anchor titles (and the names of their top tags), without login, cookies or
|
||||
user names. Feeds and feedback stay private to this server; cache entries are
|
||||
not tied to a user.
|
||||
|
||||
### Stop reasons
|
||||
|
||||
The admin panel shows the last harvest as `status · requests/budget · stop
|
||||
reason · cache files`.
|
||||
|
||||
| Stop reason | Meaning | What happens next |
|
||||
|---|---|---|
|
||||
| `budget` | The request budget ran out | Normal; the next harvest continues where the cache is stale |
|
||||
| `rate-limited` | AniList answered 429 | Harvesting pauses; refresh buttons show "Paused by AniList" until the next nightly or admin harvest |
|
||||
| `forbidden` | AniList answered 401/403 | Same pause |
|
||||
| `challenge` | AniList answered with an HTML or challenge page | Same pause |
|
||||
| `upstream-errors` | Five timeouts or server errors in a row | The harvest ends; nothing is paused |
|
||||
| `error` | A local fault (for example a cache write) | See the reader log |
|
||||
|
||||
Discovery never retries with a different identity, user agent or host. A
|
||||
re-scoring failure after a harvest is shown as the admin panel's error line.
|
||||
|
||||
## Data and API
|
||||
|
||||
- `data/discovery.json`: atomic, reader-owned candidate and per-user feed
|
||||
snapshots. A previous valid version is retained as `discovery.json.prev`.
|
||||
A corrupt file makes reads/runs fail closed rather than overwrite it.
|
||||
snapshots plus the last harvest record and the admin's harvest settings. A
|
||||
previous valid version is retained as `discovery.json.prev`. A corrupt file
|
||||
makes reads/runs/harvests fail closed rather than overwrite it.
|
||||
- `data/discovery-cache/`: harvested AniList responses; safe to delete.
|
||||
- `data/db.json`: consent in user settings and explicit feedback under
|
||||
`discoveryFeedback`. The account-deletion route removes that feedback.
|
||||
- `GET /api/discovery`: caller's own feed, settings, evidence, and feedback only.
|
||||
- `POST /api/discovery/feedback`: caller's candidate choice; requires opt-in.
|
||||
- `GET /api/admin/discovery` and `POST /api/admin/discovery/run`: admin-only
|
||||
status and manual generation. A run keeps the last good feed on failure.
|
||||
- Routes: [architecture.md](architecture.md) (`/api/discovery*`,
|
||||
`/api/admin/discovery*`) and §6.6 there for the harvester.
|
||||
|
||||
The job asks the private scraper state API for queued titles to avoid recommending
|
||||
them. If that API is unavailable, the run still uses only cached sources; the
|
||||
@@ -58,15 +116,13 @@ APP_IMAGE=mangacircus:discovery-test npm run test:discovery-ui
|
||||
|
||||
The dry run reads `db.json`, a saved library index if present, Greatest, and
|
||||
cached AniList metadata. It writes nothing and does not contact providers. The
|
||||
Compose/browser suite uses a disposable data tree and queue, not the live one.
|
||||
Compose/browser suite uses a disposable data tree and queue, not the live one,
|
||||
and points `DISCOVERY_ANILIST_URL` at a closed local port so no harvest reaches
|
||||
AniList.
|
||||
|
||||
## Deliberate limits and next stage
|
||||
## Limits
|
||||
|
||||
This slice has no scheduled run, external metadata discovery, LLM, admin seed
|
||||
form, edition-finding action, or explicit purge control for old feed snapshots.
|
||||
Disabling consent stops feed display and future personalized generation, but
|
||||
existing snapshot/feedback is retained until account deletion or a later data
|
||||
management feature. Candidate quality is limited by the curated list and local
|
||||
metadata cache. Add external public metadata feeds and optional LLM only behind
|
||||
the existing bounded candidate/evidence contract after this deterministic path
|
||||
has been used and reviewed.
|
||||
There is no admin seed form, edition-finding action, LLM rationale, or explicit
|
||||
purge control for old feed snapshots. Disabling consent stops feed display,
|
||||
harvesting and future personalized generation for that account, but existing
|
||||
snapshot/feedback is retained until account deletion.
|
||||
Reference in new issue
Block a user