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

4.2 KiB
Raw Blame History

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

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