Metadata-Version: 2.4
Name: compendium-ils
Version: 1.2.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

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).
- **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`).

## 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).
