Files
bike_admin/AGENTS.md
T
Jake 12494bf30c Docs: document MOT/insurance renewals, due-soon state, new settings
- README: feature list now covers renewal tracking, the due-soon service
  state, and the full event-type set.
- AGENTS: add the three hint settings, compute_renewal_due, favicon.svg,
  and test_renewals.py to the layouts.
2026-09-10 10:24:25 +01:00

96 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md
Guidance for working in this repo.
## What this is
`bike-admin` is a small single-user web app for logging bike service/repair
"events" (type, date, mileage, cost, notes) and seeing per-bike hints: a
**service-due** status derived from the newest mileage and the most recent
`service` event, plus **MOT / insurance renewal** statuses derived from the most
recent `mot` / `insurance` events.
Stack: FastAPI (ASGI) + stdlib `sqlite3` (WAL) + Jinja2 + HTMX. Server-rendered
pages with HTMX partial swaps on the bike page. No build step. Dependencies via
`uv` (see `pyproject.toml`).
## Commands
```bash
uv sync --all-extras # install all deps (fastapi, dev tools)
uv run uvicorn app.main:app --reload # run dev server (http://127.0.0.1:8000)
uv run pytest -q # run the test suite
uv run ruff check app tests # lint
uv run ruff format --check app tests # format check
uv run ruff format app tests # apply formatting
```
There is **no `main.py` at the repo root** — the app is the `app` package,
entry point `app.main:app`.
## Configuration
All settings come from env vars (or a `.env` file) via `pydantic-settings`,
`env_prefix="BIKE_ADMIN_"` (see `app/config.py`).
- **Required:** `BIKE_ADMIN_USERNAME`, `BIKE_ADMIN_PASSWORD`. The app **raises at
startup** if `BIKE_ADMIN_PASSWORD` is not set (no insecure default).
- `BIKE_ADMIN_SECRET_KEY` — used to derive the per-session CSRF token.
- `BIKE_ADMIN_IS_PRODUCTION=true` — adds `Secure` to the session cookie (set when
behind a TLS reverse proxy over HTTPS).
- `BIKE_ADMIN_BIND_HOST` / `BIND_PORT` — bind address/port.
- `BIKE_ADMIN_DATA_DIR` — directory for the SQLite file (`bike_admin.db`).
- Hints: `BIKE_ADMIN_SERVICE_DUE_SOON_MILES` (service "book soon" lead time,
default 200), `BIKE_ADMIN_RENEWAL_DUE_SOON_DAYS` (MOT/insurance "due soon"
lead time, default 60), `BIKE_ADMIN_RENEWAL_MONTHS` (MOT/insurance validity,
default 12).
## Architecture / layout
- `app/config.py` – `Settings` (pydantic-settings) + `get_settings()` (cached).
- `app/db.py` – SQLite schema + typed DAO helpers + `get_conn()` context manager.
- `app/security.py`– Argon2 password hashing, DB session tokens, CSRF, login throttle.
- `app/services.py`– Business logic: `compute_service_due`, `compute_renewal_due`,
`summarize_bike`, `all_summaries`.
- `app/main.py` – App factory, lifespan (schema init + user seed), security-header
middleware, all routes, HTMX partial rendering.
- `app/templates/` – Jinja2 templates (login, dashboard, bike detail, event partial).
- `app/static/` – `main.css`, `favicon.svg`, and vendored `htmx.min.js`.
- `tests/` – `conftest.py` (Client helper + temp-data-dir env) and per-area
tests (`test_auth`, `test_due`, `test_renewals`, `test_events`, `test_security`).
## Conventions & gotchas
- **Starlette's `TemplateResponse(request, name, context)` signature is required.**
This `starlette` version (1.6.0) uses the new signature — calling
`TemplateResponse(name, context)` raises `TypeError: unhashable type: 'dict'`.
Always pass `request` as the first arg (see all call sites in `app/main.py`).
- **Templates must not contain single `-` inside `{{ }}` as a bare token**, and
especially avoid `else '- '` — Jinja will mis-lex it (unbalanced braces →
`TemplateSyntaxError: unexpected char '?' at ...`). Use
`{% if ... %}{{ x }}{% else %}-{% endif %}` instead.
- **CSRF:** every mutating form must carry the `csrf` hidden input (token =
HMAC of `SECRET_KEY` over the session cookie). `_csrf(request)` in `app/main.py`
returns it for the current session.
- **Throttling is per-IP and stored in the `login_attempts` table** for the last
`LOGIN_WINDOW_SECONDS`.
- **Single user** model; `bikes` and `events` are global (no per-user scoping).
### Testing
- Tests use `pythonpath = ["."]` (set in `pyproject.toml`) so `import app.*` works.
- `conftest.py` has an autouse `env` fixture that points `BIKE_ADMIN_DATA_DIR`,
`BIKE_ADMIN_SECRET_KEY`, `BIKE_ADMIN_USERNAME`, and `BIKE_ADMIN_PASSWORD` to a
fresh temp dir + known values per test, and clears the `get_settings()` cache.
- The `Client` helper wraps `TestClient` with `.login()`, `.csrf()`,
`.create_bike()`, `.create_event()` helpers.
- `TestClient` is instantiated with `follow_redirects=False` so 303s are observed
(otherwise successful logins redirect to `/` and you lose the `set-cookie`).
### Security headers
Applied via an HTTP middleware in `app/main.py`:
`X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy`,
`Permissions-Policy`, and a strict CSP (`default-src 'self'`, `script-src 'self'`,
`frame-ancestors 'none'`).