This commit is contained in:
2026-09-09 17:49:01 +01:00
commit 99c15e23bb
30 changed files with 3028 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
# 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'`).