Two apps: FastAPI (/api) serves a multi-tenant gap API with OpenAPI/Swagger; React + Vite (/web) is the consultant dashboard (filters + URL state, virtualised grid, optimistic annotation save).
- Python 3.11+ (3.14 tested) with
pip - Node 20+ with
npm
cd api
python -m pip install -r requirements.txt
.\start.ps1On Git Bash / macOS / Linux, use ./start.sh instead of .\start.ps1.
Equivalent manual command:
python -m uvicorn app.main:app --reload --host 127.0.0.1 --port 8000- OpenAPI JSON: http://127.0.0.1:8000/openapi.json
- Swagger UI (Screenshot 6): http://127.0.0.1:8000/docs
Every request must send header X-Tenant-Id (e.g. tenant-a … tenant-d). Missing header → 400.
The API also seeds on startup when gaps.db is missing or empty. To seed or re-seed without relying on that:
cd api
python seed_database.py # only if gaps.db is empty
python seed_database.py --force # delete gaps.db first, then seed (stop uvicorn first)Or: .\seed.ps1 / .\seed.ps1 --force (Windows), ./seed.sh / ./seed.sh --force (Unix).
Data is SQLite at api/gaps.db. By default the seed script loads 100 000 synthetic gaps round-robin across four tenants (tenant-a … tenant-d).
- Override count:
S4_SEED_GAP_COUNT(e.g.5000for a quicker demo). - Tests: under
pytest, the default seed is 520 rows so integration tests stay fast.
cd web
npm install
npm run devOpen http://127.0.0.1:5173. Optional: copy web/.env.example to web/.env and set VITE_API_BASE if the API is not on http://localhost:8000.
The table virtualizes row rendering and loads data in pages (default page_size=100 in the URL, max 1000). Scrolling near the bottom fetches the next page (infinite scroll) so the client does not request millions of rows at once.
Backend (Starlette TestClient issues real ASGI requests against a temp SQLite DB per test):
cd api
python -m pytest tests -qFrontend:
cd web
npm run test:runAutomated (CI-friendly): npm run test:run includes web/src/components/GapTable.a11y.test.tsx, which runs axe-core via jest-axe against the rendered grid and asserts no critical violations (color-contrast is skipped in jsdom; validate contrast in a real browser).
CLI scan (full page in Chrome): With both API and npm run dev running:
cd web
npm run a11yThis uses @axe-core/cli against http://127.0.0.1:5173/. If Chrome / ChromeDriver versions are mismatched, sync them with:
npx browser-driver-manager install chromeFix any critical violations before shipping.
- Dashboard with 500+ rows (virtualised list) and summary H/M/L cards populated.
- Filters applied (e.g. severity H, module FI), URL query string shows the same filters, table shows a reduced set.
- Details dialog: edit annotation → Save; DevTools Network shows
PATCH /gaps/{id}withX-Tenant-Id. - Tenant dropdown switched (
tenant-a↔tenant-b): data and summary change; no IDs from the other tenant appear. - Terminal output:
pytest+npm run test:rungreen (Screenshot 5). - Swagger UI at
/docs(Screenshot 6).
api/— FastAPI, SQLite store, seed, integration testsweb/— React + TypeScript dashboard, Vitest + Testing LibraryDECISIONS.md— stack rationale, table strategy, optimistic rollback, scale notes