{"id":49914486,"url":"https://github.com/guycorbaz/mybibli","last_synced_at":"2026-05-25T00:12:35.207Z","repository":{"id":351080597,"uuid":"1209391213","full_name":"guycorbaz/mybibli","owner":"guycorbaz","description":"Personal library management","archived":false,"fork":false,"pushed_at":"2026-05-13T19:33:55.000Z","size":5360,"stargazers_count":1,"open_issues_count":71,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-13T21:29:48.546Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/guycorbaz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-13T11:34:49.000Z","updated_at":"2026-05-13T19:33:57.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/guycorbaz/mybibli","commit_stats":null,"previous_names":["guycorbaz/mybibli"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/guycorbaz/mybibli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guycorbaz%2Fmybibli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guycorbaz%2Fmybibli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guycorbaz%2Fmybibli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guycorbaz%2Fmybibli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/guycorbaz","download_url":"https://codeload.github.com/guycorbaz/mybibli/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guycorbaz%2Fmybibli/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33107612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T04:41:52.686Z","status":"ssl_error","status_checked_at":"2026-05-16T04:41:52.009Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-16T15:08:26.658Z","updated_at":"2026-05-25T00:12:35.199Z","avatar_url":"https://github.com/guycorbaz.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cpicture\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/mybibli-logo/svg/mybibli-logo-dark.svg\"\u003e\n    \u003cimg src=\"docs/mybibli-logo/svg/mybibli-logo.svg\" alt=\"mybibli\" width=\"440\"\u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\n![CI](https://github.com/guycorbaz/mybibli/actions/workflows/ci.yml/badge.svg?branch=main)\n[![Version](https://img.shields.io/github/v/tag/guycorbaz/mybibli?label=version\u0026sort=semver\u0026color=blue)](https://github.com/guycorbaz/mybibli/releases)\n[![License: AGPL v3](https://img.shields.io/badge/license-AGPL--3.0--or--later-lightgrey)](LICENSE)\n\n\u003e Personal library cataloging for home collectors.\n\n**Status:** in production since v1.1.1 (2026-05-14). 10 epics shipped; project is in GH-issue-driven polish mode. Current release: `v1.7.7`. Pre-built images on Docker Hub at [`gcorbaz/mybibli`](https://hub.docker.com/r/gcorbaz/mybibli). See [ROADMAP.md](ROADMAP.md) for what's coming next.\n\n## What it is\n\n`mybibli` is a self-hosted web app to catalog, locate, and loan your personal library. It is designed for a single household, running on your own hardware (typically a NAS or home server). No cloud sync, no telemetry — all data stays on your local network.\n\nBuilt for collectors who want more than a spreadsheet:\n\n- **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.\n- **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.\n- **Series + collection awareness.** Gap detection on series volumes, Dewey-based browsing, similar-titles section.\n- **Storage-location tracking.** Configurable hierarchy (room → shelf → row → …), barcode-on-shelf workflow.\n- **Loan management.** Borrower CRUD, loan registration with automatic location restoration on return, overdue threshold (admin-configurable), per-borrower history.\n- **Multi-role auth.** Anonymous (read-only), Librarian (catalog + loans), Admin (everything). Session inactivity timeout with keep-alive toast. FR/EN language toggle with per-user preference.\n- **Hardened by construction.** Strict Content Security Policy (no `unsafe-inline`/`unsafe-eval`), CSRF synchronizer-token middleware on every state-changing request (with a server-rendered \"session expired\" feedback when the token drifts — see [`docs/auth-threat-model.md`](docs/auth-threat-model.md)), scanner-guard against burst-keyboard input leaking into modals.\n- **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 (overdue threshold, provider API keys, default language), trash view + restore + permanent delete, configurable auto-purge after 30 days.\n- **First-launch setup wizard.** Fresh installs walk through Admin → Providers → Preferences → Done; the gate middleware redirects every route to `/setup` until completion. Idempotent — interruptions resume at the right step server-side.\n- **Mobile-aware + WCAG 2.2 AA accessible.** Dual-surface mobile UX on data-dense pages (desktop tables collapse into mobile cards, admin tabs collapse into a `\u003cselect\u003e` dropdown), full keyboard navigation with shortcuts cheat-sheet (`?`), contextual help-icon tooltips, and an axe-core CI gate that covers every reachable surface including entity-detail routes and the first-launch wizard.\n\n## Screenshots\n\nLive production install (`v1.7.7`, household NAS, 140+ volumes catalogued and growing):\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/01-home-desktop.png\" alt=\"mybibli home page — search bar, genre filters, dashboard counters, and a recent-additions strip with cover thumbnails.\" width=\"780\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eHome — search, genre filters, dashboard counters (\"À traiter\" / \"Aperçu de la collection\"), recent additions with cover thumbnails.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/04-locations-desktop.png\" alt=\"mybibli locations page — hierarchical tree of rooms, bookcases and shelves with per-node volume counts and edit/delete affordances.\" width=\"780\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eLocations — configurable hierarchy (room → bookcase → shelf …), per-node volume counts, inline create / edit / delete.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/09-audit-desktop.png\" alt=\"mybibli shelf-audit page — list of volumes flagged for physical verification, with resolved location and V-code per row.\" width=\"780\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eShelf-audit — volumes flagged \"À contrôler\" (single or bulk-per-shelf), sorted by location → V-code, with one-click clear per row.\u003c/em\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/10-admin-health-desktop.png\" alt=\"mybibli admin Health tab — entity counts, MariaDB version, disk usage, and per-provider reachability probes.\" width=\"780\"\u003e\n  \u003cbr\u003e\n  \u003cem\u003eAdmin \u0026gt; Health — entity counts, MariaDB version, disk usage, and metadata-provider reachability probes refreshed every 5 minutes in the background.\u003c/em\u003e\n\u003c/p\u003e\n\n## Tech stack\n\n- **Backend:** Rust 2024 edition + [Axum](https://github.com/tokio-rs/axum) 0.8\n- **Database:** MariaDB via [SQLx](https://github.com/launchbadge/sqlx) 0.8 (offline query cache committed in `.sqlx/`)\n- **Templates:** [Askama](https://github.com/djc/askama) 0.15 (compile-time type-checked)\n- **Frontend:** [HTMX](https://htmx.org/) 2.0 + [Tailwind CSS](https://tailwindcss.com/) v4 — no SPA framework, zero inline scripts/styles (CSP `script-src 'self'`, `style-src 'self'`)\n- **i18n:** [rust-i18n](https://github.com/longbridgeapp/rust-i18n) — French + English\n- **Auth:** session cookie (`HttpOnly`, `SameSite=Lax`) + per-session CSRF synchronizer token; argon2 password hashing\n- **Testing:** `cargo test` (~525 lib unit), `#[sqlx::test]` (~95 DB integration tests across 10+ files), [Playwright](https://playwright.dev/) (~160 E2E specs across two CI lanes — the seeded-stack suite and a dedicated wizard-E2E lane that runs on a fresh empty database)\n\n## Quick start (end users)\n\nPre-built images are published to Docker Hub at [`gcorbaz/mybibli`](https://hub.docker.com/r/gcorbaz/mybibli) — `:latest` tracks the highest semver, individual tags pin to the exact release. For development against the source tree, see **Development** below.\n\n### Installation notes\n\nInstall **v1.1.0 or later**. Pre-1.1.0 images (`v1.0.0` … `v1.0.5`) shipped seed migrations that created default `admin/admin` and `librarian/librarian` credentials on every fresh install, bypassing the first-launch wizard ([#173](https://github.com/guycorbaz/mybibli/issues/173)). The seed gate landed in v1.1.0 and is the install floor; pre-1.1.0 Docker Hub tags have been removed. `:latest` and every published tag from 1.1.0 onwards are safe — a fresh install greets you with the setup wizard. If you happen to have an older deployment, wipe the database and reinstall before adding any data.\n\n**Skipping intermediate versions is supported.** mybibli ships releases at a brisk pace; you do not need to upgrade through every intermediate tag. The migration runner applies every pending migration in timestamp order at boot, schema migrations are purely additive (no `DROP COLUMN` / `DROP TABLE`), and the few data backfills are idempotent — so a jump from, say, `v1.3` directly to the latest tag is safe for the database. Take a backup before upgrading (there is no automatic rollback). See chapter 8 (\"Upgrade and migration\") of the user manual for the full procedure and the one pre-1.1.4 cover-JPG caveat.\n\n### Persistent storage — volumes you need\n\n`docker-compose.yml` declares **three named volumes**. The first two MUST survive container upgrades; the third is forensic-only:\n\n- `mybibli_db_data` → `/var/lib/mysql` — your catalog (titles, volumes, loans, etc.) — **mandatory**\n- `mybibli_covers` → `/app/covers` — downloaded cover JPGs (issue [#213](https://github.com/guycorbaz/mybibli/issues/213)) — **mandatory**\n- `mybibli_logs` → `/var/log/mybibli` — daily-rotating log files (CR [#301](https://github.com/guycorbaz/mybibli/issues/301), v1.7.0+) — **optional but recommended** for production debuggability; can be wiped at any time\n\nIf you deploy `docker-compose.yml` from the repo unchanged (v1.7.0+), you already have all three. Back the data + covers volumes up together — losing one without the other leaves DB references pointing at missing files (or vice versa). The logs volume is forensic-only; it doesn't need backup.\n\n**Upgrading from a pre-1.1.4 install?** Pre-1.1.4 `docker-compose.yml` did not declare `mybibli_covers`, so the cover JPGs lived inside the container's writable layer and were lost on every `docker compose up -d` after a `pull`. Adding the volume now preserves covers fetched from this point forward, but does NOT restore the ones that disappeared on prior upgrades. To recover, re-trigger metadata fetch from each affected title's detail page (the \"Re-fetch metadata\" button). A bulk-fetch admin action is tracked at issue [#214](https://github.com/guycorbaz/mybibli/issues/214).\n\n**Upgrading from a pre-1.7.0 install?** The `mybibli_logs` volume is new in v1.7.0. Without it, log files write to the container's ephemeral writable layer and are lost on every `docker compose up -d` after a `pull` — which defeats the purpose of CR [#301](https://github.com/guycorbaz/mybibli/issues/301)'s persistent-log feature. Add this block to your existing `docker-compose.yml`:\n\n```yaml\nservices:\n  mybibli:\n    volumes:\n      - mybibli_logs:/var/log/mybibli   # ← add this line\n\nvolumes:\n  mybibli_logs:                          # ← and this declaration\n```\n\n(Or use a bind mount: `- /your/host/path:/var/log/mybibli` if you prefer logs visible directly in DSM File Station / your journald shipper. See chapter 12 of the manual.)\n\n**Synology DSM / bind-mount users:** comment out the `mybibli_covers:/app/covers` line in `docker-compose.yml` and uncomment the bind-mount line right below it, then set `COVERS_HOST_PATH` in your `.env` to the host path you want — Synology File Station / your rsync routine will see the covers directly. The same pattern applies to `mybibli_logs` via `LOGS_HOST_PATH`.\n\n## Configuration\n\nAll deployment-time settings are environment variables — there is no\nconfig file. `.env.example` is the canonical reference: every variable\nthe Rust binary reads or that `docker-compose.yml` interpolates is\nlisted and commented there. Copy it to `.env` and adjust for your\ndeployment:\n\n```bash\ncp .env.example .env\n$EDITOR .env\ndocker compose up\n```\n\nThe variables are grouped in seven sections:\n\n1. **Database connection** — `DATABASE_URL` plus the `MYSQL_*` parts\n   used by the bundled `db` service.\n2. **HTTP server** — `HOST`, `PORT`, `HOST_PORT` (the host-side port\n   published by Docker).\n3. **Application** — `MYBIBLI_LOG_LEVEL` (v1.7.0+, tracing filter;\n   prod-safe default `info`; also flippable at runtime from\n   `/admin \u003e System` without a redeploy), `MYBIBLI_LOG_DIR` (v1.7.0+,\n   in-container path for daily-rotating log files; default\n   `/var/log/mybibli`; mapped to the `mybibli_logs` named volume — see\n   \"Persistent storage\" above. `LOGS_HOST_PATH` is an optional\n   bind-mount override), `RUST_LOG` (legacy fallback, honored when\n   `MYBIBLI_LOG_LEVEL` is unset), `APP_LANGUAGE` (`en`, `fr`, `de`, or\n   `it` — v1.7.0 added DE + IT), `COVERS_DIR` (filesystem path for\n   downloaded cover images — in Docker, the `/app/covers` directory is\n   mapped to the persistent `mybibli_covers` named volume.\n   `COVERS_HOST_PATH` is an optional bind-mount override).\n4. **Cookie \u0026 CSP hardening** — `MYBIBLI_COOKIE_SECURE` (set to `true`\n   only behind HTTPS, see issue\n   [#94](https://github.com/guycorbaz/mybibli/issues/94)),\n   `CSP_REPORT_ONLY`.\n5. **Metadata provider API keys** — `GOOGLE_BOOKS_API_KEY`,\n   `OMDB_API_KEY`, `TMDB_API_KEY`. Migrated ONCE into the `settings`\n   table at boot; afterwards the admin can rotate or clear them via\n   `/admin \u003e System \u003e Metadata Providers`. Re-set them in `.env` only\n   when you want the deployment-time value to win on the next reboot.\n6. **Metadata provider base URL overrides** — used exclusively by the\n   E2E test stack to point each provider at the in-tree mock server.\n   Leave unset in production.\n7. **Optional dev/test overrides** — `MYBIBLI_SKIP_SETUP`,\n   `MYBIBLI_SKIP_STARTUP_PURGE`. Strict accept-set: only `1` / `true`\n   / `TRUE` count as \"on\"; anything else is ignored.\n\nBoolean variables across the codebase use the same strict accept-set,\nwhich avoids the classic footgun where a stale shell value like `0`\nreads as \"set\" and silently flips an opt-out.\n\nShell-level env vars used for build / test commands (`SQLX_OFFLINE`,\n`TEST_ADMIN_PASSWORD`, `MYBIBLI_SETUP_E2E`, …) are documented inline\nin the **Development** section below — they do not belong in `.env`.\n\n## Development\n\n### Prerequisites\n\n- Docker + Docker Compose\n- Rust toolchain (rustup, Rust 2024 edition)\n- Node.js 20+ (for Playwright E2E tests)\n\n### Run the app locally\n\n```bash\n# Start the full stack (app + MariaDB + mock metadata providers).\n# `MYBIBLI_SKIP_SETUP=1` is baked into the test compose so existing\n# seeded specs reach their target routes without going through the\n# first-launch wizard.\ncd tests/e2e\ndocker compose -f docker-compose.test.yml up --build\n```\n\nThe app listens on `http://localhost:8080`. The seed migrations create an admin user (`admin` / `admin`, role `admin`) and a librarian (`librarian` / `librarian`, role `librarian`) **only when `MYBIBLI_SEED_DEV_USERS=1` is set** — which is baked into both `docker-compose.dev.yml` and `tests/e2e/docker-compose.test.yml`.\n\n\u003e ℹ️ **Seed users are now gated (issue #173, fixed in 1.1.0).**\n\u003e On a fresh install where `MYBIBLI_SEED_DEV_USERS` is unset, the\n\u003e seed migrations still apply but the gate in `src/services/seed_gate.rs`\n\u003e immediately soft-deletes any user whose hash still matches the\n\u003e documented seed value. The first-launch wizard at `/setup` is\n\u003e therefore reachable on every fresh production deployment. Set the\n\u003e env var to `1` only for local development and the E2E test stack —\n\u003e never in production.\n\n**Fresh-install wizard.** Story 8-8 introduced a first-launch wizard at `/setup` whose gate predicate is `(active_admin_count == 0) AND (settings.setup_completed_at IS NONE)`. Because the seed migrations create an admin before the gate is first evaluated, the wizard never triggers in practice on a default install — the password-rotation step above is the effective onboarding flow. The wizard can still be exercised by running `cargo run` against an empty DB with the seed migrations skipped. `MYBIBLI_SKIP_SETUP=1` (strict accept-set: `1` / `true` / `TRUE`) is the explicit bypass.\n\n### Build \u0026 check (native)\n\n```bash\ncargo check                          # Fast type-check\ncargo build                          # Full debug build\ncargo clippy -- -D warnings          # Lint (zero-warnings policy)\n```\n\n### Unit tests\n\n```bash\nSQLX_OFFLINE=true cargo test --lib   # ~525 unit tests, ~5 s\ncargo test config::                  # Module-scoped\ncargo test \u003cname\u003e -- --nocapture     # Single test with output\n```\n\n### DB integration tests\n\n```bash\ndocker compose -f tests/docker-compose.rust-test.yml up -d\nSQLX_OFFLINE=true \\\nDATABASE_URL='mysql://root:root_test@localhost:3307/mybibli_rust_test' \\\ncargo test --test find_similar \\\n           --test find_by_location_dewey \\\n           --test metadata_fetch_dewey \\\n           --test metadata_fetch_race \\\n           --test seeded_users \\\n           --test setup_wizard\n```\n\nEach test gets a fresh DB via `#[sqlx::test(migrations = \"./migrations\")]`. The CI `db-integration` job runs the same allowlist — when adding a new `tests/*.rs` file, append `--test \u003cname\u003e` to both this command and `.github/workflows/_gates.yml::db-integration`.\n\n### E2E tests\n\nThe Playwright suite has **two CI lanes**:\n\n```bash\ncd tests/e2e\n\n# Lane 1 — seeded-stack (most specs). MYBIBLI_SKIP_SETUP=1 baked in.\ndocker compose -f docker-compose.test.yml up --build -d\nnpm test                             # Full suite, parallel mode\n\n# Lane 2 — wizard E2E (story 8-8). Fresh DB, MYBIBLI_SKIP_SETUP unset.\ndocker compose -f docker-compose.test.yml -f docker-compose.wizard.yml up -d --build --wait\ndocker compose -f docker-compose.test.yml -f docker-compose.wizard.yml exec -T db \\\n    mariadb -uroot -proot_test mybibli_test -e \"DELETE FROM sessions; DELETE FROM users;\"\ndocker compose -f docker-compose.test.yml -f docker-compose.wizard.yml restart mybibli\nMYBIBLI_SETUP_E2E=1 npx playwright test specs/journeys/setup-wizard.spec.ts\n\n# Single spec from the seeded suite\nnpx playwright test specs/journeys/\u003cspec\u003e.spec.ts\n```\n\nA `waitForTimeout(...)` grep gate (`tests/e2e` only) blocks any new arbitrary-sleep call — use DOM-state assertions instead. Enforced both locally and in the CI `e2e` job.\n\n### Stack reset\n\n`./scripts/e2e-reset.sh` does a single-command teardown + rebuild + wait-for-ready when local DB state is polluted. Use after long-running dev sessions where E2E specs see stale rows from prior runs.\n\n### Database migrations\n\nMigrations live in `migrations/`. SQLx offline cache in `.sqlx/` is checked into the repo and must stay in sync:\n\n```bash\ncargo sqlx prepare                   # Regenerate after query changes\ncargo sqlx prepare --check --workspace -- --all-targets\n```\n\n### i18n\n\nLocale files in `locales/{en,fr}.yml`. After adding or renaming keys:\n\n```bash\ntouch src/lib.rs \u0026\u0026 cargo build      # Force proc-macro rebuild (rust-i18n)\n```\n\n## Repository layout\n\n```\nsrc/\n├── routes/          # HTTP handlers — thin, delegate to services\n│   ├── admin.rs            # Admin shell + tab routing + user management (8-1, 8-3)\n│   ├── admin_reference_data.rs  # Genres / states / roles / node types CRUD (8-4)\n│   ├── admin_system.rs     # System settings forms (8-5)\n│   ├── auth.rs             # Login / logout\n│   ├── catalog.rs          # Cataloging routes\n│   ├── locations.rs        # Storage location tree\n│   ├── loans.rs            # Loans + borrowers\n│   ├── setup.rs            # First-launch setup wizard (8-8)\n│   └── …\n├── services/        # Business logic, domain rules\n│   ├── admin_health.rs     # Health-tab data builders (8-1)\n│   ├── admin_system.rs     # K/V settings save + cache reload (8-5/8-8)\n│   ├── auth.rs             # Shared session-rotation chain (8-8)\n│   ├── auto_purge.rs       # 30-day soft-delete hard-purge (8-7)\n│   ├── locking.rs          # Optimistic-lock check helpers\n│   ├── password.rs         # argon2 hashing\n│   ├── setup.rs            # Setup wizard step resolution + writers (8-8)\n│   ├── soft_delete.rs      # Soft-delete with table whitelist\n│   └── …\n├── middleware/      # Axum middleware\n│   ├── auth.rs             # Session extractor + role gating\n│   ├── csp.rs              # Content-Security-Policy + hardening headers (7-4)\n│   ├── csrf.rs             # CSRF synchronizer-token middleware (8-2)\n│   ├── htmx.rs             # HTMX request/response helpers\n│   ├── locale.rs           # Locale resolution chain (7-3)\n│   ├── logging.rs          # tracing layer\n│   ├── pending_updates.rs  # OOB metadata-update delivery\n│   └── setup_gate.rs       # First-launch wizard gate (8-8)\n├── models/          # DB models + queries (SQLx)\n├── metadata/        # External metadata providers + KEYED_PROVIDERS const\n├── tasks/           # Background tokio tasks\n│   ├── anonymous_session_purge.rs  # Daily purge of stale anon sessions (8-2)\n│   ├── auto_purge_scheduler.rs     # Daily soft-delete hard-purge (8-7)\n│   ├── metadata_fetch.rs           # Async ISBN→metadata resolution\n│   └── provider_health.rs          # 5-min provider reachability pings (8-1)\n├── config.rs        # Env vars + `AppSettings` (DB-backed K/V cache)\n├── lib.rs           # `AppState` definition\n├── main.rs          # Startup chain (migrations → settings → registry → routes)\n├── templates_audit.rs  # Architectural-invariant tests (CSP / CSRF / hx-confirm)\n└── error/           # AppError enum + IntoResponse\n\ntemplates/\n├── layouts/         # base.html (admin + library) and bare.html (login + setup)\n├── pages/           # Full-page templates (catalog, admin, setup, …)\n├── components/      # Reusable Askama macros (cover, similar_titles, setup_progress, …)\n└── fragments/       # HTMX partial responses + admin form fragments\n\nstatic/\n├── css/             # Tailwind output\n└── js/              # ES modules (csrf.js, scanner-guard.js, inline-form.js, …)\n\nmigrations/          # SQLx migrations (timestamped)\nlocales/             # rust-i18n YAML files (en.yml, fr.yml — keys at root, no language wrapper)\ndocs/                # Coding conventions + architectural references\ntests/\n├── *.rs             # DB integration tests (#[sqlx::test])\n└── e2e/             # Playwright specs + Docker test stacks\n    ├── docker-compose.test.yml     # Seeded-stack lane (MYBIBLI_SKIP_SETUP=1)\n    └── docker-compose.wizard.yml   # Wizard-E2E override (MYBIBLI_SKIP_SETUP=\"\")\n```\n\n## Documentation\n\nProduct and planning documents are versioned under `_bmad-output/`:\n\n- [`planning-artifacts/product-brief-mybibli.md`](_bmad-output/planning-artifacts/product-brief-mybibli.md) — product vision\n- [`planning-artifacts/prd.md`](_bmad-output/planning-artifacts/prd.md) — functional requirements (121 FRs), NFRs, user journeys\n- [`planning-artifacts/architecture.md`](_bmad-output/planning-artifacts/architecture.md) — technical decisions + ARs\n- [`planning-artifacts/ux-design-specification.md`](_bmad-output/planning-artifacts/ux-design-specification.md) — UX design (30 UX-DRs)\n- [`planning-artifacts/epics.md`](_bmad-output/planning-artifacts/epics.md) — epic breakdown + FR coverage map\n- [`implementation-artifacts/sprint-status.yaml`](_bmad-output/implementation-artifacts/sprint-status.yaml) — live sprint state\n- [`implementation-artifacts/epic-*-retro-*.md`](_bmad-output/implementation-artifacts/) — per-epic retrospectives\n\nCoding 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).\n\n## Roadmap\n\n| Epic | Title | Status |\n|------|-------|--------|\n| 1 | Je catalogue mon premier livre | ✅ done |\n| 2 | Je sais où sont mes livres | ✅ done |\n| 3 | Tous mes médias sont gérés | ✅ done |\n| 4 | Je gère mes prêts | ✅ done |\n| 5 | Mes séries et ma collection | ✅ done |\n| 6 | Pipeline CI/CD et fiabilité | ✅ done |\n| 7 | Accès multi-rôle \u0026 Sécurité | ✅ done |\n| 8 | Administration \u0026 Configuration | ✅ done |\n| 9 | Polish UX \u0026 Accessibilité | ✅ done |\n| 10 | Mobile UX \u0026 sécurité closeout | ✅ done |\n\nmybibli 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); v1.1.2 closed Epic 10 (mobile UX dual-surface pattern, CSRF rejection UX, axe-core entity-detail coverage); v1.1.3 was the first production-driven-feedback patch (home-page layout reorder so filter results land above the fold; locations tree rows clickable); v1.1.4 fixed issue [#213](https://github.com/guycorbaz/mybibli/issues/213) — the published `docker-compose.yml` template now persists cover JPGs in the `mybibli_covers` named volume so they survive container upgrades; v1.1.5 shipped the production-feedback iteration: [#203](https://github.com/guycorbaz/mybibli/issues/203) defensive genre fallback when metadata fetch returns nothing (the title save flow no longer trips an FK violation), [#218](https://github.com/guycorbaz/mybibli/issues/218) defense-in-depth hardening of the modal-Confirm retarget guard (now requires the `HX-Request` pairing HTMX always sets), and the docs slice of [#202](https://github.com/guycorbaz/mybibli/issues/202) — the user manual's metadata-providers chapter gets a step-by-step Google Cloud Console walk-through for the Google Books API key plus a Troubleshooting section for empty-metadata scans; v1.1.6 closed [#225](https://github.com/guycorbaz/mybibli/issues/225) — when the metadata provider chain resolves a title but the resolving provider returned no cover URL (BnF never ships image URLs in its UNIMARC payload, and Google Books occasionally lacks `imageLinks` for self-published titles), mybibli falls back to the Open Library Covers API by ISBN so the title gets a jacket image instead of a blank placeholder; v1.1.7 closed [#228](https://github.com/guycorbaz/mybibli/issues/228) — same fallback now also runs on the user-triggered \"Re-fetch metadata\" button (it was only wired into the background scan in 1.1.6), so existing FR titles cataloged before the cover fix can now actually receive their covers on re-fetch; v1.1.8 closed [#232](https://github.com/guycorbaz/mybibli/issues/232) — third and last bug of the cover-fallback saga. mybibli used to record every change to a title's genre in `manually_edited_fields`, which permanently pinned re-fetches to the conflict-confirmation flow and silently dropped the cover (because the form's \"accept cover\" checkbox defaulted to unchecked). The release stops tracking `genre_id` there, ships a one-shot migration that strips the spurious entry from existing rows, and adds the first illustrations to the README, the manual and the website; v1.1.9 closed [#238](https://github.com/guycorbaz/mybibli/issues/238) — saving a title where any numeric field (page count, track count, total duration, issue number) was empty used to return a silent HTTP 422 to the browser (no UI feedback). The four `Option\u003ci32\u003e` form fields now treat empty input as `None` instead of refusing to deserialize. v1.2.0 is the first minor release of the post-v1 era, themed \"See what you own, find it faster\" — six CRs bundled together: a chip showing each title's Dewey code next to its genre ([#236](https://github.com/guycorbaz/mybibli/issues/236)), a sort-link row to sort series by Dewey or title in addition to position ([#235](https://github.com/guycorbaz/mybibli/issues/235)), a fold/unfold toggle on the `/locations` tree with localStorage persistence ([#200](https://github.com/guycorbaz/mybibli/issues/200)), an \"Uncategorized\" filter chip on home that surfaces titles with no real genre yet ([#205](https://github.com/guycorbaz/mybibli/issues/205)), a spinner on the metadata-conflict \"Apply\" button ([#215](https://github.com/guycorbaz/mybibli/issues/215)), and a one-click admin action to re-fetch covers on every title that's missing one ([#214](https://github.com/guycorbaz/mybibli/issues/214)). v1.2.1 is a four-fix patch on top of 1.2.0: deleting a series with assigned titles now surfaces the meaningful \"cannot delete: N titles assigned\" message instead of a generic internal-error copy ([#139](https://github.com/guycorbaz/mybibli/issues/139)); the help-icon button no longer sits inside its associated `\u003clabel\u003e` element (HTML5 compliance + screen-reader cleanup, [#154](https://github.com/guycorbaz/mybibli/issues/154)); an anonymous user clicking the Return-loan modal trigger from `/borrower/:id` now bounces back to that borrower page after login instead of always landing on `/loans` ([#133](https://github.com/guycorbaz/mybibli/issues/133)); and the 403 Forbidden error body finally renders in the user's UI language instead of falling back to the default locale ([#219](https://github.com/guycorbaz/mybibli/issues/219)). v1.2.2 is a three-fix defense-in-depth patch around the soft-deleted-genre semantics: a stale `?filter=genre:N` link (from a tab opened before an admin deleted that genre) now drops the chip and surfaces a localized \"this genre no longer exists\" notice instead of returning an empty list ([#112](https://github.com/guycorbaz/mybibli/issues/112)); SearchService SQL switches `JOIN genres` to `LEFT JOIN` + `COALESCE` so a title whose genre row got soft-deleted (an orphan-FK state that story 8-4's guard prevents via the supported path, but could be reached by a future migration or manual `UPDATE`) still appears in the catalog with a \"(Genre supprimé)\" placeholder ([#107](https://github.com/guycorbaz/mybibli/issues/107)); and the home dashboard's \"Stats by genre\" panel now uses the full active-catalog count as its denominator so per-row percentages reflect the true share even if orphan-FK titles exist (the displayed total then sums to \u003c100%, the gap being the orphan delta — [#111](https://github.com/guycorbaz/mybibli/issues/111)). v1.3.0 is the second themed minor — \"Plan your next purchase\". It ships two roadmap-slotted CRs: an inline per-volume table on `/title/:id` (between the contributor block and the similar-titles strip) with the V-code, the resolved location, the condition state, and Librarian-gated Edit + Delete affordances via a UX-DR8 modal ([#209](https://github.com/guycorbaz/mybibli/issues/209)); and the first-class wish list at `/wishlist` — add books by scanning an ISBN (with provider-chain preview) OR typing a free-form title; mark-as-bought via UX-DR8 modal; auto-link banner on the catalog scan when a freshly cataloged ISBN matches a wish list entry; bookstore-friendly print view at `/wishlist/print` for the browser PDF dialog; mobile-card dual-surface; new home-dashboard counter; full audit trail ([#242](https://github.com/guycorbaz/mybibli/issues/242)). v1.3.1 is a four-papercut polish patch surfaced within hours of v1.3.0's production deployment: a loading spinner on the wish list \"Rechercher\" ISBN-lookup button so the 2–5s provider chain feedback isn't silent ([#258](https://github.com/guycorbaz/mybibli/issues/258)); a help-icon tooltip explaining what the **Omnibus** checkbox does in the title-detail series-assignment form ([#259](https://github.com/guycorbaz/mybibli/issues/259)); the wish list print page refactored from a dense table to a card-based layout with cover thumbnails + page footer counter, and opening in a new tab so the user doesn't lose context ([#260](https://github.com/guycorbaz/mybibli/issues/260)); and the home page's \"Recent additions\" section folded by default under a native `\u003cdetails\u003e` (localStorage-persisted choice) to reduce vertical clutter as the catalog grows ([#261](https://github.com/guycorbaz/mybibli/issues/261)). v1.4.0 is the third themed minor — \"Bring your own agent\". It ships a JSON HTTP API at `/api/v1/*` ([#241](https://github.com/guycorbaz/mybibli/issues/241)) with API-key authentication (argon2-hashed, 12-char prefix index, `Authorization: Bearer` or `X-API-Key`), two coexisting scopes (read-only / read-and-write), a focused four-field write surface (`PATCH /api/v1/titles/{id}` for `subtitle`, `description`, `dewey_code`, `genre_id` with optimistic-lock 409 + audit trail), and an admin tab `/admin?tab=api_keys` to mint keys (plaintext shown exactly once in a UX-DR8 modal), see them in a six-column list with last-used timestamps, and revoke them. The CSRF middleware short-circuits on `/api/*` because bearer-token auth doesn't ride on cookies. New manual chapter 11 (\"API \u0026 integrations\", EN + FR) walks through the LLM-classifier use case with curl, Python, and pseudocode examples. v1.5.0 is the fourth themed minor — \"Know what you have, and what it's worth\". It bundles five CRs: a Library of Congress metadata provider for English-language books ([#263](https://github.com/guycorbaz/mybibli/issues/263), slotted between Google Books and Open Library in the chain — fills the older / academic / US-government gap); a donut chart replacing the bar-chart stats-by-genre on home ([#265](https://github.com/guycorbaz/mybibli/issues/265), Chart.js v4 vendored, 5% threshold rolls small genres into an \"Other\" bucket with a `\u003cdetails\u003e` listing the rolled-up names); two title-lifecycle affordances ([#271](https://github.com/guycorbaz/mybibli/issues/271) delete a title with zero volumes via a UX-DR8 modal, [#272](https://github.com/guycorbaz/mybibli/issues/272) edit ISBN/ISSN/UPC in the metadata-edit form with checksum + duplicate-collision guards); a real server-rendered PDF export for the wish list ([#266](https://github.com/guycorbaz/mybibli/issues/266), genpdf + DejaVu Sans vendored, no browser print-to-PDF round-trip); and the headline feature — per-volume **collection valuation** with purchase-price + estimated-current-value columns, per-currency totals + breakdowns by genre + by series on a new `/stats/value` page, and an opt-in home-dashboard indicator (default OFF, admin toggle) ([#243](https://github.com/guycorbaz/mybibli/issues/243), CHF as the seeded default currency, admin-overridable). v1.5.1 is a five-fix production-hygiene patch shipped within hours of the v1.5.0 NAS upgrade after Guy validated the new features and surfaced the bugs: the Dockerfile was missing `COPY static/fonts/` so `/wishlist/export.pdf` crashed in production (#281); Trash permanent-delete on titles failed because the 4 child FKs weren't cascaded, fixed by extending `TrashService::permanent_delete` with the same FK-ordered chain `auto_purge` already maintained (#282); `/admin?tab=system` gained the missing \"Library valuation\" form section to toggle the v1.5.0 #243 settings that previously existed in code but had no UI (#283); revoked API keys can now be hard-deleted from the admin panel (Active keys still require Revoke first; soft-delete via direct UPDATE, separate from the Trash flow because a deleted plaintext is gone forever and \"restore\" is meaningless — #284); and the admin Health-tab provider probe stopped marking every provider Unreachable — `probe_once` now sends a User-Agent and treats any HTTP response as Reachable (only network-level errors → Unreachable, #285). v1.5.2 is a one-fix hotfix on top of v1.5.1: the v1.5.0 #243 valuation workflow was partially broken — UPDATE landed correctly, but every subsequent volume read crashed because SQLx 0.8 can't decode a raw MariaDB `DECIMAL(10,2)` into `Option\u003cf64\u003e` without the `bigdecimal`/`rust_decimal` feature flag. `VolumeModel::find_by_id` and `find_by_label` now wrap the two DECIMAL columns with `CAST(... AS DOUBLE)` — same pattern the aggregation queries already used and that kept `/stats/value` working. Two new sqlx::test regression guards INSERT a volume with non-NULL valuation columns and assert round-trip ([#288](https://github.com/guycorbaz/mybibli/issues/288)). v1.6.0 is the fifth themed minor — \"Tighten the catalog\". It ships four CRs targeting catalog hygiene: a storage location can now be marked **organizational** to refuse volume assignments — useful for nested containers that act as folders, not shelves ([#280](https://github.com/guycorbaz/mybibli/issues/280), checkbox on the create/edit form, flip-with-volumes guard, volume-edit picker rejects organizational targets); a **shelf-audit workflow** lets a librarian mark a volume \"À contrôler\" (single or in bulk for an entire shelf), surfaced via an amber home-dashboard indicator and a dedicated `/audit` list sorted by location → V-code ([#237](https://github.com/guycorbaz/mybibli/issues/237), `volumes.under_audit_since DATETIME NULL` migration, audit-trail entries for every mark/clear); a **\"Titles without volumes\"** filter chip on the home dashboard surfaces titles that have no active volume row — either soft-deleted away or never shelved ([#279](https://github.com/guycorbaz/mybibli/issues/279), Librarian+ only, `NOT EXISTS` SQL guard, mirrors the #205 Uncategorized chip shape); and the home page's list view is now a real **sortable table** (Title · Author · Genre · Dewey · Volumes), replacing the cards-stacked-vertically layout — column headers issue HTMX `hx-get` with `hx-push-url=true`, mirroring `/location/:id` ([#250](https://github.com/guycorbaz/mybibli/issues/250), grid view unchanged, sort dropdown hidden in list mode). v1.6.1 is a one-fix hotfix on v1.6.0 surfaced within hours of the prod NAS upgrade: ticking the new #280 \"Emplacement organisationnel\" checkbox on a *root* location returned `400 Failed to deserialize form body: parent_id: cannot parse integer from empty string`. The location-edit form's `\u003cselect name=\"parent_id\"\u003e\u003coption value=\"\"\u003eNone\u003c/option\u003e...\u003c/select\u003e` submits `parent_id=` (empty) for root locations, and `#[serde(default)] Option\u003cu64\u003e` only short-circuits ABSENT fields — empty strings still hit the u64 parser. New `deserialize_optional_u64` helper (sibling of the existing `_i32` and `_f64` ones) treats empty as `None`; wired to `CreateLocationForm.parent_id`, `UpdateLocationForm.parent_id`, AND `VolumeEditForm.condition_state_id` (latent there too — picking \"no condition\" would 400 the same way) ([#296](https://github.com/guycorbaz/mybibli/issues/296)). Latent since the location-edit feature shipped; only surfaced now because CR #280 gave users a new reason to edit root containers. **v1.6.2** is a dependency-hygiene patch: `cargo update` applied 74 caret-compatible bumps including axum 0.8.8 → 0.8.9, tokio 1.50 → 1.52, openssl 0.10.76 → 0.10.80 (security-relevant), rustls 0.23.37 → 0.23.40, hyper 1.8 → 1.9, tower-http 0.6.8 → 0.6.11, and the wasm-bindgen / icu_* / zerocopy stacks. No code changes; 909 unit tests + integration suite + `sqlx-prepare --check` all green. `cargo audit` confirms no new advisories — the 2 pre-existing upstream-blocked ones (`rsa` via sqlx-mysql, `time 0.2` via genpdf) remain. **v1.7.0** is the sixth themed minor — \"Reach more users, debug more easily\". Three CRs: a **German UI translation** ([#275](https://github.com/guycorbaz/mybibli/issues/275), ~900 keys, Sie-form formal) and an **Italian UI translation** ([#276](https://github.com/guycorbaz/mybibli/issues/276), ~900 keys, tu-form informal, Italian guillemets «…») bring mybibli's interface to four languages; plus a **persistent log directory with rotation** ([#301](https://github.com/guycorbaz/mybibli/issues/301), new `mybibli_logs` named volume, daily rotation, configurable retention, admin-controlled log level via `/admin \u003e System` without redeploy). New manual chapter 12 (\"Operations \u0026 debugging\") in EN + FR. **v1.7.1** ships the missing slice of v1.7.0 (#308 — log-level admin UI that release notes had promised but code didn't ship) plus four production-surfaced bugfixes from the v1.7.0 NAS upgrade: provider-health probe timeout too tight for typical home-NAS DNS+TLS handshakes ([#310](https://github.com/guycorbaz/mybibli/issues/310), `MYBIBLI_PROVIDER_HEALTH_TIMEOUT_SECS` env var, default 10s); `DELETE /admin/api-keys/{id}` returned 422 because HTMX `hx-delete` puts form fields in the query string ([#309](https://github.com/guycorbaz/mybibli/issues/309)); bulk cover-refetch silently no-oped on cached titles ([#311](https://github.com/guycorbaz/mybibli/issues/311), new `force_refresh` parameter); DB pool exhaustion during 64-title bulk-refetch ([#312](https://github.com/guycorbaz/mybibli/issues/312), Semaphore-capped concurrency ≤ pool_size − 2). **v1.7.2** closes the two workflow gaps surfaced after the v1.7.1 NAS upgrade: HTMX search swap now emits BOTH the list-mode `\u003ctable\u003e` and the grid-mode cards wrapper so both surfaces stay correct across mode toggle ([#315](https://github.com/guycorbaz/mybibli/issues/315), CSS class on `#browse-results` picks which renders); and `/title/:id` exposes proper Librarian+ Add / Remove affordances for the contributor block — the backend routes existed since v1.0 but were only wired into the catalog scan flow ([#316](https://github.com/guycorbaz/mybibli/issues/316) + [#318](https://github.com/guycorbaz/mybibli/issues/318)). **v1.7.3** is a one-fix hotfix for a latent regression that had shipped silently since v1.6.0: `/audit` returned 500 the moment a flagged volume existed ([#321](https://github.com/guycorbaz/mybibli/issues/321), `Option\u003cu64\u003e` vs `CAST AS SIGNED` type mismatch — textbook CLAUDE.md MariaDB gotcha #2). The empty-result branch never decoded a row and CI E2E coverage seeded zero flagged volumes, so the bug only fired in prod the moment a librarian marked the first volume \"À contrôler\" on the v1.7.2 NAS. Same root cause also closed [#317](https://github.com/guycorbaz/mybibli/issues/317) — the \"large black circle with cross\" icon previously reported on `/audit` was the browser's default error glyph on the 500 page. **v1.7.4** is a polish patch bundling one user-visible fix and eight code-review-finding clean-ups from the issue queue: manual title creation 422 on empty numeric fields ([#323](https://github.com/guycorbaz/mybibli/issues/323), third recurrence of the #238 / #296 gotcha pattern); plus the clean-ups — setup-wizard recap fallback to EN for unknown locales ([#97](https://github.com/guycorbaz/mybibli/issues/97), v1.7.0 added DE+IT but the recap routed both to FR), duplicate `version=N` in trash permanent-delete modal POST URL ([#78](https://github.com/guycorbaz/mybibli/issues/78)), auto-purge drain-cap inflating `errors_count` instead of using a dedicated `tables_capped` counter ([#77](https://github.com/guycorbaz/mybibli/issues/77)), unvalidated `entity_type` query filter in `permanent_delete_confirm` ([#76](https://github.com/guycorbaz/mybibli/issues/76)), three independent parsers of `setup_completed_at` consolidated into one helper ([#95](https://github.com/guycorbaz/mybibli/issues/95)), CI safety net asserting `ALLOWED_TABLES` ⊆ `PURGE_DELETION_ORDER` ([#63](https://github.com/guycorbaz/mybibli/issues/63)), unused `AdminAuditModel::list()` deleted ([#72](https://github.com/guycorbaz/mybibli/issues/72)), `nav.js` burst-threshold clamped to 10ms floor ([#147](https://github.com/guycorbaz/mybibli/issues/147)), and a new CI grep gate against unscoped `a[href^=/title/]` selectors in E2E specs ([#106](https://github.com/guycorbaz/mybibli/issues/106)). Companion verification: integration test [#320](https://github.com/guycorbaz/mybibli/issues/320) locks the manually-edited Dewey-preserve-on-re-fetch contract. **v1.7.5** is a focused patch bundling one user-visible fix and seven hardening commits: Add / Remove contributor on `/title/:id` were silently failing with `htmx:targetError` because the page was missing the `#feedback-list` slot the contributor form hardcodes ([#325](https://github.com/guycorbaz/mybibli/issues/325), regression of #318 / v1.7.2 — also adds a `templates_audit` test that panics on any future page that references `hx-target=\"#feedback-list\"` without declaring the slot); hardening — locked FR95's `/series` anonymous-readable contract ([#152](https://github.com/guycorbaz/mybibli/issues/152), missing role-gate test added), `forms_include_csrf_token` audit now enforces strict first-child placement of `_csrf_token` ([#42](https://github.com/guycorbaz/mybibli/issues/42)), `modal_confirm_retarget_guard` tolerates JSON-object HX-Trigger shape ([#220](https://github.com/guycorbaz/mybibli/issues/220), defensive), `scanner-guard.js` honors `maxLength` and skips while IME composition is active ([#31](https://github.com/guycorbaz/mybibli/issues/31)), `admin_system::run_provider_update` consolidated onto the existing `save_setting` (DRY, [#90](https://github.com/guycorbaz/mybibli/issues/90)), validation error on `save_loans_settings` + `save_language_settings` re-renders the form with the user's submitted value + emits `HX-Trigger: validation-error` so `csrf.js` opts the 400 body in ([#91](https://github.com/guycorbaz/mybibli/issues/91)), and new `rust_emitted_post_forms_include_csrf_token` audit walking `src/**/*.rs` for inline `\u003cform method=\"POST\"\u003e` literals without `_csrf_token` nearby ([#48](https://github.com/guycorbaz/mybibli/issues/48)). **v1.7.6** is a focused patch with two user-visible feature fixes and three hardening commits: `media_type` is now editable on `/title/:id` ([#331](https://github.com/guycorbaz/mybibli/issues/331)) — a BD scanned via ISBN that the BnF resolved as a \"book\" can finally be reclassified through a dropdown on the edit form, with an `admin_audit` entry recording the before/after; manual cover upload ([#335](https://github.com/guycorbaz/mybibli/issues/335)) — Librarian+ can drop a JPG / PNG / WebP / GIF on `/title/:id` as the safety net for titles where no provider has a cover (the Open Library Covers fallback misses most French BDs, mangas, and small-publisher technical books — diagnosed in [#333](https://github.com/guycorbaz/mybibli/issues/333) on the prod NAS at 73 / 146 miss rate), with the cover URL marked `manually_edited` so the bulk-refetch admin action will not clobber it; hardening — recent-additions card test coverage closes the `Some(d)/Some(c)` branch gap ([#109](https://github.com/guycorbaz/mybibli/issues/109)), CSRF exempt-route comparison tolerates path variants Axum routes identically ([#40](https://github.com/guycorbaz/mybibli/issues/40), `POST /login/` and `POST //login` now bypass CSRF as intended), and `session_resolve_middleware` skips static-asset routes (`/static`, `/covers`, `/logo`) — every home-page load previously fanned out dozens of session-row reads on the asset fetches ([#36](https://github.com/guycorbaz/mybibli/issues/36)). **v1.7.7 (current)** is a test-rigor patch bundling one user-visible enhancement and seven code-review-finding clean-ups: the series detail page (`/series/:id`) now renders each position as a card with cover thumbnail + \"Vol. N\" prefix + title + author instead of a compact position cell, with the same card shape for missing volumes so collectors see at a glance which slots are still gaps ([#336](https://github.com/guycorbaz/mybibli/issues/336), responsive 2-col mobile → 6-col xl, preserves the existing ARIA grid/gridcell contract); plus the test-rigor sweep — DB-snapshot helper proving anonymous contributor POST left the DB untouched ([#43](https://github.com/guycorbaz/mybibli/issues/43)), real seeded items + real assertions in the admin permanent-delete spec ([#62](https://github.com/guycorbaz/mybibli/issues/62), drops the `if (tableExists)` silent no-op pattern), cross-row concurrency E2E for admin system settings ([#88](https://github.com/guycorbaz/mybibli/issues/88), two `browser.newContext()` admin sessions race on different settings rows), 2 missing locale-resolution-chain branches backfilled ([#89](https://github.com/guycorbaz/mybibli/issues/89), Accept-Language wins, user pref wins), Rust integration test for the concurrent first-launch race ([#96](https://github.com/guycorbaz/mybibli/issues/96), two `tokio::spawn` Step 1 POSTs assert exactly one admin row), dashboard indicator no-op fixed for unshelved via seeded scan ([#119](https://github.com/guycorbaz/mybibli/issues/119), overdue gap tracked as follow-up #340), and a template-layer lock for the AC5 single-active-filter contract ([#120](https://github.com/guycorbaz/mybibli/issues/120), two render unit tests). v1.7 is currently the last themed minor on the roadmap; future work is GH-issue-driven polish and production-driven fixes unless a new theme is greenlit. See [`epics.md`](_bmad-output/planning-artifacts/epics.md) for the full breakdown and [`sprint-status.yaml`](_bmad-output/implementation-artifacts/sprint-status.yaml) for the story-by-story state.\n\n## License\n\nLicensed under the **GNU Affero General Public License v3.0 or later** (AGPL-3.0-or-later). See [`LICENSE`](LICENSE) for the full text.\n\nAGPL was chosen deliberately to keep mybibli and any fork freely modifiable by end users, including forks that are hosted as a service: if you run a modified version, you must offer the corresponding source to your users.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguycorbaz%2Fmybibli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguycorbaz%2Fmybibli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguycorbaz%2Fmybibli/lists"}