- 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.
4.7 KiB
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
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 ifBIKE_ADMIN_PASSWORDis not set (no insecure default). BIKE_ADMIN_SECRET_KEY— used to derive the per-session CSRF token.BIKE_ADMIN_IS_PRODUCTION=true— addsSecureto 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 vendoredhtmx.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. Thisstarletteversion (1.6.0) uses the new signature — callingTemplateResponse(name, context)raisesTypeError: unhashable type: 'dict'. Always passrequestas the first arg (see all call sites inapp/main.py). - Templates must not contain single
-inside{{ }}as a bare token, and especially avoidelse '- '— Jinja will mis-lex it (unbalanced braces →TemplateSyntaxError: unexpected char '?' at ...). Use{% if ... %}{{ x }}{% else %}-{% endif %}instead. - CSRF: every mutating form must carry the
csrfhidden input (token = HMAC ofSECRET_KEYover the session cookie)._csrf(request)inapp/main.pyreturns it for the current session. - Throttling is per-IP and stored in the
login_attemptstable for the lastLOGIN_WINDOW_SECONDS. - Single user model;
bikesandeventsare global (no per-user scoping).
Testing
- Tests use
pythonpath = ["."](set inpyproject.toml) soimport app.*works. conftest.pyhas an autouseenvfixture that pointsBIKE_ADMIN_DATA_DIR,BIKE_ADMIN_SECRET_KEY,BIKE_ADMIN_USERNAME, andBIKE_ADMIN_PASSWORDto a fresh temp dir + known values per test, and clears theget_settings()cache.- The
Clienthelper wrapsTestClientwith.login(),.csrf(),.create_bike(),.create_event()helpers. TestClientis instantiated withfollow_redirects=Falseso 303s are observed (otherwise successful logins redirect to/and you lose theset-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').