Metadata-Version: 2.4
Name: compendium-ils
Version: 1.3.0
Summary: A library card catalog system for physical items
Project-URL: Homepage, https://github.com/statyk/compendium
Project-URL: Repository, https://github.com/statyk/compendium
Project-URL: Issues, https://github.com/statyk/compendium/issues
Project-URL: Documentation, https://github.com/statyk/compendium/blob/master/README.md
Author-email: Shawn <spmoore01@gmail.com>
License: MIT
License-File: LICENSE
Keywords: books,catalog,circulation,ils,library,lms,marc,opac
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Other Audience
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Other/Nonlisted Topic
Requires-Python: >=3.11
Requires-Dist: alembic>=1.13
Requires-Dist: bcrypt>=4.0
Requires-Dist: cryptography>=42.0
Requires-Dist: defusedxml>=0.7.1
Requires-Dist: fastapi>=0.111
Requires-Dist: httpx>=0.27
Requires-Dist: jinja2>=3.1
Requires-Dist: pydantic-settings>=2.0
Requires-Dist: pyjwt>=2.8
Requires-Dist: pymarc>=5.2
Requires-Dist: python-barcode>=0.16.1
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: reportlab>=4.0
Requires-Dist: sqlalchemy<3.0,>=2.0
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn[standard]>=0.29
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: testcontainers[postgres]>=4.8; extra == 'dev'
Provides-Extra: e2e
Requires-Dist: pytest-playwright>=0.5; extra == 'e2e'
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
Description-Content-Type: text/markdown

# Compendium

