diff --git a/CLAUDE.md b/CLAUDE.md index 7d4b59fd..6a58286f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,7 +105,7 @@ If/when multi-tenancy is on the roadmap, both #54 and #16 should be re-opened to ### Source Layout -- `src/routes/` — HTTP handlers. Thin: extract params, call service, return response. `admin.rs` ships the `/admin` page (tabs: health, users, reference_data, trash, system) — admin-only. +- `src/routes/` — HTTP handlers. Thin: extract params, call service, return response. `admin.rs` ships the `/admin` page shell and its Health and Trash tabs; the other four tabs live in `admin_users.rs`, `admin_reference_data.rs` (+ `admin_labels.rs`), `admin_system.rs` and `admin_api_keys.rs` — six tabs in all, admin-only. - `src/services/` — Business logic. All domain rules live here, never in handlers. `admin_health.rs` owns Health-tab data builders (entity counts, trash count, MariaDB version cache, disk usage). - `src/models/` — Database models. SQL queries, row mapping, `DbPool` parameter. - `src/middleware/` — Axum middleware: `auth.rs` (Session extractor), `htmx.rs` (HxRequest + HtmxResponse), `pending_updates.rs` (OOB metadata delivery), `logging.rs`, `csp.rs` (Content-Security-Policy + hardening headers, story 7-4). diff --git a/README.md b/README.md index 61f0e3c6..eb3f1fa1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Built for collectors who want more than a spreadsheet: -- **Barcode-first cataloging.** Scan an ISBN / EAN-13 and the title resolves asynchronously through a metadata provider chain (BnF, Google Books, Open Library, Library of Congress, MusicBrainz, OMDb, TMDb, BDGest), with cover-image download and similar-title detection. +- **Barcode-first cataloging.** Scan an ISBN / EAN-13 and the title resolves asynchronously through a metadata provider chain (BnF, Google Books, Open Library, Library of Congress, K10plus, MusicBrainz, OMDb, TMDb, BDGest), with cover-image download and similar-title detection. - **Multi-media support.** Books, BD/comics (with multi-position omnibus volumes), audio releases, films/series — each typed correctly and with the right metadata provider chosen automatically. - **Series + collection awareness.** Gap detection on series volumes, Dewey-based browsing, similar-titles section. - **Storage-location tracking.** Configurable hierarchy (room → shelf → row → …), barcode-on-shelf workflow, with a 30-second **Undo** on the last shelving or batch-location action. @@ -286,10 +286,15 @@ cargo sqlx prepare --check --workspace -- --all-targets ### i18n -Locale files in `locales/{en,fr}.yml`. After adding or renaming keys: +Locale files in `locales/{en,fr,de,it}.yml` — **four** locales, and a key +must exist in all four. `tests/locale_parity.rs` fails the build on any +key present in one file and missing from another (it also checks that +every translation carries the same `%{...}` placeholders as the English +reference). After adding or renaming keys: ```bash touch src/lib.rs && cargo build # Force proc-macro rebuild (rust-i18n) +cargo test --test locale_parity # All four files agree ``` ## Repository layout @@ -360,7 +365,26 @@ tests/ ## Documentation -Product and planning documents are versioned under `_bmad-output/`: +### For people running mybibli + +- **[User manual](docs/manual/)** — the end-user book, one self-contained PDF per language: [English](docs/manual/mybibli-manual-en.pdf) · [French](docs/manual/mybibli-manual-fr.pdf). Installation, configuration, daily use, metadata providers, roles, backup and restore, upgrades and release notes, troubleshooting, the HTTP API, operations. LaTeX sources under `docs/manual/{en,fr}/`, built with `docs/manual/build.sh`. +- [`SECURITY.md`](SECURITY.md) — supported versions, and how to report a vulnerability privately. + +### For people working on it + +- [`CLAUDE.md`](CLAUDE.md) — coding conventions, architecture patterns, and the Foundation Rules. The de-facto architecture reference for the shipped code. +- [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) — how to file, patch, and behave. +- [`docs/ci-cd.md`](docs/ci-cd.md) — the four CI gates, Docker Hub publishing, release procedure. +- [`docs/auth-threat-model.md`](docs/auth-threat-model.md) — the auth surface (CSRF, cookies, session policy) and its accepted posture for the single-tenant LAN/NAS shape. Read it before touching anything in that area. +- [`docs/route-role-matrix.md`](docs/route-role-matrix.md) — every route with its role gate and CSRF status. +- [`docs/permanent-delete-and-purge.md`](docs/permanent-delete-and-purge.md) — soft delete, Trash, FK ordering, and the auto-purge scheduler. +- [`docs/error-message-style.md`](docs/error-message-style.md) — the contract every `error.*` i18n key answers to. +- [`docs/unimarc-mapping.md`](docs/unimarc-mapping.md) — field-to-zone mapping for the UNIMARC-aligned cataloging fields. +- [`docs/accessibility-audit.md`](docs/accessibility-audit.md) — the WCAG 2.2 AA audit, with its date and its scope. + +### Product and planning + +Versioned under `_bmad-output/`: - [`planning-artifacts/product-brief-mybibli.md`](_bmad-output/planning-artifacts/product-brief-mybibli.md) — product vision - [`planning-artifacts/prd.md`](_bmad-output/planning-artifacts/prd.md) — functional requirements (121 FRs), NFRs, user journeys @@ -370,8 +394,6 @@ Product and planning documents are versioned under `_bmad-output/`: - [`implementation-artifacts/sprint-status.yaml`](_bmad-output/implementation-artifacts/sprint-status.yaml) — live sprint state - [`implementation-artifacts/epic-*-retro-*.md`](_bmad-output/implementation-artifacts/) — per-epic retrospectives -Coding conventions and architecture rules for contributors are in [`CLAUDE.md`](CLAUDE.md). CI/CD pipeline, Docker Hub publishing, and release procedure are documented in [`docs/ci-cd.md`](docs/ci-cd.md). The auth surface (CSRF, cookies, session policy) and its accepted posture for the single-tenant LAN/NAS deployment shape are formalized in [`docs/auth-threat-model.md`](docs/auth-threat-model.md). - ## Roadmap | Epic | Title | Status | @@ -387,7 +409,11 @@ Coding conventions and architecture rules for contributors are in [`CLAUDE.md`]( | 9 | Polish UX & Accessibilité | ✅ done | | 10 | Mobile UX & sécurité closeout | ✅ done | -mybibli has been live in production since v1.1.1 (2026-05-14) on the household NAS that drove the project. v1.0.0 shipped after Epic 9 close (2026-05-10) as the first production-ready build; v1.1.0 added the seed-gate + audit trio (mandatory install floor — see "Installation notes" above); the themed minors v1.2 through v1.8 then delivered the original feature roadmap (browse & find, wishlist, HTTP API, valuation & stats, catalog hygiene, de/it locales + runtime logging, cover handling), each followed by production-driven patch trains. Since v1.8 the project runs in GH-issue-driven polish mode; the current release is v1.18.0 (2026-08-18) — a single-feature minor: [#443](https://github.com/guycorbaz/mybibli/issues/443) **management labels**, an admin-defined vocabulary a librarian applies to titles *and* volumes alike — flag an item "À vérifier", then find everything flagged from a dedicated `/labels` page that drills down to the members and through to their detail pages. Labels are internal: a visitor never sees them. **One additive migration** (three tables). The navigation bar now switches to its menu below 1024px instead of 768px, because the new entry no longer fitted on a tablet. It follows v1.17.0 (2026-08-18) — a metadata-clarity minor: a metadata-clarity minor: [#202](https://github.com/guycorbaz/mybibli/issues/202) a failed metadata lookup now says *why* — every source searched and none holds the book, a source that was busy, or a source never asked for want of an API key — three situations that previously shared one message and call for opposite reactions; [#206](https://github.com/guycorbaz/mybibli/issues/206) genre and Dewey are grouped into a Classification section that states which of the two a metadata fetch may overwrite (the genre never, the Dewey code until you edit it). No migration. It follows v1.16.0 (2026-08-17) — a bibliographic-coverage and operability minor: [#450](https://github.com/guycorbaz/mybibli/issues/450) K10plus, the German union catalogue, joins the metadata chain as a zone completer and immediately becomes its leading contributor (45 titles / 100 UNIMARC zones, against 29 / 61 for the Library of Congress), gated by ISBN prefix; [#459](https://github.com/guycorbaz/mybibli/issues/459) `MYBIBLI_RESET_ADMIN`, a one-shot startup hatch that recovers a locked-out administrator without a database console; [#457](https://github.com/guycorbaz/mybibli/issues/457) the proposed L-code now counts soft-deleted rows so it is genuinely free; [#458](https://github.com/guycorbaz/mybibli/issues/458) four intermittently failing E2E specs deflaked at the source. No migration. It follows v1.15.0 (2026-08-13) — a production-log-review minor: [#202](https://github.com/guycorbaz/mybibli/issues/202) metadata provenance recorded and displayed per title, [#424](https://github.com/guycorbaz/mybibli/issues/424) light-mode contrast raised to the WCAG AA floor, [#419](https://github.com/guycorbaz/mybibli/issues/419) a third throttle-retry tier for bulk metadata runs, [#449](https://github.com/guycorbaz/mybibli/issues/449) a seven-character build commit in the startup log; one additive migration. It follows v1.14.0 (2026-07-28) — a cataloging-fix and bibliographic-coverage minor: [#440](https://github.com/guycorbaz/mybibli/issues/440)/[#441](https://github.com/guycorbaz/mybibli/issues/441)/[#442](https://github.com/guycorbaz/mybibli/issues/442) fix three defects in the scan flow (volume labels attaching to the previous title when cataloguing several UPC items in a row, a misleading "not found" when the active title had been deleted, and V-code labels staying locked after a volume was deleted), and [#439](https://github.com/guycorbaz/mybibli/issues/439) adds Library of Congress MARC 21 records so the UNIMARC-aligned fields also fill for English-language books the BnF does not hold. It follows v1.13.0 (2026-07-24) — a UNIMARC-themed feature minor: [#389](https://github.com/guycorbaz/mybibli/issues/389) six UNIMARC-aligned cataloging fields (statement of responsibility, edition statement, collection title/number, general note, original title) captured automatically from the BnF and shown on the title page, a new Health-tab *Backfill metadata from BnF* bulk action to fill them on already-cataloged titles, and [#434](https://github.com/guycorbaz/mybibli/issues/434) a per-title cataloging log summary; the field-to-zone mapping ships as [`docs/unimarc-mapping.md`](docs/unimarc-mapping.md), record import/export having since been dropped (2026-08-17) as out of scope for a household library. It follows v1.12.0 (2026-07-11) — a 2-issue feature minor: [#427](https://github.com/guycorbaz/mybibli/issues/427) two new cover sources (BnF Couvertures legal-deposit scans + Inventaire.io) that recover roughly half of the previously unfindable French/Swiss covers, and [#428](https://github.com/guycorbaz/mybibli/issues/428) a label-printing helper showing the highest V/L-codes in use on the catalog page — and v1.11.0 (same day) — a 4-issue minor driven by a production-log review: [#418](https://github.com/guycorbaz/mybibli/issues/418) persistent session cookie + admin-configurable inactivity timeout (tablet screen-locks no longer log you out mid-cataloguing), [#419](https://github.com/guycorbaz/mybibli/issues/419) bulk cover-refetch pacing + throttle back-off + completion summary, [#416](https://github.com/guycorbaz/mybibli/issues/416) daily auto-purge unblocked (orphan session rows), [#417](https://github.com/guycorbaz/mybibli/issues/417) dashboard-chip log-noise fix. It follows v1.10.0 (2026-07-01) — a single-feature minor closing [#9](https://github.com/guycorbaz/mybibli/issues/9) (undo the last scan action from the catalog feedback list within a 30-second window) — and the v1.9 line (2026-06-11): v1.9.0, the first issue-driven feature minor — [#367](https://github.com/guycorbaz/mybibli/issues/367) saved custom searches, [#396](https://github.com/guycorbaz/mybibli/issues/396) per-provider metadata-timeout overrides, [#405](https://github.com/guycorbaz/mybibli/issues/405)/[#406](https://github.com/guycorbaz/mybibli/issues/406) runtime log-level fixes — patched the same day by v1.9.1 ([#403](https://github.com/guycorbaz/mybibli/issues/403) German/Italian localization of the last two client-side messages, [#412](https://github.com/guycorbaz/mybibli/issues/412) CI test de-flake). The full release-by-release history lives in [ROADMAP.md](ROADMAP.md) and on the [GitHub releases page](https://github.com/guycorbaz/mybibli/releases). See [`epics.md`](_bmad-output/planning-artifacts/epics.md) for the epic breakdown and [`sprint-status.yaml`](_bmad-output/implementation-artifacts/sprint-status.yaml) for the story-by-story state. +mybibli has been live in production since v1.1.1 (2026-05-14) on the household NAS that drove the project. v1.0.0 shipped after Epic 9 close (2026-05-10) as the first production-ready build; v1.1.0 added the seed-gate + audit trio (mandatory install floor — see "Installation notes" above); the themed minors v1.2 through v1.8 then delivered the original feature roadmap (browse & find, wishlist, HTTP API, valuation & stats, catalog hygiene, de/it locales + runtime logging, cover handling), each followed by production-driven patch trains. Since v1.8 the project runs in GH-issue-driven polish mode. + +**Current release: `v1.18.0`** (2026-08-18) — management labels ([#443](https://github.com/guycorbaz/mybibli/issues/443)): an admin-defined vocabulary a librarian applies to titles *and* volumes from one shared list, with a `/labels` page that drills down to what carries each label. Labels stay internal — a visitor never sees them. One additive migration; the navigation bar now switches to its menu below 1024px, because the new entry no longer fitted on a tablet. + +**Release-by-release history lives in [ROADMAP.md](ROADMAP.md)** — one section per version, and the canonical copy. It is deliberately not repeated here: the [GitHub releases page](https://github.com/guycorbaz/mybibli/releases) carries the same notes as published artifacts, chapter 8 of the [user manual](docs/manual/) carries them offline, and the [website roadmap](https://guycorbaz.github.io/mybibli/roadmap.html) tells the same story for a different audience. See [`epics.md`](_bmad-output/planning-artifacts/epics.md) for the epic breakdown and [`sprint-status.yaml`](_bmad-output/implementation-artifacts/sprint-status.yaml) for the story-by-story state. ## License diff --git a/ROADMAP.md b/ROADMAP.md index aeeabfde..ec0f0b31 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -32,13 +32,27 @@ mybibli follows [Semantic Versioning](https://semver.org/) 2.0. ## Now -**Current stable: [`v1.18.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.18.0)** — a single-feature minor on v1.17.0 (shipped 2026-08-18): [#443](https://github.com/guycorbaz/mybibli/issues/443) **management labels**. An admin defines a vocabulary once — "À vérifier", "À relire", whatever the practice calls for — and a librarian applies it to titles *and* volumes from that one shared list; a dedicated `/labels` page lists the vocabulary with its counts, drills down to the members of each label in two sections (titles above volumes), and links through to their detail pages. Labels are internal: an anonymous visitor sees none of it, and the handlers do not even run the queries for them. **One additive migration** (`labels`, `title_labels`, `volume_labels`) — the first migration since v1.15.0, so a downgrade is a question worth asking again. The nav bar now switches to its menu below 1024px instead of 768px: the new entry brought an admin to eight links, which overflowed a tablet viewport by 86px. Shipped in three reviewed tranches (#473, #474, #475); see the v1.18.0 section below. The prior release [`v1.17.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.17.0) — a 2-issue metadata-clarity minor on v1.16.0 (shipped 2026-08-18): [#202](https://github.com/guycorbaz/mybibli/issues/202) a metadata lookup that finds nothing now says which of three things happened — every source searched and none holds the book, a source too busy to answer, or a source never asked because no API key is set — three situations that had shared one message and call for opposite reactions; [#206](https://github.com/guycorbaz/mybibli/issues/206) genre and Dewey are grouped into a Classification section that states the asymmetry between them (no fetch ever writes the genre; the Dewey code is proposed until you edit it yourself). **No migration.** Both issues came from the same production user, eight months apart, and both turned out to be about the interface telling the truth rather than the machinery being wrong; see the v1.17.0 section below. The prior release [`v1.16.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.16.0) — a 4-issue minor on v1.15.0 (shipped 2026-08-17): [#450](https://github.com/guycorbaz/mybibli/issues/450) K10plus, the German union catalogue, joins the chain as a zone completer and immediately becomes its leading contributor — 45 titles and 100 UNIMARC zones on the production catalog against the Library of Congress's 29 and 61 — gated by ISBN prefix (978-0/1/2/3) so only requests that can plausibly hit are paced; [#459](https://github.com/guycorbaz/mybibli/issues/459) `MYBIBLI_RESET_ADMIN`, a one-shot startup hatch that puts a locked-out administrator back in without a database console; [#457](https://github.com/guycorbaz/mybibli/issues/457) the proposed L-code now counts soft-deleted rows, so it is a code that is actually free; [#458](https://github.com/guycorbaz/mybibli/issues/458) four intermittently failing E2E specs deflaked — three distinct root causes — restoring the CI signal. **No migration.** Shipped through two release candidates: rc.1 was withdrawn before publication when its own gate caught a rate-limiter contention bug, and rc.2 ran a full production backfill whose logs settled the K10plus decision; see the v1.16.0 section below. The prior release [`v1.15.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.15.0) — a 4-issue minor on v1.14.1 (shipped 2026-08-13), bundling the August production-log review: [#202](https://github.com/guycorbaz/mybibli/issues/202) titles now record and display which provider resolved their metadata, so a thin record can be told apart from one no provider answered; [#424](https://github.com/guycorbaz/mybibli/issues/424) light-mode contrast raised above the WCAG AA floor on the home and locations pages, where the offending elements only rendered once data existed; [#419](https://github.com/guycorbaz/mybibli/issues/419) a third retry tier for bulk metadata runs, measured from production logs — request spacing was shown NOT to be the lever; [#449](https://github.com/guycorbaz/mybibli/issues/449) the startup log shortens the build commit to 7 characters. **One additive migration** (`titles.metadata_source`, nullable — pre-existing rows read as "unknown provenance"); see the v1.15.0 section below. The prior release [`v1.14.1`](https://github.com/guycorbaz/mybibli/releases/tag/v1.14.1) — a one-issue patch on v1.14.0 (shipped 2026-07-28): [#447](https://github.com/guycorbaz/mybibli/issues/447) the application now logs its version, build commit and profile at startup, so a log file read in isolation identifies the build that produced it; no migration. The underlying minor [`v1.14.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.14.0) — a cataloging-fix and bibliographic-coverage minor on v1.13.0 (shipped 2026-07-28): [#440](https://github.com/guycorbaz/mybibli/issues/440) volume labels no longer attach to the previous title when several UPC items are catalogued in a row, [#441](https://github.com/guycorbaz/mybibli/issues/441) a deleted active title now says so instead of blaming the scanned label, [#442](https://github.com/guycorbaz/mybibli/issues/442) a deleted volume's V-code label is reusable without an admin purge (refused when it carries loan history), and [#439](https://github.com/guycorbaz/mybibli/issues/439) Library of Congress MARC 21 records fill the UNIMARC-aligned fields for English-language books that the BnF does not hold — the two national libraries now complete each other field by field, first source wins; **no migration**; note that French-prefix titles the BnF could not describe stay empty, LoC does not hold them either; see the v1.14.0 section below. The prior release [`v1.13.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.13.0) — a UNIMARC-themed feature minor on v1.12.0 (shipped 2026-07-24): [#389](https://github.com/guycorbaz/mybibli/issues/389) six UNIMARC-aligned cataloging fields (200$f, 205$a, 225$a/$v, 300$a, 500$a) captured from the BnF at scan time and shown on the title page, plus a Health-tab *Backfill metadata from BnF* bulk action for already-cataloged titles; [#434](https://github.com/guycorbaz/mybibli/issues/434) per-title cataloging log summary; one additive migration; record import/export (ISO 2709 / UNIMARC XML) was dropped on 2026-08-17 — SIGB record exchange makes no sense for a household library; see the v1.13.0 section below. The prior release [`v1.12.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.12.0) — a 2-issue feature minor on v1.11.0 (shipped 2026-07-11, same day as v1.11.0): [#427](https://github.com/guycorbaz/mybibli/issues/427) BnF Couvertures + Inventaire.io cover fallbacks (live-tested to recover ~half of the previously unfindable FR/CH covers), [#428](https://github.com/guycorbaz/mybibli/issues/428) highest used V/L-code line on /catalog for label printing; no migration; see the v1.12.0 section below. The prior release [`v1.11.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.11.0) — a 4-issue minor on v1.10.0 (shipped 2026-07-11), bundling the July production-log review: [#418](https://github.com/guycorbaz/mybibli/issues/418) persistent session cookie + admin-configurable inactivity timeout, [#419](https://github.com/guycorbaz/mybibli/issues/419) bulk cover-refetch pacing/back-off/summary, [#416](https://github.com/guycorbaz/mybibli/issues/416) auto-purge unblocked, [#417](https://github.com/guycorbaz/mybibli/issues/417) log-noise fix; one additive settings migration; see the v1.11.0 section below. The prior release [`v1.10.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.10.0) — a single-feature minor on v1.9.1 (shipped 2026-07-01): [#9](https://github.com/guycorbaz/mybibli/issues/9) undo the last scan action (shelving a volume or activating a batch location) from the catalog feedback list within a 30-second window; no migration; see the v1.10.0 section below. The prior line [`v1.9.1`](https://github.com/guycorbaz/mybibli/releases/tag/v1.9.1) — a 2-issue patch on v1.9.0 (shipped 2026-06-11): [#403](https://github.com/guycorbaz/mybibli/issues/403) the last two client-side messages (modal-busy guard, server-error feedback) now come from the #i18n-bundle data island and localize in German/Italian, [#412](https://github.com/guycorbaz/mybibli/issues/412) CI de-flake of the recent-activity boundary tests; see the v1.9.1 section below. The underlying feature minor [`v1.9.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.9.0) — 4 issues on v1.8.6 (same day): [#367](https://github.com/guycorbaz/mybibli/issues/367) saved custom searches (name and save a home search composition — free text + filter + sort — then re-run, rename or delete it from a dropdown next to the sort control), [#396](https://github.com/guycorbaz/mybibli/issues/396) per-provider metadata-timeout overrides in /admin > System, [#406](https://github.com/guycorbaz/mybibli/issues/406) admin log-level form no longer 409s on consecutive saves, and [#405](https://github.com/guycorbaz/mybibli/issues/405) transport-crate log noise capped at the default level; see the v1.9.0 section below. The feature build for v1.1 through v1.8 is complete; v1.8 was the last *themed* minor and the project is in **GH-issue-driven polish mode** — subsequent minors (v1.9.0 is the first) bundle the issue-driven work that crossed the feature threshold, patches ship independently for production bugs, and new feature CRs queue in the parking lot until a coherent theme emerges or a roadmap-slotted candidate (e.g., the v2.0 classification refactor) is greenlit. Release-by-release history: the per-version sections below, the [GitHub releases page](https://github.com/guycorbaz/mybibli/releases), and chapter 8 of the user manual. - -Open issues against the live line: - -*(none)* - -**The backlog is empty** — no open issue, for the first time since the project opened. New work starts from a fresh report. +**Current stable: [`v1.18.0`](https://github.com/guycorbaz/mybibli/releases/tag/v1.18.0)** +(2026-08-18) — management labels ([#443](https://github.com/guycorbaz/mybibli/issues/443)): +an admin-defined vocabulary a librarian applies to titles *and* volumes +from one shared list, with a `/labels` page of its own. One additive +migration. The section below has the detail; every release before it has +its own section further down, and this is the file that carries them — +the README quotes only the current release, and the website tells the +same story for a different audience. + +**Merged on `main`, not yet released** — the three change requests from +the 2026-08-18 security review: +[#480](https://github.com/guycorbaz/mybibli/issues/480) the seed gate +hard-deletes its artefacts instead of parking them in the Trash, +[#478](https://github.com/guycorbaz/mybibli/issues/478) the Trash panel's +Restore button reaches a route that exists, and +[#479](https://github.com/guycorbaz/mybibli/issues/479) a cover decode +runs under an allocation budget. No migration. The version they ship +under is not decided yet. + +**Open issues: none.** The tracker has been empty since the review +closed. New work starts from a fresh report. ## v1.18.0 — management labels *(shipped)* @@ -169,26 +183,17 @@ backwards-compatible path is found. ## Parking lot — no scheduled version -Tracked but not slotted. Will be picked up opportunistically when a -related area is being worked on, or rolled into a v1.x release if they -happen to fit a theme. - -### Code-review findings (technical debt) - -- [#220](https://github.com/guycorbaz/mybibli/issues/220) — CSRF whitelist parser handles comma-separated HX-Trigger only. -- [#217](https://github.com/guycorbaz/mybibli/issues/217) — `originatesFromConfirm` would tag `X-Modal-Confirm` on nested-form submits inside modals. -- [#216](https://github.com/guycorbaz/mybibli/issues/216) — `AppError::IntoResponse` returns HTML fragment for direct browser nav (non-HTMX). -- [#152](https://github.com/guycorbaz/mybibli/issues/152) — Add `anonymous_gets_200_on_series` test to `role_gating.rs`. -- [#151](https://github.com/guycorbaz/mybibli/issues/151) — Align UX spec + 9.18 epic spec nav-link tables with shipped implementation. -- [#148](https://github.com/guycorbaz/mybibli/issues/148) — `nav.js` burst detector — Bluetooth Android scanners emit `Unidentified`. -- [#147](https://github.com/guycorbaz/mybibli/issues/147) — `nav.js` `getBurstThresholdMs` accepts `n=1` (1ms) — pathological admin-config protection. -- [#146](https://github.com/guycorbaz/mybibli/issues/146) — E2E `navbar-hamburger` Test 5 — `simulateScan` trailing Enter race on `/login`. -- [#145](https://github.com/guycorbaz/mybibli/issues/145) — `nav.js` burst threshold (50ms) may be tight for very fast typists / mechanical keyboards. - -### Larger CRs awaiting prioritization - -- [#202](https://github.com/guycorbaz/mybibli/issues/202) — Broader per-provider visibility (metadata-source badge, per-provider retry, structured error surfacing). The docs slice already shipped in v1.5; the UX slice is deeper work. -- [#389](https://github.com/guycorbaz/mybibli/issues/389) — UNIMARC conformance for title cataloging. Align the title data model on the UNIMARC standard (zone/subfield mapping) so records are standard-conformant and interoperable with library systems (BnF, Koha, PMB). Deep data-model + architecture work; a documented field⇄zone mapping is the first slice. Periphery: an eventual UNIMARC import/export path (ISO 2709 / UNIMARC XML). +Work that is tracked but not slotted lives in the +[issue tracker](https://github.com/guycorbaz/mybibli/issues) under +`type:change-request`, `type:code-review-finding` and +`type:known-failure` (Foundation Rule 11 — GitHub Issues is the single +source of truth, and this file deliberately keeps no second copy of it). +Such items are picked up opportunistically when a related area is being +worked on, or rolled into a v1.x release if they happen to fit a theme. + +*The parking lot is currently empty.* The eleven findings this section +used to list — #145 to #152, #202, #216, #217, #220, #389 — have all +been closed. ## Past releases diff --git a/docs/accessibility-audit.md b/docs/accessibility-audit.md index 1c206fde..00067d96 100644 --- a/docs/accessibility-audit.md +++ b/docs/accessibility-audit.md @@ -1,6 +1,23 @@ -# Accessibility Audit — Story 9-22 (2026-05-10) - -End-to-end WCAG 2.2 AA audit of mybibli. Three layers: +# Accessibility audit — story 9-22 (2026-05-10) + +**Status:** a dated snapshot, not a standing claim. The audit below was +run on 2026-05-10 over the surfaces that existed then — thirteen URLs, +listed in the next section. Everything shipped since has an automated +axe-core gate only if it was added to +`tests/e2e/specs/accessibility-full.spec.ts`, and several were not: +`/labels` (1.18.0), `/wishlist`, `/stats/value` and `/audit` are not in +it, and none of them has been through the manual keyboard and contrast +passes recorded here. (`/setup` is the exception among the newer +surfaces: story 10-5 put an axe check on it inside the wizard E2E lane, +`tests/e2e/specs/journeys/setup-wizard.spec.ts`, because the wizard +predicate needs a stack that lane alone provides.) + +Read this file as "these thirteen surfaces were audited on that date, +and the automated part of it still runs on every PR" — not as "mybibli +is WCAG 2.2 AA". Extending the spec to the newer surfaces is the +prerequisite for the stronger claim. + +WCAG 2.2 AA audit of mybibli, in three layers: 1. **Automated**: `tests/e2e/specs/accessibility-full.spec.ts` runs `axe-core` (`@axe-core/playwright`) on 13 surfaces with the `wcag2a`, `wcag2aa`, `wcag22aa` tag sets. **13/13 passing as of 2026-05-10.** 2. **Manual keyboard navigation**: checklist below. diff --git a/docs/ci-cd.md b/docs/ci-cd.md index f2ed1206..5aecb30b 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -2,22 +2,28 @@ This document describes the GitHub Actions pipeline, Docker Hub publishing, and repo configuration for mybibli. Target audience: future maintainers (and AI agents) bringing up a replica or debugging a failed run. -## Overview — the 3-gate model +## Overview — the 4-gate model -Every push on any branch and every PR targeting `main` runs three parallel jobs that must all pass for the run to be green: +Every push on any branch and every PR targeting `main` runs four parallel jobs that must all pass for the run to be green: | Gate | Purpose | Typical duration | |------|---------|------------------| -| `rust-tests` | Lint + unit/bin tests + SQLx offline-cache check | ~5 min | -| `db-integration` | `#[sqlx::test]` suites against a MariaDB 10.11 service container | ~5 min | -| `e2e` | Full Playwright suite against the Docker Compose stack | ~15–20 min | +| `rust-tests` | Lint + unit/bin tests + SQLx offline-cache check | ~7 min | +| `db-integration` | Every `tests/*.rs` suite against a MariaDB 10.11 service container | ~7 min | +| `e2e` | Full Playwright suite against the seeded Docker Compose stack | ~15–20 min | +| `e2e-wizard` | The first-launch wizard spec against a stack with no seeded users | ~10 min | -The three gates are exposed as a reusable workflow at `.github/workflows/_gates.yml` (called via `uses: ./.github/workflows/_gates.yml`). Both `ci.yml` and `release.yml` call the same gates so CI and release paths are guaranteed identical. +The gates are exposed as a reusable workflow at `.github/workflows/_gates.yml` (called via `uses: ./.github/workflows/_gates.yml`). Both `ci.yml` and `release.yml` call the same gates so CI and release paths are guaranteed identical. After the gates pass: -- **On push to `main`** — nothing else runs. `ci.yml` ends after the 3 gates. **No Docker Hub publish on main pushes** (per "version stricte" policy: images ship only at semver releases, never per mid-Epic story merge). -- **On `v*.*.*` tag push** — `release.yml` runs `verify-version` (Cargo.toml vs. tag), re-runs the 3 gates, then `publish` pushes `gcorbaz/mybibli:` and `gcorbaz/mybibli:latest`. +- **On push to `main`** — nothing else runs. `ci.yml` ends after the gates. **No Docker Hub publish on main pushes** (per "version stricte" policy: images ship only at semver releases, never per mid-Epic story merge). +- **On `v*.*.*` tag push** — `release.yml` runs `verify-version` (Cargo.toml vs. tag), re-runs the gates, then `publish` pushes `gcorbaz/mybibli:` and `gcorbaz/mybibli:latest`. + +Two workflows run outside the gate model: + +- **`audit.yml`** — a RustSec advisory scan. Triggered by changes to `Cargo.toml` / `Cargo.lock` / `.cargo/audit.toml`, and **daily on a schedule**, which is the load-bearing part: an advisory is published against a version you already ship, without any commit on your side, so a scanner wired only to `push` is silent exactly when it matters. Vulnerabilities fail the job; informational warnings (unmaintained / unsound / yanked) are reported without failing. Accepted advisories, with the reason each is accepted, live in `.cargo/audit.toml`. +- **`pages.yml`** — deploys `website/` to GitHub Pages on pushes to `main` that touch the site, the logo or the screenshots. Not a gate; it never reports a status on a PR. Docker Hub publishing is the exclusive responsibility of `release.yml`; both `` and `latest` tags are produced only on `v*.*.*` tag pushes. @@ -43,8 +49,9 @@ Docker Hub publishing is the exclusive responsibility of `release.yml`; both ``. @@ -62,6 +69,18 @@ Docker Hub publishing is the exclusive responsibility of `release.yml`; both ``; full compose logs uploaded as `compose-logs-` (7-day retention). - Post-step (`if: always()`): `docker compose ... down -v` cleans up containers and volumes. +### `e2e-wizard` + +- Runner: `ubuntu-latest`, Node 20 LTS +- Stack: the same compose file plus `tests/e2e/docker-compose.wizard.yml`, which unsets `MYBIBLI_SKIP_SETUP` **and** `MYBIBLI_SEED_DEV_USERS`. The seed gate then clears the seeded users and the seeded session row during the boot the `--wait` already waits for — the lane deliberately does no SQL wipe of its own, so a broken gate fails it instead of hiding behind a manual cleanup ([#480](https://github.com/guycorbaz/mybibli/issues/480)). +- Commands: + ```bash + MYBIBLI_SETUP_E2E=1 npx playwright test specs/journeys/setup-wizard.spec.ts + ``` + The spec skips itself unless `MYBIBLI_SETUP_E2E=1`, so it stays inert in the seeded `e2e` lane. +- Why a second lane: the base stack pins `MYBIBLI_SKIP_SETUP=1` so every other spec reaches its target route directly. The wizard needs the opposite — a fresh install where `/setup` is reachable — and the two cannot coexist in one stack. +- On failure: the wizard Playwright report and the compose logs are uploaded, same shape as the `e2e` job. + ## Secrets | Name | Purpose | @@ -111,10 +130,11 @@ Navigate to **Settings → Branches → Branch protection rules → Add rule** ( - **Require approvals:** OFF (solo-maintainer mode — see rationale below). 3. **Require status checks to pass before merging:** ON - **Require branches to be up to date before merging:** ON - - **Required status checks** — add EXACTLY these three (the parent job that calls the reusable workflow is named `gates`): + - **Required status checks** — add EXACTLY these four (the parent job that calls the reusable workflow is named `gates`): - `gates / Rust tests + clippy + sqlx-prepare` - `gates / DB integration tests` - `gates / Playwright E2E` + - `gates / Playwright wizard E2E` > GitHub shows reusable-workflow jobs as ` / ` where the display name is the `name:` field of each job in `_gates.yml` (NOT the job ID). If the labels do not appear in the autocomplete, trigger a push so a full CI run indexes the check names, then edit the protection rule. 4. **Do NOT add release-only jobs to required checks.** `verify-version` and `publish` from `release.yml` only run on `v*.*.*` tag pushes; they never report a status on PRs, so requiring them would lock every PR out of merging. (Note: `ci.yml` no longer has a `docker-publish` job — Docker Hub publishing is now release-only per "version stricte" policy.) diff --git a/docs/dockerhub-overview.md b/docs/dockerhub-overview.md index e6b309e7..1f649e2d 100644 --- a/docs/dockerhub-overview.md +++ b/docs/dockerhub-overview.md @@ -22,9 +22,10 @@ mybibli is designed for one household on one LAN. Login attempts are not rate-li ## What it is -- **Barcode-first cataloging.** Scan ISBN / EAN-13 → metadata resolves asynchronously through a provider chain (BDGest → BnF → Google Books → Library of Congress → Open Library → MusicBrainz → OMDb → TMDB) with cover-image download and similar-title detection. +- **Barcode-first cataloging.** Scan ISBN / EAN-13 → metadata resolves asynchronously through a provider chain (BDGest → BnF → Google Books → Library of Congress → K10plus → Open Library → MusicBrainz → OMDb → TMDb) with cover-image download and similar-title detection. - **Multi-media.** Books, BD/comics with multi-position omnibus volumes, audio, films/series — each typed correctly with the right provider chosen automatically. - **Series + collection awareness.** Gap detection on series volumes, Dewey-based browsing, similar-titles section. +- **Management labels.** An admin defines a vocabulary once ("To check", "Re-read", "Damaged binding"); a librarian applies any of it to titles *and* to individual copies from that one shared list, and a `/labels` page lists the vocabulary with its counts and drills down to what carries each label. Internal to the library — an anonymous visitor sees none of it. - **Storage-location tracking.** Configurable hierarchy (room → shelf → row → …), barcode-on-shelf workflow, per-location volume list, optional organizational containers (folders, not shelves), shelf-audit workflow ("À contrôler") with home-dashboard indicator. - **Loan management.** Borrower CRUD, loan registration with automatic location restoration on return, overdue threshold (admin-configurable), per-borrower history. - **Wishlist + valuation.** First-class `/wishlist` with provider-chain ISBN preview + free-form add, mark-as-bought, server-rendered PDF export. Optional per-volume `purchase_price` + `current_value` with per-currency totals and a `/stats/value` page (default OFF, admin opt-in). @@ -34,7 +35,7 @@ mybibli is designed for one household on one LAN. Login attempts are not rate-li - **Admin panel.** Health dashboard (entity counts, MariaDB version, disk usage, provider reachability), user management with last-active-admin guard, editable reference data (genres, volume states, contributor roles, location node types), system settings (loans / providers / language / valuation / **logging level**), trash view + restore + permanent delete, configurable auto-purge after 30 days. - **Production observability** (v1.7.0+, completed in v1.7.1). Persistent daily-rotating log files with 30-day in-process purge. Admin-controlled log level (`trace` / `debug` / `info` / `warn` / `error` or full `tracing-subscriber` `EnvFilter` directives) flippable from `/admin > System` without a redeploy. - **First-launch setup wizard.** Fresh installs walk through Admin → Providers → Preferences → Done. -- **Mobile-aware + WCAG 2.2 AA accessible.** Dual-surface mobile UX (desktop tables collapse into cards, admin tabs into ``), full keyboard navigation with shortcuts cheat-sheet (`?`), contextual help-icon tooltips, and an axe-core CI gate over the main surfaces (thirteen of them, listed in [`docs/accessibility-audit.md`](https://github.com/guycorbaz/mybibli/blob/main/docs/accessibility-audit.md) — the newer pages are not in the set yet). ## Quick start @@ -105,7 +106,10 @@ Open `http://localhost:8080`. The first-launch wizard greets you. Create the adm ### Environment reference -All deployment-time settings are environment variables — no config file. The canonical reference with every variable commented is [`.env.example`](https://github.com/guycorbaz/mybibli/blob/main/.env.example) in the repo. Grouped sections: database, HTTP server, application (incl. logging — `MYBIBLI_LOG_LEVEL` / `MYBIBLI_LOG_DIR` / `LOGS_HOST_PATH` / `MYBIBLI_PROVIDER_HEALTH_TIMEOUT_SECS`), cookie & CSP hardening, metadata providers, dev / test overrides. +Configuration comes in two layers, and the distinction matters: + +- **Deployment-time, environment variables** — database URL, host/port, cookie and CSP hardening, log directory, the dev/test overrides. There is no config file; the canonical reference with every variable commented is [`.env.example`](https://github.com/guycorbaz/mybibli/blob/main/.env.example) in the repo. +- **Run-time, the admin panel** — loan thresholds, session timeout, provider API keys and per-provider timeouts, interface language, valuation display, log level. These live in the database and are edited in **Admin → System**, taking effect on the next request with no restart. Several of them (`MYBIBLI_LOG_LEVEL`, `MYBIBLI_PROVIDER_HEALTH_TIMEOUT_SECS`, the provider API keys) *also* exist as environment variables: that is a one-shot seeding path for a fresh deployment, applied while the stored value is still the default. Once you save the setting in the panel, the panel wins and the variable is inert. ### Bind-mount alternatives (Synology DSM, journald shipping, etc.) diff --git a/docs/manual/en/02-configuration.tex b/docs/manual/en/02-configuration.tex index e0997a07..f95cd5cf 100644 --- a/docs/manual/en/02-configuration.tex +++ b/docs/manual/en/02-configuration.tex @@ -10,14 +10,18 @@ \chapter{Configuration} Everything in this chapter is configured from the \texttt{/admin}\index{admin panel} panel — there is no separate -configuration file. The panel has five tabs: +configuration file. The panel has six tabs: \begin{itemize} \item \textbf{Health} — read-only status dashboard. \item \textbf{Users} — create / deactivate users, change roles. - \item \textbf{Reference data} — the four taxonomy tables. - \item \textbf{Trash} — view and restore soft-deleted items. + \item \textbf{Reference data} — the taxonomy tables, and the + management-label vocabulary (section~\ref{sec:management-labels}). + \item \textbf{Trash} — view, restore and permanently delete + soft-deleted items. \item \textbf{System} — global settings. + \item \textbf{API keys} — mint and revoke the keys the HTTP API + authenticates with (chapter~\ref{ch:api}). \end{itemize} Only the \texttt{Admin} role sees the panel; \texttt{Librarian} and @@ -73,7 +77,17 @@ \subsection*{Location node types} is refused as long as one location still references it. \end{tip} -\section{Labels: V-codes and L-codes}\label{sec:labels} +\section{Spine labels: V-codes and L-codes}\label{sec:labels} + +\begin{note} +\textbf{Two things are called ``labels'' in mybibli.} This section is +about the \emph{spine labels} you print and stick on a book or a shelf +— the barcodes that make scanning work. The other kind, added in +1.18.0, is the \emph{management label} you put on a record to say +something about its state (``To check'', ``Damaged binding''); those +live in section~\ref{sec:management-labels}. Nothing is printed for +them, and they never leave the application. +\end{note} mybibli prints two kinds of barcode labels: diff --git a/docs/manual/en/03-usage.tex b/docs/manual/en/03-usage.tex index 705ab8ee..97744a6e 100644 --- a/docs/manual/en/03-usage.tex +++ b/docs/manual/en/03-usage.tex @@ -119,6 +119,59 @@ \subsection{Saved searches}\label{sec:saved-searches} builder. The control is visible to librarians and administrators only. \end{note} +\section{Management labels}\label{sec:management-labels} + +A genre says what a book \emph{is}. A condition state says how a copy +has \emph{aged}. Neither says ``I am not sure this record is right, +come back to it'' --- and that sentence is the one a working catalogue +needs most. Since 1.18.0, \emph{management +labels}\index{label!management} carry it. + +They are not the spine labels of section~\ref{sec:labels}: nothing is +printed, nothing is scanned, and an anonymous visitor never sees them. +They exist for the person maintaining the library. + +\subsection*{Defining the vocabulary} + +An admin creates the vocabulary once, in \emph{Administration +\textrightarrow{} Reference data \textrightarrow{} Labels}: ``To +check'', ``Re-read'', ``Damaged binding'', ``Lent informally'' --- +whatever your practice calls for. Labels are renamed and deleted from +the same place, and like every other reference value they are never +translated: what you type is what everyone sees, in any interface +language. + +Keep the list short. A vocabulary of six labels is used; a vocabulary +of thirty is a second genre taxonomy nobody maintains. + +\subsection*{Putting a label on something} + +A librarian applies labels from the \emph{Labels} section of a title +page or of a single volume page, choosing from that one shared +vocabulary. The distinction matters: + +\begin{itemize} + \item on a \textbf{title}, the label is about the record --- ``the + metadata here looks wrong''; + \item on a \textbf{volume}, it is about that physical copy --- ``this + one has a damaged binding'', which says nothing about the other + two copies on the shelf. +\end{itemize} + +\subsection*{Finding what carries a label} + +\texttt{/labels} lists the vocabulary with a count against each entry. +Opening one shows what carries it, in two sections --- titles above +volumes --- each row linking through to its detail page. That is the +whole point: a label you cannot find your way back from is a note in a +drawer. + +\begin{note} +Labels are internal. The handlers do not even run the queries for an +anonymous visitor, so nothing leaks through the public catalogue --- +including the fact that a label exists. +\end{note} + \section{Moving a volume to another location}\label{sec:move-volume} \begin{enumerate} @@ -170,24 +223,53 @@ \subsection*{Overdue loans} \texttt{/loans} page and on the home-page \emph{Overdue} indicator. You can run \texttt{/loans?filter=overdue} for a focused list. -\section{Library audit}\label{sec:audit} +\section{Shelf audit}\label{sec:audit} An \emph{audit}\index{audit} is the routine ``walk the shelves and -confirm everything is where the database says it is''. mybibli does -not ship a dedicated audit mode — the equivalent workflow is: +confirm everything is where the database says it is''. Since 1.7.x +mybibli carries the workflow rather than leaving you to improvise it +around the search page. -\begin{enumerate} - \item Filter \texttt{/search} by the location you intend to - audit, sorted by V-code. - \item Walk the shelf, scanning each V-code. - \item For any volume scanned that is \emph{not} in the filtered - list, mybibli surfaces a \emph{Volume not in this location} - message. Move it (section~\ref{sec:move-volume}) and move - on. - \item Any volume \emph{in} the filtered list that you did - \emph{not} scan after walking the shelf is suspect — either - misplaced or missing. Mark it for follow-up. -\end{enumerate} +The unit of work is a flag on a volume: \emph{to check}. You raise the +flag, walk the shelf, and lower it as each copy is confirmed. + +\subsection*{Raising the flag} + +\begin{itemize} + \item \textbf{One volume} — on its detail page, \emph{Mark to check}. + Useful when a reader mentions that something looks wrong, or + when a scan surfaced a volume where it should not have been. + \item \textbf{A whole shelf} — on a storage location's page, + \emph{Mark all volumes for audit}. This is how a planned pass + starts: flag the shelf on Sunday evening, walk it during the + week. +\end{itemize} + +A flagged volume carries a \emph{To check} chip wherever it is listed, +so the flag is visible without opening the audit view. + +\subsection*{Walking the shelf} + +The home dashboard shows a \emph{Volumes to check} indicator with a +count and a link; \texttt{/audit} is the list itself, sorted by +location then V-code — the order you physically walk in. Each row +carries a \emph{Checked} button: press it as you confirm the copy in +your hand, and the row leaves the list. + +Two situations resolve themselves as you go: + +\begin{itemize} + \item A volume on the shelf that the list does not expect — scan it; + mybibli says it belongs elsewhere and you move it + (section~\ref{sec:move-volume}). + \item A volume the list expects and the shelf does not hold — leave + the flag raised. An unfinished audit list is a to-do list, not + a failure; it is the only record of what you could not find. +\end{itemize} + +\emph{Clear all flags} empties the list in one action. Use it when you +abandon a pass, not to finish one — a flag cleared without the volume +in your hand records something you did not verify. \begin{tip} A quarterly audit catches misplaced volumes early — the longer a @@ -290,3 +372,33 @@ \subsection{Auto-detect on catalog scan} it?} One click on the banner's \emph{Remove from wish list} button opens the same \emph{Mark as bought} modal, and confirming removes the entry. No silent deletions. + +\section{What the library is worth}\label{sec:valuation} + +Two optional amounts can be recorded against each volume: what it +\emph{cost} you (purchase price) and what it is \emph{worth} now +(current value), each with its own currency. Both are blank by default +and nothing forces you to fill them --- a library that is not an +investment can ignore this section entirely. + +If you do record them, \texttt{/stats/value}\index{valuation} sums them +across the catalogue: a total per currency, then a breakdown by genre +and by series. Volumes with no value set are skipped rather than +counted as zero, so a partially-valued catalogue reports the truth +about the part you have valued rather than a diluted average. + +\begin{itemize} + \item The \textbf{default currency} for new valuations is set in + \emph{Administration \textrightarrow{} System}; it is only a + pre-fill, and any volume can carry another. + \item The \textbf{home-dashboard card} showing the estimated total is + \emph{off} by default. Turn it on in the same place if you want + the figure in front of you every day --- some households do not. +\end{itemize} + +\begin{note} +Insurance is the usual reason to keep these figures. If that is yours, +record the purchase price as well as the current value: the first is a +fact you can evidence, the second an estimate you will be asked to +justify. +\end{note} diff --git a/docs/manual/en/05-metadata-providers.tex b/docs/manual/en/05-metadata-providers.tex index d971b548..9353cb5c 100644 --- a/docs/manual/en/05-metadata-providers.tex +++ b/docs/manual/en/05-metadata-providers.tex @@ -22,6 +22,9 @@ \section{The provider chain} Library of Congress\index{Library of Congress} & Books & United States national library catalogue; the anglophone counterpart to the BnF. Also supplies MARC~21 zones. & no \\ +K10plus\index{K10plus} & Books & German union +catalogue. Zone completion only — it never resolves a title on its +own. Consulted for ISBN prefixes 978-0/1/2/3. & no \\ Open Library\index{Open Library} & Books & Volunteer catalogue; broad English / multilingual coverage. & no \\ Google Books\index{Google Books} & Books & Commercial; good @@ -40,7 +43,7 @@ \section{The provider chain} matches are recorded but mybibli keeps walking the chain until it finds a clean one or runs out of providers. -\subsection*{Bibliographic zones: BnF and Library of Congress} +\subsection*{Bibliographic zones: the three catalogues} Six detailed cataloguing fields — statement of responsibility, edition statement, collection title and number, general note, @@ -53,11 +56,25 @@ \subsection*{Bibliographic zones: BnF and Library of Congress} zones, and a French title the BnF could not describe will not be rescued by the Library of Congress. +A third catalogue joined them in 1.16.0: \textbf{K10plus}\index{K10plus}, +the German union catalogue. It behaves differently from the other two — +it never resolves a title, it only completes zones on a record another +provider already resolved — and on this catalogue it turned out to be +the most productive of the three, filling zones on titles neither +national library carried. It is consulted only for ISBN prefixes 978-0, +978-1, 978-2 and 978-3: the anglophone leftovers it exists for, its own +German-language home ground, and the French prefix the BnF had already +failed on. Any other prefix is skipped without a request, which keeps +the one-request-per-second budget for the calls that can plausibly +answer. + When the provider that resolved a title fills only some of the six -zones, mybibli asks the other library to complete the ones left -empty. The rule is \emph{first source wins}: a value already present -is never overwritten. Only these six zones are merged — the title, -authors and cover always come from a single provider. +zones, mybibli asks the others to complete the ones left empty. The +rule is \emph{first source wins}: a value already present is never +overwritten. Only these six zones are merged — the title, authors and +cover always come from a single provider. + +None of this needs a key or any configuration. The \emph{Backfill metadata from libraries} button on the Health tab applies the same treatment to titles already in the catalogue. diff --git a/docs/manual/fr/02-configuration.tex b/docs/manual/fr/02-configuration.tex index 3880e167..1bc6745c 100644 --- a/docs/manual/fr/02-configuration.tex +++ b/docs/manual/fr/02-configuration.tex @@ -11,16 +11,20 @@ \chapter{Configuration} Tout se configure depuis le panneau \texttt{/admin}\index{panneau admin} — il n'y a pas de fichier de -configuration séparé. Le panneau a cinq onglets : +configuration séparé. Le panneau a six onglets : \begin{itemize} \item \textbf{Health} — tableau de bord en lecture seule. \item \textbf{Users} — créer/désactiver des utilisateurs, changer de rôle. - \item \textbf{Reference data} — les quatre tables taxonomiques. - \item \textbf{Trash} — voir et restaurer les éléments - soft-deleted. + \item \textbf{Reference data} — les tables taxonomiques, et le + vocabulaire des étiquettes de gestion + (section~\ref{sec:management-labels}). + \item \textbf{Trash} — voir, restaurer et supprimer définitivement + les éléments soft-deleted. \item \textbf{System} — paramètres globaux. + \item \textbf{API keys} — créer et révoquer les clés avec + lesquelles l'API HTTP s'authentifie (chapitre~\ref{ch:api}). \end{itemize} Seul le rôle \texttt{Admin} voit le panneau ; les utilisateurs @@ -80,7 +84,18 @@ \subsection*{Types de nœud de localisation} référence. \end{tip} -\section{Étiquettes : codes V et codes L}\label{sec:labels} +\section{Étiquettes de dos : codes V et codes L}\label{sec:labels} + +\begin{note} +\textbf{Deux choses s'appellent « étiquettes » dans mybibli.} Cette +section traite des \emph{étiquettes de dos} que l'on imprime et que +l'on colle sur un livre ou une étagère — les codes-barres qui rendent +le scan possible. L'autre sorte, apparue en 1.18.0, est +l'\emph{étiquette de gestion} que l'on pose sur une notice pour dire +quelque chose de son état (« À vérifier », « Reliure abîmée ») ; elles +sont décrites en section~\ref{sec:management-labels}. Rien ne s'imprime +pour elles, et elles ne sortent jamais de l'application. +\end{note} mybibli imprime deux types d'étiquettes à code-barres : diff --git a/docs/manual/fr/03-usage.tex b/docs/manual/fr/03-usage.tex index 59424107..9e284fda 100644 --- a/docs/manual/fr/03-usage.tex +++ b/docs/manual/fr/03-usage.tex @@ -129,6 +129,60 @@ \subsection{Recherches sauvegardées}\label{sec:saved-searches} que pour les bibliothécaires et les administrateurs. \end{note} +\section{Étiquettes de gestion}\label{sec:management-labels} + +Un genre dit ce qu'un livre \emph{est}. Un état dit comment un +exemplaire a \emph{vieilli}. Ni l'un ni l'autre ne dit « je ne suis pas +sûr que cette notice soit juste, il faudra y revenir » --- et c'est la +phrase dont un catalogue vivant a le plus besoin. Depuis la 1.18.0, les +\emph{étiquettes de gestion}\index{étiquette!de gestion} la portent. + +Ce ne sont pas les étiquettes de dos de la section~\ref{sec:labels} : +rien ne s'imprime, rien ne se scanne, et un visiteur anonyme ne les voit +jamais. Elles existent pour la personne qui tient la bibliothèque. + +\subsection*{Définir le vocabulaire} + +Un administrateur crée le vocabulaire une fois, dans +\emph{Administration \textrightarrow{} Données de référence +\textrightarrow{} Étiquettes} : « À vérifier », « À relire », « Reliure +abîmée », « Prêté sans fiche » --- ce que votre pratique réclame. On les +renomme et on les supprime au même endroit, et comme toute autre valeur +de référence elles ne sont jamais traduites : ce que vous saisissez est +ce que tout le monde voit, quelle que soit la langue de l'interface. + +Gardez la liste courte. Un vocabulaire de six étiquettes s'utilise ; +un vocabulaire de trente est une seconde taxonomie de genres que +personne n'entretient. + +\subsection*{Poser une étiquette} + +Un bibliothécaire pose les étiquettes depuis la section +\emph{Étiquettes} d'une page de titre ou d'une page de volume, en +choisissant dans ce vocabulaire commun. La distinction compte : + +\begin{itemize} + \item sur un \textbf{titre}, l'étiquette parle de la notice --- « les + métadonnées ont l'air fausses » ; + \item sur un \textbf{volume}, elle parle de cet exemplaire physique + --- « celui-ci a la reliure abîmée », ce qui ne dit rien des + deux autres exemplaires du rayon. +\end{itemize} + +\subsection*{Retrouver ce qui porte une étiquette} + +\texttt{/labels} liste le vocabulaire avec un compte en regard de chaque +entrée. En ouvrir une montre ce qui la porte, en deux sections --- les +titres au-dessus des volumes --- chaque ligne menant à sa page de +détail. C'est tout l'intérêt : une étiquette d'où l'on ne peut pas +revenir est un mot dans un tiroir. + +\begin{note} +Les étiquettes sont internes. Les gestionnaires n'exécutent même pas les +requêtes pour un visiteur anonyme : rien ne fuit par le catalogue +public --- pas même le fait qu'une étiquette existe. +\end{note} + \section{Déplacer un volume vers un autre lieu}\label{sec:move-volume} \begin{enumerate} @@ -181,26 +235,57 @@ \subsection*{Prêts en retard} page \texttt{/loans} et sur l'indicateur \emph{En retard} de la page d'accueil. \texttt{/loans?filter=overdue} donne une liste focalisée. -\section{Audit de la bibliothèque}\label{sec:audit} +\section{Audit des rayons}\label{sec:audit} Un \emph{audit}\index{audit} c'est le rituel « je parcours les étagères et je confirme que tout est bien à la place que la base -indique ». mybibli ne livre pas un mode audit dédié — le flux -équivalent est : +indique ». Depuis la 1.7.x, mybibli porte ce flux au lieu de vous +laisser l'improviser autour de la page de recherche. -\begin{enumerate} - \item Filtrez \texttt{/search} par le lieu que vous comptez - auditer, trié par code V. - \item Parcourez le rayon en scannant chaque code V. - \item Pour tout volume scanné qui \emph{n'est pas} dans la liste - filtrée, mybibli affiche un message \emph{Volume absent de - ce lieu}. Déplacez-le (section~\ref{sec:move-volume}) et - continuez. - \item Tout volume \emph{dans} la liste filtrée que vous - \emph{n'avez pas} scanné après avoir parcouru le rayon est - suspect — soit déplacé, soit manquant. Marquez-le pour - suivi. -\end{enumerate} +L'unité de travail est un drapeau posé sur un volume : +\emph{à contrôler}. On lève le drapeau, on parcourt le rayon, et on +l'abaisse à mesure que chaque exemplaire est confirmé. + +\subsection*{Lever le drapeau} + +\begin{itemize} + \item \textbf{Un volume} — depuis sa page de détail, + \emph{Marquer à contrôler}. Utile quand un lecteur signale que + quelque chose cloche, ou qu'un scan a fait apparaître un volume + là où il n'aurait pas dû être. + \item \textbf{Un rayon entier} — depuis la page d'un lieu de + rangement, \emph{Marquer tous les volumes à contrôler}. C'est + ainsi qu'une passe planifiée commence : on marque le rayon le + dimanche soir, on le parcourt dans la semaine. +\end{itemize} + +Un volume marqué porte une pastille \emph{À contrôler} partout où il +est listé : le drapeau se voit sans ouvrir la vue d'audit. + +\subsection*{Parcourir le rayon} + +Le tableau de bord d'accueil affiche un indicateur \emph{Volumes à +contrôler} avec son compte et un lien ; \texttt{/audit} est la liste +elle-même, triée par lieu puis par code V — l'ordre dans lequel on +marche. Chaque ligne porte un bouton \emph{Contrôlé} : on l'actionne +quand on tient l'exemplaire, et la ligne quitte la liste. + +Deux situations se dénouent en chemin : + +\begin{itemize} + \item Un volume sur le rayon que la liste n'attend pas — scannez-le ; + mybibli signale qu'il appartient ailleurs et vous le déplacez + (section~\ref{sec:move-volume}). + \item Un volume que la liste attend et que le rayon ne porte pas — + laissez le drapeau levé. Une liste d'audit inachevée est une + liste de choses à faire, pas un échec : c'est le seul relevé de + ce que vous n'avez pas retrouvé. +\end{itemize} + +\emph{Effacer tous les drapeaux} vide la liste d'un coup. À utiliser +quand on abandonne une passe, pas pour en terminer une — un drapeau +effacé sans avoir le volume en main enregistre une vérification qui +n'a pas eu lieu. \begin{tip} Un audit trimestriel attrape les volumes mal rangés tôt — plus un @@ -309,3 +394,36 @@ \subsection{Auto-détection au scan catalogue} \emph{Retirer de la wish list} ouvre la même modale \emph{Marquer comme acheté}, et la confirmation supprime l'entrée. Aucune suppression silencieuse. + +\section{Ce que vaut la bibliothèque}\label{sec:valuation} + +Deux montants facultatifs peuvent être enregistrés sur chaque volume : +ce qu'il vous a \emph{coûté} (prix d'achat) et ce qu'il \emph{vaut} +aujourd'hui (valeur actuelle), chacun avec sa devise. Les deux sont +vides par défaut et rien ne force à les remplir --- une bibliothèque qui +n'est pas un placement peut ignorer cette section. + +Si vous les renseignez, \texttt{/stats/value}\index{valorisation} en +fait la somme sur tout le catalogue : un total par devise, puis une +ventilation par genre et par série. Les volumes sans valeur sont +ignorés plutôt que comptés à zéro : un catalogue partiellement valorisé +dit donc la vérité sur la partie que vous avez valorisée, au lieu de +diluer la moyenne. + +\begin{itemize} + \item La \textbf{devise par défaut} des nouvelles valorisations se + règle dans \emph{Administration \textrightarrow{} System} ; ce + n'est qu'un pré-remplissage, chaque volume peut en porter une + autre. + \item La \textbf{carte du tableau de bord} qui affiche le total estimé + est \emph{désactivée} par défaut. Activez-la au même endroit si + vous voulez ce chiffre sous les yeux chaque jour --- ce n'est pas + le souhait de tous les foyers. +\end{itemize} + +\begin{note} +L'assurance est la raison habituelle de tenir ces chiffres. Si c'est la +vôtre, notez le prix d'achat autant que la valeur actuelle : le premier +est un fait que vous pouvez prouver, la seconde une estimation qu'on +vous demandera de justifier. +\end{note} diff --git a/docs/manual/fr/05-metadata-providers.tex b/docs/manual/fr/05-metadata-providers.tex index 91ed3c4a..89acb343 100644 --- a/docs/manual/fr/05-metadata-providers.tex +++ b/docs/manual/fr/05-metadata-providers.tex @@ -23,6 +23,9 @@ \section{La chaîne de fournisseurs} Library of Congress\index{Library of Congress} & Livres & Catalogue de la bibliothèque nationale des États-Unis ; pendant anglophone de la BnF. Fournit aussi les zones MARC~21. & non \\ +K10plus\index{K10plus} & Livres & Catalogue +collectif allemand. Complète les zones uniquement — il ne résout +jamais un titre seul. Consulté pour les préfixes ISBN 978-0/1/2/3. & non \\ Open Library\index{Open Library} & Livres & Catalogue bénévole ; large couverture anglaise / multilingue. & non \\ Google Books\index{Google Books} & Livres & Commercial ; bon @@ -42,7 +45,7 @@ \section{La chaîne de fournisseurs} enregistrées mais mybibli continue à parcourir la chaîne jusqu'à trouver une correspondance propre ou épuiser les fournisseurs. -\subsection*{Zones bibliographiques : BnF et Library of Congress} +\subsection*{Zones bibliographiques : les trois catalogues} Six champs de catalogage détaillés — mention de responsabilité, mention d'édition, titre et numéro de collection, note générale, @@ -55,12 +58,27 @@ \subsection*{Zones bibliographiques : BnF et Library of Congress} titre français que la BnF ne décrit pas ne sera pas rattrapé par la Library of Congress. -Quand le fournisseur qui a résolu le titre ne remplit qu'une partie -de ces six zones, mybibli interroge l'autre bibliothèque pour -compléter les zones restées vides. La règle est \emph{la première -source l'emporte} : une valeur déjà présente n'est jamais écrasée. -Seules ces six zones sont fusionnées — le titre, les auteurs et la -couverture proviennent toujours d'un seul et même fournisseur. +Un troisième catalogue les a rejointes en 1.16.0 : +\textbf{K10plus}\index{K10plus}, le catalogue collectif allemand. Il se +comporte autrement que les deux autres — il ne résout jamais un titre, +il ne fait que compléter les zones d'une notice qu'un autre fournisseur +a déjà résolue — et il s'est révélé le plus productif des trois sur ce +catalogue, remplissant des zones sur des titres qu'aucune des deux +bibliothèques nationales ne portait. Il n'est consulté que pour les +préfixes ISBN 978-0, 978-1, 978-2 et 978-3 : les restes anglophones pour +lesquels il existe, son propre domaine germanophone, et le préfixe +français sur lequel la BnF avait déjà échoué. Tout autre préfixe est +ignoré sans requête, ce qui réserve le budget d'une requête par seconde +aux appels qui ont une chance d'aboutir. + +Quand le fournisseur qui a résolu le titre ne remplit qu'une partie de +ces six zones, mybibli interroge les autres pour compléter les zones +restées vides. La règle est \emph{la première source l'emporte} : une +valeur déjà présente n'est jamais écrasée. Seules ces six zones sont +fusionnées — le titre, les auteurs et la couverture proviennent toujours +d'un seul et même fournisseur. + +Rien de tout cela ne demande de clé ni de configuration. Le bouton \emph{Compléter les métadonnées via les bibliothèques} de l'onglet Santé applique le même traitement aux titres déjà diff --git a/docs/manual/mybibli-manual-en.pdf b/docs/manual/mybibli-manual-en.pdf index 6c45b624..5d9c7515 100644 Binary files a/docs/manual/mybibli-manual-en.pdf and b/docs/manual/mybibli-manual-en.pdf differ diff --git a/docs/manual/mybibli-manual-fr.pdf b/docs/manual/mybibli-manual-fr.pdf index 0d423695..26c879bc 100644 Binary files a/docs/manual/mybibli-manual-fr.pdf and b/docs/manual/mybibli-manual-fr.pdf differ diff --git a/docs/architecture.md b/docs/permanent-delete-and-purge.md similarity index 88% rename from docs/architecture.md rename to docs/permanent-delete-and-purge.md index 59467390..c7aa714e 100644 --- a/docs/architecture.md +++ b/docs/permanent-delete-and-purge.md @@ -1,14 +1,17 @@ -# mybibli Architecture Reference +# Permanent delete, Trash and auto-purge -This document records cross-cutting design decisions that apply to the whole -codebase. Story-specific decisions live in -`_bmad-output/implementation-artifacts/`; this file is for patterns whose -shape doesn't change between stories. +**Scope:** this file documents one subsystem — how a soft-deleted row +travels to the Trash, what hard-deletes it, in which order, and what the +audit trail keeps. It is not the architecture reference it used to be +titled as; for that, read the "Architecture" section of +[`CLAUDE.md`](../CLAUDE.md) (patterns as shipped) and +[`_bmad-output/planning-artifacts/architecture.md`](../_bmad-output/planning-artifacts/architecture.md) +(the design decisions and ARs behind them). -For developer-facing conventions (build commands, foundation rules, lint -gates), see `CLAUDE.md`. +Story-specific decisions live in `_bmad-output/implementation-artifacts/`; +what follows is the shape that doesn't change between stories. -## Permanent Delete & Auto-Purge +## The delete-and-purge pipeline Soft-deleted items are hard-purged after a fixed **30-day retention window**. The window is enforced by two complementary mechanisms: diff --git a/docs/route-role-matrix.md b/docs/route-role-matrix.md index 3a1b08c3..ba1096b7 100644 --- a/docs/route-role-matrix.md +++ b/docs/route-role-matrix.md @@ -1,9 +1,27 @@ -# Route Role Matrix +# Route role matrix -**Status:** authoritative reference for Story 7-1 (anonymous browsing + role gating). -**Last updated:** 2026-04-28 (story 8-5 — 3 system-settings save routes added under `/admin/system/*`). +**What this is:** every route the router registers, with the role it +requires and whether it is subject to CSRF. Generated from +`src/routes/*.rs` and verified by hand. -## CSRF exemption (story 8-2) +**Last updated:** 2026-09-11 — **complete**: 172 method+path pairs over +148 paths. The previous revision stopped at story 8-5 (2026-04-28) and +documented 82 of them; everything added since — the setup wizard, the +HTTP API, the wishlist, saved searches, labels, API keys, the shelf +audit, every confirmation modal — was missing. + +**How the roles below were established:** each row's role is the +`Session::require_role` / `require_role_with_return` call at the top of +its handler, or the `ApiKeyAuth` / `ApiKeyWrite` extractor in its +signature. A row reading **Anonymous** means the handler asks for +nothing — which is the intent for the public catalogue, and an accident +in two places (see *Anomalies* at the end). + +**Keeping it honest:** a new route means a new row, in the PR that adds +it. The count above is the check — if `grep -c '\.route(' src/routes/*.rs` +disagrees with it, this file has drifted again. + +## CSRF exemption Every state-changing method (POST / PUT / PATCH / DELETE) on every route in this matrix requires a matching `X-CSRF-Token` header or `_csrf_token` form @@ -25,195 +43,345 @@ exempt route requires a visible edit to `CSRF_EXEMPT_ROUTES` in Ordering: `Anonymous < Librarian < Admin`. A route requiring `Librarian` is also accessible to `Admin` (enforced by `Session::require_role`, `src/middleware/auth.rs`). -## Policy decisions (Task 1) +## Standing policy decisions 1. **Location mutations → Librarian**, except `DELETE /locations/{id}` which remains **Admin**. Rationale: daily cataloging (scan → shelve → occasionally create a new location) must not require Guy's intervention. Destructive removal of a location impacts taxonomy structure and loan/volume references, so deletion stays Admin. 2. **Borrower mutations → Librarian**, except `DELETE /borrower/{id}` which remains **Admin**. Rationale: consistent with `POST /borrowers` (already Librarian). Destructive removal of personal data (soft-delete but cascades loan deactivation) stays Admin. 3. **Anonymous reach** covers catalog browsing and detail pages only. All loan/borrower surfaces — including list pages — stay Librarian (Epic 7 scope: "anonymous visibility excludes loan-related data"). -4. **`GET /catalog`** becomes Anonymous (AC #1). It's the primary entry for visitors. -5. **`GET /locations`** (tree browser) becomes Anonymous (AC #1 "location browse page"). +4. **`GET /catalog`** is Anonymous — the primary entry for visitors. +5. **`GET /locations`** (tree browser) is Anonymous, like the rest of the browse surface. ## Matrix -Columns: `method | path | current_role | target_role | csrf_exempt | note`. Rows where `current ≠ target` are the Task 2 worklist. `csrf_exempt` is `yes` only for `POST /login` (per top section) and `—` for non-mutating methods (GET / HEAD / OPTIONS bypass CSRF at the middleware level); all other mutating rows are implicitly `no`. - -### Root / infrastructure - -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/` | Anonymous | Anonymous | — | Homepage with search. Librarian-only metadata-error badge is template-gated. | -| GET | `/health` | Anonymous | Anonymous | — | Liveness probe. | -| POST | `/session/keepalive` | Anonymous | Anonymous | no | Ping; updates `last_activity`. Anonymous sessions noop. | - -### Auth (`src/routes/auth.rs`) - -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/login` | Anonymous | Anonymous | — | Accepts `?next=` (Task 3). Redirects Librarian+ to `/catalog`. | -| POST | `/login` | Anonymous | Anonymous | **yes** | No authenticated session at request time; `SameSite=Lax` mitigates login-CSRF. Frozen in `CSRF_EXEMPT_ROUTES`. | -| GET | `/logout` | — | **405** | — | **CHANGE (story 8-2)** — GET `/logout` removed; the router only exposes POST so a cross-origin `` or mistyped anchor cannot end a session. | -| POST | `/logout` | Anonymous | Anonymous | no | Requires CSRF token. The nav-bar logout is a POST form (story 8-2). | -| POST | `/language` | Anonymous | Anonymous | no | Language toggle (story 7-3). Requires CSRF token (story 8-2 added hidden `_csrf_token` to the nav-bar form). Anonymous visitors carry a CSRF token via the lazy-anonymous session row. | - -### Catalog (`src/routes/catalog.rs`) - -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/catalog` | Librarian | **Anonymous** | — | **CHANGE** — AC #1. Scan-field + edit affordances template-gated. | -| POST | `/catalog/scan` | Librarian | Librarian | no | — | -| POST | `/catalog/scan-with-type` | Librarian | Librarian | no | — | -| GET | `/catalog/title/new` | Librarian | Librarian | — | — | -| POST | `/catalog/title` | Librarian | Librarian | no | — | -| GET | `/catalog/title/fields/{media_type}` | Librarian | Librarian | — | HTMX fragment. | -| GET | `/catalog/contributors/form` | Librarian | Librarian | — | — | -| GET | `/catalog/contributors/search` | Librarian | Librarian | — | — | -| POST | `/catalog/contributors/add` | Librarian | Librarian | no | — | -| POST | `/catalog/contributors/remove` | Librarian | Librarian | no | — | -| POST | `/catalog/contributors/update` | Librarian | Librarian | no | — | -| DELETE | `/catalog/contributors/{id}` | Librarian | Librarian | no | — | -| DELETE | `/catalog/title/{id}` | Librarian | Librarian | no | — | -| DELETE | `/catalog/volume/{id}` | Librarian | Librarian | no | — | +Columns: `method | path | role | csrf | note`. `csrf` is `—` for GET (the middleware only guards state-changing methods), `no` for every mutating route — meaning a token is required — and **yes** only for the single exempt route, `POST /login`. + +### Root and infrastructure + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/health` | Anonymous | — | Liveness probe. Whitelisted by the setup gate so an orchestrator can reach it before the wizard runs. | + +### Admin shell, Health tab and Trash (`src/routes/admin.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/admin` | Admin | — | Six-tab shell; Librarian → 403, Anonymous → 303 /login. | +| GET | `/admin/health` | Admin | — | Health panel fragment (HTMX + direct). | +| POST | `/admin/health/bulk-cover-refetch` | Admin | no | Long-running background action; paced and back-off'd (#419). | +| POST | `/admin/health/bulk-metadata-backfill` | Admin | no | Long-running background action over already-cataloged titles (#389). | +| GET | `/admin/trash` | Admin | — | List soft-deleted items; filter by type, search by name; paginated (25/page). | +| GET | `/admin/trash/{table}/{id}/permanent-delete` | Admin | — | Show confirmation modal with friction (type name to enable button). | +| POST | `/admin/trash/{table}/{id}/permanent-delete` | Admin | no | Hard-delete soft-deleted item; create audit entry; return feedback + OOB swap. | +| POST | `/admin/trash/{table}/{id}/restore` | Admin | no | Clear `deleted_at`, bump `version`. POST, not GET, so it rides the CSRF layer (#478). Returns the conflict modal first when restoring would clash with a newer relationship. | + +### Admin → API keys (CR #241) (`src/routes/admin_api_keys.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/admin/api-keys` | Admin | — | | +| POST | `/admin/api-keys` | Admin | no | | +| DELETE | `/admin/api-keys/{id}` | Admin | no | | +| GET | `/admin/api-keys/{id}/delete-modal` | Admin | — | | +| POST | `/admin/api-keys/{id}/revoke` | Admin | no | | +| GET | `/admin/api-keys/{id}/revoke-modal` | Admin | — | | + +### Admin → Reference data → Labels (#443) (`src/routes/admin_labels.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/admin/reference-data/labels` | Admin | — | | +| POST | `/admin/reference-data/labels` | Admin | no | | +| POST | `/admin/reference-data/labels/{id}/delete` | Admin | no | | +| GET | `/admin/reference-data/labels/{id}/delete-modal` | Admin | — | | +| POST | `/admin/reference-data/labels/{id}/rename` | Admin | no | | + +### Admin → Reference data (`src/routes/admin_reference_data.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/admin/reference-data` | Admin | — | Reference Data panel (4 sub-sections). | +| GET | `/admin/reference-data/contributor-roles` | Admin | — | Contributor roles section list fragment. | +| POST | `/admin/reference-data/contributor-roles` | Admin | no | Create role (reactivates on collision). | +| POST | `/admin/reference-data/contributor-roles/{id}/delete` | Admin | no | Soft-delete role with usage guard. | +| GET | `/admin/reference-data/contributor-roles/{id}/delete-modal` | Admin | — | Delete-confirm modal fragment. | +| POST | `/admin/reference-data/contributor-roles/{id}/rename` | Admin | no | Rename role with optimistic locking. | +| GET | `/admin/reference-data/genres` | Admin | — | Genres section list fragment. | +| POST | `/admin/reference-data/genres` | Admin | no | Create genre (reactivates soft-deleted on name match). | +| POST | `/admin/reference-data/genres/{id}/delete` | Admin | no | Soft-delete; refused with 409 if `count_usage > 0`. | +| GET | `/admin/reference-data/genres/{id}/delete-modal` | Admin | — | Delete-confirm modal fragment. | +| POST | `/admin/reference-data/genres/{id}/rename` | Admin | no | Rename with optimistic locking. | +| GET | `/admin/reference-data/node-types` | Admin | — | Location node types section list fragment. | +| POST | `/admin/reference-data/node-types` | Admin | no | Create node type. | +| POST | `/admin/reference-data/node-types/{id}/delete` | Admin | no | Soft-delete node type; usage guard matches by name. | +| GET | `/admin/reference-data/node-types/{id}/delete-modal` | Admin | — | Delete-confirm modal fragment. | +| POST | `/admin/reference-data/node-types/{id}/rename` | Admin | no | Transactional rename — cascades to `storage_locations.node_type` (loose VARCHAR FK). | +| GET | `/admin/reference-data/volume-states` | Admin | — | Volume states section list fragment. | +| POST | `/admin/reference-data/volume-states` | Admin | no | Create volume state with `is_loanable` flag. | +| POST | `/admin/reference-data/volume-states/{id}/delete` | Admin | no | Soft-delete with usage guard. | +| GET | `/admin/reference-data/volume-states/{id}/delete-modal` | Admin | — | Delete-confirm modal fragment. | +| POST | `/admin/reference-data/volume-states/{id}/loanable` | Admin | no | Toggle `is_loanable`; surfaces warning modal if active loans exist. | +| POST | `/admin/reference-data/volume-states/{id}/loanable/confirm` | Admin | no | Apply loanable toggle force=true (forward-only). | +| POST | `/admin/reference-data/volume-states/{id}/rename` | Admin | no | Rename with optimistic locking. | +| GET | `/admin/reference-data/volume-states/{id}/row` | Admin | — | Re-render row partial — used by Cancel-on-warning to revert checkbox visual state. | + +### Admin → System settings (`src/routes/admin_system.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/admin/system` | Admin | — | System settings panel (3 forms: Loans, Providers, Language). | +| POST | `/admin/system/language` | Admin | no | Save default language (FR / EN); reloads cache; takes effect on next anonymous fresh-visitor request. | +| POST | `/admin/system/loans` | Admin | no | Save overdue threshold; reloads `AppSettings` cache. | +| POST | `/admin/system/log-level` | Admin | no | | +| POST | `/admin/system/provider-timeouts` | Admin | no | | +| POST | `/admin/system/providers` | Admin | no | Save 3 provider API keys (Google Books / OMDb / TMDb) in one transaction; NoChange/Clear/Set state machine; reloads cache. | +| POST | `/admin/system/sessions` | Admin | no | | +| POST | `/admin/system/timeouts` | Admin | no | | +| POST | `/admin/system/valuation` | Admin | no | | + +### Admin → Users (`src/routes/admin_users.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/admin/users` | Admin | — | List + form container (story 8-3 implements). | +| POST | `/admin/users` | Admin | no | Create user; validates; hashes password Argon2. | +| GET | `/admin/users/new` | Admin | — | Create user form fragment (HTMX). | +| POST | `/admin/users/{id}` | Admin | no | Update user (role, username, optional password). | +| POST | `/admin/users/{id}/deactivate` | Admin | no | Soft-delete user + invalidate sessions (atomic tx). | +| GET | `/admin/users/{id}/deactivate-modal` | Admin | — | | +| GET | `/admin/users/{id}/edit` | Admin | — | Edit user form fragment (HTMX, pre-filled). | +| POST | `/admin/users/{id}/reactivate` | Admin | no | Clear `deleted_at`; user can log in again. | + +### HTTP API v1 — bearer-token auth, no session (`src/routes/api_v1.rs`) + +These routes authenticate with `Authorization: Bearer ` (or +`X-API-Key`), never with the session cookie, so `src/middleware/csrf.rs` +short-circuits the whole `/api/` prefix before its token check — the +`csrf` column below is therefore "not applicable" rather than "exempt". +Keys are minted and revoked in **Admin → API keys**; the scope +(read / write) is carried by the key, which is why the role column names +the key rather than a user. + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/api/v1/dewey/{prefix}` | API key *(read)* | n/a | Dewey suggestions for a prefix. | +| GET | `/api/v1/genres` | API key *(read)* | n/a | Genre vocabulary. | +| GET | `/api/v1/locations` | API key *(read)* | n/a | Storage-location tree. | +| GET | `/api/v1/series` | API key *(read)* | n/a | Series list. | +| GET | `/api/v1/titles` | API key *(read)* | n/a | Title list; paginated. | +| GET | `/api/v1/titles/{id}` | API key *(read)* | n/a | One title. | +| PATCH | `/api/v1/titles/{id}` | API key *(write)* | n/a | A read-only key gets 403 here. | + +### Shelf audit (CR #237) (`src/routes/audit.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/audit` | Librarian | — | | +| POST | `/audit/clear-all` | Librarian | no | | +| POST | `/location/{id}/mark-audit` | Librarian | no | | +| POST | `/volume/{id}/clear-audit` | Librarian | no | | +| POST | `/volume/{id}/mark-audit` | Librarian | no | | + +### Auth and language (`src/routes/auth.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| POST | `/language` | Anonymous | no | Language toggle (story 7-3). Requires CSRF token (story 8-2 added hidden `_csrf_token` to the nav-bar form). Anonymous visitors carry a CSRF token via the lazy-anonymous session row. | +| GET | `/login` | Anonymous | — | Accepts `?next=` (Task 3). Redirects Librarian+ to `/catalog`. | +| POST | `/login` | Anonymous | **yes** | No authenticated session at request time; `SameSite=Lax` mitigates login-CSRF. Frozen in `CSRF_EXEMPT_ROUTES`. | +| POST | `/logout` | Anonymous | no | Requires CSRF token. The nav-bar logout is a POST form (story 8-2). | -### Titles (`src/routes/titles.rs`) +### Borrowers (`src/routes/borrowers.rs`) -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/title/{id}` | Anonymous | Anonymous | — | — | -| GET | `/title/{id}/metadata` | Anonymous | Anonymous | — | Embeds Librarian-only edit/redownload buttons; template-gated. | -| GET | `/title/{id}/edit` | Librarian | Librarian | — | — | -| POST | `/title/{id}` | Librarian | Librarian | no | — | -| POST | `/title/{id}/redownload` | Librarian | Librarian | no | — | -| POST | `/title/{id}/confirm-metadata` | Librarian | Librarian | no | — | -| POST | `/title/{id}/series` | Librarian | Librarian | no | — | -| POST | `/title/{id}/series/{assignment_id}/remove` | Librarian | Librarian | no | — | -| POST | `/title/{id}/series-remove` | Librarian | Librarian | no | — | - -### Volumes (handled in `catalog.rs`) - -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/volume/{id}` | Anonymous | Anonymous | — | — | -| GET | `/volume/{id}/edit` | Librarian | Librarian | — | — | -| POST | `/volume/{id}/update` | Librarian | Librarian | no | — | +| method | path | role | csrf | note | +|---|---|---|---|---| +| DELETE | `/borrower/{id}` | Admin | no | **Smoke-test 403 target (AC #9).** Destructive; stays Admin (decision 2a exception). | +| GET | `/borrower/{id}` | Librarian | — | — | +| POST | `/borrower/{id}` | Librarian | no | decision 2a. | +| GET | `/borrower/{id}/delete-modal` | Admin | — | | +| GET | `/borrower/{id}/edit` | Librarian | — | decision 2a. | +| GET | `/borrowers` | Librarian | — | — | +| POST | `/borrowers` | Librarian | no | — | +| GET | `/borrowers/search` | Librarian | — | — | +| GET | `/contributor/{id}/delete-modal` | Admin | — | | +| GET | `/saved-searches/{id}/delete-modal` | Admin | — | | +| GET | `/series/{id}/delete-modal` | Admin | — | | + +### Catalog and scanning (`src/routes/catalog.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/catalog` | Anonymous | — | AC #1. Scan-field + edit affordances template-gated. | +| POST | `/catalog/contributors/add` | Librarian | no | — | +| GET | `/catalog/contributors/form` | Librarian | — | — | +| POST | `/catalog/contributors/remove` | Librarian | no | — | +| GET | `/catalog/contributors/search` | Librarian | — | — | +| POST | `/catalog/contributors/update` | Librarian | no | — | +| DELETE | `/catalog/contributors/{id}` | Librarian | no | — | +| POST | `/catalog/scan` | Librarian | no | — | +| POST | `/catalog/scan-with-type` | Librarian | no | — | +| POST | `/catalog/title` | Librarian | no | — | +| GET | `/catalog/title/fields/{media_type}` | Librarian | — | HTMX fragment. | +| GET | `/catalog/title/new` | Librarian | — | — | +| DELETE | `/catalog/title/{id}` | Librarian | no | — | +| DELETE | `/catalog/volume/{id}` | Librarian | no | — | +| POST | `/debug/session-timeout` | Admin | no | | +| POST | `/session/keepalive` | Admin | no | Ping; updates `last_activity`. Anonymous sessions noop. | +| DELETE | `/title/{id}` | Librarian | no | | +| GET | `/title/{id}/contributor-form` | Librarian | — | | +| DELETE | `/volume/{id}` | Librarian | no | | +| GET | `/volume/{id}` | Anonymous | — | — | +| GET | `/volume/{id}/delete-modal` | Librarian | — | | +| GET | `/volume/{id}/edit` | Librarian | — | — | +| POST | `/volume/{id}/update` | Librarian | no | — | + +### Scan undo (#9) (`src/routes/catalog_undo.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| POST | `/catalog/undo` | Librarian | no | | ### Contributors (`src/routes/contributors.rs`) -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/contributor/{id}` | Anonymous | Anonymous | — | — | +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/contributor/{id}` | Anonymous | — | — | -### Series (`src/routes/series.rs`) +### Applying labels to titles and volumes (#443) (`src/routes/entity_labels.rs`) -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/series` | Anonymous | Anonymous | — | — | -| GET | `/series/{id}` | Anonymous | Anonymous | — | — | -| GET | `/series/new` | Librarian | Librarian | — | — | -| POST | `/series` | Librarian | Librarian | no | — | -| GET | `/series/{id}/edit` | Librarian | Librarian | — | — | -| POST | `/series/{id}` | Librarian | Librarian | no | — | -| DELETE | `/series/{id}` | Librarian | Librarian | no | — | - -### Locations (`src/routes/locations.rs`) - -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/location/{id}` | Anonymous | Anonymous | — | — | -| GET | `/locations` | Librarian | **Anonymous** | — | **CHANGE** — AC #1 "location browse page". | -| GET | `/locations/next-lcode` | Admin | **Librarian** | — | **CHANGE** — decision 1a. Used by create form. | -| GET | `/locations/{id}/edit` | Admin | **Librarian** | — | **CHANGE** — decision 1a. | -| POST | `/locations` | Admin | **Librarian** | no | **CHANGE** — decision 1a. | -| POST | `/locations/{id}` | Admin | **Librarian** | no | **CHANGE** — decision 1a. | -| DELETE | `/locations/{id}` | Admin | Admin | no | Destructive; stays Admin (decision 1a exception). Used as smoke-test 403 candidate. | +| method | path | role | csrf | note | +|---|---|---|---|---| +| POST | `/title/{id}/labels/attach` | Librarian | no | | +| POST | `/title/{id}/labels/{label_id}/detach` | Librarian | no | | +| POST | `/volume/{id}/labels/attach` | Librarian | no | | +| POST | `/volume/{id}/labels/{label_id}/detach` | Librarian | no | | -### Borrowers (`src/routes/borrowers.rs`) +### Home and search (`src/routes/home.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/` | Anonymous | — | Homepage with search. Librarian-only metadata-error badge is template-gated. | + +### Home scan field (`src/routes/home_scan.rs`) -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/borrowers` | Librarian | Librarian | — | — | -| POST | `/borrowers` | Librarian | Librarian | no | — | -| GET | `/borrowers/search` | Librarian | Librarian | — | — | -| GET | `/borrower/{id}` | Librarian | Librarian | — | — | -| GET | `/borrower/{id}/edit` | Admin | **Librarian** | — | **CHANGE** — decision 2a. | -| POST | `/borrower/{id}` | Admin | **Librarian** | no | **CHANGE** — decision 2a. | -| DELETE | `/borrower/{id}` | Admin | Admin | no | **Smoke-test 403 target (AC #9).** Destructive; stays Admin (decision 2a exception). | +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/scan` | Anonymous | — | | + +### The /labels page (#443) (`src/routes/labels_page.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/labels` | Librarian | — | | +| GET | `/labels/{id}` | Librarian | — | | ### Loans (`src/routes/loans.rs`) -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/loans` | Librarian | Librarian | — | — | -| POST | `/loans` | Librarian | Librarian | no | — | -| GET | `/loans/scan` | Librarian | Librarian | — | — | -| POST | `/loans/{id}/return` | Librarian | Librarian | no | — | - -### Admin (`src/routes/admin.rs`) - -Story 8-1 introduced the `/admin` surface. Every handler's first line is `session.require_role_with_return(Role::Admin, …)?` — Librarian → 403 FeedbackEntry body; Anonymous → 303 → `/login?next=%2Fadmin`. All read-only in 8-1; mutations land in 8-2+. - -| method | path | current | target | csrf_exempt | note | -|--------|--------------------------|---------|--------|-------------|-------------------------------------------------------------| -| GET | `/admin` | — | Admin | — | New (8-1). 5-tab shell; Librarian → 403, Anonymous → 303 /login. | -| GET | `/admin/health` | — | Admin | — | New (8-1). Health panel fragment (HTMX + direct). | -| GET | `/admin/users` | — | Admin | — | New (8-1). List + form container (story 8-3 implements). | -| GET | `/admin/users/new` | — | Admin | — | New (8-3). Create user form fragment (HTMX). | -| POST | `/admin/users` | — | Admin | no | New (8-3). Create user; validates; hashes password Argon2. | -| GET | `/admin/users/{id}/edit` | — | Admin | — | New (8-3). Edit user form fragment (HTMX, pre-filled). | -| POST | `/admin/users/{id}` | — | Admin | no | New (8-3). Update user (role, username, optional password). | -| POST | `/admin/users/{id}/deactivate` | — | Admin | no | New (8-3). Soft-delete user + invalidate sessions (atomic tx). | -| POST | `/admin/users/{id}/reactivate` | — | Admin | no | New (8-3). Clear `deleted_at`; user can log in again. | -| GET | `/admin/reference-data` | — | Admin | — | New (8-1, filled by 8-4). Reference Data panel (4 sub-sections). | -| GET | `/admin/reference-data/genres` | — | Admin | — | New (8-4). Genres section list fragment. | -| POST | `/admin/reference-data/genres` | — | Admin | no | New (8-4). Create genre (reactivates soft-deleted on name match). | -| POST | `/admin/reference-data/genres/{id}/rename` | — | Admin | no | New (8-4). Rename with optimistic locking. | -| GET | `/admin/reference-data/genres/{id}/delete-modal` | — | Admin | — | New (8-4). Delete-confirm modal fragment. | -| POST | `/admin/reference-data/genres/{id}/delete` | — | Admin | no | New (8-4). Soft-delete; refused with 409 if `count_usage > 0`. | -| GET | `/admin/reference-data/volume-states` | — | Admin | — | New (8-4). Volume states section list fragment. | -| POST | `/admin/reference-data/volume-states` | — | Admin | no | New (8-4). Create volume state with `is_loanable` flag. | -| POST | `/admin/reference-data/volume-states/{id}/rename` | — | Admin | no | New (8-4). Rename with optimistic locking. | -| GET | `/admin/reference-data/volume-states/{id}/delete-modal` | — | Admin | — | New (8-4). Delete-confirm modal fragment. | -| POST | `/admin/reference-data/volume-states/{id}/delete` | — | Admin | no | New (8-4). Soft-delete with usage guard. | -| POST | `/admin/reference-data/volume-states/{id}/loanable` | — | Admin | no | New (8-4). Toggle `is_loanable`; surfaces warning modal if active loans exist. | -| POST | `/admin/reference-data/volume-states/{id}/loanable/confirm` | — | Admin | no | New (8-4). Apply loanable toggle force=true (forward-only). | -| GET | `/admin/reference-data/volume-states/{id}/row` | — | Admin | — | New (8-4). Re-render row partial — used by Cancel-on-warning to revert checkbox visual state. | -| GET | `/admin/reference-data/contributor-roles` | — | Admin | — | New (8-4). Contributor roles section list fragment. | -| POST | `/admin/reference-data/contributor-roles` | — | Admin | no | New (8-4). Create role (reactivates on collision). | -| POST | `/admin/reference-data/contributor-roles/{id}/rename` | — | Admin | no | New (8-4). Rename role with optimistic locking. | -| GET | `/admin/reference-data/contributor-roles/{id}/delete-modal` | — | Admin | — | New (8-4). Delete-confirm modal fragment. | -| POST | `/admin/reference-data/contributor-roles/{id}/delete` | — | Admin | no | New (8-4). Soft-delete role with usage guard. | -| GET | `/admin/reference-data/node-types` | — | Admin | — | New (8-4). Location node types section list fragment. | -| POST | `/admin/reference-data/node-types` | — | Admin | no | New (8-4). Create node type. | -| POST | `/admin/reference-data/node-types/{id}/rename` | — | Admin | no | New (8-4). Transactional rename — cascades to `storage_locations.node_type` (loose VARCHAR FK). | -| GET | `/admin/reference-data/node-types/{id}/delete-modal` | — | Admin | — | New (8-4). Delete-confirm modal fragment. | -| POST | `/admin/reference-data/node-types/{id}/delete` | — | Admin | no | New (8-4). Soft-delete node type; usage guard matches by name. | -| GET | `/admin/trash` | — | Admin | — | New (8-1, filled by 8-6 & 8-7). List soft-deleted items; filter by type, search by name; paginated (25/page). | -| GET | `/admin/trash/{table}/{id}/permanent-delete` | — | Admin | — | New (8-7). Show confirmation modal with friction (type name to enable button). | -| POST | `/admin/trash/{table}/{id}/permanent-delete` | — | Admin | no | New (8-7). Hard-delete soft-deleted item; create audit entry; return feedback + OOB swap. | -| GET | `/admin/system` | — | Admin | — | New (8-1, filled by 8-5). System settings panel (3 forms: Loans, Providers, Language). | -| POST | `/admin/system/loans` | — | Admin | no | New (8-5). Save overdue threshold; reloads `AppSettings` cache. | -| POST | `/admin/system/providers` | — | Admin | no | New (8-5). Save 3 provider API keys (Google Books / OMDb / TMDb) in one transaction; NoChange/Clear/Set state machine; reloads cache. | -| POST | `/admin/system/language` | — | Admin | no | New (8-5). Save default language (FR / EN); reloads cache; takes effect on next anonymous fresh-visitor request. | - -### Static - -| method | path | current | target | csrf_exempt | note | -|---|---|---|---|---|---| -| GET | `/static/*` | Anonymous | Anonymous | — | ServeDir; CSS/JS/favicon. | -| GET | `/covers/*` | Anonymous | Anonymous | — | ServeDir; cover images. | - -## Task 2 worklist (`current ≠ target`) - -1. `GET /catalog` → drop `require_role`. -2. `GET /locations` → drop `require_role`. -3. `GET /locations/next-lcode` → `Admin` → `Librarian`. -4. `GET /locations/{id}/edit` → `Admin` → `Librarian`. -5. `POST /locations` → `Admin` → `Librarian`. -6. `POST /locations/{id}` → `Admin` → `Librarian`. -7. `GET /borrower/{id}/edit` → `Admin` → `Librarian`. -8. `POST /borrower/{id}` → `Admin` → `Librarian`. - -9 changes to route guards; 58 handlers audited; **2 routes remain Admin**: `DELETE /borrower/{id}` and `DELETE /locations/{id}`. - -## Smoke-test target (AC #9) - -`DELETE /borrower/{id}` is the 403 assertion target. Flow: librarian creates a borrower → attempts DELETE → server returns 403 Forbidden (not a redirect) with a `FeedbackEntry` body. +| method | path | role | csrf | note | +|---|---|---|---|---| +| POST | `/debug/seed-overdue-loan` | Admin | no | | +| GET | `/loans` | Librarian | — | — | +| POST | `/loans` | Librarian | no | — | +| GET | `/loans/scan` | Librarian | — | — | +| POST | `/loans/{id}/return` | Librarian | no | — | +| GET | `/loans/{id}/return-modal` | Librarian | — | | + +### Storage locations (`src/routes/locations.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/location/{id}` | Anonymous | — | — | +| GET | `/locations` | Anonymous | — | AC #1 "location browse page". | +| POST | `/locations` | Librarian | no | decision 1a. | +| GET | `/locations/next-lcode` | Librarian | — | decision 1a. Used by create form. | +| DELETE | `/locations/{id}` | Admin | no | Destructive; stays Admin (decision 1a exception). Used as smoke-test 403 candidate. | +| POST | `/locations/{id}` | Librarian | no | decision 1a. | +| GET | `/locations/{id}/edit` | Librarian | — | decision 1a. | + +### Saved searches (#367) (`src/routes/saved_searches.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| POST | `/saved-searches` | Librarian | no | | +| POST | `/saved-searches/{id}/delete` | Librarian | no | | +| POST | `/saved-searches/{id}/rename` | Librarian | no | | +| GET | `/saved-searches/{id}/rename-modal` | Librarian | — | | + +### Series (`src/routes/series.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/series` | Anonymous | — | — | +| POST | `/series` | Librarian | no | — | +| GET | `/series/new` | Librarian | — | — | +| DELETE | `/series/{id}` | Librarian | no | — | +| GET | `/series/{id}` | Anonymous | — | — | +| POST | `/series/{id}` | Librarian | no | — | +| GET | `/series/{id}/edit` | Librarian | — | — | + +### First-launch setup wizard (story 8-8) (`src/routes/setup.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/setup` | Anonymous | — | 404 once the wizard has completed — single-use. Reachable only while the setup gate is active (`active_admin_count == 0 AND setup_completed_at IS NULL`); the gate, not a role, is what protects it. | +| POST | `/setup/complete` | Anonymous | no | Writes `setup_completed_at`; the gate becomes a no-op forever after. | +| POST | `/setup/step-1` | Anonymous | no | Creates the first admin and authenticates the session. Protected by the setup gate, not by a role — there is no user to have one yet. | +| POST | `/setup/step-2` | Anonymous | no | Provider API keys. Setup gate. | +| POST | `/setup/step-3` | Anonymous | no | Preferences. Setup gate. | + +### Valuation and statistics (`src/routes/stats.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/stats/value` | Librarian | — | | + +### Title lifecycle (`src/routes/title_lifecycle.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/title/{id}/delete-modal` | Librarian | — | | + +### Titles (`src/routes/titles.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/title/{id}` | Anonymous | — | — | +| POST | `/title/{id}` | Librarian | no | — | +| POST | `/title/{id}/confirm-metadata` | Librarian | no | — | +| POST | `/title/{id}/cover` | Librarian | no | | +| GET | `/title/{id}/cover/upload-modal` | Librarian | — | | +| GET | `/title/{id}/edit` | Librarian | — | — | +| GET | `/title/{id}/metadata` | Anonymous | — | Embeds Librarian-only edit/redownload buttons; template-gated. | +| POST | `/title/{id}/redownload` | Librarian | no | — | +| POST | `/title/{id}/series` | Librarian | no | — | +| POST | `/title/{id}/series-remove` | Librarian | no | — | +| POST | `/title/{id}/series/{assignment_id}/remove` | Librarian | no | — | + +### Wishlist (`src/routes/wishlist.rs`) + +| method | path | role | csrf | note | +|---|---|---|---|---| +| GET | `/wishlist` | Librarian | — | | +| POST | `/wishlist` | Librarian | no | | +| GET | `/wishlist/export.pdf` | Anonymous | — | **Anonymous by omission** — same as `/wishlist/print`. See the anomalies section. | +| GET | `/wishlist/new` | Librarian | — | | +| POST | `/wishlist/preview-isbn` | Librarian | no | | +| GET | `/wishlist/print` | Anonymous | — | **Anonymous by omission** — the handler takes no `Session`, so the printable wishlist is served to anyone who knows the URL, while `/wishlist` itself is Librarian. Not a deliberate policy; see the anomalies section. | +| DELETE | `/wishlist/{id}` | Librarian | no | | +| GET | `/wishlist/{id}` | Librarian | — | | +| GET | `/wishlist/{id}/delete-modal` | Librarian | — | | + + +## Anomalies + +Two rows above document behaviour that reads as an oversight rather than +a decision. They are recorded here rather than quietly fixed, because +changing them changes behaviour and that belongs in its own change: + +- `GET /wishlist/print` and `GET /wishlist/export.pdf` take no `Session` + and are therefore readable by anyone who knows the URL, while + `GET /wishlist` requires Librarian. Standing policy 3 above says + anonymous reach covers "catalog browsing and detail pages only"; a + printable wish list is neither. + +Anything else in this file that looks wrong is more likely to be the +file than the code — check the handler before trusting the row. diff --git a/src/middleware/csp.rs b/src/middleware/csp.rs index 8315a937..dee88335 100644 --- a/src/middleware/csp.rs +++ b/src/middleware/csp.rs @@ -31,7 +31,8 @@ use axum::response::Response; /// Strict CSP directives — production default. /// /// Single-line value: browsers accept either form, single-line is simpler for -/// log diffing. Update in lockstep with `architecture.md` § CSP Directives. +/// log diffing. Update in lockstep with +/// `_bmad-output/planning-artifacts/architecture.md` § CSP Directives. /// /// `img-src` allowlist mirrors the metadata providers grep-confirmed in /// `src/metadata/*.rs` (OpenLibrary, Google Books, TMDB, MusicBrainz cover diff --git a/src/routes/admin.rs b/src/routes/admin.rs index 1696871e..3cb8ac6f 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,13 +1,12 @@ -//! Admin page shell + Health tab (story 8-1) + Users panel (story 8-3). +//! Admin page shell + Health tab + Trash. //! -//! One entry point (`GET /admin`) with five tabs. Health and Users are complete; -//! the other three are stubs that later Epic 8 stories fill in exactly one at -//! a time: -//! - Users → story 8-3 ✓ -//! - Reference → story 8-4 -//! - System → story 8-5 -//! - Trash (view) → story 8-6 -//! - Trash (purge) → story 8-7 +//! One entry point (`GET /admin`) with six tabs, all shipped: +//! - Health → this module +//! - Users → `admin_users.rs` +//! - Reference → `admin_reference_data.rs` + `admin_labels.rs` +//! - System → `admin_system.rs` +//! - Trash → this module (view, restore, permanent delete) +//! - API keys → `admin_api_keys.rs` //! //! Middleware order follows AR16 — admin routes live at the top level //! alongside the non-catalog routes so they skip `pending_updates_middleware` diff --git a/website/index.html b/website/index.html index 508973d7..29e96243 100644 --- a/website/index.html +++ b/website/index.html @@ -244,7 +244,7 @@

Barcode-first cataloging

- Scan an ISBN or EAN-13. Title metadata resolves asynchronously through a chain of eight providers — BnF, Google Books, Open Library, Library of Congress, MusicBrainz, OMDb, TMDb, BDGest — with cover-image download. + Scan an ISBN or EAN-13. Title metadata resolves asynchronously through a chain of nine providers — BnF, Google Books, Open Library, Library of Congress, K10plus, MusicBrainz, OMDb, TMDb, BDGest — with cover-image download.