M39: harden application and acceptance gates
This commit is contained in:
@@ -1,5 +1,78 @@
|
||||
# Project state
|
||||
|
||||
## M39 — Hardening review (2026-08-16)
|
||||
|
||||
Full-repository audit (backend, frontend, infra, docs) followed by targeted fixes. Every
|
||||
change is covered by the existing gates plus new regression tests; nothing in the locked
|
||||
scope changed.
|
||||
|
||||
- **Security**: `get_settings()` refuses to boot with `MOBILITYOPS_ENV=production` while
|
||||
`APP_SECRET`/`MOBILITYOPS_CALLBACK_TOKEN` (or the MCP token when registration is on)
|
||||
still hold placeholder values (`insecure_default_secrets`). `POST /api/v1/demo/reset`
|
||||
now returns 404 outside demo mode (it previously only checked `DEMO_ALLOW_RESET`).
|
||||
Failed password logins are throttled per client IP (`app/core/ratelimit.py`,
|
||||
`LOGIN_MAX_FAILURES`/`LOGIN_FAILURE_WINDOW_SECONDS`, 429 + `Retry-After`; only failures
|
||||
count, so tests are unaffected). OIDC email-based linking of an existing local account
|
||||
now requires `email_verified: true` (absent claim = unverified). n8n service tokens are
|
||||
compared with `hmac.compare_digest`; the return callback body is a bounded Pydantic model
|
||||
(`ReturnCallbackIn`, malformed `correlation_id` → 422 instead of 500).
|
||||
- **Correctness**: dashboard "today" buckets bookings by the Europe/Brussels calendar day
|
||||
instead of the UTC date; audit export accepts naive datetimes (were 500) and
|
||||
`correlation_id` is validated as UUID (was a DB error); paged booking lists for an
|
||||
unknown `vehicle_ref` keep the page shape; the data-quality scan skips anonymised
|
||||
customers (they were re-flagged as `missing_required_field` after every scan);
|
||||
`resolve_odometer_regression` locks booking→vehicle like every other flow (was the
|
||||
opposite order → deadlock risk); `merge_customers` locks both rows in a deterministic
|
||||
order, rejects already-merged customers (`CUSTOMER_ALREADY_MERGED`, localised in the
|
||||
three locales) and validates override lengths against the column sizes; a booking that
|
||||
was `blocked` at checkout can now be cancelled (it had no exit state); demo reset no
|
||||
longer wipes `revoked_sessions` (logged-out cookies were revived).
|
||||
- **Idempotency**: `idempotency_records.request_fingerprint` (migration `0a4c1d2e3f5b`);
|
||||
replaying an `Idempotency-Key` with a different body → 409 `IDEMPOTENCY_KEY_REUSED`.
|
||||
- **Observability**: unmatched paths are labelled `<unmatched>` in the HTTP metrics
|
||||
(404 probes no longer create unbounded Prometheus series). Integration status reads the
|
||||
latest heartbeat/failure per workflow with `DISTINCT ON` instead of loading every row.
|
||||
- **Frontend**: nginx hashed-asset regex never matched Vite's `name-HASH.js` output, so
|
||||
bundles were served `no-cache`; fixed (`Cache-Control: public, max-age=1y, immutable`),
|
||||
plus gzip and `server_tokens off`. Global search and the Vehicles/Bookings/Audit/DQ
|
||||
lists abort stale requests and use `replace` navigation (no history entry per
|
||||
keystroke). Bookings/Audit date filters use Brussels day boundaries via
|
||||
`brusselsDateTime.ts`. DQ "demo scenarios only" is a server-side filter (`demo_only`)
|
||||
so it spans all pages. `AbortSignal.any` fallback, `sessionStorage` parse guard,
|
||||
per-action error notices on booking detail, dead `.about-cta` guide target fixed.
|
||||
- **Build/CI**: root and frontend `.dockerignore`; backend image runs as non-root `app`;
|
||||
`VITE_API_BASE_URL` is a build arg (was a no-op runtime env); CI gains an `e2e` job that
|
||||
runs the full Playwright suite against the Compose stack; `npm audit --audit-level=high`.
|
||||
`httpx2` moved to dev extras (Starlette TestClient), unused `pytest-asyncio` removed.
|
||||
- **Tests**: `tests/test_migrations.py` upgrades an empty database through Alembic and
|
||||
asserts `compare_metadata` is empty (found and fixed a real drift: missing
|
||||
`index=True` on `Customer.anonymized_at`). `tests/test_hardening.py` covers the items
|
||||
above. `contracts/openapi.yaml` regenerated (it lacked `complete-requirements` and
|
||||
`schedule`).
|
||||
- **Docs**: `docs/18-visual-product-roadmap.md` → `docs/19-…` (duplicate number),
|
||||
`FILE_INDEX.md` regenerated from `git ls-files`, README points at `N8N_WEBHOOK_URL`
|
||||
and the definitive acceptance summary, `.gitignore` covers `.claude/settings.local.json`,
|
||||
`*.tgz`, `*.dump`, `backups/`.
|
||||
- Gates run from this checkout: 261 backend tests, Ruff, mypy (59 files), frontend
|
||||
`tsc -b && vite build`, and the complete Playwright suite (153 tests) against a local
|
||||
nginx + uvicorn + PostgreSQL 16 stack — all green.
|
||||
- **Lint**: ESLint 9 flat config (`frontend/eslint.config.js`) with typescript-eslint,
|
||||
`react-hooks` (`rules-of-hooks` + `exhaustive-deps` as errors; the React-Compiler-era
|
||||
`set-state-in-effect`/`purity`/`refs` rules are off because the app's "reset then fetch
|
||||
in an effect" pattern is deliberate) and `jsx-a11y`. `npm run lint` = `tsc -b --noEmit &&
|
||||
eslint .`; wired into `make lint` and CI. Fixed the real findings: missing `t`
|
||||
dependencies in `BookingDetail`/`Dashboard`/`DataQualityIssueDetail` loaders, missing
|
||||
`setCollapsedToChip` in `DemoGuide`, stale-object dependency in the reschedule
|
||||
pre-fill. `i18next`/`react-i18next` pinned exactly; `engines.node >= 22`.
|
||||
- README headline no longer links to the LAN-only demo (`192.168.10.150`); it gives the
|
||||
two-command local run and points to `deploy/unraid/README.md` for the hosted reference.
|
||||
- Left as-is on purpose: the bundled `n8n` service still starts with `make demo` (the
|
||||
runbook's local automation demo relies on it; production disables it via
|
||||
`compose.unraid.yaml`).
|
||||
- Exact next action: rebuild images (`docker compose build`) so the new migration
|
||||
applies on the next `up`; on Unraid confirm `.env` has a real `MOBILITYOPS_CALLBACK_TOKEN`
|
||||
and `APP_SECRET` before deploying, because production now refuses placeholders.
|
||||
|
||||
## Publication and Unraid deployment (2026-08-02)
|
||||
|
||||
- Unraid deployment is live at `http://192.168.10.150:1236` from
|
||||
|
||||
Reference in New Issue
Block a user