Files
bike_admin/AGENTS.md
T
2026-09-09 17:49:01 +01:00

89 lines
4.2 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 a per-bike **service-due**
hint derived from the newest mileage and the most recent `service` event.
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`).
## 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`, `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` and vendored `htmx.min.js`.
- `tests/` – `conftest.py` (Client helper + temp-data-dir env) and per-area
tests.
## 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'`).