My Orders: year calendar and lunch profile first, chips and "How you compare" removed, open and paid payments merged into one table (open first, then newest). Activity is now a line chart with a cumulative switch and finer buckets (daily up to ~3 months, weekly up to ~13). Cards get subtle elevation and tighter spacing. Responsive: phone tab strip instead of the side menu, heatmap fits narrow screens, grouped column-chart labels are truncated, the manager weekday table scrolls instead of overflowing. Tooling: viewports.mjs (presets, overflow detector), shoot.mjs --sizes and --select, tests/responsive.ui.spec.ts, docs/ui-testing.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
5.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
Lunchtime is a self-hosted group lunch ordering app: FastAPI + SQLite backend (Python 3.12), React 18 + TypeScript + Vite + Ant Design frontend, nginx in front. All app code lives under src/. No linter is configured; end-to-end/API tests live in src/e2e/ (Playwright, see below).
Workflow
Development happens solely on main: no feature branches, worktrees or PRs. Commit directly to main.
Commands
Full dev stack (backend with --reload, Vite HMR, nginx, Mailpit); also what the devcontainer starts:
cd src && docker compose up -d # app at http://localhost:8020, Mailpit at http://localhost:8025
Without Docker (native stack: uvicorn --reload on :5000, Vite HMR on :8020 proxying /api to the backend, Mailpit on :8025 / SMTP :1025):
src/dev/devenv.sh setup # backend venv (src/backend/.venv), npm ci for frontend + e2e, Mailpit binary into src/.devenv/bin
src/dev/devenv.sh start # idempotent; waits until everything answers. Also: stop | restart | status | logs [service] | reset (wipes DB + uploads)
cd src/frontend && npx tsc --noEmit # type checking (vite build does not run tsc)
The native stack uses src/dev/config.yaml (via the CONFIG_PATH env var) instead of the root config.yaml, stores data in src/backend/data/, sets ADMIN_EMAILS=admin@example.com, and keeps pids/logs in src/.devenv/. API_PROXY_TARGET turns on Vite's /api proxy; it's unset under Compose, where nginx does the routing.
Testing (requires a running stack; both Compose and native serve the app on :8020)
cd src/e2e
npx playwright test # everything; --project=api or --project=ui to narrow down
node shoot.mjs / /create "{order}" "{admin}" /stats [--as=user|admin|anon] [--dark] [--mobile] [--full] [--sizes=check|all|WxH,...] [--select=css]
- Logging in uses the real flow:
lib.mjssignIn(email?)registers (or migrates, for an existing email), reads the confirmation mail from Mailpit's API and confirms it, giving back a user ID. In the browser, a user is "logged in" by setting localStoragelunchtime_tokenbefore load (loginAsinfixtures.ts). Fixtures:user(fresh per test),admin(the ADMIN_EMAILS account),userPage. lib.mjsalso hasapi()(fetch withX-User-Id),createOrder()(withSAMPLE_MENU) andsubmit(). Use them to seed data instead of clicking through the UI.- After any layout change run the responsive checks (phone, tablet, desktop; overflow detection;
tests/responsive.ui.spec.ts): seedocs/ui-testing.md.shoot.mjs --sizes=...exits 1 and printsOVERFLOWwhen a page is wider than its viewport. shoot.mjsprints PNG paths (default dirsrc/e2e/screenshots/, gitignored) plus console errors and failed requests.{order}/{admin}seed an order that has a menu and two submissions. Use it to view the UI after visual changes.- Ant Design Select: its
role=optionnodes are hidden a11y helpers, so click.ant-select-item-optioninstead. - Playwright is pinned to 1.65.0 to match the Chromium build in
PLAYWRIGHT_BROWSERS_PATH. If they diverge,npx playwright install chromium.
CI (.gitea/workflows/build-and-push-images.yaml) builds and pushes backend/frontend images with Kaniko on changes to src/**.
Architecture
Backend (src/backend/app/): almost every route is in the single main.py (~1300 lines), with Pydantic request models at the top. services/ holds helpers (accounts/tokens, email, per-order file storage, submissions, config loading); db.py creates the schema inline in init_db() (CREATE TABLE IF NOT EXISTS plus ad-hoc ALTER TABLE migrations — there is no migration tool) and maps rows to dicts.
Auth model: there are no passwords or sessions. The client generates a user ID and sends it in the X-User-Id header (get_existing_user_id dependency). Per-order access is via tokens stored in user_order_tokens (admin_token = order owner, submission_token = participant); routes under /api/orders/{id}/admin/... check the caller's admin token. Email confirmation, user-ID change, email change and account migration flows use account_confirmation_tokens and send mail through email_service.
Storage is split: SQLite (burger_orders.db in DB_PATH) holds orders, submissions, user profiles and tokens. Per-order content is stored as files under data/ next to the app, keyed by the order's admin_token: configs/<token>.json (menu/form definition), descriptions/<token>.md, templates/, payments/, and uploaded images/ (served at /api/images). Deleting an order must also delete these files (see delete_order in main.py).
App config: config.yaml (mounted read-only at /app/config.yaml) controls public_base_url, SMTP settings and UI announcements; loaded by services/config_service.py and exposed via GET /api/config.
Frontend (src/frontend/src/): no router library — lib/routing.ts parses window.location into home | create | order | admin routes and App.tsx switches between the four views/. lib/api.ts wraps fetch (adds X-User-Id, clears the stored ID on "invalid user id" 401s); lib/services/* are typed per-domain API clients; hooks/useApiRequest.ts and components/utils/AsyncContent.tsx handle loading/error state. lib/orderTemplating.ts and lib/orderFormatting.ts render submissions into the text that is shared/exported from the form config. TODO.md tracks planned admin-view work.
Dev note: Compose mounts only backend/app and selected frontend source into the containers, so changes to requirements.txt/package.json need a rebuild. src/frontend/dist/ is committed build output.