[![PyPI](https://img.shields.io/pypi/v/compendium-ils)](https://pypi.org/project/compendium-ils/)

A library card catalog system for physical items — books, vinyl records, DVDs, CDs.

## Features

- **Catalog** — add items by ISBN / UPC / MusicBrainz ID / TMDb ID / title search (Google Books / Open Library, MusicBrainz, TMDb) or manually for obscure items; search and browse works and copies; faceted discovery (media type, decade, availability)
- **Circulation** — checkout, checkin, loan renewal with category-aware per-media-type loan policies; lost / damaged / claims-returned states; self-checkout kiosk mode; library hours and holiday calendar so due dates never land on closed days and overdue fines skip closed days
- **Holds** — patron reservation queue; immediate promotion when a copy is available; suspend/resume; auto-expiry via maintenance command
- **Fines** — configurable per-policy overdue rates with caps and grace periods; lost/damaged fees; threshold-based checkout/hold blocking; pay/waive workflow; per-patron and bulk overdue assessment
- **Notifications** — outbox-pattern email delivery (hold-ready, due-soon, overdue) drained by a cron-invoked CLI; admin viewer + retry; per-patron opt-out; configurable retention
- **Reports** — checkouts/month, popular works, dormant items (weeding list), current overdues; CSV export; Chart.js trendlines
- **Patrons & cards** — patron categories (Adult/Child/Staff/Teacher seeded), card expiry with maintenance auto-deactivation, optional 1:1 patron↔user link for self-service
- **Bulk import/export** — round-trippable CSV; MARC21 binary + MARCXML import/export; LibraryThing TSV import; GoodReads CSV import (all with lenient encoding for messy real-world exports)
- **Backup/restore** — portable JSONL tarballs; backend-agnostic (SQLite ↔ Postgres); doubles as a DB migration path
- **Labels** — Avery-template item labels (spine / pocket) and patron cards (full / sticker) as PDFs. Spine templates: `avery-5167-spine` (½" narrow face), `avery-5160-spine` (1" medium face, rotated), `avery-5160` (flat wrap-around, centered text), `avery-22805`/`avery-22806` (square classification labels). Live in-page SVG preview updates as you change kind/template/fields — no PDF round-trip needed.
- **Curated lists** — librarian-editable named collections of Works ("Staff picks", "Summer reads") with slugs, per-work annotations, public/private and featured toggles; featured lists appear as a shelf on the OPAC landing page; public browse at `/ui/lists`
- **Auth** — five preset roles (ReadOnly, Patron, Librarian, SystemAdmin, Administrator) plus custom roles via the admin UI; JWT for API, cookie-based for web UI
- **Audit log** — synchronous trail of administrative mutations (Librarian + system tier); queryable via web UI, CLI, or REST
- **DB-editable settings** — most configuration knobs (library name, fines, kiosk timeout, SMTP, retention, configurable nav shortcuts, etc.) editable from the UI / CLI / API; env vars still win as a break-glass
- **Remote phone scanner** — pair a phone to the circulation desk via QR code; the phone camera dispatches scans to the desk in real time without installing an app. Supports checkout, checkin, and catalog-lookup modes. Pairing is ephemeral: a short-TTL claim secret lives in the QR; after the phone claims it, the secret rotates to a session cookie; the librarian can unpair from the desk at any time. Requires HTTPS on both the server and the phone browser (secure context for camera). Settings: `public_base_url` (DB-editable, env `COMPENDIUM_PUBLIC_BASE_URL`) and `scan_session_minutes` (DB-editable, env `COMPENDIUM_SCAN_SESSION_MINUTES`, default 60).
- **Circulate by ISBN/UPC** — checkout, checkin, and renew accept a book's printed ISBN barcode (or a disc's UPC) in place of a Compendium item label, so home and classroom libraries don't need to print labels. With several copies of the same title on loan, checkin asks the desk which copy came back rather than guessing. Toggle with the `circulation_scan_isbn_enabled` setting (env `COMPENDIUM_CIRCULATION_SCAN_ISBN_ENABLED`, default on) at Admin → Settings → Circulation.
- **Web UI** — HTMX + Jinja2 browser interface with catalog search, circulation desk (camera-based barcode scanning), patron self-service, light/dark/auto theme; per-user nav shortcut overrides via browser localStorage
- **REST API** — FastAPI; consumed by the web UI and available for integrations
- **CLI** — full librarian + sysadmin workflow without running a server, including stdin/stdout (`-`) for backup, import/export, and labels

## Install

```bash
# Install the CLI as a standalone tool (recommended for production)
uv tool install compendium-ils           # SQLite only
uv tool install "compendium-ils[postgres]"  # + Postgres support

# Or with pip
pip install compendium-ils
pip install "compendium-ils[postgres]"
```

The installed command is **`compendium`**.

## Quick start

### Prerequisites

Python 3.11.4 or newer. (Backup restore uses the tarfile `data` filter from
PEP 706, which was backported to 3.11.4.)

**Debian / Ubuntu**
```bash
sudo apt-get update
sudo apt-get install -y python3 python3-venv python3-dev build-essential
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
```

**RedHat / CentOS / Fedora**
```bash
sudo dnf install -y python3 python3-devel gcc
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
```

**macOS**
```bash
brew install python uv
```

### Run

```bash
# Install dependencies
uv sync --extra dev

# Initialise the database (creates ./compendium.db with SQLite by default)
uv run compendium db init

# Create an Administrator account
uv run compendium user add --username admin --role Administrator

# Add a book by ISBN (looks up metadata from Google Books when key is set, else Open Library)
uv run compendium item add --isbn 9780441013593

# Add a patron
uv run compendium patron add --name "Alice Example"

# Start the server (web UI at http://localhost:8000/ui/catalog)
uv run compendium serve
```

Log in at `http://localhost:8000/ui/login` with the username and password you set above.

## CLI reference

Run `compendium --help` for the full command tree, or `compendium <group> --help` for the subcommands of a specific group.

**Catalog & cataloging**
| Group | Common subcommands |
|---|---|
| `item` | `add` (--isbn / --upc / --mbid / --tmdb-id / --title), `add-manual`, `show`, `list`, `withdraw`, `set-loanable`; `note add/list/delete`; `declare-lost`, `mark-damaged`, `clear-lost`, `clear-damage` |
| `work` | `search`, `show`, `new-arrivals`, `recently-returned` |
| `creator` | `list`, `show`, `merge` |
| `curated-list` | `create`, `list`, `show`, `edit`, `delete`, `add-work`, `remove-work`, `reorder` |
| `branch` | `list`, `set` |
| `import` | `csv`, `marc` (use `-` for stdin) |
| `export` | `csv`, `marc` (use `-` for stdout) |

**Circulation**
| Group | Common subcommands |
|---|---|
| `loan` | `checkout`, `checkin`, `renew`, `active`, `list` (system-wide), `history`, `item-history` |
| `claim` | `returned` (patron disputes), `verify` (found), `write-off`, `list` |
| `hold` | `place`, `cancel`, `list`, `queue`, `suspend`, `resume`, `list-suspended` |
| `fine` | `list`, `pay`, `waive`, `assess`, `assess-overdue` |

**Patrons & accounts**
| Group | Common subcommands |
|---|---|
| `patron` | `add`, `list` (`--include-inactive`), `set`, `link-user`, `unlink-user`, `deactivate`, `reactivate` |
| `patron-category` | `list`, `create`, `update`, `delete` |
| `user` | `add` (default --role Administrator), `list` (`--include-inactive`), `set-role`, `set-password`, `deactivate`, `reactivate` |
| `role` | `list`, `create`, `update`, `clone` |
| `policy` | `list`, `create`, `set` (configures fines, category-aware) |

**Reporting & labels**
| Group | Common subcommands |
|---|---|
| `reports` | `checkouts`, `popular`, `dormant`, `overdues` (each supports `--format csv`) |
| `labels` | `templates`, `spine`, `pocket`, `barcode`, `patron-card`, `patron-sticker` (output `-o -` writes PDF to stdout; use `--show`/`--hide` for field toggles, e.g. `--show barcode` on spine) |
| `audit` | `list` (filters: `--entity`, `--id`, `--user-id`, `--limit`) |

**Operations**
| Command | Description |
|---|---|
| `compendium db init` / `db upgrade` / `db history` | Migrate the database |
| `compendium serve` | Start the API + web UI server |
| `compendium backup --output <path-or->` | Write a portable JSONL tarball backup |
| `compendium restore <path-or->` | Restore from a backup tarball (lenient — auto-migrates) |
| `compendium settings list/get/set/reset` | Inspect & edit DB-backed site settings |
| `compendium maintenance ...` | Cron-invoked tasks: `expire-holds`, `resume-expired-suspends`, `assess-overdue-fines`, `queue-due-soon-notices`, `queue-overdue-notices`, `send-queued-notifications`, `prune-notifications`, `prune-audit-log`, `deactivate-expired-patrons`, `prune-cover-cache`, `prune-metadata-cache`, `prune-scan-pairings`, `refresh-metadata` |
| `compendium metadata cache stats` | Show metadata cache row counts by adapter and TTL status |
| `compendium metadata cache clear` | Delete all metadata cache rows (audited) |

File-argument commands (`backup`, `restore`, `import`, `export`, `labels spine/pocket/barcode/patron-card/patron-sticker`) accept `-` for stdin/stdout. Status messages are routed to stderr in stdio mode so they don't corrupt binary output.

## Web UI

Start the server with `compendium serve` and open your browser to `http://localhost:8000/ui/catalog`.

"Audience" lists the *minimum* preset that can access each route. Administrator can access everything. SystemAdmin gets the System-tier rows; slimmed Librarian gets librarian-tier rows but not System-tier.

| URL | Audience | Description |
|---|---|---|
| `/ui/catalog` | Anyone | Search catalog with facets (media type, decade, availability); featured curated lists shelf |
| `/ui/catalog/{work_id}` | Anyone | Work detail, items, place-hold button |
| `/ui/lists` | Anyone | Public OPAC browse of public curated lists |
| `/ui/lists/{slug}` | Anyone | Public curated list detail with annotated works |
| `/ui/login` | Anyone | Login form |
| `/ui/me/loans` | Patron | Active loans with inline renew + "I returned this" claim |
| `/ui/me/holds` | Patron | Active holds with inline cancel + suspend/resume |
| `/ui/me/fines` | Patron | Outstanding and historical fines |
| `/ui/me/preferences` | Patron | Notification opt-out |
| `/ui/me/password` | Patron | Self-service password change |
| `/ui/circ` | Librarian | Circulation desk — checkout / checkin / renew |
| `/ui/kiosk` | Librarian | Self-checkout kiosk landing/session (patron card-number-only auth) |
| `/ui/items/new` | Librarian | Add item by ISBN / UPC / MBID / TMDb ID / title search (with barcode scanner) |
| `/ui/items/new/manual` | Librarian | Manually add an item not found in external sources |
| `/ui/items/{barcode}` | Librarian | Item detail, loan history, withdraw, set-loanable, lost/damaged/claims actions |
| `/ui/patrons` | Librarian | Patron list |
| `/ui/patrons/new` | Librarian | Create patron (with category + expiry) |
| `/ui/patrons/{card}` | Librarian | Patron detail with active loans, holds, link/unlink user, deactivate, reactivate |
| `/ui/patrons/{card}/loans` | Librarian | Patron loan history (active / returned / all) |
| `/ui/patrons/{card}/fines` | Librarian | Patron fines with pay/waive |
| `/ui/admin/loans` | Librarian | All active loans (system-wide) with overdue/due-soon filters |
| `/ui/admin/fines` | Librarian | All outstanding fines (system-wide) with running total |
| `/ui/admin/holds` | Librarian | All active holds with status/branch/work filters |
| `/ui/admin/claims` | Librarian | Outstanding claims-returned investigations |
| `/ui/admin/notifications` | Librarian | Notification log + manual retry |
| `/ui/admin/import` | Librarian | Bulk CSV / GoodReads CSV / LibraryThing TSV / MARC import |
| `/ui/admin/export` | Librarian | Bulk CSV/MARC export |
| `/ui/admin/patron-categories` | Librarian | Manage patron categories |
| `/ui/curated-lists` | Librarian | Curated list admin CRUD (`curatedlist.manage`) |
| `/ui/admin/settings/general` | Librarian | Library name, default theme, guest search |
| `/ui/admin/settings/circulation` | Librarian | Currency, fine thresholds, hold/overdue/due-soon defaults |
| `/ui/admin/settings/kiosk` | Librarian | Kiosk idle timeout |
| `/ui/scan/pair` | Librarian | Generate a QR code to pair a phone as a remote barcode scanner |
| `/ui/scan/desk` | Librarian | Desk-side view: shows live scan count and unpair button for an active phone session |
| `/ui/scan/phone/*` | Phone browser | Phone-side claim, mode selector, and continuous scan loop (camera; requires HTTPS / secure context) |
| `/ui/policies` | Librarian | Loan policy list with inline edit |
| `/ui/policies/new` | Librarian | Create loan policy (per media type + patron category) |
| `/ui/branches` | Librarian | Branch list with classification scheme |
| `/ui/branches/{id}/edit` | Librarian | Edit a branch's default classification scheme |
| `/ui/reports` | Librarian | Reports landing — checkouts, popular, dormant, overdues |
| `/ui/labels` | Librarian | Generate item-label and patron-card PDFs (Avery templates) |
| `/ui/audit` | Librarian | Audit log viewer |
| `/ui/users` | SystemAdmin | User list |
| `/ui/users/new` | SystemAdmin | Create user |
| `/ui/users/{username}` | SystemAdmin | User detail — change role, deactivate, reactivate, reset password |
| `/ui/roles` | SystemAdmin | Role list |
| `/ui/roles/new` | SystemAdmin | Create custom role |
| `/ui/roles/{id}` | SystemAdmin | Role detail — edit permissions, clone |
| `/ui/admin/system/smtp` | SystemAdmin | SMTP host/port/from settings |
| `/ui/admin/system/secrets` | SystemAdmin | Encrypted secrets (SMTP password, TMDb / Google Books API keys) — requires `COMPENDIUM_SECRET_KEY` |
| `/ui/admin/system/retention` | SystemAdmin | Notification + audit retention, batch sizes |

Guest catalog search is enabled by default (toggle via `/ui/admin/settings/general` or `COMPENDIUM_GUEST_SEARCH_ENABLED=false`).

ISBN/UPC circulation fallback is enabled by default (toggle via `/ui/admin/settings/circulation` or `COMPENDIUM_CIRCULATION_SCAN_ISBN_ENABLED=false`).

## REST API endpoints

The API is mounted at the root. Authenticate with `POST /auth/login` and pass the result as `Authorization: Bearer <token>`. Interactive docs live at `http://localhost:8000/docs` when the server is running.

Below is a high-level inventory grouped by concern; the OpenAPI document is the source of truth for parameters and bodies.

| Group | Common endpoints | Min permission |
|---|---|---|
| **Auth** | `POST /auth/login` | none |
| **Catalog** | `GET /works/search`, `/works/new-arrivals`, `/works/recently-returned`; `GET /items/{barcode}`; `POST /items/{barcode}/{withdraw,loanable,lost,damaged,clear-lost,clear-damage}` | guest / `item.view` / `item.delete` / `fine.manage` |
| **Patrons** | `GET/POST /patrons`, `PATCH /patrons/{card}`, `POST /patrons/{card}/{deactivate,reactivate}`, `POST /patrons/{card}/account`, `GET/POST /patron-categories`, `PATCH/DELETE /patron-categories/{id}` | `patron.manage` / `patron.account.manage` for account endpoints |
| **Loans** | `POST /loans/checkout`, `/loans/{id}/{checkin,renew}`; `GET /loans` (system-wide), `/loans/patron/{card}`, `/loans/item/{barcode}` | `loan.*` (see below) |
| **Claims** | `GET /claims`; `POST /claims/{barcode}/{returned,verify,write-off}` | `loan.checkin` |
| **Holds** | `GET /holds`, `/holds/queue/{work_id}`; `POST/DELETE /holds`, `/holds/{id}`; `POST /holds/{id}/{suspend,resume}` | `hold.*` |
| **Fines** | `GET /fines`, `GET/POST /patrons/{card}/fines`, `POST /fines/{id}/{pay,waive}`, `POST /patrons/{card}/fines/assess-overdue` | `fine.manage` / `fine.view.self` |
| **Self-service** | `GET /me/loans`, `/me/holds`; `POST /me/holds`, `/me/holds/{id}/{suspend,resume}`; `DELETE /me/holds/{id}`; `POST /me/loans/{id}/{renew,claim-returned}` | `*.self` permissions |
| **Notifications** | `GET /notifications`, `POST /notifications/{id}/retry` | `notification.manage` |
| **Reports** | `GET /reports/{checkouts,popular,dormant,overdues}` | `report.view` |
| **Bulk import/export** | `POST /import/{csv,goodreads,librarything,marc}` (multipart), `GET /export/{csv,marc}` (streaming) | `catalog.import` / `item.view` |
| **Labels** | `GET /labels/items`, `/labels/patrons` (PDF) | `labels.generate` |
| **Policies** | `GET/POST /policies` | `item.view` / `policy.edit` |
| **Users** | `POST /users`, `POST /users/{username}/{deactivate,reactivate}`, `POST/DELETE /users/{username}/patron` | `user.manage` |
| **Curated lists** | `GET/POST /curated-lists`, `PATCH/DELETE /curated-lists/{slug}`, `POST/DELETE /curated-lists/{slug}/works` | guest / `curatedlist.manage` |
| **Settings** | `GET /settings/`, `GET/PATCH/DELETE /settings/{key}` | `patron.manage` (librarian-tier) / `system.manage` (system-tier) |
| **Calendar** | `GET /library-hours/`, `PATCH /library-hours/{weekday}`; `GET/POST /closed-dates/`, `PATCH/DELETE /closed-dates/{id}` | `calendar.manage` |
| **Audit** | `GET /audit/` | `audit.view` |

The "min permission" column lists the lowest preset role that's allowed. Administrator (wildcard) covers everything. Slimmed Librarian covers librarian-tier endpoints; SystemAdmin covers user/role/system-tier endpoints.

## Configuration

Compendium settings come from three layers, in order of precedence:

1. **Environment variable** (`COMPENDIUM_<KEY>`) — break-glass override, wins over everything.
2. **`site_setting` table** — DB-backed, editable from `/ui/admin/settings/*`, the CLI (`compendium settings ...`), or the API (`PATCH /settings/{key}`). Changes take effect on the next page render — no restart.
3. **Registry default** — fallback hard-coded in `services/settings_registry.py`.

Most settings (library name, theme, fine thresholds, hold/overdue defaults, kiosk timeout, SMTP host/port/from, retention) are **DB-editable**. A handful stay env-only because they're either secrets or required before the DB is reachable:

| Env var | Why env-only |
|---|---|
| `COMPENDIUM_DATABASE_URL` | Bootstrap — needed before any DB read |
| `COMPENDIUM_JWT_SECRET_KEY` | Secret — **required**; server refuses to start with the built-in default. Generate with `python -c "import secrets; print(secrets.token_urlsafe(48))"`. Set `COMPENDIUM_ALLOW_INSECURE_JWT=1` to bypass for first-run/dev only. |
| `COMPENDIUM_JWT_ALGORITHM` / `COMPENDIUM_JWT_EXPIRE_MINUTES` | Auth deployment knobs |
| `COMPENDIUM_SSL_CERTFILE` / `COMPENDIUM_SSL_KEYFILE` | OS-level paths |
| `COMPENDIUM_SECURE_COOKIES` | Default `true`; set `false` only for plain-HTTP LAN deployments (browsers won't send `Secure` cookies over non-HTTPS, except localhost). |
| `COMPENDIUM_SECRET_KEY` | Encryption key for secrets stored in the DB (SMTP password, API keys). Optional — set to enable the `/ui/admin/system/secrets` page. Generate with `compendium keygen --secret`. |
| `COMPENDIUM_TMDB_API_KEY` | TMDb API key. Override — also settable via Admin → System → Secrets when `COMPENDIUM_SECRET_KEY` is configured. |
| `COMPENDIUM_GOOGLE_BOOKS_API_KEY` | Google Books API key (primary book metadata source when set). Override — also settable via Admin → System → Secrets when `COMPENDIUM_SECRET_KEY` is configured. |
| `COMPENDIUM_BOOK_METADATA_SOURCE_PREFERENCE` | Primary book metadata source: `googlebooks` (default; uses GB when key is set, else OL) or `openlibrary`. DB-editable at **Admin → System → Metadata Sources**. |
| `COMPENDIUM_BOOK_METADATA_FALLBACK_ENABLED` | When `true` (default), a miss from the primary book adapter automatically tries the secondary (GB→OL and OL→GB symmetric). Set `false` to disable. DB-editable at **Admin → System → Metadata Sources**. |
| `COMPENDIUM_METADATA_CACHE_TTL_DAYS` | Positive-hit TTL for the metadata cache in days (default 30). Negative (not-found) entries always expire after 24 hours. DB-editable at **Admin → System → Metadata Sources**. |
| `COMPENDIUM_SMTP_PASSWORD` | SMTP password. Override — also settable via Admin → System → Secrets when `COMPENDIUM_SECRET_KEY` is configured. |
| `COMPENDIUM_MAX_UPLOAD_BYTES` | Hard cap on bulk-import upload size (default 100 MB / 104857600). Env-only so a compromised admin token can't raise it. |
| `COMPENDIUM_LOGIN_MAX_FAILURES` | Max consecutive failed logins before the identity is throttled (default 10). DB-editable at **System → Security**. |
| `COMPENDIUM_LOGIN_FAILURE_WINDOW_SECONDS` | Sliding window for counting failures in seconds (default 300 = 5 min). DB-editable at **System → Security**. |
| `COMPENDIUM_PUBLIC_BASE_URL` | External HTTPS origin used to build QR codes for phone pairing (e.g. `https://library.example.org`). The QR base URL is normally derived from the request, honoring `X-Forwarded-Proto` — the bundled nginx sets this header, so the shipped Docker stack works without this setting. Set it when your proxy does not set that header, or to pin a specific hostname. A non-HTTPS value is refused at QR-render time. DB-editable at **Admin → Settings → General**; env wins. |
| `COMPENDIUM_SCAN_SESSION_MINUTES` | How long a paired phone session stays active in minutes (default 60). DB-editable at **Admin → Settings → General**; env wins. |

For everything else, run `compendium settings list` to see the current value, source (env vs db/default), and per-key help text. The web admin UI shows the same with an "⚠ Overridden by env var" indicator on rows where an env var is currently masking the DB value.

See [`docs/deployment.md`](docs/deployment.md) for full deployment guidance, or [`docker/README.md`](docker/README.md) for the bundled Docker Compose setup (app + Postgres + nginx with auto-generated self-signed TLS).

#### Docker quick start

A pre-built multi-arch image (`linux/amd64` + `linux/arm64`) is published to
GitHub Container Registry on every release:

```bash
cd docker
cp .env.example .env && $EDITOR .env   # set passwords and JWT secret
docker compose pull
docker compose up -d
```

Image: `ghcr.io/statyk/compendium` — tags `latest`, `X.Y.Z`, `X.Y` are
published for each release. Pin a version in `.env` with
`COMPENDIUM_IMAGE=ghcr.io/statyk/compendium:1.0.2`.

### PostgreSQL

SQLite is the default and fine for home or classroom use (up to ~10k items). For larger collections or multiple concurrent writers, use PostgreSQL:

```bash
uv sync --extra postgres
```

Then set:

```dotenv
COMPENDIUM_DATABASE_URL=postgresql+psycopg://compendium:<password>@localhost:5432/compendium
```

and run `compendium db init` to apply migrations. Full setup (creating the role and database, TLS, backups) is in [`docs/deployment.md`](docs/deployment.md#postgresql-setup).

## Scheduled maintenance

Several maintenance commands need to run on a cadence — most importantly the
email outbox drain (`send-queued-notifications`), without which queued
notifications never go out. Install the bundled crontab via:

```bash
scripts/install-cron.sh
```

By default this writes the project path into the crontab and points logs at
`$HOME/.local/state/compendium/maintenance.log`. Override either with flags:

```bash
scripts/install-cron.sh --project-dir /opt/compendium --log-file journal
scripts/install-cron.sh --log-file /var/log/compendium/maintenance.log
```

`--log-file journal` routes output to the systemd journal (view with
`journalctl -t compendium-maintenance -f`). For paths the installer can't
create unprivileged (e.g. `/var/log/...`), it prints the one-time `sudo`
command and exits without touching the crontab.

See [`docs/crontab.sample`](docs/crontab.sample) for the full schedule and
[`docs/compendium.service.sample`](docs/compendium.service.sample) for
running the daemon under systemd.

## Running tests

```bash
uv run pytest -q
```

Tests are split into `tests/unit/` (no DB, mock repos) and `tests/integration/` (SQLite in-memory).

### Browser tests (E2E)

Browser tests run against a real `compendium serve` subprocess in Chromium. They are excluded from the default `pytest` run and require a one-time install:

```bash
uv sync --extra e2e
playwright install chromium
```

Then:

```bash
uv run pytest -m e2e
```

Expected wall time: 30–60 seconds. Tests live in `tests/e2e/`. The `test_csp_no_console_errors.py` test is the keystone: it navigates to every major page and asserts no console errors, which catches CSP/HTMX loading regressions that unit tests miss.

## Layout

```
src/compendium/
├── domain/        # models, enums, permissions, errors
├── repositories/  # base protocols + SQLAlchemy implementations
├── services/      # business logic (catalog, circulation, holds, patrons, policies, auth, audit)
├── api/           # FastAPI routes + Pydantic schemas + JWT auth
├── web/           # HTMX + Jinja2 web UI + CSRF protection
├── cli/           # Typer CLI commands
├── config/        # settings, seed data
└── db/            # engine factory, session lifecycle
```

## Changelog

Release-by-release changes are recorded in [CHANGELOG.md](CHANGELOG.md).

## License

MIT (to be finalised before first release).
