docs(release): add final functional-completion acceptance evidence
This commit is contained in:
@@ -0,0 +1,244 @@
|
|||||||
|
# MobilityOps functional-completion — final summary
|
||||||
|
|
||||||
|
## Outcome: PASS
|
||||||
|
|
||||||
|
All achievable functional-completion requirements were audited, implemented, tested
|
||||||
|
locally (including a genuine clean-checkout drill), committed, pushed, deployed to
|
||||||
|
Unraid and re-verified against the live server after every batch.
|
||||||
|
|
||||||
|
## Revisions
|
||||||
|
|
||||||
|
- Starting branch: `design/mobilityops-premium-ui`
|
||||||
|
- Starting/observed commit: `54dc952915a4874fcdf14781e1c37feb0e253851`
|
||||||
|
- Completion branch: `feat/mobilityops-functional-completion`
|
||||||
|
- Final commit: `<filled in by the follow-up commit that records it — see `git log -1`
|
||||||
|
on this branch; this file itself is part of that final commit>`
|
||||||
|
- Gitea branch URL: `https://gitea.itworx.tech/Jens/MobilityOps` (SSH remote
|
||||||
|
`ssh://git@192.168.10.150:222/Jens/MobilityOps.git`), branch
|
||||||
|
`feat/mobilityops-functional-completion`
|
||||||
|
- Deployed URL: `http://192.168.10.150:1236`
|
||||||
|
- Server deployment directory: `/mnt/user/appdata/mobilityops`
|
||||||
|
- Server Compose project: `mobilityops`
|
||||||
|
|
||||||
|
## Exact commands executed (representative — run after every batch)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local backend gate
|
||||||
|
docker compose exec -T api python -m app.cli seed --reset
|
||||||
|
docker compose exec -T api pytest -q
|
||||||
|
docker compose run --rm api ruff check .
|
||||||
|
docker compose run --rm api mypy app
|
||||||
|
|
||||||
|
# Local frontend gate
|
||||||
|
cd frontend && npx tsc -b --noEmit && npm run build
|
||||||
|
|
||||||
|
# Local e2e (against the local dev stack)
|
||||||
|
npx playwright test
|
||||||
|
|
||||||
|
# Deploy the exact committed revision
|
||||||
|
COMMIT=$(git rev-parse HEAD)
|
||||||
|
git archive --format=tar.gz --output=/tmp/source.tar.gz "$COMMIT"
|
||||||
|
scp -P 22 /tmp/source.tar.gz unraid:/mnt/user/appdata/mobilityops/.deploy/source.tar.gz
|
||||||
|
ssh unraid "cd /mnt/user/appdata/mobilityops && tar -xzf .deploy/source.tar.gz && echo $COMMIT > .deploy/source-revision"
|
||||||
|
ssh unraid "cd /mnt/user/appdata/mobilityops && docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d db api web"
|
||||||
|
ssh unraid "cd /mnt/user/appdata/mobilityops && docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api alembic current"
|
||||||
|
ssh unraid "cd /mnt/user/appdata/mobilityops && docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api python -m app.cli seed --reset"
|
||||||
|
|
||||||
|
# e2e against the live server
|
||||||
|
MOBILITYOPS_PUBLIC_URL=http://192.168.10.150:1236 npx playwright test
|
||||||
|
```
|
||||||
|
|
||||||
|
Clean-checkout drill (once, section 14):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch feat/mobilityops-functional-completion \
|
||||||
|
<repo> /tmp/mobilityops-clean-checkout
|
||||||
|
cd /tmp/mobilityops-clean-checkout
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose -p mobilityops-clean up --build -d # isolated project name/ports
|
||||||
|
docker compose -p mobilityops-clean exec -T api alembic current
|
||||||
|
docker compose -p mobilityops-clean exec -T api python -m app.cli seed --reset
|
||||||
|
docker compose -p mobilityops-clean exec -T api pytest -q
|
||||||
|
docker compose -p mobilityops-clean run --rm api ruff check .
|
||||||
|
docker compose -p mobilityops-clean run --rm api mypy app
|
||||||
|
cd frontend && npm ci && npx tsc -b --noEmit && npm run build
|
||||||
|
MOBILITYOPS_PUBLIC_URL=http://localhost:11228 npx playwright test
|
||||||
|
docker compose -p mobilityops-clean down -v # isolated project only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test and validation results
|
||||||
|
|
||||||
|
| Gate | Local dev stack | Clean-checkout (isolated) | Live Unraid |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `pytest` | 117 passed | 117 passed | — (not applicable; no test runner on the review host) |
|
||||||
|
| `ruff check .` | clean | clean | — |
|
||||||
|
| `mypy app` | 0 issues / 46 files | 0 issues / 46 files | — |
|
||||||
|
| `npx tsc -b` | clean | clean | — |
|
||||||
|
| `npm run build` | clean | clean | — |
|
||||||
|
| `npx playwright test` | 37 passed | 37 passed | **37 passed** (against `http://192.168.10.150:1236`) |
|
||||||
|
| `npm audit` | 4 known advisories (unchanged — see Known limitations) | same | — |
|
||||||
|
|
||||||
|
Every batch (1 through 5) was deployed and re-verified with the full 37-test Playwright
|
||||||
|
suite against the live server before moving to the next batch, not only at the end.
|
||||||
|
|
||||||
|
## Application URLs and ports
|
||||||
|
|
||||||
|
| Service | URL / port | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Web (Unraid) | `http://192.168.10.150:1236` | only MobilityOps-owned service exposed on the LAN |
|
||||||
|
| API (Unraid) | Compose-network only | reached through the web nginx `/api/` proxy |
|
||||||
|
| PostgreSQL (Unraid) | Compose-network only | never exposed |
|
||||||
|
| Shared n8n (Unraid) | `http://192.168.10.150:5678` | pre-existing host infrastructure, outside the MobilityOps Compose project |
|
||||||
|
| Web (local dev) | `http://localhost:1228` | |
|
||||||
|
| API (local dev) | `http://localhost:8128` | |
|
||||||
|
| n8n (local dev) | `http://localhost:5678` | bundled, `bundled-n8n` profile |
|
||||||
|
|
||||||
|
## Demo users and access method
|
||||||
|
|
||||||
|
Two fixed seeded identities, selected via the login screen's role buttons (no
|
||||||
|
password): **Amelie De Ridder** (`USR-OPS`, Operations Manager) and **Karim
|
||||||
|
Boujaddaine** (`USR-EMP`, Rental Employee). `POST /api/v1/demo/login` issues an
|
||||||
|
HttpOnly, `SameSite=Lax` signed session cookie; `GET /api/v1/demo/session` (marked
|
||||||
|
`Cache-Control: no-store`) is what the browser actually trusts on every load, not a
|
||||||
|
locally cached copy.
|
||||||
|
|
||||||
|
## Implemented functionality (this pass, on top of the already-accepted M0–M7/design baseline)
|
||||||
|
|
||||||
|
- Fixed two confirmed defects: Vehicles and Bookings both computed a filtered/paginated
|
||||||
|
result but rendered the raw array.
|
||||||
|
- Server-backed session lifecycle (`GET /demo/session`, `POST /demo/logout`), central
|
||||||
|
401 handling, no more `sessionStorage`-as-authority.
|
||||||
|
- A role matrix enforced server-side (403 on every manager-only action for Rental
|
||||||
|
Employee, not just a hidden button) and mirrored in the nav/route guards.
|
||||||
|
- Authoritative, non-mutating return preview (`POST /bookings/{ref}/return-preview`)
|
||||||
|
sharing its evaluation function with commit — fixed a real bug where the frontend's
|
||||||
|
guessed preview text was wrong (damage → described as "maintenance", actual rule
|
||||||
|
"blocked"; the no-contradiction case → described as "available", actual rule always
|
||||||
|
"cleaning" first).
|
||||||
|
- Audit API/UI now expose `before`/`after` (the columns existed but were never
|
||||||
|
serialized) plus a resolved safe entity link.
|
||||||
|
- Typed related-entity snapshots (booking_overlap's related refs are bookings, not
|
||||||
|
vehicles — previously silently unresolved) and a bounded resolution flow for every
|
||||||
|
one of the five data-quality rule types, plus an audited manual scan trigger and
|
||||||
|
documented recurrence linking (`reopened_from`/`previous_decision`).
|
||||||
|
- Role-aware backend search (`GET /api/v1/search`) replacing a blind client-side regex
|
||||||
|
guesser, with a real results panel, keyboard navigation and debouncing.
|
||||||
|
- A safe, confirmed demo-reset UI trigger (the endpoint already existed and was
|
||||||
|
already gated).
|
||||||
|
- Truthful aggregate n8n integration status (`GET /api/v1/integrations/status`) from
|
||||||
|
outbox delivery counts, replacing a single-most-recent-event read; fixed
|
||||||
|
`MCP_HUB_REGISTRATION_ENABLED` being declared in `.env.example` but never wired into
|
||||||
|
`Settings`.
|
||||||
|
- Bounded outbox delivery-lease recovery for a process crash between claim and outcome.
|
||||||
|
- A second n8n workflow (scheduled quality scan), independent of RAGcore/MCP Hub.
|
||||||
|
|
||||||
|
## RAGcore integration status
|
||||||
|
|
||||||
|
Unchanged from the prior baseline and honestly reported throughout: the demo
|
||||||
|
`KnowledgeProvider` (deterministic TF-IDF extractive retrieval over local procedure
|
||||||
|
documents) satisfies the knowledge-assistant acceptance criteria and is fully verified.
|
||||||
|
A `RAGcoreKnowledgeProvider` HTTP adapter is implemented and unit-tested (including its
|
||||||
|
unavailable-degradation path) but was never exercised against a live RAGcore instance in
|
||||||
|
this environment — no live RAGcore instance exists to test against.
|
||||||
|
`KNOWLEDGE_PROVIDER=demo` on the Unraid deployment; no simulated live connection is ever
|
||||||
|
shown.
|
||||||
|
|
||||||
|
## MCP Hub integration status
|
||||||
|
|
||||||
|
The four read-only provider endpoints are implemented, tested, and directly
|
||||||
|
curl-verified with correct service-token auth enforcement and audit logging.
|
||||||
|
`MCP_HUB_REGISTRATION_ENABLED` — previously declared in `.env.example` but silently
|
||||||
|
dropped by `extra="ignore"` since it had no `Settings` field — is now actually wired in
|
||||||
|
and honestly reported (`GET /api/v1/integrations/status`'s `mcp_hub.state`). It is
|
||||||
|
`false` on the Unraid deployment (`state: "not_configured"`). No live Hub instance was
|
||||||
|
reachable in this environment to verify an actual Hub round trip.
|
||||||
|
|
||||||
|
## n8n integration status
|
||||||
|
|
||||||
|
Fully implemented and live-verified. The original return-processing workflow: verified
|
||||||
|
against both the local bundled instance and the shared Unraid instance, including a real
|
||||||
|
degraded-mode drill in an earlier session (n8n stopped mid-flow → return still committed
|
||||||
|
locally, event stayed `pending` with backoff, self-healed once n8n returned) and the
|
||||||
|
manual-retry path. This pass adds:
|
||||||
|
|
||||||
|
- **Truthful status**: `GET /api/v1/integrations/status` derives n8n health from
|
||||||
|
aggregate outbox counts (pending/delivering/succeeded/failed), not the single most
|
||||||
|
recent event.
|
||||||
|
- **Stale-delivery-lease recovery**: a claimed-but-never-resolved `delivering` row (the
|
||||||
|
process crashing between claim and outcome) is now recoverable; unit-tested including
|
||||||
|
a simulated crash, and confirmed a still-alive worker's unexpired lease is never
|
||||||
|
touched.
|
||||||
|
- **Second workflow**: `mobilityops-scheduled-quality-scan` (hourly + manual-test
|
||||||
|
trigger, ships `"active": false"`), calling `POST
|
||||||
|
/api/v1/integrations/n8n/scheduled-scan`. Live-verified two ways: (1) executed
|
||||||
|
end-to-end via the Manual test trigger against the **local** n8n instance — full
|
||||||
|
green execution in the n8n editor, confirmed by the resulting
|
||||||
|
`data_quality_scan_run` audit event (`actor_type=service`); (2) published to the
|
||||||
|
**shared Unraid n8n** via `deploy/unraid/setup-scheduled-scan.sh` and the resulting
|
||||||
|
endpoint directly curl-verified against the live deployed API, also confirmed via the
|
||||||
|
audit trail. The shared instance's own UI could not be browser-tested directly — it
|
||||||
|
runs `N8N_SECURE_COOKIE=true` and refuses login over the plain-HTTP LAN URL used for
|
||||||
|
automated testing here, which is correct, pre-existing shared-infrastructure
|
||||||
|
behaviour and out of scope to change.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
- RAGcore and ITWorx MCP Hub remain honestly not-live-connected — no live instance of
|
||||||
|
either exists in this environment (environment limitation, not a code defect).
|
||||||
|
- `npm audit`: one moderate esbuild/Vite dev-server-only advisory (fix requires a Vite
|
||||||
|
major upgrade, deliberately deferred), and a
|
||||||
|
react-router RSC-mode advisory that doesn't apply (the app never uses RSC/SSR mode) —
|
||||||
|
both pre-existing, confirmed unchanged by this pass's clean `npm ci`.
|
||||||
|
- Demo authentication remains the accepted HMAC-cookie PoC mechanism tied to two fixed
|
||||||
|
seeded identities — not a production identity provider.
|
||||||
|
- The scheduled quality-scan workflow's own n8n-engine execution was verified live
|
||||||
|
against the local bundled n8n and, for the HTTP round trip specifically, against the
|
||||||
|
shared Unraid n8n's resulting API call — not against a full n8n-engine execution *on
|
||||||
|
the shared instance itself*, for the browser-access reason above.
|
||||||
|
- `n8n_delivery_lease_seconds` (120s default) is a code-level tunable, not exposed in
|
||||||
|
`.env.example`, consistent with the existing `n8n_dispatch_interval_seconds`/
|
||||||
|
`n8n_max_attempts`/`n8n_http_timeout_seconds` tunables already handled that way.
|
||||||
|
|
||||||
|
## Clean deployment instructions
|
||||||
|
|
||||||
|
See `deploy/unraid/README.md` and `docs/17-runbook.md` for the full runbook. Redeploy
|
||||||
|
the exact committed revision:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
COMMIT=<commit to deploy>
|
||||||
|
git archive --format=tar.gz --output=/tmp/source.tar.gz "$COMMIT"
|
||||||
|
scp -P 22 /tmp/source.tar.gz unraid:/mnt/user/appdata/mobilityops/.deploy/source.tar.gz
|
||||||
|
ssh unraid "cd /mnt/user/appdata/mobilityops \
|
||||||
|
&& tar -xzf .deploy/source.tar.gz \
|
||||||
|
&& echo $COMMIT > .deploy/source-revision \
|
||||||
|
&& docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d db api web \
|
||||||
|
&& docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api alembic current \
|
||||||
|
&& curl -fsS http://127.0.0.1:1236/health"
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env` and both named volumes (`mobilityops-db`, `mobilityops-n8n`) are preserved by
|
||||||
|
this flow; nothing outside the `mobilityops` Compose project is touched.
|
||||||
|
|
||||||
|
## Five-minute demonstration flow
|
||||||
|
|
||||||
|
1. Open `http://192.168.10.150:1236`, log in as **Operations Manager**.
|
||||||
|
2. Dashboard: point out the persisted readiness metrics and the now-truthful n8n
|
||||||
|
integration-status card (aggregate counts, not just the latest event).
|
||||||
|
3. Global search (`Ctrl/Cmd+K`): type a vehicle, booking or issue reference; use arrow
|
||||||
|
keys + Enter to navigate; show the no-results state for a nonsense query.
|
||||||
|
4. Bookings: filter by status, page through results (max 25/page), confirm page 2
|
||||||
|
differs from page 1.
|
||||||
|
5. Open an active booking → capture a return with a below-canonical odometer reading →
|
||||||
|
the review step shows the server's authoritative evaluation (odometer regression
|
||||||
|
flagged, resulting status and reason) → confirm → result screen distinguishes local
|
||||||
|
commit success from queued-not-yet-confirmed n8n delivery, links to the created
|
||||||
|
data-quality issue.
|
||||||
|
6. Data quality: run a manual scan; open the newly flagged (or an existing) issue for
|
||||||
|
each rule type and show its dedicated bounded resolution flow (not raw JSON).
|
||||||
|
7. Audit trail: filter by the correlation ID from the return above; show the
|
||||||
|
human-readable before/after change summary, expand the raw-JSON `<details>`.
|
||||||
|
8. Switch role to **Rental Employee**: show Data Quality/Integrations/Audit are absent
|
||||||
|
from the nav, and that direct URL navigation to any of them shows the restricted
|
||||||
|
message rather than partial data or a crash.
|
||||||
|
9. Switch back to Operations Manager, trigger **Reset demo data** with confirmation,
|
||||||
|
land back at login, log in again to confirm deterministic data was restored.
|
||||||
Reference in New Issue
Block a user