Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
014caba7d5 | ||
|
|
e531740bb9 | ||
|
|
921c6878a6 | ||
|
|
cd55d6854f | ||
|
|
70e2648b03 | ||
|
|
f8eff37982 | ||
|
|
25cc7a1b0d | ||
|
|
6fd1d5e5e9 | ||
|
|
54b7719759 | ||
|
|
a19613d5e9 | ||
|
|
b62ecf66d8 | ||
|
|
97b8cd1c8c | ||
|
|
50f9bb471a | ||
|
|
719b710926 | ||
|
|
441b3624e1 | ||
|
|
ccae0538d7 | ||
|
|
7d935c5a7d | ||
|
|
51c3c90fa1 | ||
|
|
d6a566e1a1 | ||
|
|
4b727a109f | ||
|
|
1bf72c39e3 | ||
|
|
55e8ebd81e | ||
|
|
b2bdb78baa | ||
|
|
fe92a7f491 | ||
|
|
19df5f7508 | ||
|
|
51d488a634 | ||
|
|
da2f0956c9 | ||
|
|
13f8db7573 | ||
|
|
e6ec89658e | ||
|
|
9d71555135 | ||
|
|
81de78bd8b | ||
|
|
444e61253b | ||
|
|
81e3fd63bd | ||
|
|
b0706989db | ||
|
|
2036e8b4ec | ||
|
|
51b0ade7e7 | ||
|
|
cea0825d60 | ||
|
|
dd3acd872c | ||
|
|
00191e9b54 | ||
|
|
a24098c583 | ||
|
|
95c91797fa | ||
|
|
ec02aca0fd | ||
|
|
acd8b82b09 | ||
|
|
9e4fca5708 | ||
|
|
0045778dbb | ||
|
|
24dcb3494c | ||
|
|
a830e8a2d0 | ||
|
|
ca66083c8b | ||
|
|
ae39a8947f | ||
|
|
a9f48d6880 | ||
|
|
6859249570 | ||
|
|
1ca70187a2 | ||
|
|
26819354ee | ||
|
|
9dfbd7c4bf | ||
|
|
809ba0ddcc | ||
|
|
c7492bf6ad | ||
|
|
82a933f6cd | ||
|
|
be33b46228 | ||
|
|
b44915ff35 | ||
|
|
eecfcb4b79 | ||
|
|
cb7edb0b84 | ||
|
|
efab8d816f | ||
|
|
cfffb1ce54 | ||
|
|
29325b6c27 | ||
|
|
6365586e82 | ||
|
|
e1b700b10e | ||
|
|
b00d33af11 | ||
|
|
90cc3cf378 | ||
|
|
0935901f11 | ||
|
|
f0f1be83ae | ||
|
|
689e499634 | ||
|
|
c3f1cfc699 | ||
|
|
509cb95110 | ||
|
|
5dda5742e4 | ||
|
|
152d847a26 | ||
|
|
f715085f65 | ||
|
|
5d7a5e7359 | ||
|
|
fe06ff75a1 | ||
|
|
8030753dbc | ||
|
|
686795a452 | ||
|
|
2ee8b2d82b | ||
|
|
58fb515337 | ||
|
|
218599af7d | ||
|
|
15bdbe40ac | ||
|
|
4a3c3bd0a9 | ||
|
|
3f13912739 | ||
|
|
b2e1ae7f17 | ||
|
|
de151914b6 | ||
|
|
e577c16db5 | ||
|
|
c194c18ca9 | ||
|
|
948d5eb6a6 | ||
|
|
3cd9ddfa66 | ||
|
|
0bfcf71ff7 | ||
|
|
4cdf667dc1 | ||
|
|
0ef4a6fa98 | ||
|
|
2648cef8e3 | ||
|
|
aacf0e04bc | ||
|
|
ad1182582d | ||
|
|
f2cdad194c | ||
|
|
13ad2ba6a3 | ||
|
|
086dfed992 | ||
|
|
64cc96fa4b | ||
|
|
319f43312e | ||
|
|
a2433d7fa3 | ||
|
|
453c7241fe | ||
|
|
529e7364a9 | ||
|
|
7d686ae2aa | ||
|
|
c2b8268927 | ||
|
|
9e9dd8e0e3 | ||
|
|
4faac24b5a | ||
|
|
3808bbe132 | ||
|
|
6f77a30dce | ||
|
|
e5307a7c0f | ||
|
|
dee8f2f7e9 | ||
|
|
57992bf153 | ||
|
|
727c19a779 | ||
|
|
2ae2044e3a | ||
|
|
34df66d28c | ||
|
|
3ebca9e9b7 | ||
|
|
b66521da82 | ||
|
|
0571a40649 | ||
|
|
4227fe4f58 | ||
|
|
c790ec99cb | ||
|
|
fd390df423 | ||
|
|
e5d8466266 | ||
|
|
0da5251524 | ||
|
|
2afceea5e4 | ||
|
|
cf4d8e3649 | ||
|
|
aaa1630535 | ||
|
|
167bf49b6e | ||
|
|
fd0c55b13b | ||
|
|
05628936ca | ||
|
|
b341436e77 | ||
|
|
4049c0c6b1 | ||
|
|
e39c0a1dd6 | ||
|
|
bbdb4a9ae8 | ||
|
|
e0c107a94a | ||
|
|
59cb4c062e | ||
|
|
b79d485ef1 | ||
|
|
c0995b762e | ||
|
|
5f0eaa59b0 | ||
|
|
09173a4740 | ||
|
|
9468cc3e21 | ||
|
|
f0d641198c | ||
|
|
77208b857a | ||
|
|
e427313bce | ||
|
|
d17af1c52a | ||
|
|
94cfb7bcbb | ||
|
|
37a362c4a0 | ||
|
|
1fbb20b1ab | ||
|
|
f7805579f7 | ||
|
|
de0bdea84f | ||
|
|
284b3c7394 | ||
|
|
2e4fb43f09 | ||
|
|
cda2c32bd0 | ||
|
|
7851e807fa | ||
|
|
a7ac5ed9d0 | ||
|
|
1e407754e6 | ||
|
|
1fdd2b3ccf | ||
|
|
ac4b1636fe | ||
|
|
e6539d17b6 | ||
|
|
6deb95524d | ||
|
|
18344bc8b7 | ||
|
|
18a765d623 | ||
|
|
845db14e17 | ||
|
|
337f8716bb | ||
|
|
257a4cf6c0 | ||
|
|
4a268c7351 | ||
|
|
294a8176d1 | ||
|
|
a5024f7190 | ||
|
|
38f654b97a | ||
|
|
f04a81f6c7 | ||
|
|
07d5605812 | ||
|
|
65835ea40a | ||
|
|
5fa4fe0811 | ||
|
|
cf9a889547 | ||
|
|
ddc3a98e4b | ||
|
|
6b864596e0 | ||
|
|
14c2ad3ee8 | ||
|
|
9fff84dc68 | ||
|
|
c63903cc94 | ||
|
|
ac427f4427 | ||
|
|
728e380d63 | ||
|
|
8989ffb23c | ||
|
|
7c94eb9e87 | ||
|
|
e0c7ed6011 | ||
|
|
5b2827eb7e | ||
|
|
8a3a43d4ac | ||
|
|
ff118dd66d | ||
|
|
824048b9d4 | ||
|
|
c981aad2a3 | ||
|
|
e115031a57 | ||
|
|
ec8f809497 | ||
|
|
4a0a4d1cb4 | ||
|
|
1867828a9d | ||
|
|
4437b8792a | ||
|
|
4bc3e33953 | ||
|
|
477b5e7ce9 | ||
|
|
6e227a214a | ||
|
|
9bd6bea759 | ||
|
|
7e34f55005 | ||
|
|
f5212959b4 | ||
|
|
62ac9f825c | ||
|
|
bdc58f396e | ||
|
|
e1f0ad8431 | ||
|
|
760f3b6ee2 | ||
|
|
ffc88e33b4 | ||
|
|
56a65b2364 | ||
|
|
063a8f9a2d | ||
|
|
938a739dfe |
@@ -0,0 +1,49 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
.agents
|
||||||
|
.codex
|
||||||
|
.claude
|
||||||
|
.dyad
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.vs
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
|
.ruff_cache
|
||||||
|
.mypy_cache
|
||||||
|
node_modules
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
dist
|
||||||
|
artifacts
|
||||||
|
coverage
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
secrets
|
||||||
|
credentials
|
||||||
|
.state
|
||||||
|
data
|
||||||
|
backups
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.zip
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -2,14 +2,82 @@ COMPOSE_PROJECT_NAME=mobilityops
|
|||||||
MOBILITYOPS_ENV=development
|
MOBILITYOPS_ENV=development
|
||||||
MOBILITYOPS_DEMO_MODE=true
|
MOBILITYOPS_DEMO_MODE=true
|
||||||
MOBILITYOPS_PUBLIC_URL=http://localhost:1228
|
MOBILITYOPS_PUBLIC_URL=http://localhost:1228
|
||||||
MOBILITYOPS_API_URL=http://localhost:8128
|
# Build-time API origin baked into the web bundle. Leave empty: the SPA calls its own
|
||||||
|
# origin and nginx proxies /api to the API (required by the CSP connect-src 'self').
|
||||||
|
VITE_API_BASE_URL=
|
||||||
DATABASE_URL=postgresql+psycopg://mobilityops:mobilityops@db:5432/mobilityops
|
DATABASE_URL=postgresql+psycopg://mobilityops:mobilityops@db:5432/mobilityops
|
||||||
POSTGRES_DB=mobilityops
|
POSTGRES_DB=mobilityops
|
||||||
POSTGRES_USER=mobilityops
|
POSTGRES_USER=mobilityops
|
||||||
POSTGRES_PASSWORD=mobilityops
|
POSTGRES_PASSWORD=mobilityops
|
||||||
|
# Signs session cookies. With MOBILITYOPS_ENV=production the API refuses to start while
|
||||||
|
# this (or MOBILITYOPS_CALLBACK_TOKEN) still holds its placeholder value.
|
||||||
APP_SECRET=replace-in-production
|
APP_SECRET=replace-in-production
|
||||||
DEMO_TODAY=2026-08-01
|
|
||||||
TZ=Europe/Brussels
|
TZ=Europe/Brussels
|
||||||
|
# Session cookie Secure flag. Development on localhost may use false; production startup
|
||||||
|
# requires both an HTTPS public URL and this value set to true.
|
||||||
|
SESSION_COOKIE_SECURE=false
|
||||||
|
|
||||||
|
# Optional OpenID Connect login. Public demo role buttons remain available when enabled.
|
||||||
|
OIDC_ENABLED=false
|
||||||
|
OIDC_PROVIDER_NAME=Organisatieaccount
|
||||||
|
OIDC_ISSUER_URL=
|
||||||
|
OIDC_CLIENT_ID=
|
||||||
|
OIDC_CLIENT_SECRET=
|
||||||
|
OIDC_REDIRECT_URI=
|
||||||
|
OIDC_ALLOWED_EMAIL_DOMAINS=
|
||||||
|
OIDC_AUTO_PROVISION=true
|
||||||
|
OIDC_DEFAULT_ROLE=rental_employee
|
||||||
|
|
||||||
|
# Observability: JSON logs are always enabled. Set a token only if /metrics is exposed
|
||||||
|
# outside the private Compose network; Prometheus can send it as a bearer token.
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
# Required when the observability profile is enabled. Keep private and high entropy.
|
||||||
|
METRICS_BEARER_TOKEN=replace-me-private-metrics-token
|
||||||
|
GRAFANA_ADMIN_USER=admin
|
||||||
|
GRAFANA_ADMIN_PASSWORD=change-me-before-start
|
||||||
|
# Alertmanager sends every firing/resolved alert and the continuous watchdog to this
|
||||||
|
# owner-managed receiver. Production must route it to a channel that is actually watched.
|
||||||
|
ALERTMANAGER_WEBHOOK_URL=https://n8n.itworx.tech/webhook/mobilityops-alerts
|
||||||
|
|
||||||
|
# Verified scheduled PostgreSQL backups (Unraid override).
|
||||||
|
BACKUP_INTERVAL_SECONDS=86400
|
||||||
|
BACKUP_RETENTION_DAYS=30
|
||||||
|
BACKUP_MINIMUM_COPIES=7
|
||||||
|
# Restore the newest dump into a disposable database at least weekly. Backup health also
|
||||||
|
# requires a successful drill within eight days.
|
||||||
|
BACKUP_RESTORE_DRILL_INTERVAL_SECONDS=604800
|
||||||
|
# Set both values to copy every verified backup to an independently mounted path.
|
||||||
|
BACKUP_SECONDARY_DESTINATION=
|
||||||
|
MOBILITYOPS_BACKUP_DIR=./backups/postgres
|
||||||
|
MOBILITYOPS_BACKUP_SECONDARY_DIR=./backups/offsite
|
||||||
|
# Optional real off-site copy through the official rclone OneDrive adapter. OAuth state
|
||||||
|
# lives only in MOBILITYOPS_RCLONE_CONFIG_DIR and must never be committed.
|
||||||
|
BACKUP_OFFSITE_INTERVAL_SECONDS=900
|
||||||
|
RCLONE_ONEDRIVE_REMOTE=onedrive
|
||||||
|
RCLONE_ONEDRIVE_PATH=FleetOps/backups
|
||||||
|
MOBILITYOPS_RCLONE_CONFIG_DIR=./.secrets/rclone
|
||||||
|
MOBILITYOPS_OFFSITE_VERIFY_DIR=./backups/offsite-verify
|
||||||
|
|
||||||
|
# Privacy governance defaults.
|
||||||
|
PRIVACY_MINIMUM_BOOKING_RETENTION_DAYS=30
|
||||||
|
PRIVACY_AUDIT_RETENTION_DAYS=2555
|
||||||
|
PRIVACY_AUDIT_EXPORT_MAX_ROWS=10000
|
||||||
|
|
||||||
|
# Demo presentation (fictional org identity, badge/manifest, reset safety valve).
|
||||||
|
# DEMO_ALLOW_RESET=false permanently disables POST /api/v1/demo/reset (403), independent
|
||||||
|
# of role -- a safety valve for any environment where the dataset must not be rebuildable.
|
||||||
|
DEMO_ORGANIZATION_NAME=Northstar Mobility
|
||||||
|
DEMO_TIMEZONE=Europe/Brussels
|
||||||
|
DEMO_ALLOW_RESET=true
|
||||||
|
# Prevent public visitors from repeatedly rebuilding the shared dataset. Concurrent
|
||||||
|
# resets are always rejected using both process and PostgreSQL advisory locks.
|
||||||
|
DEMO_RESET_COOLDOWN_SECONDS=60
|
||||||
|
|
||||||
|
# Operational mode: set MOBILITYOPS_DEMO_MODE=false and provide the first manager.
|
||||||
|
# Keep these values in a secret store or an untracked production .env file.
|
||||||
|
INITIAL_ADMIN_EMAIL=
|
||||||
|
INITIAL_ADMIN_PASSWORD=
|
||||||
|
INITIAL_ADMIN_DISPLAY_NAME=Operations Manager
|
||||||
|
|
||||||
# n8n
|
# n8n
|
||||||
N8N_BASE_URL=http://n8n:5678
|
N8N_BASE_URL=http://n8n:5678
|
||||||
@@ -19,17 +87,33 @@ N8N_BASIC_AUTH_ACTIVE=true
|
|||||||
N8N_BASIC_AUTH_USER=admin
|
N8N_BASIC_AUTH_USER=admin
|
||||||
N8N_BASIC_AUTH_PASSWORD=change-me
|
N8N_BASIC_AUTH_PASSWORD=change-me
|
||||||
MOBILITYOPS_CALLBACK_TOKEN=replace-me-n8n-callback-token
|
MOBILITYOPS_CALLBACK_TOKEN=replace-me-n8n-callback-token
|
||||||
|
# Sent as the X-Fleet-Ops-Trigger-Token header when Fleet Ops calls the n8n return-
|
||||||
|
# processing webhook, so the webhook trigger can require Header Auth instead of being
|
||||||
|
# publicly callable by anyone who discovers the URL. Must match the value stored in
|
||||||
|
# n8n's "Fleet Ops Webhook Trigger Token" Header Auth credential.
|
||||||
|
MOBILITYOPS_WEBHOOK_TRIGGER_TOKEN=replace-me-n8n-webhook-trigger-token
|
||||||
|
|
||||||
# RAGcore integration
|
# RAGcore integration
|
||||||
KNOWLEDGE_PROVIDER=demo
|
KNOWLEDGE_PROVIDER=demo
|
||||||
RAGCORE_BASE_URL=http://ragcore-api:8000
|
RAGCORE_BASE_URL=http://ragcore-api:8000
|
||||||
|
# Optional existing Docker network used by hosted deployments to reach RAGcore through
|
||||||
|
# its private service alias instead of exposing RAGcore on the LAN.
|
||||||
|
RAGCORE_DOCKER_NETWORK=
|
||||||
RAGCORE_TENANT=northstar-mobility-demo
|
RAGCORE_TENANT=northstar-mobility-demo
|
||||||
RAGCORE_WORKSPACE=mobilityops
|
RAGCORE_WORKSPACE=mobilityops
|
||||||
RAGCORE_COLLECTION=internal-procedures
|
RAGCORE_COLLECTION=internal-procedures
|
||||||
RAGCORE_API_TOKEN=
|
RAGCORE_API_TOKEN=
|
||||||
|
# UUID of the RAGcore knowledge space procedures were synced into (see workflow 3).
|
||||||
|
RAGCORE_SPACE_ID=
|
||||||
|
# Skip the slower generation endpoint temporarily after a timeout/non-2xx response and
|
||||||
|
# use the still-grounded extractive search fallback immediately.
|
||||||
|
RAGCORE_ANSWERS_CIRCUIT_BREAKER_SECONDS=60
|
||||||
|
|
||||||
# ITWorx MCP Hub integration
|
# ITWorx MCP Hub integration. Registration itself is catalog-driven on the Hub's own
|
||||||
|
# side (it reconciles its catalog into the gateway; Fleet Ops never pushes a
|
||||||
|
# registration call) -- MCP_HUB_BASE_URL is only used here for an honest reachability
|
||||||
|
# health check surfaced on the integration status page.
|
||||||
MCP_HUB_REGISTRATION_ENABLED=false
|
MCP_HUB_REGISTRATION_ENABLED=false
|
||||||
MCP_HUB_BASE_URL=http://itworx-mcp-hub:8000
|
MCP_HUB_BASE_URL=http://itworx-mcp-hub:8000
|
||||||
MCP_HUB_SERVICE_TOKEN=replace-me-mcp-hub-token
|
MCP_HUB_SERVICE_TOKEN=replace-me-mcp-hub-token
|
||||||
MCP_PROVIDER_ID=mobilityops
|
MCP_PROVIDER_ID=fleet-ops
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
|
*.ps1 text eol=crlf
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jpg binary
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.webp binary
|
||||||
|
*.zip binary
|
||||||
|
|
||||||
|
# Generated operational evidence is not part of a source release archive.
|
||||||
|
/artifacts export-ignore
|
||||||
|
/PROJECT_STATE.md export-ignore
|
||||||
|
/MASTER_BUILD_PROMPT.md export-ignore
|
||||||
|
/FILE_INDEX.md export-ignore
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: MobilityOps browser canary
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "37 4 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mobilityops-browser-canary
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
chromium:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
- name: Install locked Chromium runtime
|
||||||
|
working-directory: frontend
|
||||||
|
run: |
|
||||||
|
npm ci --no-audit --no-fund
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
- name: Run non-destructive production canary
|
||||||
|
working-directory: frontend
|
||||||
|
env:
|
||||||
|
MOBILITYOPS_PUBLIC_URL: https://fleetops.itworx.tech
|
||||||
|
run: npx playwright test --config=playwright.live.config.ts --project=chromium
|
||||||
|
- name: Upload failure evidence
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3; Gitea-compatible artifact protocol
|
||||||
|
with:
|
||||||
|
name: browser-canary-failure
|
||||||
|
path: |
|
||||||
|
frontend/playwright-live-report
|
||||||
|
frontend/test-results
|
||||||
|
if-no-files-found: ignore
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
name: MobilityOps acceptance
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mobilityops-ci-${{ gitea.repository }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
acceptance:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Determine validation scope
|
||||||
|
id: scope
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
base_sha="${{ gitea.event.pull_request.base.sha }}"
|
||||||
|
if git diff --quiet "$base_sha...HEAD" -- . ':(exclude).gitea/workflows/**'; then
|
||||||
|
echo "full=false" >> "$GITEA_OUTPUT"
|
||||||
|
echo "Workflow-only change: the protected lightweight gate is sufficient."
|
||||||
|
else
|
||||||
|
echo "full=true" >> "$GITEA_OUTPUT"
|
||||||
|
echo "Product or test change: running the complete acceptance gate."
|
||||||
|
fi
|
||||||
|
- name: Secret scan
|
||||||
|
uses: trufflesecurity/trufflehog@b9dd330365132cd2d01dd5dc8a857a056a2544e1 # v3.79.0
|
||||||
|
with:
|
||||||
|
path: ./
|
||||||
|
extra_args: --only-verified
|
||||||
|
- name: Backend tests in isolated PostgreSQL stack
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
run: sh scripts/run-isolated-tests.sh
|
||||||
|
- name: Backend static and contract checks
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
run: |
|
||||||
|
docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --build --rm api ruff check app tests
|
||||||
|
docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --rm api mypy app
|
||||||
|
docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml run --rm \
|
||||||
|
-v "$PWD:/repo:ro" api python /repo/scripts/check-contracts.py
|
||||||
|
python scripts/check-source-budgets.py
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
- name: Install frontend dependencies once
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
working-directory: frontend
|
||||||
|
run: npm ci --no-audit --no-fund
|
||||||
|
- name: Frontend lint, build, budget and dependency audit
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
working-directory: frontend
|
||||||
|
run: |
|
||||||
|
npm run lint
|
||||||
|
npm run build
|
||||||
|
npm run budget
|
||||||
|
npm audit --audit-level=high
|
||||||
|
- name: Start the demo stack
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
# Acceptance tests intentionally reset their isolated demo dataset per scenario.
|
||||||
|
printf '\nDEMO_RESET_COOLDOWN_SECONDS=0\n' >> .env
|
||||||
|
docker compose -p mobilityops-e2e up --build -d db api web
|
||||||
|
docker network connect mobilityops-e2e_mobilityops "$HOSTNAME"
|
||||||
|
for _attempt in $(seq 1 60); do
|
||||||
|
if curl -fsS http://web/health/ready >/dev/null 2>&1; then break; fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
curl -fsS http://web/health/ready
|
||||||
|
docker compose -p mobilityops-e2e exec -T api python -m app.cli seed --reset
|
||||||
|
- name: Install acceptance browsers
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
working-directory: frontend
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
- name: Run browser acceptance and live smoke suites
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
working-directory: frontend
|
||||||
|
env:
|
||||||
|
MOBILITYOPS_PUBLIC_URL: http://web
|
||||||
|
run: |
|
||||||
|
# Pixel baselines are workstation/rendering specific; keep the PR gate functional.
|
||||||
|
npx playwright test --grep-invert "visual hierarchy"
|
||||||
|
npx playwright test --config=playwright.live.config.ts --project=chromium
|
||||||
|
- name: Run concurrent persisted-read smoke
|
||||||
|
if: steps.scope.outputs.full == 'true'
|
||||||
|
run: python scripts/run-readonly-load-smoke.py --base-url http://web
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: failure() && steps.scope.outputs.full == 'true'
|
||||||
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3; Gitea-compatible artifact protocol
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: |
|
||||||
|
frontend/playwright-report
|
||||||
|
frontend/playwright-live-report
|
||||||
|
if-no-files-found: ignore
|
||||||
|
- name: Stack logs on failure
|
||||||
|
if: failure() && steps.scope.outputs.full == 'true'
|
||||||
|
run: docker compose -p mobilityops-e2e logs --tail=200 api web
|
||||||
|
- name: Remove CI stacks
|
||||||
|
if: always() && steps.scope.outputs.full == 'true'
|
||||||
|
run: |
|
||||||
|
docker network disconnect mobilityops-e2e_mobilityops "$HOSTNAME" 2>/dev/null || true
|
||||||
|
docker compose -p mobilityops-e2e down -v --remove-orphans
|
||||||
|
docker compose -p mobilityops-ci -f compose.yaml -f compose.test.yaml down -v --remove-orphans
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: MobilityOps live probe
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "7 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mobilityops-live-probe
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
public-probe:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 3
|
||||||
|
steps:
|
||||||
|
- name: Verify HTTPS readiness and certificate horizon
|
||||||
|
run: |
|
||||||
|
curl --fail --silent --show-error --retry 3 https://fleetops.itworx.tech/health/ready
|
||||||
|
openssl s_client -servername fleetops.itworx.tech -connect fleetops.itworx.tech:443 </dev/null 2>/dev/null \
|
||||||
|
| openssl x509 -checkend 1209600 -noout
|
||||||
|
- name: Report successful external heartbeat
|
||||||
|
env:
|
||||||
|
HEARTBEAT_URL: ${{ secrets.LIVE_CANARY_HEARTBEAT_URL }}
|
||||||
|
run: |
|
||||||
|
if [ -n "$HEARTBEAT_URL" ]; then
|
||||||
|
curl --fail --silent --show-error --retry 3 "$HEARTBEAT_URL"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: Managed validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
profile:
|
||||||
|
description: Allowlisted validation profile
|
||||||
|
required: true
|
||||||
|
default: full
|
||||||
|
type: choice
|
||||||
|
options: [test, lint, typecheck, build, security, full]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: managed-validation-${{ gitea.repository }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
full:
|
||||||
|
name: full
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- name: Validate repository with a bounded profile
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
REQUESTED_PROFILE: ${{ inputs.profile }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
profile="${REQUESTED_PROFILE:-full}"
|
||||||
|
case "${profile}" in
|
||||||
|
test|lint|typecheck|build|security|full) ;;
|
||||||
|
*) echo "Profile is not allowlisted" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
git diff --check
|
||||||
|
if git grep -nE '^(<<<<<<< |=======$|>>>>>>> )' -- . ':!*.lock' ':!*.patch'; then
|
||||||
|
echo "Unresolved merge markers detected" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f pyproject.toml || -f requirements.txt ]]; then
|
||||||
|
# Compile only tracked Python sources. Running compileall after a
|
||||||
|
# Node install would otherwise traverse node_modules and turn a
|
||||||
|
# lightweight baseline into a large runner workload.
|
||||||
|
git ls-files -z '*.py' | xargs -0 -r python -m py_compile
|
||||||
|
if [[ -f uv.lock ]]; then
|
||||||
|
python -m venv "${RUNNER_TEMP}/managed-uv"
|
||||||
|
uv_python="${RUNNER_TEMP}/managed-uv/bin/python"
|
||||||
|
"${uv_python}" -m pip install --disable-pip-version-check uv==0.10.0
|
||||||
|
managed_uv="${RUNNER_TEMP}/managed-uv/bin/uv"
|
||||||
|
export UV_PROJECT_ENVIRONMENT="${RUNNER_TEMP}/managed-project-venv"
|
||||||
|
"${managed_uv}" sync --locked
|
||||||
|
export PATH="${UV_PROJECT_ENVIRONMENT}/bin:${PATH}"
|
||||||
|
if [[ "${profile}" == test || "${profile}" == full ]]; then
|
||||||
|
if "${managed_uv}" run python -c 'import pytest' 2>/dev/null; then
|
||||||
|
"${managed_uv}" run python -m pytest
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "${profile}" == lint || "${profile}" == full ]]; then
|
||||||
|
if "${managed_uv}" run python -c 'import ruff' 2>/dev/null; then
|
||||||
|
"${managed_uv}" run python -m ruff check .
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ -f requirements.txt ]]; then
|
||||||
|
python -m venv "${RUNNER_TEMP}/managed-python"
|
||||||
|
managed_python="${RUNNER_TEMP}/managed-python/bin/python"
|
||||||
|
"${managed_python}" -m pip install --disable-pip-version-check -r requirements.txt
|
||||||
|
export PATH="${RUNNER_TEMP}/managed-python/bin:${PATH}"
|
||||||
|
if [[ "${profile}" == test || "${profile}" == full ]]; then
|
||||||
|
if "${managed_python}" -c 'import pytest' 2>/dev/null; then
|
||||||
|
"${managed_python}" -m pytest
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prepare Python before invoking Node scripts. Polyglot repositories
|
||||||
|
# commonly delegate their test script to Python and need the managed
|
||||||
|
# virtual environment to be active first.
|
||||||
|
if [[ -f package.json ]]; then
|
||||||
|
corepack enable
|
||||||
|
if [[ -f pnpm-lock.yaml ]]; then
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
[[ "${profile}" == test || "${profile}" == full ]] && pnpm --if-present test
|
||||||
|
[[ "${profile}" == lint || "${profile}" == full ]] && pnpm --if-present lint
|
||||||
|
[[ "${profile}" == typecheck || "${profile}" == full ]] && pnpm --if-present typecheck
|
||||||
|
[[ "${profile}" == build || "${profile}" == full ]] && pnpm --if-present build
|
||||||
|
elif [[ -f package-lock.json ]]; then
|
||||||
|
npm ci
|
||||||
|
[[ "${profile}" == test || "${profile}" == full ]] && npm run --if-present test
|
||||||
|
[[ "${profile}" == lint || "${profile}" == full ]] && npm run --if-present lint
|
||||||
|
if [[ "${profile}" == typecheck || "${profile}" == full ]]; then
|
||||||
|
npm run --if-present typecheck
|
||||||
|
fi
|
||||||
|
[[ "${profile}" == build || "${profile}" == full ]] && npm run --if-present build
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f go.mod ]]; then
|
||||||
|
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
|
||||||
|
go test ./...
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -f Cargo.toml ]]; then
|
||||||
|
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
|
||||||
|
cargo test --locked
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if compgen -G '*.sln' >/dev/null; then
|
||||||
|
if [[ "${profile}" == test || "${profile}" == build || "${profile}" == full ]]; then
|
||||||
|
dotnet test --configuration Release
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
name: MobilityOps release evidence
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-evidence:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
- name: Build commit-labelled release images
|
||||||
|
run: |
|
||||||
|
docker build --target runtime --build-arg VCS_REF="$GITHUB_SHA" --tag mobilityops-api-release --file backend/Dockerfile .
|
||||||
|
docker build --build-arg VCS_REF="$GITHUB_SHA" --tag mobilityops-web-release frontend
|
||||||
|
docker build --build-arg VCS_REF="$GITHUB_SHA" --tag mobilityops-backup-tools-release --file deploy/unraid/Dockerfile.backup-tools .
|
||||||
|
- name: Scan all release images
|
||||||
|
run: |
|
||||||
|
for image in mobilityops-api-release mobilityops-web-release mobilityops-backup-tools-release; do
|
||||||
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v mobilityops-release-trivy:/root/.cache/ \
|
||||||
|
aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 \
|
||||||
|
image --scanners vuln --severity HIGH,CRITICAL \
|
||||||
|
--ignore-unfixed --exit-code 1 "$image"
|
||||||
|
done
|
||||||
|
- name: Generate CycloneDX SBOMs with the pinned scanner image
|
||||||
|
run: |
|
||||||
|
for component in api web backup-tools; do
|
||||||
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v "$PWD:/work" -w /work \
|
||||||
|
aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 \
|
||||||
|
image --format cyclonedx --output "mobilityops-${component}-sbom.cdx.json" \
|
||||||
|
"mobilityops-${component}-release"
|
||||||
|
done
|
||||||
|
- name: Record immutable image metadata
|
||||||
|
run: |
|
||||||
|
docker image inspect mobilityops-api-release > mobilityops-api-image.json
|
||||||
|
docker image inspect mobilityops-web-release > mobilityops-web-image.json
|
||||||
|
docker image inspect mobilityops-backup-tools-release > mobilityops-backup-tools-image.json
|
||||||
|
python scripts/generate-release-provenance.py
|
||||||
|
sha256sum mobilityops-*-sbom.cdx.json mobilityops-*-image.json release-provenance.json > SHA256SUMS
|
||||||
|
- name: Upload release evidence
|
||||||
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3; Gitea-compatible artifact protocol
|
||||||
|
with:
|
||||||
|
name: mobilityops-${{ github.ref_name }}-evidence
|
||||||
|
path: |
|
||||||
|
mobilityops-*-sbom.cdx.json
|
||||||
|
mobilityops-*-image.json
|
||||||
|
release-provenance.json
|
||||||
|
SHA256SUMS
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: MobilityOps security
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "17 3 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mobilityops-security
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
images:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Build production images once
|
||||||
|
run: |
|
||||||
|
docker build --target runtime --build-arg VCS_REF="$GITHUB_SHA" \
|
||||||
|
--tag mobilityops-api-ci --file backend/Dockerfile .
|
||||||
|
docker build --build-arg VCS_REF="$GITHUB_SHA" \
|
||||||
|
--tag mobilityops-web-ci frontend
|
||||||
|
- name: Scan production images for fixed HIGH and CRITICAL vulnerabilities
|
||||||
|
run: |
|
||||||
|
for image in mobilityops-api-ci mobilityops-web-ci; do
|
||||||
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
docker.io/aquasec/trivy@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e \
|
||||||
|
image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed --no-progress "$image"
|
||||||
|
done
|
||||||
|
- name: Scan repository secrets and misconfiguration
|
||||||
|
uses: docker://docker.io/aquasec/trivy@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e
|
||||||
|
with:
|
||||||
|
args: fs --scanners misconfig,secret --exit-code 1 --no-progress .
|
||||||
|
- name: Remove temporary image tags
|
||||||
|
if: always()
|
||||||
|
run: docker image rm mobilityops-api-ci mobilityops-web-ci || true
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: Unraid autoredeploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths-ignore:
|
||||||
|
- ".gitea/**"
|
||||||
|
- "docs/**"
|
||||||
|
- "**/*.md"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: unraid-production-mobilityops
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy mobilityops
|
||||||
|
runs-on: unraid-deploy
|
||||||
|
timeout-minutes: 180
|
||||||
|
steps:
|
||||||
|
- name: Deploy exact Gitea revision
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
docker exec gitea-deploy-control \
|
||||||
|
/opt/gitea-deploy/deploy.py deploy \
|
||||||
|
"$GITHUB_REPOSITORY" "$GITHUB_SHA"
|
||||||
|
|
||||||
@@ -1,16 +1,66 @@
|
|||||||
|
# Secrets and local configuration
|
||||||
.env
|
.env
|
||||||
.venv/
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
secrets/
|
||||||
|
credentials/
|
||||||
|
|
||||||
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.venv/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Frontend and test output
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
coverage/
|
coverage/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
*.pyc
|
*.tsbuildinfo
|
||||||
.DS_Store
|
|
||||||
|
# Runtime data and local infrastructure state
|
||||||
|
.state/
|
||||||
|
data/
|
||||||
|
backups/local/
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Generated release/design evidence. Maintained documentation belongs in docs/.
|
||||||
|
artifacts/**/final-summary.md
|
||||||
|
artifacts/deployment/
|
||||||
|
artifacts/design-validation/current/
|
||||||
|
artifacts/**/screenshots/generated/
|
||||||
|
|
||||||
|
# Local AI/editor state
|
||||||
|
.codex/
|
||||||
|
.claude/
|
||||||
|
.agents/
|
||||||
|
.dyad/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.tsbuildinfo
|
.vs/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Archives and local release bundles
|
||||||
|
*.zip
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
*.tgz
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes are documented here. The project follows semantic release tags for
|
||||||
|
the deployable PoC; detailed validation evidence remains in `PROJECT_STATE.md`.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Added contract-drift, accessibility, Firefox smoke and frontend asset-budget gates.
|
||||||
|
- Added immutable commit-labelled deployment with automatic application rollback.
|
||||||
|
- Added real PostgreSQL restore drills and routed Alertmanager notifications.
|
||||||
|
- Added RAGcore generation circuit breaking and retrieval telemetry.
|
||||||
|
- Added scheduled dependency maintenance, dual-image vulnerability scans and release SBOMs.
|
||||||
|
|
||||||
|
## [1.0.0-poc] - 2026-08-21
|
||||||
|
|
||||||
|
- Completed the locked Fleet Ops proof of concept: operational core, transactional returns,
|
||||||
|
data quality, n8n orchestration, grounded RAGcore knowledge, read-only MCP integration,
|
||||||
|
privacy governance, observability, backup/recovery and full browser acceptance.
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# Binding instructions for Claude
|
|
||||||
|
|
||||||
## Operating mode
|
|
||||||
|
|
||||||
Work autonomously. Do not ask the user product, architecture, naming, UI, scope or implementation questions already answered in this repository. Record a reasonable assumption in an ADR only when a genuine gap blocks implementation.
|
|
||||||
|
|
||||||
Use normal or medium reasoning for routine work. Reserve high reasoning for an actual cross-service design conflict or a persistent failure after evidence-driven debugging.
|
|
||||||
|
|
||||||
Continue from milestone to milestone until every acceptance criterion is satisfied. Do not stop merely because one milestone is complete.
|
|
||||||
|
|
||||||
## Token and tool efficiency
|
|
||||||
|
|
||||||
1. Read `START_HERE.md`, this file, `PROJECT_STATE.md` and `docs/15-build-plan.md` first.
|
|
||||||
2. Read only the milestone-specific documents named in the build plan.
|
|
||||||
3. Do not repeatedly reread all documentation.
|
|
||||||
4. Keep explanations terse; spend effort on implementation and validation.
|
|
||||||
5. Update `PROJECT_STATE.md` after each milestone with decisions, commands, evidence and the exact next action.
|
|
||||||
6. Prefer focused file inspection and targeted tests over broad repository scans.
|
|
||||||
7. Do not generate large speculative documents after implementation starts.
|
|
||||||
|
|
||||||
## Scope discipline
|
|
||||||
|
|
||||||
- Build the locked PoC only.
|
|
||||||
- Do not add accounting, payments, public reservations, a generic CRM, inventory, HR, a second RAG stack, a separate MCP server or autonomous write actions.
|
|
||||||
- Do not modify the RAGcore or ITWorx MCP Hub repositories. Integrate only through documented contracts and configurable adapters.
|
|
||||||
- Keep critical business rules in MobilityOps code, not in n8n or prompts.
|
|
||||||
- No direct MCP Hub or RAGcore access to the MobilityOps database.
|
|
||||||
|
|
||||||
## Product quality
|
|
||||||
|
|
||||||
- No dead buttons, empty routes, unexplained placeholders or hardcoded dashboard metrics.
|
|
||||||
- Every visible number must derive from persisted data.
|
|
||||||
- All important state changes must be audited.
|
|
||||||
- AI must never invent an answer when RAGcore is unavailable or returns insufficient evidence.
|
|
||||||
- Vehicle returns must commit locally even when n8n is unavailable; orchestration becomes pending and retryable.
|
|
||||||
- External dependencies require timeouts, bounded retries, health state and graceful degradation.
|
|
||||||
- Demo data must be clearly labelled synthetic.
|
|
||||||
|
|
||||||
## Engineering rules
|
|
||||||
|
|
||||||
- Backend: Python, FastAPI, SQLAlchemy 2, Alembic, PostgreSQL.
|
|
||||||
- Frontend: React, TypeScript, Vite, accessible responsive UI.
|
|
||||||
- Validation: Pydantic at API boundaries and database constraints for invariants.
|
|
||||||
- Use UUID primary keys internally and stable human-readable public references.
|
|
||||||
- Store UTC timestamps; render Europe/Brussels in the UI.
|
|
||||||
- API paths start with `/api/v1`.
|
|
||||||
- Use an outbox record for reliable post-commit n8n delivery.
|
|
||||||
- Tests must cover domain rules, API contracts and the five-minute Playwright demo.
|
|
||||||
- Generate and commit dependency lockfiles.
|
|
||||||
|
|
||||||
## Git workflow
|
|
||||||
|
|
||||||
Create one coherent commit per milestone after its validation passes. Suggested message format:
|
|
||||||
|
|
||||||
`M1: implement operational core`
|
|
||||||
|
|
||||||
Never rewrite already accepted milestone history unless necessary to fix a regression.
|
|
||||||
|
|
||||||
## Definition of done
|
|
||||||
|
|
||||||
The project is done only when `docs/14-testing-and-acceptance.md` passes from a clean checkout and `PROJECT_STATE.md` contains the final evidence summary.
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
MobilityOps contributions must preserve fleet-data privacy, deterministic demo behaviour and the fail-closed integration boundaries documented in `SECURITY.md`.
|
||||||
|
|
||||||
|
- use synthetic vehicles, customers, bookings, returns, telematics events and identity claims in tests and screenshots;
|
||||||
|
- never commit production databases, exports, operator inventories, private service URLs, tokens, backups or generated browser evidence;
|
||||||
|
- keep external integrations configurable through environment variables or explicit deployment configuration;
|
||||||
|
- document new personal-data fields, retention, authorization, audit and deletion/export behaviour;
|
||||||
|
- add negative tests for authentication, authorization, duplicate handling, webhook validation, path containment and stale/unavailable providers;
|
||||||
|
- review dependencies, images and browser assets for provenance and redistribution rights.
|
||||||
|
|
||||||
|
Run the relevant backend, frontend, migration, integration, Compose and managed-validation gates before review. Security-sensitive findings belong through the private process in `SECURITY.md`.
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# File index
|
|
||||||
|
|
||||||
- `.env.example`
|
|
||||||
- `.gitignore`
|
|
||||||
- `CLAUDE.md`
|
|
||||||
- `MASTER_BUILD_PROMPT.md`
|
|
||||||
- `Makefile`
|
|
||||||
- `PROJECT_STATE.md`
|
|
||||||
- `README.md`
|
|
||||||
- `START_HERE.md`
|
|
||||||
- `backend/Dockerfile`
|
|
||||||
- `backend/app/__init__.py`
|
|
||||||
- `backend/app/core/__init__.py`
|
|
||||||
- `backend/app/core/config.py`
|
|
||||||
- `backend/app/main.py`
|
|
||||||
- `backend/pyproject.toml`
|
|
||||||
- `backend/tests/test_health.py`
|
|
||||||
- `compose.yaml`
|
|
||||||
- `contracts/events.schema.json`
|
|
||||||
- `contracts/mcp-tools.json`
|
|
||||||
- `contracts/openapi.yaml`
|
|
||||||
- `contracts/ragcore-contract-assumptions.md`
|
|
||||||
- `docs/00-product-brief.md`
|
|
||||||
- `docs/01-scope-and-non-goals.md`
|
|
||||||
- `docs/02-user-stories.md`
|
|
||||||
- `docs/03-architecture.md`
|
|
||||||
- `docs/04-domain-model.md`
|
|
||||||
- `docs/05-api-contract.md`
|
|
||||||
- `docs/06-ui-ux.md`
|
|
||||||
- `docs/07-data-quality.md`
|
|
||||||
- `docs/08-return-workflow.md`
|
|
||||||
- `docs/09-ragcore-integration.md`
|
|
||||||
- `docs/10-mcp-hub-integration.md`
|
|
||||||
- `docs/11-n8n-integration.md`
|
|
||||||
- `docs/12-security-and-audit.md`
|
|
||||||
- `docs/13-seed-and-demo-scenarios.md`
|
|
||||||
- `docs/14-testing-and-acceptance.md`
|
|
||||||
- `docs/15-build-plan.md`
|
|
||||||
- `docs/16-portfolio-case-study.md`
|
|
||||||
- `docs/17-runbook.md`
|
|
||||||
- `docs/deferred.md`
|
|
||||||
- `frontend/Dockerfile`
|
|
||||||
- `frontend/index.html`
|
|
||||||
- `frontend/nginx.conf`
|
|
||||||
- `frontend/package.json`
|
|
||||||
- `frontend/src/App.tsx`
|
|
||||||
- `frontend/src/main.tsx`
|
|
||||||
- `frontend/src/styles.css`
|
|
||||||
- `frontend/tsconfig.json`
|
|
||||||
- `frontend/vite.config.ts`
|
|
||||||
- `knowledge/manifest.json`
|
|
||||||
- `knowledge/procedures/01-vehicle-checkout.md`
|
|
||||||
- `knowledge/procedures/02-vehicle-return.md`
|
|
||||||
- `knowledge/procedures/03-damage-handling.md`
|
|
||||||
- `knowledge/procedures/04-odometer-anomalies.md`
|
|
||||||
- `knowledge/procedures/05-cleaning-checklist.md`
|
|
||||||
- `knowledge/procedures/06-maintenance-escalation.md`
|
|
||||||
- `knowledge/procedures/07-customer-documents.md`
|
|
||||||
- `knowledge/procedures/08-privacy.md`
|
|
||||||
- `knowledge/procedures/09-booking-conflicts.md`
|
|
||||||
- `knowledge/procedures/10-roles-and-escalation.md`
|
|
||||||
- `n8n/README.md`
|
|
||||||
- `n8n/mobilityops-return-processing.json`
|
|
||||||
- `seed/README.md`
|
|
||||||
- `seed/bookings.csv`
|
|
||||||
- `seed/customers.csv`
|
|
||||||
- `seed/data_quality_issues.csv`
|
|
||||||
- `seed/generate_seed.py`
|
|
||||||
- `seed/inspections.csv`
|
|
||||||
- `seed/maintenance.csv`
|
|
||||||
- `seed/vehicles.csv`
|
|
||||||
- `seed/workflow_runs.csv`
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Jens Caers
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Paste this once into Claude Code
|
|
||||||
|
|
||||||
Build MobilityOps autonomously from this repository.
|
|
||||||
|
|
||||||
First read `START_HERE.md`, `CLAUDE.md`, `PROJECT_STATE.md` and `docs/15-build-plan.md`. Treat the repository specifications and contracts as binding. Do not ask me questions that the files already answer, do not broaden the PoC, and do not stop after a milestone.
|
|
||||||
|
|
||||||
Implement the milestones in order. For each milestone:
|
|
||||||
|
|
||||||
1. read only the documents listed for that milestone;
|
|
||||||
2. implement the smallest complete solution;
|
|
||||||
3. run the specified validation plus relevant regression tests;
|
|
||||||
4. fix failures using evidence rather than guesses;
|
|
||||||
5. update `PROJECT_STATE.md` with concise evidence and the exact next step;
|
|
||||||
6. commit the completed milestone;
|
|
||||||
7. continue immediately.
|
|
||||||
|
|
||||||
MobilityOps owns operational data and business rules. RAGcore owns retrieval and grounded answers. ITWorx MCP Hub owns MCP publication and policy. n8n only orchestrates post-commit workflows. Use configurable adapters and working degraded modes so the core demo remains usable when any external service is absent.
|
|
||||||
|
|
||||||
The finished PoC must be reproducible from a clean checkout, have no dead UI, use deterministic synthetic data, support the documented five-minute demo, and satisfy every criterion in `docs/14-testing-and-acceptance.md`.
|
|
||||||
|
|
||||||
Keep chat output brief. Spend the available context on code, tests, validation and final evidence. Begin now and continue until the repository is complete.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: up down logs test lint seed reset n8n-setup demo e2e
|
.PHONY: up down logs test lint contracts seed reset n8n-setup n8n-setup-scan demo e2e live-smoke
|
||||||
|
|
||||||
up:
|
up:
|
||||||
docker compose up --build -d
|
docker compose up --build -d
|
||||||
@@ -10,11 +10,17 @@ logs:
|
|||||||
docker compose logs -f --tail=200
|
docker compose logs -f --tail=200
|
||||||
|
|
||||||
test:
|
test:
|
||||||
docker compose run --rm api pytest
|
sh scripts/run-isolated-tests.sh
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
docker compose run --rm api ruff check .
|
docker compose -f compose.yaml -f compose.test.yaml run --build --rm api ruff check app tests
|
||||||
docker compose run --rm api mypy app
|
docker compose -f compose.yaml -f compose.test.yaml run --rm api mypy app
|
||||||
|
cd frontend && npm run lint
|
||||||
|
|
||||||
|
contracts:
|
||||||
|
docker compose -f compose.yaml -f compose.test.yaml run --build --rm \
|
||||||
|
-v "$(CURDIR):/repo:ro" api python /repo/scripts/check-contracts.py
|
||||||
|
python scripts/check-source-budgets.py
|
||||||
|
|
||||||
seed:
|
seed:
|
||||||
docker compose exec api python -m app.cli seed --reset
|
docker compose exec api python -m app.cli seed --reset
|
||||||
@@ -26,15 +32,28 @@ reset:
|
|||||||
# One-time per environment: imports and activates the n8n return-processing workflow.
|
# One-time per environment: imports and activates the n8n return-processing workflow.
|
||||||
# The n8n owner account itself cannot be scripted safely and must be created once at
|
# The n8n owner account itself cannot be scripted safely and must be created once at
|
||||||
# http://localhost:5678/setup (any email/password, no verification required) before
|
# http://localhost:5678/setup (any email/password, no verification required) before
|
||||||
# this target's activation takes effect. See docs/17-runbook.md.
|
# this target's activation takes effect. The workflow also needs the "Fleet Ops Webhook
|
||||||
|
# Trigger Token" and "Fleet Ops Service Token" Header Auth credentials created manually in
|
||||||
|
# the n8n UI before it will actually process a return -- see docs/17-runbook.md.
|
||||||
n8n-setup:
|
n8n-setup:
|
||||||
docker compose exec n8n n8n import:workflow --input=//imports/mobilityops-return-processing.json
|
docker compose exec n8n n8n import:workflow --input=//imports/workflows/fleet-ops-vehicle-return.json
|
||||||
docker compose exec n8n n8n publish:workflow --id=mobilityops-return-processing
|
docker compose exec n8n n8n publish:workflow --id=mobilityops-return-processing
|
||||||
docker compose restart n8n
|
docker compose restart n8n
|
||||||
|
|
||||||
|
# One-time per environment: imports and activates the scheduled quality-scan workflow.
|
||||||
|
# Same owner-account and credential preconditions as n8n-setup above (this workflow only
|
||||||
|
# needs "Fleet Ops Service Token").
|
||||||
|
n8n-setup-scan:
|
||||||
|
docker compose exec n8n n8n import:workflow --input=//imports/workflows/fleet-ops-data-quality-scan.json
|
||||||
|
docker compose exec n8n n8n publish:workflow --id=mobilityops-scheduled-quality-scan
|
||||||
|
docker compose restart n8n
|
||||||
|
|
||||||
# Full deterministic demo bootstrap: build, migrate (automatic on api startup), seed.
|
# Full deterministic demo bootstrap: build, migrate (automatic on api startup), seed.
|
||||||
demo: up
|
demo: up
|
||||||
docker compose exec api python -m app.cli seed --reset
|
docker compose exec api python -m app.cli seed --reset
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
cd frontend && npx playwright test
|
cd frontend && npx playwright test
|
||||||
|
|
||||||
|
live-smoke:
|
||||||
|
cd frontend && npx playwright test --config=playwright.live.config.ts
|
||||||
|
|||||||
@@ -1,322 +0,0 @@
|
|||||||
# Project state
|
|
||||||
|
|
||||||
## Publication and Unraid deployment (2026-08-02)
|
|
||||||
|
|
||||||
- Unraid deployment is live at `http://192.168.10.150:1236` from
|
|
||||||
`/mnt/user/appdata/mobilityops`, Compose project `mobilityops`.
|
|
||||||
- Deployment config commits: `07ab7a3`, `847cd05`, `e1a1c67`, `1e13943`. The accepted
|
|
||||||
baseline `4bf9afbeff44088864e0844769d4dd0e4089d85b` remains intact.
|
|
||||||
- MobilityOps PostgreSQL, API and web services are healthy. Only web port 1236 is exposed
|
|
||||||
by the MobilityOps Compose project; API and PostgreSQL remain internal. Automation uses
|
|
||||||
the server's existing shared n8n at `http://192.168.10.150:5678`; no second MobilityOps
|
|
||||||
n8n container is running.
|
|
||||||
- Migrations are at `e7b08389f47f (head)` and deterministic seed counts match final
|
|
||||||
acceptance. A Chrome smoke test covered every requested page and a real return; its n8n
|
|
||||||
event succeeded on attempt 1. Browser console and recent service log scans were clean.
|
|
||||||
- RAGcore is disabled in favor of the honest local demo provider. MCP Hub registration is
|
|
||||||
disabled. The MobilityOps workflow is published in the existing n8n and live-verified.
|
|
||||||
- Local post-change gates: 66 backend tests, Ruff, mypy (44 files), and frontend production
|
|
||||||
build all pass. Evidence is in `artifacts/deployment/unraid-summary.md`.
|
|
||||||
- Published to the private Gitea repository
|
|
||||||
`https://gitea.itworx.tech/Jens/MobilityOps`. `master` is the default branch; the full
|
|
||||||
commit history and baseline commit are present; zero tags exist; remote hygiene is
|
|
||||||
clean. `origin` uses the SSH clone URL supplied by Gitea.
|
|
||||||
- Exact next action: none — repository publication and Unraid deployment are complete.
|
|
||||||
|
|
||||||
## Current milestone
|
|
||||||
|
|
||||||
M7 — complete. All milestones (M0–M7) done, plus a full post-M7 final-acceptance audit (see below). See `artifacts/final-acceptance/summary.md` for the definitive acceptance evidence (supersedes `artifacts/evidence/final-summary.md`, which is kept as historical M7 evidence).
|
|
||||||
|
|
||||||
## Locked decisions
|
|
||||||
|
|
||||||
- Product name: MobilityOps.
|
|
||||||
- Fictitious tenant: Northstar Mobility Demo.
|
|
||||||
- PoC only; all operational and knowledge data are synthetic.
|
|
||||||
- Core stack and boundaries are defined in `CLAUDE.md` and `docs/03-architecture.md`.
|
|
||||||
- RAGcore and ITWorx MCP Hub are external central services.
|
|
||||||
- n8n receives post-commit events through an outbox dispatcher.
|
|
||||||
- SQLAlchemy 2 declarative models cover the full domain model (`backend/app/models/`); enums are plain `String` columns validated at the Pydantic/service layer, not native PG enums (simpler migrations).
|
|
||||||
- `backend/requirements.lock` is compiled inside a `python:3.12-slim` container (matches the Dockerfile base image) via `pip-compile --extra dev`; regenerate the same way if `pyproject.toml` changes.
|
|
||||||
- Frontend dependencies pinned (no more `"latest"`); `package-lock.json` committed; Docker build uses `npm ci`.
|
|
||||||
- Demo auth is a lightweight HMAC-signed cookie (`app/core/security.py`), not a real password/JWT flow — matches "Demo role buttons create an authenticated session; they do not bypass authorization middleware." Two fixed demo users (`USR-OPS` operations_manager, `USR-EMP` rental_employee) are created by the seed loader, not from a CSV (no `users.csv` in `seed/`).
|
|
||||||
- Seed loader (`backend/app/seed_loader.py`) only supports `seed --reset` (always rebuilds); there is no incremental/idempotent-without-reset mode, since the acceptance criteria only require deterministic reset, not partial import.
|
|
||||||
- `DataQualityIssue.entity_ref`/`related_ref` from the CSVs are resolved to `entity_type`/`entity_id` (UUID) at load time per the domain model; the original human-readable refs are kept in `evidence_json` (`entity_ref`, `related_refs`) since the API and UI need them and re-resolving UUID→public_ref on every read would be wasteful.
|
|
||||||
- `backend/app/core/config.py` added `app_secret`, `session_cookie_name`, `session_ttl_seconds`, `seed_dir` (`/app/seed` in-container), `cors_allow_origins` (comma-separated string, not a list — simpler with pydantic-settings env parsing), `demo_today` (drives the dashboard's "Today" section against the deterministic anchor date, default `2026-08-01`).
|
|
||||||
- `compose.yaml` api build context changed from `./backend` to repo root with `dockerfile: backend/Dockerfile`, so the image can `COPY seed ./seed` (seed CSVs are outside `backend/`).
|
|
||||||
- Frontend: added `react-router-dom@7.18.2` (bumped from 6.x to clear two real advisories — open redirect + arbitrary constructor injection in v6). One residual `npm audit` finding (RSC-mode CSRF, GHSA-qwww-vcr4-c8h2) does not apply — this SPA never uses React Router's RSC/SSR mode.
|
|
||||||
- Nav/pages built so far: Dashboard, Vehicles (list+detail with tabs), Bookings (list+detail), Audit. Data Quality, Knowledge and Automation nav items are intentionally omitted until M3/M5/M4 build the pages behind them — CLAUDE.md forbids dead routes/placeholders.
|
|
||||||
- Return workflow (`app/services/returns.py`): the spec's "validate submitted reading against booking start reading" step was dropped as a hard rejection. For the seeded S1 scenario, a booking's `start_odometer_km` can already equal the vehicle's canonical odometer, so any regression-testing value would also be below the booking start, making a hard floor there indistinguishable from — and in conflict with — the documented soft-regression path. Only one odometer check exists now: submitted vs. the vehicle's *canonical* odometer (`vehicle.odometer_km`), matching the domain-model invariant verbatim ("a return with a lower submitted reading is recorded as an inspection and issue, while canonical odometer remains unchanged").
|
|
||||||
- Idempotency: new `idempotency_records` table (migration `e7b08389f47f`), unique on `idempotency_key`, keyed to `booking_id`. Same key + same booking replays the stored response; same key + different booking → 409 `IDEMPOTENCY_KEY_REUSED`; different key on an already-returned booking → 409 `INVALID_BOOKING_STATE`. Concurrency is enforced by `SELECT ... FOR UPDATE` on the booking row (re-checked for the idempotency record immediately after acquiring the lock, as a safety net for two simultaneous identical-key requests racing the pre-lock check).
|
|
||||||
- `seed_loader.clear_all()` must delete `idempotency_records` before `bookings` (FK) — easy to forget when adding new booking-referencing tables; the ordering list at the top of `seed_loader.py` is the single place to update.
|
|
||||||
- Inspection `public_ref` is assigned as `INSP-{count+1:04d}` from a live count query (not gap-safe, fine for a PoC single-writer demo, would need a sequence for real concurrency-safe numbering).
|
|
||||||
- Found and fixed during browser verification (not caught by pytest, since it's a UI-only defect): `ReturnForm` originally held its own `result` state and was conditionally rendered only when `booking.status === "active"`; once the return succeeded the booking flipped to `returned` and React unmounted the form before the user ever saw the result panel. Fixed by lifting the result into `BookingDetail` (`ReturnResultPanel` is now a sibling, not nested in `ReturnForm`). Also found: `OutboxEvent.event_id`'s Python-side `default=uuid.uuid4` on the mapped_column only applies at flush/commit time, so reading `event.event_id` before `db.commit()` returned `None` (rendered as the literal string "None" in the result panel); fixed by assigning `event_id=uuid.uuid4()` explicitly at construction. Lesson: SQLAlchemy column `default=` callables are not available on the in-memory Python object until flush — never rely on the generated value for a same-transaction response body without an explicit `db.flush()` or an explicit Python-side assignment.
|
|
||||||
- Operational note for this environment: `docker compose run --rm api ...` (used for tests/lint) only starts a throwaway one-off container — it does **not** update the long-running `api`/`web` service containers. After any code change meant to be verified live (browser, curl), `docker compose up -d --build <service>` is required, not just `docker compose build`.
|
|
||||||
|
|
||||||
## Completed evidence
|
|
||||||
|
|
||||||
### M0 — Reproducible foundation
|
|
||||||
- Added `backend/app/core/db.py` (engine/session), `backend/app/models/*` (User, Customer, Vehicle, Booking, Inspection, MaintenanceRecord, DataQualityIssue, OutboxEvent, AuditEvent), Alembic config (`backend/alembic.ini`, `backend/alembic/env.py`) and initial migration `backend/alembic/versions/c9498525abb5_initial_schema.py`.
|
|
||||||
- Commands run and verified from this checkout:
|
|
||||||
- `docker compose build api` — OK
|
|
||||||
- `docker compose run --rm api alembic upgrade head` — applied cleanly to empty DB, created 9 tables + `alembic_version`.
|
|
||||||
- `docker compose run --rm api pytest -q` — 1 passed.
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed (added `extend-exclude = ["alembic/versions"]` to `backend/pyproject.toml` for autogenerated migration line length).
|
|
||||||
- `docker compose up -d --build` — all 4 services healthy: `curl http://localhost:8128/health` → `{"status":"ok",...}`; `curl -o /dev/null -w "%{http_code}" http://localhost:1228/` → 200; `curl http://localhost:5678/healthz` → 200.
|
|
||||||
- Fixed a real scaffold bug: `frontend/src/App.tsx` used `import.meta.env` without a `vite/client` types reference, which broke `npm run build` in Docker (works fine under plain `vite dev` because Vite injects the global at dev-time but `tsc -b` still type-checks it). Added `frontend/src/vite-env.d.ts`.
|
|
||||||
- `make` is not installed in this Windows/git-bash shell — validated the underlying `docker compose ...` commands directly instead (Makefile targets are thin wrappers around them and are correct as written for a Linux/CI shell or WSL).
|
|
||||||
- Known accepted gap: `npm audit` reports 1 moderate/1 high transitive `esbuild` advisory (dev-server-only, fixed only by a Vite 8 major bump); left as-is for the PoC, noted here rather than silently upgrading a major version.
|
|
||||||
|
|
||||||
### M1 — Operational core
|
|
||||||
- Backend additions: `app/core/security.py` (HMAC-signed session cookies), `app/api/deps.py` (`get_current_user`, `require_operations_manager`), `app/core/errors.py` (`AppError` + the documented `{"error": {...}}` shape wired as a FastAPI exception handler for both `AppError` and `HTTPException`), `app/seed_loader.py`, `app/cli.py` (`python -m app.cli seed --reset`), `app/services/audit.py`, `app/schemas.py`, routers under `app/api/routers/` (`demo`, `dashboard`, `vehicles`, `bookings`, `audit`).
|
|
||||||
- Frontend additions: React Router-based app shell (`src/App.tsx`, `src/components/Layout.tsx`, `src/components/RequireAuth.tsx`), `AuthContext`, typed `api` client (`src/api/client.ts`, `src/api/types.ts`), pages `Login`, `Dashboard`, `Vehicles`/`VehicleDetail`, `Bookings`/`BookingDetail`, `Audit`. Full responsive stylesheet (`src/styles.css`) covering nav collapse and table→card layout under 700px, visible focus states, no hover-only actions.
|
|
||||||
- Commands run and verified from this checkout (container rebuilt each time to pick up code changes):
|
|
||||||
- `docker compose run --rm api pytest -q` — **19 passed** (new: `test_seed.py`, `test_auth.py`, `test_dashboard.py`, `test_vehicles.py`, `test_bookings.py`, `test_audit.py`; tests seed the real Postgres via `reset_and_seed` in a session fixture, then exercise the FastAPI app through `TestClient`, not mocks).
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed (added `ignore = ["B008"]` — FastAPI's `Depends()`-as-default is idiomatic, not a real bug).
|
|
||||||
- `npm run build` (local, Node 24) — clean `tsc -b && vite build`.
|
|
||||||
- `docker compose up -d --build` then `docker compose exec api python -m app.cli seed --reset` — counts: `users:2 customers:180 vehicles:50 bookings:246 inspections:75 maintenance:40 data_quality_issues:15 workflow_runs:20`.
|
|
||||||
- `curl` end-to-end: `POST /api/v1/demo/login` sets cookie and returns the user; unauthenticated `GET /api/v1/dashboard` → 401 with the documented error shape; authenticated dashboard/vehicle-detail return real seeded data (verified metrics `available:21 rented:11 cleaning:6 maintenance:5 blocked:7`, matching the 50 seeded vehicles).
|
|
||||||
- Browser smoke test (Chrome via MCP) at desktop width: login page → Operations Manager login → Dashboard (metrics + attention items + today + recent automation all populated) → Vehicle detail `MO-016` (tabs render, "Needs attention" badge correct — it's `DQ-DEMO-OVERLAP`/`DQ-DEMO-STATUS`) → Booking detail `BK-DEMO-RETURN` (matches S1 scenario: vehicle `MO-024`, status `active`, start odometer `53610`). Responsive CSS (`@media max-width:700px`) was written and code-reviewed but the automated resize during this session didn't visibly reflect in the captured screenshot (likely a screenshot-timing quirk of the browser tool, not necessarily a real bug) — **treat the ≤360px layout as visually unverified** and re-check with a real device/DevTools emulation before final acceptance (M7).
|
|
||||||
- Known accepted gap carried over from M0: `npm audit` residual `esbuild`/Vite-8 dev-server-only advisory.
|
|
||||||
|
|
||||||
### M2 — Vehicle return vertical slice
|
|
||||||
- Backend additions: `app/models/idempotency.py` (`IdempotencyRecord`), migration `e7b08389f47f_idempotency_records`, `app/services/returns.py` (`register_vehicle_return` — full transaction: row locks, idempotency replay, inspection, canonical-odometer update or regression issue, vehicle status derivation, two audit events, `vehicle.returned.v1` outbox event matching `contracts/events.schema.json`, next-booking-risk lookup), `POST /api/v1/bookings/{public_ref}/return` wired in `app/api/routers/bookings.py` with required `Idempotency-Key` header.
|
|
||||||
- Frontend additions: `components/ReturnForm.tsx` (form + `ReturnResultPanel`), wired into `pages/BookingDetail.tsx` (shown only when `booking.status === "active"`; result persists via lifted state after the booking flips to `returned`).
|
|
||||||
- Commands run and verified from this checkout:
|
|
||||||
- `docker compose run --rm api pytest -q` — **26 passed**, including `tests/test_return.py` (success/canonical-update, S1 regression scenario by name, damage→blocked, idempotent replay, reject-already-returned, missing-header validation, and a real multi-threaded concurrent-submission test against Postgres asserting exactly 1×201 + 2×409).
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed.
|
|
||||||
- `npm run build` — clean.
|
|
||||||
- `docker compose up -d --build` (all services) then `docker compose exec api python -m app.cli seed --reset`, then a full browser run of the S1 demo scenario against `BK-DEMO-RETURN`/`MO-024`: submitted 53000 km (below canonical 54820) → result panel showed `INSP-0076`, `resulting_vehicle_status: maintenance` (correctly derived, since canonical 54820 ≥ `next_service_km` 40000), `DQ-RET-0076` created, automation event queued with a real UUID, "no upcoming booking" risk; vehicle detail page confirmed odometer unchanged at 54,820 km and a "Needs attention" badge.
|
|
||||||
- Both real defects listed above (form disappearing before showing its result; `event_id` reading as `None`) were **found via the browser run, not by pytest** — the test suite asserted on API response shape/values, not on what the UI actually rendered after a status transition. Worth remembering for M3+: UI state-after-mutation bugs need a browser check, not just API tests.
|
|
||||||
|
|
||||||
### M3 — Data Quality Workbench
|
|
||||||
- `app/services/data_quality.py`: `run_scan()` implements all five rules and is called automatically at the end of `seed_loader.reset_and_seed()` (after `db.commit()` of the base seed), plus exposed as `POST /api/v1/data-quality/scan` (Operations Manager only). Idempotency is simplified from the doc's literal `(rule_type, entity_type, entity_id, evidence fingerprint)` to just `(rule_type, entity_type, entity_id)` while an issue is open — see rationale below.
|
|
||||||
- Router `app/api/routers/data_quality.py`: `GET /issues` (filters status/rule_type/severity), `GET /issues/{ref}` (adds `entity_snapshot`/`related_snapshots` for the UI), `POST /issues/{ref}/defer`, `/reject`, `/merge-customers` (Operations Manager only — enforced via `require_operations_manager`), `POST /scan`.
|
|
||||||
- **Real bug found and fixed during this milestone, before any browser check**: the first cut of DQ-03 (odometer regression) compared every historical *returned* booking's `end_odometer_km` against the vehicle's *current* `odometer_km`. Since the seed generator assigns `vehicle.odometer_km` independently of booking history (see `seed/generate_seed.py`), this is true for nearly every historical booking by construction (odometer is monotonically increasing over time, so all-but-the-latest reading is "below current") — it produced 51 false-positive issues out of 50 vehicles on first run. Fixed twice: first attempt (compare only the single most-recent booking against canonical) still produced the same problem because canonical itself is disconnected from booking history in this dataset; the working fix compares each vehicle's *own returned-booking sequence* against itself (each booking's end reading vs. the immediately preceding one, chronologically) — a self-consistency check that doesn't depend on the unrelated `vehicle.odometer_km` field at all. Final deterministic seed+scan totals: 15 CSV-seeded + 11 scan-discovered = **26** open/resolved `data_quality_issues` (breakdown: 14 vehicle_status_conflict, 5 missing_required_field, 3 possible_duplicate_customer, 3 odometer_regression, 1 booking_overlap). `tests/test_seed.py`'s exact-count assertion was updated from 15 to 26 accordingly — if the scan logic changes again, update that count.
|
|
||||||
- Idempotency simplification rationale: the doc's fingerprint-based key would make the scan blind to issues it structurally can't compute a matching fingerprint for against the CSV-seeded rows (which don't carry a fingerprint field), producing duplicate issues for the same real-world problem (e.g. a second `MO-016` overlap issue next to the seeded `DQ-DEMO-OVERLAP`). Using `(rule_type, entity_type, entity_id)` alone while open is a stricter, safe simplification: it can never falsely suppress an issue for a *different* entity, and per-entity there's realistically only one meaningful open issue of a given rule type at a time for this PoC's scope.
|
|
||||||
- Merge UI intentionally does **not** use `window.confirm()` — a native dialog blocks further automation/testing and isn't screen-reader-distinguishable from page content the same way a rendered `role="alertdialog"` panel is. Built an inline two-step confirm instead (`ReturnForm`-style pattern reused).
|
|
||||||
- `AttentionItem` gained an `issue_ref` field (dashboard now links attention items straight to `/data-quality/{issue_ref}` instead of only to vehicles); dashboard attention list capped at 8 items (was unbounded, would have shown up to 26 with the richer scan).
|
|
||||||
- Commands run and verified from this checkout:
|
|
||||||
- `docker compose run --rm api pytest -q` — **35 passed** (new `tests/test_data_quality.py`: all five rule types present, scan idempotent on rerun, scan requires Operations Manager, S2/S4 issue-detail snapshots correct, defer→reject-on-closed 409, merge requires Operations Manager, merge rejects an unrelated survivor ref, full S2 merge scenario asserting rewiring + audit + replay-is-409).
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed.
|
|
||||||
- `npm run build` — clean (had to fix two `possibly 'null'` TS errors from a closure-narrowing limitation — TS doesn't narrow `const` captured-by-closure across nested function boundaries when the value comes from an index/property expression; fixed by re-binding to explicitly-typed local consts right after the guard).
|
|
||||||
- Full browser run: Data Quality list (26 open issues, filterable) → `DQ-DEMO-DUPLICATE` two-column compare (CUS-0012 vs CUS-0178, per-field diff highlighting only where they differ) → merge with inline confirm → issue flips to `resolved` → confirmed `customer_merged` audit event with correct actor/entity/correlation → `DQ-DEMO-OVERLAP` (non-duplicate type) renders evidence JSON + defer/reject, no dead compare UI shown for a rule type it doesn't apply to.
|
|
||||||
|
|
||||||
### M4 — n8n automation
|
|
||||||
- `app/services/dispatcher.py`: background daemon thread (started/stopped via FastAPI `lifespan`, not an `on_event` hook) polling every `N8N_DISPATCH_INTERVAL_SECONDS` (default 3s). Claim step (`_claim_due_events`) is a short transaction using `SELECT ... FOR UPDATE SKIP LOCKED` that only flips `pending`→`delivering` and commits immediately; the HTTP call to n8n happens with **no open transaction**; the outcome is recorded in a separate short transaction. Exponential backoff `min(2**attempts, 60)` seconds, `N8N_MAX_ATTEMPTS=5` before a permanent `failed`.
|
|
||||||
- Dispatcher reconstructs the wire event from `contracts/events.schema.json`'s exact fields (`event_id`, `event_type`, `occurred_at`, `correlation_id`, `aggregate`, `data`) rather than forwarding `OutboxEvent.payload_json` wholesale — that column also carries an internal `aggregate_ref` convenience key (used by dashboard/workflows list rendering) that the schema's `additionalProperties: false` would reject.
|
|
||||||
- `POST /api/v1/integrations/n8n/return-callback` (`app/api/routers/integrations.py`): shared-secret auth via `X-Service-Token` header (`N8N_CALLBACK_TOKEN`, propagated to both `api` and `n8n` containers as `MOBILITYOPS_CALLBACK_TOKEN`); idempotent by `Idempotency-Key` (the event UUID) — checked by querying for an existing `AuditEvent` with that event ID in its metadata, **not** by `OutboxEvent.external_run_id`, because the dispatcher only sets that field *after* it gets n8n's final response, which happens *after* n8n has already called this callback mid-workflow — using `external_run_id` as the idempotency guard would have missed the exact redelivery case it's meant to catch.
|
|
||||||
- `GET /api/v1/workflows` + `POST /api/v1/workflows/{event_id}/retry` (`app/api/routers/workflows.py`), both Operations Manager only. Retry only allowed from `failed`; sets `pending` + clears `next_attempt_at` so the live dispatcher picks it up on its next cycle (does not reset `attempts`, so the counter reflects true delivery history).
|
|
||||||
- Automation nav + page (`pages/Automation.tsx`): table of all runs with status/attempts/last error, Retry button for `failed` rows, visible only to Operations Manager (matches backend authorization rather than just hiding a link).
|
|
||||||
- **Two real bugs found and fixed, the second only by testing the actual live n8n round-trip, not by pytest**:
|
|
||||||
1. Seed-loaded `workflow_runs.csv` rows only ever got `payload_json = {"aggregate_ref": ...}` (no `correlation_id`/`aggregate`/`data`) — fine for M1–M3 since nothing read those keys yet, but once the dispatcher tried to *redeliver* a seeded row (i.e. the S5 manual-retry demo scenario) it crashed with `KeyError: 'correlation_id'`, leaving that event stuck in `delivering` forever (the crash happened before the outcome-recording transaction). Fixed in two places: `seed_loader.py` now builds the full schema-compliant envelope for every `workflow_runs.csv` row (matching what the live M2 return flow produces), and `dispatcher._deliver_one` now catches malformed-payload `KeyError`s defensively and resolves the row to `pending`/`failed` instead of leaving it orphaned — added `test_deliver_one_handles_malformed_payload_without_getting_stuck` as a regression test for the latter.
|
|
||||||
2. This n8n image (2.32.7) has dropped `N8N_BASIC_AUTH_ACTIVE` as a UI/API gate — it requires an actual owner account via the `/setup` flow before anything (including webhook registration reliability) works correctly. Also: `n8n import:workflow` requires the workflow JSON to have a top-level `"id"` field (added `"id": "mobilityops-return-processing"`) and **always deactivates** the imported workflow regardless of its `"active"` field — activation requires `n8n publish:workflow --id=<id>` followed by a full n8n restart (documented in n8n 2.x CLI, not obvious from the docs pack). Did this manually this session via the CLI + browser setup wizard; **this is a one-time operational step that is not automated** — a truly clean checkout still needs someone to run `docker compose exec n8n n8n import:workflow --input=//imports/mobilityops-return-processing.json`, `docker compose exec n8n n8n publish:workflow --id=mobilityops-return-processing`, `docker compose restart n8n`, and complete the one-time owner setup at `http://localhost:5678/setup` (any email/password, no verification required) before the automation demo will work. `docs/17-runbook.md` should get this exact sequence in M7.
|
|
||||||
- Commands run and verified from this checkout:
|
|
||||||
- `docker compose run --rm api pytest -q` — **49 passed** (new `tests/test_dispatcher.py` — claim/deliver success/failure/backoff/exhaustion-to-failed/malformed-payload, all via `monkeypatch.setattr(dispatcher.httpx, "post", ...)`, no real network calls in tests; `tests/test_integrations.py` — callback auth, unknown-event 404, idempotent-by-event-ID with a real duplicate-call assertion; `tests/test_workflows.py` — role gating, retry-only-from-failed, S5 retry-and-audit).
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed.
|
|
||||||
- `npm run build` — clean.
|
|
||||||
- Full live round trip (not mocked): registered a real return on `BK-DEMO-RETURN` → outbox event queued → background dispatcher delivered it to the now-activated n8n workflow within its 3s poll interval → n8n called back into `/api/v1/integrations/n8n/return-callback` (200 OK, confirmed in `docker compose logs api`) → dispatcher's original POST received n8n's success response → event flipped to `succeeded` on attempt 1, visible on `/automation`.
|
|
||||||
- S5 scenario end-to-end in the browser: seeded `BK-H-0020` (`failed`, 3 attempts, "Synthetic connection timeout to n8n") → clicked Retry → `pending` → within ~3s, live dispatcher delivered it through the real n8n instance → `succeeded`, 4 attempts. This is the full documented S5 scenario working for real, not simulated.
|
|
||||||
|
|
||||||
### M5 — RAGcore knowledge integration
|
|
||||||
- `app/services/knowledge/__init__.py`: `KnowledgeProvider` Protocol (sync, not async — the rest of the backend is sync SQLAlchemy/FastAPI, so an async provider interface would have meant bridging paradigms for no benefit) with `health()`/`ask()`, plus `GroundedAnswer`/`SourceCard`/`KnowledgeHealth` Pydantic models matching `contracts/openapi.yaml`'s `GroundedAnswer` schema exactly. `get_knowledge_provider()` factory switches on `settings.knowledge_provider` ("demo" default, "ragcore" opt-in).
|
|
||||||
- `app/services/knowledge/demo.py` — `DemoKnowledgeProvider`: parses the 10 `knowledge/procedures/*.md` files' YAML frontmatter (hand-rolled flat parser, not PyYAML — avoided adding a dependency for a 6-key flat block) and `## `-delimited sections at startup, then does **TF-IDF-weighted keyword retrieval** (not naive keyword counting) with light suffix-stripping stemming (`returns`→`return`, `damaged`→`damage`). This is extractive, not generative: it returns real excerpts and a templated answer sentence, never invented text.
|
|
||||||
- **Real bug found and fixed by testing the actual S6 question, not by inspection**: naive flat keyword-overlap scoring (first cut) let the word "vehicle" — present in nearly every document's title — crowd out the actually-relevant `damage-procedure` document from the top-3 results for "What must I do when a vehicle returns with damage?", because generic words scored the same as distinctive ones. Fixed by computing corpus-wide IDF per token (`log((N+1)/(df+1)) + 1`) and weighting matches by it, so common terms contribute little and rare/distinctive terms (like "damage") dominate the ranking. Verified: the S6 question now returns `damage-procedure` and `vehicle-return-procedure` in the top 3, matching the documented expectation exactly.
|
|
||||||
- `app/services/knowledge/ragcore.py` — `RAGcoreKnowledgeProvider`: real `httpx` adapter guessing a plausible REST contract (`GET /health`, `POST /api/v1/ask`) per `contracts/ragcore-contract-assumptions.md` (RAGcore is built separately; no live instance was reachable this session to verify against). Any connection error, timeout, or malformed response degrades to `evidence_state: "unavailable"` rather than raising — this is the adapter that actually exercises the architecture's "RAGcore failure disables knowledge answers only" reliability boundary. Not wired as the active provider by default; `KNOWLEDGE_PROVIDER=ragcore` would need a real, verified base URL to turn on.
|
|
||||||
- `POST /api/v1/knowledge/questions` + `GET /api/v1/knowledge/status` (`app/api/routers/knowledge.py`). Audit event `knowledge_question_asked` logs `evidence_state`, `provider`, `source_ids`, and `question_length` only — **not** the question text itself, per `docs/12-security-and-audit.md` ("log question metadata and source IDs, not unnecessary full prompts").
|
|
||||||
- Knowledge nav + page (`pages/Knowledge.tsx`): chat-style question box, source cards (title/version/section/excerpt) prioritized over the answer text per `docs/06-ui-ux.md`, explicit `grounded`/`insufficient`/`unavailable` states with distinct visual treatment — never a fabricated-looking answer for the latter two.
|
|
||||||
- Dockerfile now also `COPY knowledge ./knowledge`; added `KNOWLEDGE_DIR` setting (`/app/knowledge/procedures` in-container, same pattern as `SEED_DIR`) rather than deriving the path from `__file__` — simpler and doesn't break if the module moves.
|
|
||||||
- Commands run and verified from this checkout:
|
|
||||||
- `docker compose run --rm api pytest -q` — **57 passed** (new `tests/test_knowledge.py`: S6 grounded-with-expected-sources, unrelated question is honestly insufficient with no fabrication, demo provider health/document count, endpoint auth required, audit doesn't leak question text, RAGcore adapter degrades to unavailable on a simulated connection error).
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed.
|
|
||||||
- `npm run build` — clean.
|
|
||||||
- Full browser run of S6 end-to-end: asked "What must I do when a vehicle returns with damage?" on `/knowledge` → grounded answer citing "Vehicle return procedure" (2 sections) and "Damage handling procedure" with real excerpts. Also asked an unrelated question ("What is the weather forecast for tomorrow?") → correctly returned "Insufficient evidence" / "No matching procedure was found" with zero sources, confirming no fabrication.
|
|
||||||
|
|
||||||
### M6 — ITWorx MCP Hub publication
|
|
||||||
- `app/api/routers/mcp_integrations.py`: four read-only endpoints under `/api/v1/integrations/mcp/` — `GET operations-summary`, `GET attention-vehicles` (query params `minimum_severity`/`date`/`limit` matching `contracts/mcp-tools.json`'s `inputSchema` exactly), `GET vehicles/{vehicle_ref}`, `POST search-knowledge` (the "narrow façade" the doc calls for — wraps M5's `get_knowledge_provider()` rather than re-implementing retrieval; the contract's tool has no MobilityOps `endpoint` field, only `routing.preferred: ragcore`, so this façade path is MobilityOps's own addition for when the Hub needs a single provider boundary, not literally specified by the contract).
|
|
||||||
- Auth: new `require_mcp_service_token` dependency in `app/api/deps.py`, same shared-secret-header shape as the M4 n8n callback (`X-Service-Token` against `MCP_HUB_SERVICE_TOKEN`) plus an optional `X-Client-Id` header (defaults to `"unknown-mcp-client"`) used as the audit actor label — the Hub's actual client-identity header name is unknown (no live Hub to confirm against), so this is a reasonable guess documented here rather than assumed silently.
|
|
||||||
- `McpVehicleDetailOut` deliberately omits `registration_number` and all customer data — narrower than the browser-facing `VehicleOut`/`VehicleDetailOut`, matching "no customer or vehicle database access" and the read-only/summary intent of an AI-facing tool. Test `test_vehicle_details_known_ref` asserts the field's absence explicitly so a future change can't silently widen the exposed surface.
|
|
||||||
- Extracted `app/services/operations.py` (`compute_metrics`, `list_attention_vehicles`) out of `app/api/routers/dashboard.py` so the MCP operations-summary/attention-vehicles endpoints and the human dashboard share one query implementation instead of two copies that could drift — the same "do not duplicate retrieval logic" principle the doc states for the knowledge tool, applied here to the operational-summary tools too.
|
|
||||||
- Every provider call writes an `AuditEvent` (`actor_type="service"`, `actor_label=X-Client-Id`, `action="mcp_tool_request"`, `metadata={tool, status}`) — MobilityOps's own record that its provider APIs were reached, independent of whatever central tool-call audit the Hub itself keeps (per `docs/10-mcp-hub-integration.md`'s audit section, the Hub owns the central log; this is the local corroborating one).
|
|
||||||
- No write/mutation endpoints exist under the `/api/v1/integrations/mcp/` namespace at all (verified by `test_no_write_endpoints_exist_under_mcp_namespace` — POST/PUT/DELETE against the vehicle-details path all 404/405) — return registration, customer merge, and any booking/vehicle mutation are correctly absent, per the doc's explicit restriction list.
|
|
||||||
- Commands run and verified from this checkout:
|
|
||||||
- `docker compose run --rm api pytest -q` — **66 passed** (new `tests/test_mcp_integrations.py`: token-required, wrong-token 401, all four tools' happy paths, severity/limit filtering, 404 for unknown vehicle, `max_sources` respected, audit actor/action verified, write-method rejection).
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed.
|
|
||||||
- Live `curl` verification against the running stack (no browser needed — these are service-to-service endpoints, not UI): missing header → 422; wrong token → 401; correct token → all four endpoints return correct data (`operations-summary` metrics match the dashboard; `attention-vehicles?minimum_severity=high` returned `MO-016`×2 and `MO-031`, all severity `high`; `vehicles/MO-016` returned the narrow read-only shape; `search-knowledge` with `max_sources=2` returned exactly 2 grounded sources for the S6 question). Confirmed via `GET /api/v1/audit?action=mcp_tool_request` that all four calls were recorded with correct `actor_type=service`, tool name, and status.
|
|
||||||
|
|
||||||
### M7 — Portfolio polish and final acceptance
|
|
||||||
- **Automated clean-checkout migrations**: `backend/entrypoint.sh` now runs `alembic upgrade head` before starting uvicorn (Dockerfile `CMD` changed from `uvicorn ...` to `./entrypoint.sh`). Verified with a true `docker compose down -v` (all volumes wiped) → `docker compose up --build -d` → all 11 tables present, `/health` and web both green, all 66 backend tests pass, with zero manual migration step.
|
|
||||||
- **n8n one-time setup scripted where it can be**: `make n8n-setup` runs the import/publish/restart sequence (previously three manual commands discovered ad hoc in M4). The owner-account creation itself cannot be scripted safely (it's an interactive one-time step in n8n 2.x's own onboarding, not a MobilityOps concern) — documented precisely in the rewritten `docs/17-runbook.md`, including the exact URL and that no email verification is required. Re-ran this full sequence from the wiped-volumes state this session and confirmed the S1 return → outbox → live n8n → callback → `succeeded` round trip works on a genuinely clean checkout, not just the already-provisioned stack from M0–M6.
|
|
||||||
- **Playwright E2E** (`frontend/e2e/demo.spec.ts`, `frontend/playwright.config.ts`): one test automating the full 9-step documented demo script end-to-end against the live stack — login, dashboard metrics, open `BK-DEMO-RETURN`, register an odometer-regression return (S1), verify the quality issue + queued automation event, merge the duplicate-customer scenario (S2), ask the damage question and verify both expected source citations (S6), inspect audit entries, and verify responsive nav + no horizontal overflow at 360px width. **Passing.** This also resolves the "≤360px layout visually unverified" gap flagged back in M1 — verified both by this test's overflow assertion and by the `9-mobile-dashboard.png` screenshot (nav wraps into rows, metric tiles collapse to a 2-column grid, no horizontal scroll).
|
|
||||||
- Added `frontend/e2e/_capture-screenshots.spec.ts` as evidence-generation tooling (underscore-prefixed, excluded from the default `playwright test` / `make e2e` run via `testIgnore` in the config — it calls `demo/reset`, which a real regression test shouldn't do as a side effect). Captured all 9 screenshots into `artifacts/evidence/screenshots/`.
|
|
||||||
- Wrote `artifacts/evidence/architecture.md` (mermaid, as-built — distinguishes verified-live components from implemented-but-never-reached-a-real-instance ones, i.e. RAGcore and the MCP Hub) and `artifacts/evidence/final-summary.md` (commit, exact commands, test counts, screenshot index, RAGcore success/unavailable evidence — including a live-demonstrated unavailable case against an unreachable host, not just the unit test — n8n success/retry evidence, MCP sample calls, known limitations, truthful portfolio wording per `docs/16-portfolio-case-study.md`'s template).
|
|
||||||
- Updated `README.md` (dropped stale "minimal bootable scaffold, not the finished application" wording and the old two-line quickstart in favor of `make demo` + a pointer to the runbook) and `docs/17-runbook.md` (full rewrite: exact bootstrap, n8n one-time setup, verification commands, required operational checks, recovery expectations).
|
|
||||||
- Final placeholder/dead-UI sweep: `grep`'d the full `frontend/src` and `backend/app` trees for scaffold/TODO/FIXME/"must be replaced" markers — none found. `FILE_INDEX.md` was left as-is; it's the original build-pack's archive-completeness manifest (a historical snapshot), not a living index that needs to track every file added since — updating it would misrepresent what it's for.
|
|
||||||
- Commands run and verified from this checkout (this milestone, cumulative across the whole build):
|
|
||||||
- `docker compose run --rm api pytest -q` — **66 passed**, ruff clean.
|
|
||||||
- `cd frontend && npm run build` — clean.
|
|
||||||
- `cd frontend && npx playwright test` — **1 passed** (full demo script, live stack).
|
|
||||||
- Full clean-checkout drill: `docker compose down -v` → `docker compose up --build -d` → `docker compose exec api python -m app.cli seed --reset` → `docker compose run --rm api pytest -q` (66 passed) → n8n owner setup + `make n8n-setup` → live S1 return round-tripped through the real n8n instance to `succeeded`.
|
|
||||||
|
|
||||||
### Final acceptance audit (post-M7)
|
|
||||||
|
|
||||||
A dedicated release-readiness audit was run after M7 claimed completion, specifically to
|
|
||||||
catch anything the milestone-by-milestone build might have missed by only ever validating
|
|
||||||
each piece in isolation.
|
|
||||||
|
|
||||||
- **Real gap found: `mypy` had never been run.** `mypy` is a declared dev dependency
|
|
||||||
(`backend/pyproject.toml`) but was never wired into any milestone's validation loop —
|
|
||||||
only `ruff` was. Running it cold surfaced **43 real type errors across 10 files**, all
|
|
||||||
pre-existing (not introduced by this audit). Triaged and fixed all of them rather than
|
|
||||||
suppressing:
|
|
||||||
- `services/returns.py`: the vehicle lookup after acquiring `FOR UPDATE` could type as
|
|
||||||
`Vehicle | None` with no runtime guard — added an explicit `if vehicle is None: raise
|
|
||||||
AppError(..., 404)`. This was a genuine defensive-programming gap (a dangling FK would
|
|
||||||
have crashed with an unhandled `AttributeError`/500 instead of a clean 404), not just
|
|
||||||
a type annotation issue.
|
|
||||||
- `api/routers/bookings.py`: same pattern for `db.get(Customer, ...)` /
|
|
||||||
`db.get(Vehicle, ...)` in `get_booking` — added a guard raising 500 with a clear
|
|
||||||
message instead of crashing on `None.public_ref`.
|
|
||||||
- `api/deps.py` + `api/routers/demo.py`: `CurrentUser.role` is a `Literal[...]`, but
|
|
||||||
`SessionPayload.role` (decoded from an HMAC-signed cookie) and `User.role` (a DB
|
|
||||||
column) are both plain `str`. Pydantic validates this at runtime already (so it was
|
|
||||||
never exploitable), but `get_current_user` now explicitly checks membership before
|
|
||||||
constructing `CurrentUser`, turning a would-be unhandled `ValidationError` (500) into
|
|
||||||
a clean 401 for a corrupted/tampered cookie — another real defensive improvement, not
|
|
||||||
just a type-checker appeasement.
|
|
||||||
- `api/routers/dashboard.py`, `api/routers/data_quality.py`: two instances of reusing
|
|
||||||
one variable name for both a `Vehicle` and a `Customer` across an if/else branch,
|
|
||||||
which is genuinely confusing to read regardless of what mypy thinks — renamed to
|
|
||||||
distinct variables (`entity`/typed union in dashboard, `customer`/`vehicle` in the
|
|
||||||
data-quality snapshot helper).
|
|
||||||
- `services/data_quality.py`, `seed_loader.py`: `Booking.__table__.update()` /
|
|
||||||
`Customer.__table__.update()` don't typecheck against SQLAlchemy 2.0's stubs (the
|
|
||||||
`.__table__` accessor is typed as the more general `FromClause`, which doesn't
|
|
||||||
declare `.update()`) — switched to the idiomatic `sqlalchemy.update(Model)` construct,
|
|
||||||
which is both correctly typed and the more modern SQLAlchemy 2.0 style anyway.
|
|
||||||
- Remaining handful (schemas.py's deprecated `conint()` → `Annotated[int, Field(...)]`,
|
|
||||||
a `Sequence` vs `list` `.sort()` call, an `assert`-guarded None-narrowing after a
|
|
||||||
`WHERE ... IS NOT NULL` filter mypy can't see through, `Result.rowcount` typing gaps)
|
|
||||||
were either latent pydantic-v1-style API usage or genuine SQLAlchemy stub limitations
|
|
||||||
— fixed with the idiomatic modern equivalent or a narrowly-scoped, commented
|
|
||||||
`# type: ignore[...]` at the exact line, never a blanket suppression.
|
|
||||||
- `make lint` now runs both `ruff check .` and `mypy app`; `mypy app` reports
|
|
||||||
**zero errors across 44 source files**.
|
|
||||||
- **No other defects found.** Re-ran the full journey matrix end-to-end against a
|
|
||||||
genuinely wiped-volumes (`docker compose down -v`) clean checkout: all 66 backend
|
|
||||||
tests, ruff, ✅; ran the demo login → dashboard → vehicle/booking detail → return
|
|
||||||
workflow → invalid-mileage rejection (422, both a negative value and a non-numeric
|
|
||||||
string) → data-quality issue review → duplicate-customer merge → audit trail →
|
|
||||||
Knowledge Assistant → live n8n round trip → MCP Hub endpoint journeys directly via
|
|
||||||
`curl` against the running stack, all correct.
|
|
||||||
- **Degraded-mode behavior explicitly re-verified live** (not just unit-tested):
|
|
||||||
stopped n8n with `docker compose stop n8n`, registered a return — it committed
|
|
||||||
(`201`, booking flipped to `returned`) exactly as required; the outbox event stayed
|
|
||||||
`pending` with real `ConnectError`s logged and exponential backoff (2 attempts over
|
|
||||||
~8s); restarted n8n and the dispatcher **self-healed** without any manual
|
|
||||||
intervention, delivering the event to `succeeded` on attempt 5. RAGcore unavailable
|
|
||||||
mode re-verified live against an unreachable host (`ConnectError` → `evidence_state:
|
|
||||||
"unavailable"`, empty answer, no fabrication). MCP Hub unavailability is
|
|
||||||
architecturally moot for MobilityOps — the Hub only ever calls *into* MobilityOps, so
|
|
||||||
there is nothing on the MobilityOps side that can degrade if the Hub is down (only the
|
|
||||||
reverse, "does an unavailable Hub break MobilityOps," which is trivially no since
|
|
||||||
nothing here calls out to it).
|
|
||||||
- **New test coverage added, no existing tests weakened**: `frontend/e2e/interactive-elements.spec.ts`
|
|
||||||
(11 Playwright tests — all seven nav items, every filter on every list page, vehicle
|
|
||||||
detail tabs, defer/reject, automation retry, knowledge form, role-switching, and
|
|
||||||
role-based page restriction) plus the existing `demo.spec.ts` — **12/12 e2e tests
|
|
||||||
passing** against the live stack.
|
|
||||||
- Verified `.env` is `.gitignore`d and was never committed (`git ls-files` /
|
|
||||||
`git log --all -p -- '*.env'` both empty); scanned full git history for AWS keys,
|
|
||||||
private-key headers, and `sk-...`-style tokens — none found. Every `Settings` field in
|
|
||||||
`backend/app/core/config.py` has a corresponding entry either directly in
|
|
||||||
`.env.example` or is derived/wired through `compose.yaml` (a few purely-internal
|
|
||||||
container-path constants like `SEED_DIR`/`KNOWLEDGE_DIR` are intentionally not
|
|
||||||
operator-configurable and correctly absent from `.env.example`).
|
|
||||||
- Grepped the full `frontend/src` and `backend/app` trees for TODO/FIXME/placeholder/
|
|
||||||
fake/stub/mock/"not implemented" markers — zero real hits (the two `placeholder=`
|
|
||||||
matches are legitimate HTML input placeholder attributes). Confirmed dashboard metrics
|
|
||||||
and all list-page data are 100% DB-backed (`compute_metrics` in
|
|
||||||
`services/operations.py`, never a literal in frontend JSX). Confirmed every frontend
|
|
||||||
route in `App.tsx` maps to an implemented page and every nav item maps to a real route
|
|
||||||
— no dead routes.
|
|
||||||
- Commands run and verified from this audit:
|
|
||||||
- `docker compose run --rm api pytest -q` — **66 passed**.
|
|
||||||
- `docker compose run --rm api ruff check .` — All checks passed.
|
|
||||||
- `docker compose run --rm api mypy app` — **Success: no issues found in 44 source files** (0 errors, down from 43).
|
|
||||||
- `cd frontend && npm run build` — clean (`tsc -b && vite build`).
|
|
||||||
- `cd frontend && npx playwright test` — **12 passed** (`demo.spec.ts` + `interactive-elements.spec.ts`).
|
|
||||||
- Full clean-checkout drill repeated from a fresh `docker compose down -v`: automatic migrations, seed, 66/66 tests, n8n owner setup + `make n8n-setup`, live return round-tripped through n8n to `succeeded`.
|
|
||||||
- See `artifacts/final-acceptance/summary.md` for the complete evidence write-up (commands, exact outputs, demo access, deployment instructions, five-minute demo flow).
|
|
||||||
|
|
||||||
## Definition of done
|
|
||||||
|
|
||||||
All eight milestones (M0–M7) are complete, and a dedicated post-M7 final-acceptance audit
|
|
||||||
found and fixed one real category of gap (`mypy` never having been run) with zero
|
|
||||||
regressions. `docs/14-testing-and-acceptance.md`'s clean-checkout acceptance list has been
|
|
||||||
walked item by item against a genuinely wiped-volumes checkout, twice (once in M7, once in
|
|
||||||
this audit), and `artifacts/final-acceptance/summary.md` is the authoritative final
|
|
||||||
evidence document. The two items not fully closed — a live RAGcore instance and a live
|
|
||||||
ITWorx MCP Hub instance — were never reachable in this environment; both integrations are
|
|
||||||
implemented, unit/contract-tested, directly verified against MobilityOps's own API, and
|
|
||||||
their unavailable-degradation paths are live-verified, but an actual round trip against
|
|
||||||
real RAGcore/Hub instances remains unconfirmed and is documented as such rather than
|
|
||||||
claimed.
|
|
||||||
|
|
||||||
## Known blockers
|
|
||||||
|
|
||||||
None. External service credentials may be absent; use the documented demo/degraded providers. The n8n workflow-activation steps are a one-time manual setup requirement in this environment (owner-account creation via n8n's own `/setup` UI cannot be scripted safely), fully documented in `docs/17-runbook.md` and scripted where possible (`make n8n-setup`). RAGcore and the ITWorx MCP Hub itself were never reachable in this environment — both integrations are implemented and directly tested/curl-verified against MobilityOps's own API, but neither a real RAGcore instance nor a real Hub round trip was available to confirm end-to-end.
|
|
||||||
|
|
||||||
## Premium Control Rail UI transformation (2026-08-02)
|
|
||||||
|
|
||||||
- Branch: `design/mobilityops-premium-ui`, branched from verified deployed revision
|
|
||||||
`dfabb41582e302f45a3de826f85f531bf23dfc8b`; master history was not rewritten.
|
|
||||||
- Audited every route at 1440, 1280, 768 and 390 px. Baseline findings and captures are
|
|
||||||
in `docs/design/current-ux-audit.md` and `artifacts/design-validation/current/`.
|
|
||||||
- Authored three twelve-screen product directions and generated representative Stitch
|
|
||||||
anchors in project `17018847755558569017`: Control Rail, Dispatch Ledger and Service
|
|
||||||
Atelier. Control Rail was selected and refined twice for hierarchy, accessibility and
|
|
||||||
responsive implementation. Decision, screen inventory, tokens and exact Stitch IDs
|
|
||||||
are in `docs/design/design-directions.md`, `docs/design/design-system.md` and
|
|
||||||
`docs/design/stitch-manifest.md`.
|
|
||||||
- Rebuilt the complete React interface around a responsive Control Rail shell: inline SVG
|
|
||||||
icon/brand system, desktop rail, named landmarks, skip link, top bar, mobile bottom
|
|
||||||
navigation, shared loading/error/empty states and reduced-motion support.
|
|
||||||
- Redesigned all shipped pages. The dashboard now prioritizes persisted readiness,
|
|
||||||
Attention and today's movements; booking results paginate at 25 rows; every responsive
|
|
||||||
table retains field labels; integrations distinguish n8n evidence, live RAGcore health
|
|
||||||
and the unconfigured MCP adapter without inventing status.
|
|
||||||
- Return registration is now capture → review → result. A regression test proves the
|
|
||||||
return endpoint is not called before confirmation; the existing idempotency and local
|
|
||||||
commit/outbox contract is unchanged.
|
|
||||||
- Final browser captures are in `artifacts/design-validation/implementation/`. DOM
|
|
||||||
measurements and Playwright both prove no horizontal overflow at 390, 768, 1280 and
|
|
||||||
1440 px. See `docs/design/implementation-validation.md`.
|
|
||||||
- Final validation commands from this branch:
|
|
||||||
- `docker compose run --rm api pytest -q` — **66 passed**.
|
|
||||||
- `docker compose run --rm api ruff check .` — **All checks passed**.
|
|
||||||
- `docker compose run --rm api mypy app` — **0 issues in 44 files**.
|
|
||||||
- `cd frontend && npm run lint` — clean TypeScript check.
|
|
||||||
- `cd frontend && npm run build` — production build succeeded (59 modules; 240.24 kB JS,
|
|
||||||
36.63 kB CSS before gzip).
|
|
||||||
- `cd frontend && playwright test --reporter=line` — **19 passed** including the full
|
|
||||||
five-minute demo, every interactive route, return review semantics and four viewport
|
|
||||||
overflow checks.
|
|
||||||
- Review deployment updated at `http://192.168.10.150:1236` with persistent PostgreSQL
|
|
||||||
data preserved. Deployed smoke: all ten authenticated routes plus login at
|
|
||||||
desktop and mobile sizes rendered without alert state or horizontal overflow; browser console had zero
|
|
||||||
warnings/errors; seven authenticated API paths returned 200; PostgreSQL/API were
|
|
||||||
healthy and the shared n8n `/healthz` returned `{"status":"ok"}`.
|
|
||||||
- Corrected the review topology after confirming the host already runs n8n on port 5678:
|
|
||||||
the temporary `mobilityops-n8n-1` container was removed without deleting its retained
|
|
||||||
volume; the bundled service is now opt-in through the `bundled-n8n` profile; the API
|
|
||||||
points to the shared n8n; and the return workflow is imported and published there.
|
|
||||||
- The existing n8n's previously empty `N8N_HOST` and `N8N_EDITOR_BASE_URL` values were
|
|
||||||
persistently set in its Unraid template. A synthetic return then completed the full
|
|
||||||
MobilityOps → shared n8n → callback round trip as `succeeded` on attempt 1, after which
|
|
||||||
deterministic demo state was restored (`BK-DEMO-RETURN` is `active`).
|
|
||||||
- Global search is now live for Control Rail sections and `MO-*`, `BK-*`, `DQ-*` public
|
|
||||||
references, including Ctrl/Cmd+K focus and a tested not-found announcement. Final local
|
|
||||||
Playwright result is **19 passed**.
|
|
||||||
- Exact next action: hand off `design/mobilityops-premium-ui` for review. The final code,
|
|
||||||
shared-n8n topology and evidence are committed, pushed and deployed; do not merge master
|
|
||||||
automatically.
|
|
||||||
@@ -1,91 +1,95 @@
|
|||||||
# MobilityOps
|
# Fleet Ops
|
||||||
|
|
||||||
**Connected operations for vehicle rental and service teams.**
|
**A recruiter-ready operations platform for vehicle rental and service teams.**
|
||||||
|
|
||||||
MobilityOps is a working proof of concept for a fictitious mobility company. It combines vehicle and booking operations, a controlled vehicle-return workflow, data-quality review, RAGcore-backed internal knowledge, n8n orchestration and read-only tools published through ITWorx MCP Hub.
|
**Try it in two commands** (`cp .env.example .env && make demo`, then open `http://localhost:1228`) · no password required · choose **Highlights in 90 seconds** for the shortest tour. The reference deployment runs on a private LAN (see [deploy/unraid/README.md](deploy/unraid/README.md)); ask for a link if you want the hosted version.
|
||||||
|
|
||||||
The web application uses the premium responsive **Control Rail** interface: a compact
|
Fleet Ops turns fragmented vehicle, booking and procedure data into one controlled operational workspace. It is a complete synthetic-data product demo: the company and records are fictional, while the workflows, persistence, validation, authorization, audit trail and integration boundaries are implemented.
|
||||||
operations-first workspace with persisted readiness metrics, evidence-led exceptions,
|
|
||||||
review-before-commit return handling and mobile navigation designed down to 390 px. See
|
|
||||||
`docs/design/design-directions.md` and `docs/design/implementation-validation.md` for the
|
|
||||||
design decision and visual evidence.
|
|
||||||
|
|
||||||
All people, companies, vehicles, bookings and documents are synthetic. The workflows, validation, integrations, audit logging and access boundaries are intended to be real.
|

|
||||||
|
|
||||||
## Scope
|
## The 90-second tour
|
||||||
|
|
||||||
The PoC implements:
|
1. Open **Highlights** from the login screen.
|
||||||
|
2. Follow a vehicle return from review to atomic commit, quality issue, outbox and correlated audit trace.
|
||||||
|
3. Compare and merge a duplicate customer with explicit human confirmation.
|
||||||
|
4. Ask the Knowledge Hub a damage question and inspect its cited procedure evidence.
|
||||||
|
5. Open **Engineering** for the architecture, reliability guarantees, test evidence and honest scope boundary.
|
||||||
|
|
||||||
- operations dashboard;
|
## What makes it more than a mock-up
|
||||||
- vehicle and booking views;
|
|
||||||
- one complete vehicle-return workflow;
|
|
||||||
- five deterministic data-quality checks;
|
|
||||||
- human review and customer merge;
|
|
||||||
- audit trail;
|
|
||||||
- RAGcore-backed knowledge assistant with citations;
|
|
||||||
- one n8n return-processing workflow;
|
|
||||||
- four read-only MCP tools through ITWorx MCP Hub;
|
|
||||||
- deterministic demo reset and five-minute showcase.
|
|
||||||
|
|
||||||
It is not an ERP, CRM, accounting package, public booking site, payment system or autonomous agent.
|
- **Transactional operations:** a return writes the inspection, vehicle/booking state, audit events and outbox record atomically. n8n downtime never rolls back the local business transaction.
|
||||||
|
- **Explainable data quality:** five persisted rule types, SLA deadlines, assignment, bulk queue controls and bounded resolution flows—not decorative warning cards.
|
||||||
|
- **Grounded knowledge:** the live deployment uses RAGcore; insufficient or unavailable evidence produces no invented answer. Citations and provider provenance remain inspectable.
|
||||||
|
- **Safe AI exposure:** four tenant-bound, service-authenticated, read-only Fleet Ops tools are published through ITWorx MCP Hub and audited with correlation IDs.
|
||||||
|
- **Operational reliability:** bounded retries, delivery leases, health/readiness, Prometheus metrics, Grafana, scheduled verified backups and graceful external-dependency degradation.
|
||||||
|
- **Real product ergonomics:** nl-BE, en-GB and fr-BE; responsive from 360 px; keyboard-accessible navigation; role-aware global search; route-level lazy loading; server-enforced permissions.
|
||||||
|
|
||||||
## Integration status
|
## Architecture
|
||||||
|
|
||||||
- **n8n**: fully implemented and verified against a real n8n instance, including
|
```mermaid
|
||||||
degraded mode (n8n stopped mid-flow → return still commits, event stays `pending`
|
flowchart LR
|
||||||
with backoff, self-heals once n8n returns) and the failed-delivery manual-retry path.
|
UI["React + TypeScript\nresponsive operations UI"] -->|session cookie| API["FastAPI\nbusiness rules + RBAC"]
|
||||||
- **RAGcore**: the demo `KnowledgeProvider` (deterministic TF-IDF extractive retrieval
|
API --> DB[(PostgreSQL)]
|
||||||
over the local procedure documents) is what satisfies the knowledge-assistant
|
API -->|grounded retrieval| RAG[RAGcore]
|
||||||
acceptance criteria and is fully verified. A `RAGcoreKnowledgeProvider` HTTP adapter is
|
DB --> OUT["Transactional outbox"]
|
||||||
implemented and unit-tested, including its unavailable-degradation path, but was never
|
OUT -->|bounded retry| N8N["Existing central n8n"]
|
||||||
exercised against a live RAGcore instance in this environment.
|
N8N -->|authenticated callback| API
|
||||||
- **ITWorx MCP Hub**: the four read-only provider endpoints are implemented, tested, and
|
HUB["ITWorx MCP Hub"] -->|4 read-only tools| API
|
||||||
directly `curl`-verified with correct auth enforcement and audit logging. No live Hub
|
```
|
||||||
instance was reachable in this environment to verify an actual Hub round trip.
|
|
||||||
|
|
||||||
See `artifacts/final-acceptance/summary.md` for full verification evidence and exact
|
Fleet Ops owns operational truth. RAGcore owns retrieval, n8n performs post-commit orchestration, and MCP Hub owns tool transport/publication. Neither RAGcore nor MCP Hub accesses the Fleet Ops database directly. See [the as-built architecture](artifacts/evidence/architecture.md).
|
||||||
commands.
|
|
||||||
|
|
||||||
## Repository map
|
## Demonstrable scope
|
||||||
|
|
||||||
- `CLAUDE.md` — binding implementation rules.
|
- dashboard, vehicle fleet, booking lifecycle and controlled returns;
|
||||||
- `MASTER_BUILD_PROMPT.md` — prompt to start an autonomous Claude run.
|
- data-quality queue, assignment, review, merge and resolution;
|
||||||
- `PROJECT_STATE.md` — short persistent project memory.
|
- correlated human-readable audit history;
|
||||||
- `docs/` — product, architecture, UX and acceptance specification.
|
- cited Knowledge Hub with honest provider state;
|
||||||
- `contracts/` — OpenAPI, event and MCP contracts.
|
- n8n delivery monitoring and manual retry;
|
||||||
- `knowledge/` — fictitious source documents for the MobilityOps RAGcore workspace.
|
- user administration, privacy export/anonymisation and retention guards;
|
||||||
- `seed/` — deterministic synthetic dataset and generator.
|
- deterministic reset with 2 users, 180 customers, 50 vehicles, 254 bookings, 75 inspections, 40 maintenance records, 33 quality issues and 20 workflow runs.
|
||||||
- `n8n/` — importable workflow definitions.
|
|
||||||
- `backend/` — FastAPI/SQLAlchemy/Alembic API.
|
|
||||||
- `frontend/` — React/TypeScript/Vite web app, including the Playwright end-to-end suite (`frontend/e2e/`).
|
|
||||||
- `artifacts/evidence/` — final acceptance evidence (screenshots, architecture, `final-summary.md`).
|
|
||||||
- `artifacts/design-validation/` — baseline audit, Stitch direction references and implemented responsive captures.
|
|
||||||
|
|
||||||
## Quickstart
|
This is deliberately not accounting, payments, a public reservation site, generic CRM, inventory, HR or an autonomous write agent.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
React, TypeScript, Vite, FastAPI, SQLAlchemy 2, PostgreSQL, Alembic, n8n, RAGcore, ITWorx MCP Hub, Docker Compose, Prometheus, Grafana and Playwright.
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
make demo
|
make demo
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds and starts the full stack (migrations run automatically) and loads the
|
|
||||||
deterministic demo dataset. See `docs/17-runbook.md` for the one-time n8n workflow setup
|
|
||||||
required for the automation demo, and the full operational runbook.
|
|
||||||
|
|
||||||
Endpoints:
|
|
||||||
|
|
||||||
- Web: `http://localhost:1228`
|
- Web: `http://localhost:1228`
|
||||||
- API health: `http://localhost:8128/health`
|
- API readiness: `http://localhost:8128/health/ready`
|
||||||
- n8n: `http://localhost:5678`
|
- Existing n8n server: point `N8N_WEBHOOK_URL` at its return-processing webhook (see `.env.example`). The bundled `n8n` service in `compose.yaml` is a local fallback only; production reuses the server's central n8n (`compose.unraid.yaml` disables the bundled one).
|
||||||
|
|
||||||
All defaults are configurable via `.env` (see `.env.example`).
|
The deterministic local knowledge provider supports clean-checkout acceptance without pretending to be the live RAGcore integration. Configuration is documented in `.env.example`; operations and recovery are in [docs/17-runbook.md](docs/17-runbook.md).
|
||||||
|
|
||||||
## Quality gates
|
## Quality gates
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test # backend: pytest (66 tests)
|
make test # isolated PostgreSQL backend suite
|
||||||
make lint # backend: ruff + mypy (strict, zero errors)
|
make lint # Ruff + strict mypy
|
||||||
make e2e # frontend: Playwright end-to-end (18 tests, live stack required)
|
make e2e # complete Playwright browser acceptance
|
||||||
|
cd frontend && npm run build
|
||||||
|
python scripts/run-readonly-load-smoke.py # while the demo stack is running
|
||||||
```
|
```
|
||||||
|
|
||||||
Frontend build/typecheck: `cd frontend && npm run build` (`tsc -b && vite build`).
|
Release-scoped results and production evidence are recorded in [artifacts/final-acceptance/summary.md](artifacts/final-acceptance/summary.md); older milestone evidence remains explicitly historical. [PROJECT_STATE.md](PROJECT_STATE.md) records the commands and exact deployment revision.
|
||||||
|
|
||||||
|
## Repository map
|
||||||
|
|
||||||
|
- `backend/` — FastAPI domain, API, migrations and tests
|
||||||
|
- `frontend/` — React app and Playwright acceptance suite
|
||||||
|
- `contracts/` — OpenAPI, event and MCP contracts
|
||||||
|
- `knowledge/` — versioned fictional procedures
|
||||||
|
- `n8n/` — importable workflow definitions for the existing server
|
||||||
|
- `seed/` — deterministic synthetic dataset
|
||||||
|
- `docs/` — architecture, security, UX, testing and runbooks
|
||||||
|
- `artifacts/` — dated, release-scoped acceptance evidence and screenshots
|
||||||
|
|
||||||
|
“MobilityOps” remains the repository/deployment identifier; **Fleet Ops** is the product name shown to users.
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
| Version | Security support |
|
||||||
|
|---|---|
|
||||||
|
| Latest tagged PoC release and current `master` | Supported |
|
||||||
|
| Older commits, branches and untagged deployments | Not supported |
|
||||||
|
|
||||||
|
MobilityOps is a synthetic-data proof of concept, not a production identity,
|
||||||
|
payments or public reservation platform. Security fixes target the current
|
||||||
|
release line only.
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
Do not disclose suspected vulnerabilities through a public issue.
|
||||||
|
|
||||||
|
Report them privately to `jens@itworx.tech` with:
|
||||||
|
|
||||||
|
- the affected revision, endpoint or component;
|
||||||
|
- reproduction steps and prerequisites;
|
||||||
|
- the observed and expected behaviour;
|
||||||
|
- the security impact;
|
||||||
|
- a minimal proof of concept, without unnecessary personal or secret data.
|
||||||
|
|
||||||
|
Receipt should be acknowledged within three business days. An initial
|
||||||
|
assessment or request for additional evidence should follow within ten
|
||||||
|
business days. Remediation timing depends on severity and reproducibility.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
In scope:
|
||||||
|
|
||||||
|
- MobilityOps backend, frontend, container and deployment code;
|
||||||
|
- authentication, authorization, tenant boundaries and audit integrity;
|
||||||
|
- database, outbox, backup and restore behaviour;
|
||||||
|
- MobilityOps-owned n8n workflow definitions;
|
||||||
|
- RAGcore and MCP Hub integration boundaries implemented in this repository.
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- denial-of-service or destructive testing against the hosted demo;
|
||||||
|
- social engineering, credential stuffing or physical attacks;
|
||||||
|
- synthetic demo-data exposure without a security-boundary failure;
|
||||||
|
- vulnerabilities solely inside RAGcore, ITWorx MCP Hub, n8n or another
|
||||||
|
third-party service. Report those to their respective owners.
|
||||||
|
|
||||||
|
Do not access data beyond what is required to demonstrate the issue, modify
|
||||||
|
shared infrastructure, interrupt other services or retain obtained secrets.
|
||||||
|
|
||||||
|
## Coordinated disclosure
|
||||||
|
|
||||||
|
Good-faith research that respects this policy and applicable law will be
|
||||||
|
handled constructively. Allow a reasonable remediation period before public
|
||||||
|
disclosure. Submitted reports and evidence are used only for investigation,
|
||||||
|
remediation and verification.
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# MobilityOps — Claude build pack
|
|
||||||
|
|
||||||
MobilityOps is a deliberately scoped, working proof of concept for a fictitious Belgian mobility-rental company. It demonstrates how operational data, workflow automation, data-quality review, a central RAG service and a central MCP hub can work together without pretending to be a full ERP.
|
|
||||||
|
|
||||||
## Use this pack efficiently
|
|
||||||
|
|
||||||
1. Extract this archive into a new repository.
|
|
||||||
2. Open the repository in Claude Code.
|
|
||||||
3. Paste the contents of `MASTER_BUILD_PROMPT.md` once.
|
|
||||||
4. Let Claude continue autonomously through the milestones in `docs/15-build-plan.md`.
|
|
||||||
5. Only intervene if the environment itself is unavailable or credentials for an external service are required.
|
|
||||||
|
|
||||||
Claude must use `PROJECT_STATE.md` as its compact memory between sessions. Do not restate the full project in later prompts.
|
|
||||||
|
|
||||||
## What is included
|
|
||||||
|
|
||||||
- locked product scope and non-goals;
|
|
||||||
- architecture and domain decisions;
|
|
||||||
- API and event contracts;
|
|
||||||
- realistic deterministic synthetic seed data;
|
|
||||||
- ten fictitious procedures for RAGcore;
|
|
||||||
- an initial n8n workflow export;
|
|
||||||
- MCP tool definitions for ITWorx MCP Hub;
|
|
||||||
- a minimal bootable frontend/API scaffold;
|
|
||||||
- acceptance criteria and a five-minute demo script;
|
|
||||||
- a master prompt optimized for one autonomous build run.
|
|
||||||
|
|
||||||
## Core responsibilities
|
|
||||||
|
|
||||||
| Component | Responsibility |
|
|
||||||
|---|---|
|
|
||||||
| MobilityOps | Operational data, business rules, UI, audit and data-quality review |
|
|
||||||
| RAGcore | Document ingestion, retrieval and source-grounded answers |
|
|
||||||
| ITWorx MCP Hub | Controlled AI access to MobilityOps tools and MobilityOps knowledge |
|
|
||||||
| n8n | Cross-system orchestration after committed domain events |
|
|
||||||
|
|
||||||
## Build principle
|
|
||||||
|
|
||||||
Finish a small vertical slice completely. No placeholder pages, fake buttons, invented production claims or scope expansion.
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated live deployment and demo evidence must stay outside source control.
|
||||||
|
demo-release/
|
||||||
|
deployment/
|
||||||
|
screenshots/
|
||||||
|
*.log
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
*.zip
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
|
||||||
|
# Keep this policy file.
|
||||||
|
!.gitignore
|
||||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 102 KiB |
@@ -1,145 +0,0 @@
|
|||||||
# MobilityOps Unraid deployment evidence
|
|
||||||
|
|
||||||
## Outcome
|
|
||||||
|
|
||||||
- Deployment: **PASS**
|
|
||||||
- Gitea publication: **PASS**
|
|
||||||
- Gitea URL: `https://gitea.itworx.tech/Jens/MobilityOps`
|
|
||||||
- Visibility: private (verified in the Gitea web UI)
|
|
||||||
- Branch: `master`
|
|
||||||
- Verified baseline commit: `4bf9afbeff44088864e0844769d4dd0e4089d85b`
|
|
||||||
- Deployment implementation commit: `1e13943cffb2da8a328b5b1ea5e9b1fe73fdd774`
|
|
||||||
- Server: `192.168.10.150`
|
|
||||||
- Server directory: `/mnt/user/appdata/mobilityops`
|
|
||||||
- Compose project: `mobilityops`
|
|
||||||
- Application URL: `http://192.168.10.150:1236`
|
|
||||||
- Port mapping: LAN `0.0.0.0:1236` / `[::]:1236` to `web:80`
|
|
||||||
|
|
||||||
## Services and health
|
|
||||||
|
|
||||||
| Service | Runtime state | Health | Host exposure |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `db` | running, 0 restarts | healthy | none (`5432/tcp` internal) |
|
|
||||||
| `api` | running, 0 restarts | healthy | none (`8000/tcp` internal) |
|
|
||||||
| `web` | running, 0 restarts | healthy | `1236:80` on LAN |
|
|
||||||
| shared host `n8n` | running | healthy | `5678:5678` on LAN; outside MobilityOps Compose |
|
|
||||||
|
|
||||||
The final review topology reuses the n8n container that was already running on the host.
|
|
||||||
Its empty public-host/editor URL settings were corrected in the persistent Unraid template
|
|
||||||
so workflow execution URLs are valid. The temporary Compose-owned n8n container was
|
|
||||||
removed without deleting its retained volume. Port `1236` was confirmed unused before the
|
|
||||||
original deployment; the application directory was created specifically for MobilityOps.
|
|
||||||
|
|
||||||
## Deployment commands
|
|
||||||
|
|
||||||
The existing SSH aliases resolve to the requested hosts and keys (`gitea.itworx.tech`
|
|
||||||
for Gitea SSH and `unraid` for root access). No key was created, copied, or replaced.
|
|
||||||
The committed source was transferred from the workstation; Unraid has no Gitea key.
|
|
||||||
|
|
||||||
Repository publication used the SSH clone URL supplied by Gitea:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git remote add origin ssh://git@192.168.10.150:222/Jens/MobilityOps.git
|
|
||||||
git push -u origin master
|
|
||||||
git push origin --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
Git and the Gitea web UI both verified `master` as the default branch, the full commit
|
|
||||||
history, baseline commit `4bf9afbeff44088864e0844769d4dd0e4089d85b`, and zero tags.
|
|
||||||
The remote tree contains no `.env`, local database, `node_modules`, virtual environment,
|
|
||||||
test cache, build cache, Playwright output, or browser binaries.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git archive --format=tar.gz --output=<temporary-archive> <commit>
|
|
||||||
scp <temporary-archive> unraid:/mnt/user/appdata/mobilityops/.deploy/source.tar.gz
|
|
||||||
ssh unraid
|
|
||||||
cd /mnt/user/appdata/mobilityops
|
|
||||||
tar -xzf .deploy/source.tar.gz
|
|
||||||
./deploy/unraid/configure-env.sh http://192.168.10.150:1236
|
|
||||||
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 \
|
|
||||||
python -m app.cli seed --reset
|
|
||||||
./deploy/unraid/setup-existing-n8n.sh \
|
|
||||||
n8n \
|
|
||||||
http://192.168.10.150:1236/api/v1/integrations/n8n/return-callback
|
|
||||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up -d db api web
|
|
||||||
```
|
|
||||||
|
|
||||||
The server `.env` was created from `.env.example`, is mode `0600`, and contains generated
|
|
||||||
runtime secrets. Secret values and n8n owner credentials remain server-only and are not
|
|
||||||
included here or in Git.
|
|
||||||
|
|
||||||
## Validation evidence
|
|
||||||
|
|
||||||
- Migration: `e7b08389f47f (head)`.
|
|
||||||
- Deterministic seed: users 2, customers 180, vehicles 50, bookings 246, inspections 75,
|
|
||||||
maintenance 40, data-quality issues 26, workflow runs 20.
|
|
||||||
- HTTP: `GET /` returned 200; `GET /health` returned
|
|
||||||
`{"status":"ok","service":"mobilityops-api"}` through the web proxy.
|
|
||||||
- Backend gates in an isolated local Compose project: 66 tests passed, Ruff clean, mypy
|
|
||||||
clean across 44 files.
|
|
||||||
- Frontend: `npm ci && npm run build` completed (`tsc -b && vite build`).
|
|
||||||
- Logs: no traceback, fatal, uncaught, or unresolved startup error in the deployment log
|
|
||||||
scan. Browser console had no warnings or errors during the smoke test.
|
|
||||||
- Browser smoke test in Chrome: Operations Manager demo login, Dashboard, Vehicles,
|
|
||||||
Bookings, Data Quality, Knowledge, Automation, and Audit all loaded from the LAN URL.
|
|
||||||
- Dashboard showed persisted seed metrics (21 available, 11 rented, 6 cleaning,
|
|
||||||
5 maintenance, 7 blocked, 22 open issues, 1 pending/failed workflow).
|
|
||||||
- Return workflow: `BK-DEMO-RETURN` accepted 54,700 km, created `INSP-0076` and
|
|
||||||
`DQ-RET-0076`, preserved the 54,820 km canonical odometer, and changed the booking to
|
|
||||||
returned.
|
|
||||||
- Shared-n8n round trip: final post-deploy event `98eb06dc-0bcc-4e3d-96ec-c23b2d266293`
|
|
||||||
reached `succeeded` on attempt 1 with no last error; the deterministic reset afterwards
|
|
||||||
restored `BK-DEMO-RETURN` to `active`.
|
|
||||||
- Data quality: `DQ-RET-0076` displayed the persisted regression evidence and related
|
|
||||||
booking/inspection references.
|
|
||||||
- Knowledge: UI truthfully showed `Provider: demo · available · 10 procedures indexed`;
|
|
||||||
the damage question returned grounded excerpts and citations from the local procedures.
|
|
||||||
|
|
||||||
## Integration status
|
|
||||||
|
|
||||||
- RAGcore: disabled for this deployment; `KNOWLEDGE_PROVIDER=demo`. No claim of a live
|
|
||||||
RAGcore connection is shown. Operational functionality is unaffected.
|
|
||||||
- ITWorx MCP Hub: registration disabled with `MCP_HUB_REGISTRATION_ENABLED=false`; the
|
|
||||||
independently authenticated provider endpoints remain available internally to the web
|
|
||||||
proxy/API boundary, but no live Hub connection is claimed.
|
|
||||||
- n8n: the existing server instance at `http://192.168.10.150:5678` is healthy; the
|
|
||||||
MobilityOps workflow is imported/published there and a real return delivery succeeded.
|
|
||||||
The bundled MobilityOps service is disabled by default in the Unraid overlay.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- RAGcore and ITWorx MCP Hub are intentionally not connected yet.
|
|
||||||
- Demo authentication remains the accepted HMAC-cookie PoC mechanism.
|
|
||||||
- The dependency advisories already documented in final acceptance remain unchanged.
|
|
||||||
|
|
||||||
## Redeploy
|
|
||||||
|
|
||||||
From the workstation, create an archive of the desired committed revision and transfer it
|
|
||||||
to `.deploy/source.tar.gz`. On Unraid, preserve `.env` and the named volumes, then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/user/appdata/mobilityops
|
|
||||||
tar -xzf .deploy/source.tar.gz
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/user/appdata/mobilityops
|
|
||||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml ps
|
|
||||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml logs --tail=200
|
|
||||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml logs -f api web
|
|
||||||
docker logs -f n8n
|
|
||||||
```
|
|
||||||
|
|
||||||
## Safe rollback
|
|
||||||
|
|
||||||
Choose a known-good commit on the workstation, archive and transfer it as above, then on
|
|
||||||
Unraid extract it over the identifiable MobilityOps source directory and run the same
|
|
||||||
`up --build -d` command. Preserve `.env` and both named volumes; do not use `down -v`,
|
|
||||||
remove volumes, prune Docker, or modify unrelated containers. Check the target commit's
|
|
||||||
Alembic compatibility before rolling application code behind the current database schema.
|
|
||||||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 71 KiB |
@@ -1,89 +0,0 @@
|
|||||||
# MobilityOps premium UI evidence summary
|
|
||||||
|
|
||||||
Date: 2026-08-02
|
|
||||||
Branch: `design/mobilityops-premium-ui`
|
|
||||||
Baseline revision: `dfabb41582e302f45a3de826f85f531bf23dfc8b`
|
|
||||||
Final design implementation commit: `1f292e14bb6a2e8ded5dc675b1b3360307d8a9ae`
|
|
||||||
Review URL: `http://192.168.10.150:1236`
|
|
||||||
|
|
||||||
## Outcome
|
|
||||||
|
|
||||||
The working PoC was transformed into the Control Rail operational interface without
|
|
||||||
changing backend contracts or adding scope. All existing journeys remain functional;
|
|
||||||
return registration gained an evidence-based review boundary before commit.
|
|
||||||
|
|
||||||
## Evidence index
|
|
||||||
|
|
||||||
- Baseline audit: `docs/design/current-ux-audit.md`
|
|
||||||
- Three directions and decision: `docs/design/design-directions.md`
|
|
||||||
- Design tokens and component rules: `docs/design/design-system.md`
|
|
||||||
- Stitch resource IDs: `docs/design/stitch-manifest.md`
|
|
||||||
- Implemented visual validation: `docs/design/implementation-validation.md`
|
|
||||||
- Baseline captures: `artifacts/design-validation/current/`
|
|
||||||
- Stitch captures: `artifacts/design-validation/stitch/`
|
|
||||||
- Final responsive captures: `artifacts/design-validation/implementation/`
|
|
||||||
|
|
||||||
## Major implementation changes
|
|
||||||
|
|
||||||
- Responsive Control Rail shell with compact top bar, desktop rail, off-canvas menu and
|
|
||||||
labelled mobile bottom navigation.
|
|
||||||
- Live readiness band, filterable Attention queue, movement timeline, honest integration
|
|
||||||
pulse and persisted activity on the operations dashboard.
|
|
||||||
- Searchable fleet and booking registries; booking client pagination limits the DOM to 25
|
|
||||||
operational rows; responsive tables retain field labels.
|
|
||||||
- Capture → review → result return workflow with calculated consequence preview and no
|
|
||||||
write request before confirmation.
|
|
||||||
- Match/conflict duplicate comparison, evidence-first knowledge, system-health cards and
|
|
||||||
expandable audit metadata.
|
|
||||||
- Inline SVG product mark, Feather-like line icon set, CSS control-centre illustration,
|
|
||||||
timeline/status motion and reduced-motion fallback; no image or motion dependency.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
| Gate | Result |
|
|
||||||
|---|---|
|
|
||||||
| Backend tests | 66 passed |
|
|
||||||
| Backend lint | ruff passed |
|
|
||||||
| Backend types | mypy: 0 issues in 44 files |
|
|
||||||
| Frontend types/build | passed; 59 modules; 240.24 kB JS and 36.63 kB CSS before gzip |
|
|
||||||
| Browser journeys | 19 passed locally |
|
|
||||||
| Horizontal overflow | none at 390/768/1280/1440 px |
|
|
||||||
| Accessibility | named landmarks, skip link, visible focus, text-plus-shape status, labelled mobile rows, reduced-motion support |
|
|
||||||
| Deployed browser smoke | passed on all 10 authenticated routes plus login at desktop and mobile sizes |
|
|
||||||
| Console/network | 0 browser warnings/errors; 7 authenticated API paths returned HTTP 200 |
|
|
||||||
| Deployed global search | Ctrl+K plus `MO-024` navigation passed against the review URL |
|
|
||||||
|
|
||||||
All displayed operational counts remain derived from the existing persisted API data.
|
|
||||||
Synthetic-data labelling is persistent on login and authenticated surfaces.
|
|
||||||
|
|
||||||
Deployed evidence is stored in `artifacts/design-validation/implementation/deployed/`.
|
|
||||||
The review stack reports healthy PostgreSQL/API state, HTTP 200 from the web application,
|
|
||||||
and healthy state from the server's existing n8n at port 5678. A synthetic return reached
|
|
||||||
`succeeded` on attempt 1 through that shared n8n and its MobilityOps callback; the demo was
|
|
||||||
then reset to its deterministic state.
|
|
||||||
|
|
||||||
## Performance observations
|
|
||||||
|
|
||||||
No runtime font, image or animation dependency was added. The application uses inline SVG
|
|
||||||
and CSS visuals, and the production bundle remains appropriate for this internal PoC.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- Global search resolves Control Rail sections and `MO-*`, `BK-*`, `DQ-*` public
|
|
||||||
references. It intentionally does not offer customer lookup because the locked PoC has
|
|
||||||
no customer detail route or cross-entity search API.
|
|
||||||
- The repository retains a bundled n8n service for standalone local clean-checkout demos.
|
|
||||||
The Unraid overlay keeps it behind the opt-in `bundled-n8n` profile; the live review
|
|
||||||
deployment uses the server's existing shared n8n instead.
|
|
||||||
- The MCP Hub state is correctly shown as not configured in the current PoC rather than
|
|
||||||
simulated as healthy.
|
|
||||||
- Live RAGcore and MCP Hub round trips remain subject to the existing environment limits
|
|
||||||
documented in `PROJECT_STATE.md`; their degradation behavior is unchanged.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
The accepted baseline remains reachable at commit
|
|
||||||
`dfabb41582e302f45a3de826f85f531bf23dfc8b`. To roll back the review deployment without
|
|
||||||
rewriting git history, archive that revision, extract it over the application source on
|
|
||||||
Unraid while preserving `.env` and Docker volumes, and run
|
|
||||||
`docker compose -p mobilityops up -d --build`. Verify `/health` and port 1236 afterwards.
|
|
||||||
@@ -1,63 +1,68 @@
|
|||||||
# MobilityOps — as-built architecture
|
# Fleet Ops — as-built architecture
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
subgraph Browser
|
UI["Fleet Ops Web\nReact + TypeScript + Vite"]
|
||||||
UI["MobilityOps Web<br/>React + TypeScript"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph MobilityOps["MobilityOps (this repo)"]
|
subgraph CORE["Fleet Ops — this repository"]
|
||||||
API["FastAPI backend<br/>/api/v1/*"]
|
API["FastAPI /api/v1\nRBAC + domain rules"]
|
||||||
DISPATCH["Outbox dispatcher<br/>background thread"]
|
OUT["Outbox dispatcher\nleases + bounded retry"]
|
||||||
DB[(PostgreSQL)]
|
DB[(PostgreSQL)]
|
||||||
|
OBS["Prometheus metrics\nGrafana dashboards"]
|
||||||
API --> DB
|
API --> DB
|
||||||
DISPATCH --> DB
|
OUT --> DB
|
||||||
|
API --> OBS
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph External["External central services"]
|
subgraph EXT["Existing external platforms"]
|
||||||
N8N["n8n<br/>return-processing workflow"]
|
N8N["Central n8n\nsecondary orchestration"]
|
||||||
RAGDEMO["Demo KnowledgeProvider<br/>TF-IDF extractive, local files"]
|
RAG["RAGcore\ngrounded procedure retrieval"]
|
||||||
RAGCORE["RAGcore<br/>(adapter built, no live instance)"]
|
HUB["ITWorx MCP Hub\ntool transport + publication"]
|
||||||
HUB["ITWorx MCP Hub<br/>(endpoints built, no live instance)"]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
UI -->|session cookie| API
|
UI -->|secure session cookie| API
|
||||||
API -->|GroundedAnswer| RAGDEMO
|
API -->|tenant/workspace adapter| RAG
|
||||||
API -.->|configurable, unavailable-safe| RAGCORE
|
OUT -->|vehicle.returned.v1| N8N
|
||||||
DISPATCH -->|POST vehicle.returned.v1| N8N
|
N8N -->|service-authenticated callback| API
|
||||||
N8N -->|callback, X-Service-Token| API
|
HUB -->|service-authenticated read-only tools| API
|
||||||
HUB -.->|X-Service-Token, read-only| API
|
|
||||||
|
|
||||||
classDef unverified stroke-dasharray: 5 5;
|
|
||||||
class RAGCORE,HUB unverified;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Dashed boxes/arrows are implemented and unit/contract-tested but were never exercised
|
## Ownership and trust boundaries
|
||||||
against a live instance in this environment (no reachable RAGcore or ITWorx MCP Hub).
|
|
||||||
Solid boxes were verified end-to-end, including a real n8n instance.
|
|
||||||
|
|
||||||
## Component responsibility (unchanged from `docs/03-architecture.md`)
|
| Component | Owns | Explicitly does not own |
|
||||||
|
|---|---|---|
|
||||||
|
| Fleet Ops | vehicles, customers, bookings, inspections, quality issues, audit, permissions, outbox state | external workflow execution or procedure retrieval |
|
||||||
|
| RAGcore | indexing/retrieval and grounded procedure evidence | Fleet Ops database or business state |
|
||||||
|
| ITWorx MCP Hub | MCP transport, connector publication and central tool-call audit | Fleet Ops database or write actions |
|
||||||
|
| n8n | post-commit workflow orchestration | critical business rules or the source-of-truth transaction |
|
||||||
|
|
||||||
| Component | Owns |
|
## End-to-end return trace
|
||||||
|---|---|
|
|
||||||
| MobilityOps | vehicles, customers, bookings, inspections, data-quality issues, audit, outbox/delivery state |
|
|
||||||
| RAGcore | procedure retrieval and grounded answers (demo provider substitutes locally) |
|
|
||||||
| ITWorx MCP Hub | MCP transport, tool publication, central tool-call audit |
|
|
||||||
| n8n | post-commit secondary orchestration only — never the source of truth for vehicle state |
|
|
||||||
|
|
||||||
## Reliability boundaries verified in this build
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Operator
|
||||||
|
participant Web
|
||||||
|
participant API
|
||||||
|
participant DB
|
||||||
|
participant n8n
|
||||||
|
Operator->>Web: Review and confirm return
|
||||||
|
Web->>API: POST return with idempotency key
|
||||||
|
API->>DB: Lock booking and validate invariants
|
||||||
|
API->>DB: Commit inspection, state, audit and outbox atomically
|
||||||
|
API-->>Web: Result + correlation ID
|
||||||
|
Web-->>Operator: Human result and full processing trace
|
||||||
|
API->>n8n: Deliver persisted outbox event
|
||||||
|
n8n->>API: Authenticated status callback
|
||||||
|
API->>DB: Persist delivery/audit evidence
|
||||||
|
```
|
||||||
|
|
||||||
1. **Return commits atomically with its outbox event** — `app/services/returns.py`, one
|
## Verified reliability properties
|
||||||
transaction; verified by `test_concurrent_returns_only_one_succeeds` (real Postgres row
|
|
||||||
locking, not mocked).
|
1. Concurrent returns serialize through PostgreSQL row locking; only one can commit.
|
||||||
2. **Outbox delivery is at-least-once, idempotent by event ID** — verified live: the n8n
|
2. Local return success is independent of n8n availability. Pending delivery remains persisted and retryable.
|
||||||
callback checks for an existing `AuditEvent` by event ID before recording a second time.
|
3. Outbox delivery is at-least-once and idempotent by event ID, with crash-recoverable leases and bounded backoff.
|
||||||
3. **RAGcore failure disables knowledge answers only** — `RAGcoreKnowledgeProvider` degrades
|
4. RAGcore failure affects knowledge answers only. The UI reports unavailable/insufficient evidence and does not invent an answer.
|
||||||
to `unavailable`; the rest of the app is unaffected because the knowledge router is the
|
5. MCP endpoints are a separate tenant-bound, client-identity-validated, read-only surface; every call is audited with a correlation ID.
|
||||||
only consumer.
|
6. Browser authorization is enforced again on the API. Hiding a navigation item is never the security boundary.
|
||||||
4. **MCP Hub failure does not affect the web application** — the four MCP provider
|
|
||||||
endpoints are a separate authenticated surface (`X-Service-Token`), invisible to the
|
The live deployment has exercised all three external boundaries. Local clean-checkout acceptance can use the deterministic extractive knowledge provider while reporting that mode honestly.
|
||||||
browser-facing API/UI.
|
|
||||||
5. **n8n failure leaves events pending with bounded retries** — verified live: a seeded
|
|
||||||
`failed` event, retried through the UI, was picked up by the background dispatcher and
|
|
||||||
delivered through the real n8n instance within one poll cycle.
|
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
# MobilityOps — final acceptance evidence
|
|
||||||
|
|
||||||
## Commit
|
|
||||||
|
|
||||||
Built on top of commit `c5b7e21f81694f0339ad31e3bf044db952d0fbe0` (M6, "implement ITWorx
|
|
||||||
MCP Hub publication"). This evidence file and the rest of M7's polish are committed as
|
|
||||||
`M7: portfolio polish and final acceptance` — run `git log --oneline` for the exact hash.
|
|
||||||
|
|
||||||
## Exact commands (clean checkout)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repo> && cd MobilityOps
|
|
||||||
cp .env.example .env
|
|
||||||
make demo # docker compose up --build -d ; migrations run automatically ; seed --reset
|
|
||||||
```
|
|
||||||
|
|
||||||
One-time n8n setup (see `docs/17-runbook.md` for full detail — this cannot be scripted
|
|
||||||
end-to-end because it requires a one-time owner account created through n8n's web UI):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# open http://localhost:5678/setup in a browser, create any owner account
|
|
||||||
make n8n-setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose run --rm api pytest -q # 66 passed
|
|
||||||
docker compose run --rm api ruff check . # All checks passed
|
|
||||||
cd frontend && npm run build # clean tsc + vite build
|
|
||||||
cd frontend && npx playwright test # 1 passed (full 5-minute demo script)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test counts
|
|
||||||
|
|
||||||
- **Backend**: 66 tests passing (`pytest`), 0 skipped, 0 failed. Ruff clean. Coverage by
|
|
||||||
area: seed determinism (2), auth/roles (4), dashboard (3), vehicles (4), bookings (3),
|
|
||||||
return workflow incl. real concurrent-submission test (9), data quality incl. S2/S4
|
|
||||||
scenarios (10), audit (2), n8n dispatcher incl. malformed-payload regression (6),
|
|
||||||
n8n callback idempotency (3), workflows/retry (4), knowledge incl. S6 scenario (7),
|
|
||||||
MCP provider endpoints (8), health (1).
|
|
||||||
- **Frontend**: `npm run build` — clean TypeScript + Vite build, zero errors.
|
|
||||||
- **End-to-end**: 1 Playwright test (`frontend/e2e/demo.spec.ts`) automating the full
|
|
||||||
documented 5-minute demo script (login → dashboard → S1 return → S2 merge → S6 knowledge
|
|
||||||
question → audit → 360px responsive check) — **passing** against the live stack.
|
|
||||||
|
|
||||||
## Screenshots of the seven main pages
|
|
||||||
|
|
||||||
Captured live against the deterministic seed (`artifacts/evidence/screenshots/`,
|
|
||||||
via `frontend/e2e/_capture-screenshots.spec.ts`):
|
|
||||||
|
|
||||||
| # | Page | File |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | Login | `1-login.png` |
|
|
||||||
| 2 | Dashboard | `2-dashboard.png` |
|
|
||||||
| 3 | Vehicles | `3-vehicles.png` |
|
|
||||||
| 4 | Bookings | `4-bookings.png` |
|
|
||||||
| 5 | Data Quality | `5-data-quality.png` |
|
|
||||||
| 6 | Knowledge (grounded S6 answer) | `6-knowledge.png` |
|
|
||||||
| 7 | Automation | `7-automation.png` |
|
|
||||||
| — | Audit (bonus, 8th nav item) | `8-audit.png` |
|
|
||||||
| — | Dashboard at 360px (responsive proof) | `9-mobile-dashboard.png` |
|
|
||||||
|
|
||||||
## RAGcore evidence
|
|
||||||
|
|
||||||
**Success (demo provider, the one actually satisfying acceptance in this environment)** —
|
|
||||||
S6 question against the real `/api/v1/knowledge/questions` endpoint:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"answer": "Per \"Vehicle return procedure\" (v2.0), section \"1. Register the return\": Open the active booking and record the ending odometer, fuel level, cleanliness, visible damage, technical warnings and relevant notes.",
|
|
||||||
"evidence_state": "grounded",
|
|
||||||
"sources": [
|
|
||||||
{"document_id": "vehicle-return-procedure", "title": "Vehicle return procedure", "version": "2.0", "section": "1. Register the return", "excerpt": "..."},
|
|
||||||
{"document_id": "vehicle-return-procedure", "title": "Vehicle return procedure", "version": "2.0", "section": "3. Determine next state", "excerpt": "..."},
|
|
||||||
{"document_id": "damage-procedure", "title": "Damage handling procedure", "version": "1.3", "section": "1. Immediate actions", "excerpt": "..."}
|
|
||||||
],
|
|
||||||
"provider": "demo",
|
|
||||||
"correlation_id": "b50094b7-1c84-4e39-9055-1dc03e8fd1f8"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Unavailable (RAGcore adapter, live-demonstrated against an unreachable host)** —
|
|
||||||
`KNOWLEDGE_PROVIDER=ragcore`, `RAGCORE_BASE_URL=http://ragcore-not-reachable:9999`:
|
|
||||||
|
|
||||||
```
|
|
||||||
health: {'provider': 'ragcore', 'available': False, 'detail': 'RAGcore unavailable: ConnectError: ...', 'document_count': 0}
|
|
||||||
ask: {'answer': '', 'evidence_state': 'unavailable', 'sources': [], 'provider': 'ragcore', 'correlation_id': 'demo-correlation'}
|
|
||||||
```
|
|
||||||
|
|
||||||
No live RAGcore instance was reachable in this environment, so the adapter's actual
|
|
||||||
request/response contract against a real RAGcore is unverified beyond this
|
|
||||||
degrade-safely behavior — see `contracts/ragcore-contract-assumptions.md` and
|
|
||||||
`PROJECT_STATE.md`'s M5 notes.
|
|
||||||
|
|
||||||
## n8n evidence
|
|
||||||
|
|
||||||
**Success** — a real return registered on `BK-DEMO-RETURN`, delivered through the actual
|
|
||||||
n8n instance (not mocked), confirmed via `GET /api/v1/workflows`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"event_id": "aa5dfeee-90ca-452a-bdd1-0a0b6d3dd63f", "event_type": "vehicle.returned.v1", "aggregate_ref": "BK-DEMO-RETURN", "status": "succeeded", "attempts": 2, "last_error": null}
|
|
||||||
```
|
|
||||||
|
|
||||||
(`attempts: 2` because the first delivery attempt landed while n8n was mid-restart from
|
|
||||||
the one-time workflow-activation step — the dispatcher's backoff-and-retry handled it
|
|
||||||
without any manual intervention, which is itself evidence of the retry behavior working.)
|
|
||||||
|
|
||||||
**Retry (S5 scenario)** — seeded `BK-H-0020` (event `00000000-...-0020`), initially
|
|
||||||
`failed` after 3 attempts with `"Synthetic connection timeout to n8n"`:
|
|
||||||
|
|
||||||
1. Before: `{"status": "failed", "attempts": 3, "last_error": "Synthetic connection timeout to n8n"}`
|
|
||||||
2. Operations Manager clicks Retry on `/automation`.
|
|
||||||
3. Within one ~3s dispatcher poll cycle, delivered through the live n8n instance.
|
|
||||||
4. After: `{"status": "succeeded", "attempts": 4, "last_error": null}`
|
|
||||||
|
|
||||||
## MCP tool sample calls
|
|
||||||
|
|
||||||
All four provider endpoints, authenticated with `X-Service-Token`:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ curl -H "X-Service-Token: <token>" http://localhost:8128/api/v1/integrations/mcp/operations-summary
|
|
||||||
{"tenant":"northstar-mobility-demo","metrics":{"available":21,"rented":11,"cleaning":6,"maintenance":5,"blocked":7,"open_quality_issues":22,"pending_or_failed_workflows":1}}
|
|
||||||
|
|
||||||
$ curl -H "X-Service-Token: <token>" "http://localhost:8128/api/v1/integrations/mcp/attention-vehicles?minimum_severity=high&limit=3"
|
|
||||||
[{"vehicle_ref":"MO-016","severity":"high","rule_type":"booking_overlap",...},
|
|
||||||
{"vehicle_ref":"MO-016","severity":"high","rule_type":"vehicle_status_conflict",...},
|
|
||||||
{"vehicle_ref":"MO-031","severity":"high","rule_type":"missing_required_field",...}]
|
|
||||||
|
|
||||||
$ curl -H "X-Service-Token: <token>" http://localhost:8128/api/v1/integrations/mcp/vehicles/MO-016
|
|
||||||
{"public_ref":"MO-016","make":"Hymer","model":"Exsis","model_year":2021,"location":"Geel","operational_status":"available","odometer_km":30497,"next_service_km":40000,"open_quality_issue_count":2,"current_booking_ref":null}
|
|
||||||
|
|
||||||
$ curl -H "X-Service-Token: <token>" -X POST -d '{"question":"What must I do when a vehicle returns with damage?","max_sources":2}' http://localhost:8128/api/v1/integrations/mcp/search-knowledge
|
|
||||||
{"answer":"Per \"Vehicle return procedure\" ...","evidence_state":"grounded","sources":[...2 items...],"provider":"demo",...}
|
|
||||||
```
|
|
||||||
|
|
||||||
Auth verified: missing header → `422`; wrong token → `401`. All four calls confirmed
|
|
||||||
recorded in `GET /api/v1/audit?action=mcp_tool_request` with `actor_type: "service"`.
|
|
||||||
|
|
||||||
No live ITWorx MCP Hub instance was reachable in this environment — these are direct
|
|
||||||
calls to MobilityOps's own provider endpoints, not a Hub round trip.
|
|
||||||
|
|
||||||
## Known PoC limitations
|
|
||||||
|
|
||||||
- **RAGcore and ITWorx MCP Hub were never reachable in this build environment.** Both
|
|
||||||
integrations are implemented against best-effort/documented contracts and are
|
|
||||||
unit/contract-tested (including their failure-degradation paths), but neither was
|
|
||||||
verified against a real counterpart service. The demo `KnowledgeProvider` is what
|
|
||||||
actually satisfies the knowledge-assistant acceptance criteria here.
|
|
||||||
- **n8n requires a one-time manual owner-account setup** per fresh environment
|
|
||||||
(`docker compose down -v` wipes it) — this is a property of the n8n 2.x image itself
|
|
||||||
(`N8N_BASIC_AUTH_ACTIVE` no longer gates the UI), not something MobilityOps can bypass.
|
|
||||||
Documented precisely in `docs/17-runbook.md`; the workflow import/activation itself
|
|
||||||
*is* scripted (`make n8n-setup`).
|
|
||||||
- **Inspection public refs are a simple `count+1` sequence**, not gap-safe under true
|
|
||||||
concurrent writers — acceptable for this single-tenant demo, would need a DB sequence
|
|
||||||
for a multi-writer production system.
|
|
||||||
- **The five data-quality rules use simplified idempotency** — `(rule_type, entity_type,
|
|
||||||
entity_id)` while open, rather than the doc's literal evidence-fingerprint scheme — see
|
|
||||||
`PROJECT_STATE.md`'s M3 notes for the reasoning (the fingerprint scheme would have let
|
|
||||||
the scan double-report issues already present in the seeded CSV).
|
|
||||||
- **No production authentication** — demo login is an HMAC-signed session cookie tied to
|
|
||||||
two fixed seeded users, appropriate for a PoC, not a real identity provider.
|
|
||||||
|
|
||||||
## Portfolio wording (truthful)
|
|
||||||
|
|
||||||
MobilityOps is a working proof of concept, not a production system and not deployed for
|
|
||||||
any real company. All customers, vehicles, bookings, and documents are synthetic
|
|
||||||
(deterministically generated). The application logic it demonstrates is real: a
|
|
||||||
transactional vehicle-return workflow with idempotency and concurrency control tested
|
|
||||||
against real concurrent database transactions; five explainable, deterministic
|
|
||||||
data-quality rules with a working customer-merge UI; a background outbox dispatcher
|
|
||||||
verified end-to-end against a real n8n instance including failure/retry; a
|
|
||||||
TF-IDF-weighted extractive knowledge assistant that never fabricates answers; and four
|
|
||||||
read-only, audited, service-authenticated integration endpoints. RAGcore and the ITWorx
|
|
||||||
MCP Hub integrations are implemented and tested in isolation but were not verified
|
|
||||||
against live instances of those systems in this environment.
|
|
||||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -1,265 +1,77 @@
|
|||||||
# MobilityOps — final acceptance audit summary
|
# Fleet Ops release acceptance
|
||||||
|
|
||||||
This audit was run after M0–M7 had already been implemented and committed, specifically
|
This file is release-scoped evidence, not a timeless claim. Older evidence under
|
||||||
to independently re-verify the finished system end to end rather than trust the
|
`artifacts/evidence/` is historical. Exact commands and production revisions are recorded
|
||||||
milestone-by-milestone build log. It found and fixed one real category of defect
|
in `PROJECT_STATE.md`.
|
||||||
(`mypy` had never been run across the whole build) and confirmed everything else — every
|
|
||||||
user journey, every button/filter/form, both external-dependency degraded modes, secret
|
|
||||||
hygiene, and the clean-checkout path — works as documented.
|
|
||||||
|
|
||||||
## Final commit
|
## 2026-08-21 release candidate
|
||||||
|
|
||||||
This audit's fixes are committed as the commit immediately following
|
- Backend: **271/271** tests passed against an isolated clean PostgreSQL database.
|
||||||
`108b5d04fc6f7c5ff9c47009032d6469df29cf3c` ("M7: portfolio polish and final acceptance").
|
- Browser acceptance: **155/155** Chromium tests passed in 6.0 minutes.
|
||||||
Run `git log -1 --format="%H %s"` for the exact hash.
|
- Live-safe browser canary: **4/4** passed across Chromium and Firefox against the local
|
||||||
|
deployed stack; unlike the acceptance suite, it never resets or mutates demo records.
|
||||||
|
- Accessibility: the principal login, dashboard, data-quality, knowledge, automation and
|
||||||
|
audit routes have no automated critical/serious WCAG 2 A/AA/2.1 AA violations.
|
||||||
|
- Frontend: TypeScript, ESLint, production build, dependency audit and per-asset JS/CSS
|
||||||
|
budgets passed; committed visual baselines cover the public entry and engineering story.
|
||||||
|
- Contracts: committed OpenAPI, event schema, MCP tools and all five n8n definitions match
|
||||||
|
their code/manifest sources.
|
||||||
|
- Recovery: a custom-format PostgreSQL dump was restored into a disposable database; the
|
||||||
|
Alembic revision and non-zero canonical table counts matched the source database.
|
||||||
|
- Operations: Prometheus/Alertmanager configuration validation passed, including the
|
||||||
|
watchdog and authenticated n8n receiver route.
|
||||||
|
|
||||||
## Exact commands executed
|
## 2026-08-21 production verification
|
||||||
|
|
||||||
Clean-checkout drill (run twice during this audit, most recently against fully wiped
|
- Immutable application revision `95c91797fa2c599443d69d9c96d83a85ee0711f7` was promoted
|
||||||
Docker volumes):
|
from a checksum-verified source archive after a fresh production backup.
|
||||||
|
- Source revision and both OCI revision labels matched. Public readiness was green,
|
||||||
|
Alembic was at head, all health-gated services were healthy and persisted demo data was
|
||||||
|
retained without a deployment reset.
|
||||||
|
- Trivy found zero fixed HIGH/CRITICAL vulnerabilities in each exact production image.
|
||||||
|
- Prometheus successfully scraped the bearer-protected API target, Alertmanager carried
|
||||||
|
the active delivery watchdog, and the authenticated n8n alert receiver remained active.
|
||||||
|
- The final non-destructive HTTPS canary passed **4/4** across Chromium and Firefox,
|
||||||
|
including the core operator routes and a grounded answer from the real knowledge stack.
|
||||||
|
|
||||||
```bash
|
## 2026-08-21 resilience upgrade verification
|
||||||
git status # working tree clean before starting
|
|
||||||
docker compose down -v # wipe all volumes — genuinely clean state
|
|
||||||
cp .env.example .env
|
|
||||||
docker compose up --build -d # migrations run automatically (backend/entrypoint.sh)
|
|
||||||
docker compose exec api python -m app.cli seed --reset
|
|
||||||
docker compose run --rm api pytest -q
|
|
||||||
docker compose run --rm api ruff check .
|
|
||||||
docker compose run --rm api mypy app
|
|
||||||
cd frontend && npm run build
|
|
||||||
cd frontend && npx playwright test
|
|
||||||
```
|
|
||||||
|
|
||||||
n8n one-time setup (owner account via browser at `http://localhost:5678/setup`, then):
|
- Immutable revision `00191e9b54ee6b961648a6e02abbb3a57957dba0` was promoted from a
|
||||||
|
checksum-verified archive after a verified production dump. A stable gateway now routes
|
||||||
|
to two revision-specific API and two web replicas; stateful services are no longer
|
||||||
|
restarted by routine application releases.
|
||||||
|
- Moving host port 1236 from the legacy web container to the gateway was a one-time
|
||||||
|
migration hand-off and produced 14 failures across 1,200 rapid probes. Future releases
|
||||||
|
do not move that port; their acceptance gate is the zero-error versioned gateway reload.
|
||||||
|
- The subsequent M50 release exercised that steady-state path: the gateway remained online,
|
||||||
|
atomically switched revision-specific API/web aliases and sustained **700/700** external
|
||||||
|
readiness probes without interruption. Post-promotion Chromium/Firefox acceptance passed
|
||||||
|
**4/4** and 360 concurrent authenticated reads had zero errors at p95 **116.4 ms**.
|
||||||
|
- The versioned gateway switch sustained **300/300** local rollout probes without an error.
|
||||||
|
Production's non-destructive Chromium/Firefox canary passed **4/4**, and 360 authenticated
|
||||||
|
concurrent reads returned zero errors at p95 **137.2 ms**.
|
||||||
|
- PostgreSQL, backup, Prometheus, Alertmanager, Grafana and the gateway all reported healthy;
|
||||||
|
Alembic was at head, the protected Prometheus target was present, and backup plus real
|
||||||
|
restore-drill evidence remained current.
|
||||||
|
- Trivy 0.74 found zero fixed HIGH/CRITICAL vulnerabilities in the exact production API,
|
||||||
|
web and gateway images. The separately built rclone/PostgreSQL backup-tools image is also
|
||||||
|
clean after rebuilding rclone 1.75.0 with Go 1.26.6.
|
||||||
|
- The OneDrive worker is deployed as an opt-in profile but is not represented as active:
|
||||||
|
it requires the owner's one-time interactive Microsoft OAuth authorization. Until that
|
||||||
|
happens, verified local backups remain the active recovery source.
|
||||||
|
|
||||||
```bash
|
## Evidence boundary
|
||||||
docker compose exec n8n n8n import:workflow --input=//imports/mobilityops-return-processing.json
|
|
||||||
docker compose exec n8n n8n publish:workflow --id=mobilityops-return-processing
|
|
||||||
docker compose restart n8n
|
|
||||||
```
|
|
||||||
|
|
||||||
Degraded-mode drills:
|
The complete local suite uses the deterministic provider and an isolated database so it is
|
||||||
|
repeatable and safely destructive. Production verification is deliberately smaller and
|
||||||
|
non-destructive; it verifies the real RAGcore/MCP/n8n health surfaces without resetting the
|
||||||
|
shared demo. A successful local result is never presented as proof that an external service
|
||||||
|
was live. The production subsection is added only after the exact committed release is
|
||||||
|
deployed and observed.
|
||||||
|
|
||||||
```bash
|
## Remaining product boundary
|
||||||
docker compose stop n8n # then register a return via the API — commits, event stays pending
|
|
||||||
docker compose start n8n # dispatcher self-heals, no manual intervention
|
|
||||||
docker compose run --rm -e KNOWLEDGE_PROVIDER=ragcore -e RAGCORE_BASE_URL=http://ragcore-not-reachable:9999 \
|
|
||||||
api python -c "from app.services.knowledge import get_knowledge_provider; ..."
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test and validation results
|
Fleet Ops remains a synthetic single-tenant PoC. It is not a production identity provider,
|
||||||
|
payment system, accounting package or public reservation platform. External RAGcore, MCP
|
||||||
| Check | Command | Result |
|
Hub and n8n services remain independently operated dependencies and are accessed only
|
||||||
|---|---|---|
|
through their documented adapters.
|
||||||
| Backend unit/integration tests | `docker compose run --rm api pytest -q` | **66 passed**, 0 failed, 0 skipped |
|
|
||||||
| Backend lint | `docker compose run --rm api ruff check .` | **All checks passed** |
|
|
||||||
| Backend type check | `docker compose run --rm api mypy app` | **Success: no issues found in 44 source files** (found and fixed 43 pre-existing errors this audit — see below) |
|
|
||||||
| Frontend build + typecheck | `cd frontend && npm run build` | Clean (`tsc -b && vite build`, zero errors) |
|
|
||||||
| End-to-end (Playwright) | `cd frontend && npx playwright test` | **12 passed** (`demo.spec.ts` — full 9-step demo script; `interactive-elements.spec.ts` — 11 tests covering every nav item, filter, tab, and role boundary) |
|
|
||||||
| Clean-checkout migrations | `docker compose down -v && docker compose up --build -d` | 11 tables created automatically, `alembic current` → `e7b08389f47f (head)`, zero manual step |
|
|
||||||
| Deterministic seed | `docker compose exec api python -m app.cli seed --reset` | `users:2 customers:180 vehicles:50 bookings:246 inspections:75 maintenance:40 data_quality_issues:26 workflow_runs:20` — identical across every reseed this session |
|
|
||||||
| Secret scan | `git ls-files \| grep -x .env`; `git log --all -p -- '*.env'`; history grep for AWS/private-key/`sk-` patterns | No `.env` ever committed; no secrets found in history |
|
|
||||||
|
|
||||||
### mypy defects found and fixed (the one real gap this audit uncovered)
|
|
||||||
|
|
||||||
`mypy` is a declared dev dependency (`backend/pyproject.toml`) but was never added to any
|
|
||||||
milestone's validation loop — only `ruff` was run throughout M0–M7. Running it cold
|
|
||||||
surfaced 43 errors across 10 files. All were triaged and fixed (not suppressed):
|
|
||||||
|
|
||||||
- **Two genuine defensive-programming gaps**, not just type-annotation issues:
|
|
||||||
- `app/services/returns.py`: the vehicle lookup after acquiring the row lock had no
|
|
||||||
`None` guard; a dangling FK would have crashed with an unhandled 500 instead of a
|
|
||||||
clean `404 VEHICLE_NOT_FOUND`. Fixed.
|
|
||||||
- `app/api/routers/bookings.py`: same pattern in `get_booking` for the customer/vehicle
|
|
||||||
lookups — now returns a clean `500` with a message instead of an `AttributeError`.
|
|
||||||
- `app/api/deps.py` / `app/api/routers/demo.py`: `CurrentUser.role` is validated by
|
|
||||||
Pydantic at runtime already, but `get_current_user` now explicitly checks role
|
|
||||||
membership before construction, turning a would-be unhandled `ValidationError` into a
|
|
||||||
clean `401` for a corrupted/tampered session cookie.
|
|
||||||
- Two instances of reusing one variable name for both a `Vehicle` and a `Customer` across
|
|
||||||
branches (`dashboard.py`, `data_quality.py`) — renamed for clarity, not just to satisfy
|
|
||||||
mypy.
|
|
||||||
- `Booking.__table__.update()` / `Customer.__table__.update()` switched to the idiomatic
|
|
||||||
`sqlalchemy.update(Model)` construct (also fixes the type error).
|
|
||||||
- Remainder: deprecated `conint()` → `Annotated[int, Field(...)]`, a `Sequence` vs `list`
|
|
||||||
`.sort()` call, an `assert`-guarded None-narrow after a `WHERE ... IS NOT NULL` filter
|
|
||||||
mypy can't see through, and a couple of narrowly-scoped `# type: ignore[...]` comments
|
|
||||||
for known SQLAlchemy stub gaps (`Result.rowcount`).
|
|
||||||
|
|
||||||
`make lint` now runs `ruff check .` **and** `mypy app`.
|
|
||||||
|
|
||||||
## Application URLs and ports
|
|
||||||
|
|
||||||
| Service | URL | Notes |
|
|
||||||
|---|---|---|
|
|
||||||
| Web (React SPA) | `http://localhost:1228` | nginx-served static build |
|
|
||||||
| API | `http://localhost:8128` | FastAPI, `/health` for liveness |
|
|
||||||
| API docs | `http://localhost:8128/docs` | auto-generated OpenAPI/Swagger UI |
|
|
||||||
| n8n | `http://localhost:5678` | requires one-time owner setup, see below |
|
|
||||||
| PostgreSQL | `localhost:5432` (container-internal only, no host port published) | |
|
|
||||||
|
|
||||||
## Demo users and access method
|
|
||||||
|
|
||||||
No passwords. Two demo-role buttons on `http://localhost:1228/login`:
|
|
||||||
|
|
||||||
- **Open as Operations Manager** → `USR-OPS`, "Amelie De Ridder". Full access: dashboard,
|
|
||||||
data-quality resolution/merge, automation retry, demo reset, MCP/service-token routes
|
|
||||||
are separate (not user-facing).
|
|
||||||
- **Open as Rental Employee** → `USR-EMP`, "Karim Boujaddaine". Can register returns and
|
|
||||||
browse vehicles/bookings/knowledge; Automation page is visible but shows a
|
|
||||||
role-restricted message instead of the delivery table (enforced both in the UI and by
|
|
||||||
the backend's `require_operations_manager` dependency — verified by
|
|
||||||
`test_retry_requires_operations_manager` and the e2e role-restriction test).
|
|
||||||
|
|
||||||
Session is an HMAC-signed, `HttpOnly` cookie (`app/core/security.py`) — a demo mechanism,
|
|
||||||
not a real identity provider (documented as a known limitation).
|
|
||||||
|
|
||||||
## Implemented functionality
|
|
||||||
|
|
||||||
- Operations dashboard with 100% database-backed metrics, attention items linking to the
|
|
||||||
underlying data-quality issue, "today" departures/returns, and recent automation runs.
|
|
||||||
- Vehicle and booking list/detail pages with working filters (status, attention-only) and
|
|
||||||
a tabbed vehicle detail view (overview/bookings/inspections/maintenance/quality).
|
|
||||||
- Full transactional vehicle-return workflow: row-locked, idempotent by
|
|
||||||
`Idempotency-Key`, canonical-odometer regression handling (never silently lowers the
|
|
||||||
canonical value), vehicle status derivation, two audit events, and a schema-compliant
|
|
||||||
outbox event — verified against real concurrent submissions (1×201 + 2×409).
|
|
||||||
- Invalid-mileage rejection: negative values and non-numeric input both correctly
|
|
||||||
rejected with `422` and a precise Pydantic validation message.
|
|
||||||
- Data Quality Workbench: five deterministic rules (duplicate customer via TF-IDF-style
|
|
||||||
weighted signal scoring, missing required field, odometer regression, booking overlap,
|
|
||||||
vehicle status conflict), issue list/detail/defer/reject, and a two-column
|
|
||||||
duplicate-customer compare-and-merge UI with an inline (non-native-dialog) confirmation
|
|
||||||
step, transactional booking rewiring, and audit logging.
|
|
||||||
- Full audit trail: every significant action (login, return, vehicle status change,
|
|
||||||
data-quality issue lifecycle, customer merge, workflow retry, demo reset, n8n
|
|
||||||
callback, MCP tool request, knowledge question) is recorded with actor, correlation ID,
|
|
||||||
and before/after state; filterable by action.
|
|
||||||
- Knowledge Assistant: deterministic TF-IDF-weighted extractive retrieval over the 10
|
|
||||||
procedure documents — never generative, always cites real excerpts, and honestly
|
|
||||||
reports `insufficient`/`unavailable` states rather than fabricating an answer.
|
|
||||||
- n8n automation: background outbox dispatcher (`FOR UPDATE SKIP LOCKED` claim,
|
|
||||||
exponential backoff, no DB transaction held during the HTTP call), a live-verified
|
|
||||||
round trip through an actual n8n workflow, manual retry for failed deliveries, and an
|
|
||||||
Automation page (Operations Manager only) showing all runs with filtering.
|
|
||||||
- Four read-only, service-token-authenticated MCP Hub provider endpoints, each recording
|
|
||||||
its own service-request audit event, with zero write/mutation endpoints anywhere in
|
|
||||||
that namespace.
|
|
||||||
- Responsive UI verified down to 360px width (nav wraps, tables become cards, metric
|
|
||||||
tiles reflow to a 2-column grid, no horizontal overflow) — both by an automated
|
|
||||||
Playwright viewport/overflow assertion and by a captured screenshot.
|
|
||||||
|
|
||||||
## RAGcore integration status: implemented, not live-verified
|
|
||||||
|
|
||||||
The active `KnowledgeProvider` in this environment is `DemoKnowledgeProvider` — fully
|
|
||||||
implemented, fully tested, fully live-verified, and what actually satisfies the
|
|
||||||
knowledge-assistant acceptance criteria. A `RAGcoreKnowledgeProvider` HTTP adapter also
|
|
||||||
exists (`app/services/knowledge/ragcore.py`), targeting a best-effort contract inferred
|
|
||||||
from `contracts/ragcore-contract-assumptions.md` (no live RAGcore API spec was available).
|
|
||||||
Its **unavailable-degradation path is live-verified this audit**: pointed at an
|
|
||||||
unreachable host, it returns `{"evidence_state": "unavailable", "answer": "", "sources":
|
|
||||||
[]}` with no fabrication, exactly as required — but an actual successful round trip
|
|
||||||
against a real RAGcore instance has never been performed, because no such instance was
|
|
||||||
reachable in this environment.
|
|
||||||
|
|
||||||
## MCP Hub integration status: implemented, not live-verified
|
|
||||||
|
|
||||||
All four contracted read-only tools (`mobilityops_get_operations_summary`,
|
|
||||||
`mobilityops_list_attention_vehicles`, `mobilityops_get_vehicle_details`,
|
|
||||||
`mobilityops_search_knowledge`) are implemented as service-token-protected endpoints under
|
|
||||||
`/api/v1/integrations/mcp/`, directly `curl`-verified this audit (auth enforcement,
|
|
||||||
correct data shape, no write methods, service-request audit logging). No live ITWorx MCP
|
|
||||||
Hub instance was reachable in this environment, so an actual Hub-mediated tool call was
|
|
||||||
never performed — only direct calls to MobilityOps's own provider API.
|
|
||||||
|
|
||||||
## n8n integration status: implemented and fully live-verified
|
|
||||||
|
|
||||||
The only external integration with a real, running counterpart service available in this
|
|
||||||
environment. Fully verified this audit, including both success and degraded paths:
|
|
||||||
|
|
||||||
- **Success**: a real return registered via the API was delivered by the background
|
|
||||||
dispatcher to an actual n8n instance (owner account + imported/activated workflow),
|
|
||||||
which called back into MobilityOps and was recorded `succeeded`.
|
|
||||||
- **Degraded mode**: `docker compose stop n8n`, then a return was registered — it
|
|
||||||
**committed successfully** (`201`, booking `status: returned` persisted) exactly as
|
|
||||||
required by the architecture's reliability boundary ("a return command and its outbox
|
|
||||||
event commit in one transaction" and "n8n failure leaves events pending with bounded
|
|
||||||
retries"). The outbox event stayed `pending` with two real `ConnectError`s logged and
|
|
||||||
exponential backoff.
|
|
||||||
- **Self-healing**: restarting n8n required no manual intervention — the background
|
|
||||||
dispatcher picked the pending event back up on its next poll cycle and delivered it to
|
|
||||||
`succeeded` (5 total attempts across the outage).
|
|
||||||
- **Manual retry (S5 scenario)**: a seeded `failed` delivery, retried from the Automation
|
|
||||||
page, moved to `pending` and was delivered to `succeeded` by the live dispatcher within
|
|
||||||
one poll cycle.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- RAGcore and the ITWorx MCP Hub were never reachable in this build/audit environment;
|
|
||||||
both integrations are implemented and tested against inferred/documented contracts but
|
|
||||||
not verified against real instances of those systems (see above).
|
|
||||||
- n8n requires a one-time, per-fresh-environment manual owner-account setup through its
|
|
||||||
own web UI (`http://localhost:5678/setup`) — a property of the n8n 2.x image itself
|
|
||||||
(`N8N_BASIC_AUTH_ACTIVE` no longer gates the UI), not something MobilityOps can bypass.
|
|
||||||
The workflow import/activation itself *is* scripted (`make n8n-setup`).
|
|
||||||
- Demo authentication is an HMAC-signed session cookie tied to two fixed seeded users —
|
|
||||||
appropriate for a PoC, not a production identity provider.
|
|
||||||
- Inspection public references are assigned via a simple `count + 1` sequence, not
|
|
||||||
gap-safe under true concurrent writers (acceptable for this single-tenant demo).
|
|
||||||
- The five data-quality rules use a simplified idempotency key
|
|
||||||
(`rule_type, entity_type, entity_id` while open) rather than the spec's literal
|
|
||||||
evidence-fingerprint scheme — documented rationale in `PROJECT_STATE.md`'s M3 notes.
|
|
||||||
- `npm audit` reports one residual moderate `esbuild`/Vite-8 dev-server-only advisory
|
|
||||||
(fixable only by a Vite major version bump) and one high `react-router` RSC-mode
|
|
||||||
advisory that does not apply to this app (it never uses React Router's RSC/SSR mode).
|
|
||||||
|
|
||||||
## Clean deployment instructions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repo> && cd MobilityOps
|
|
||||||
cp .env.example .env
|
|
||||||
make demo # build, start, migrate (automatic), seed
|
|
||||||
```
|
|
||||||
|
|
||||||
One-time n8n setup (only needed for the automation demo path; everything else works
|
|
||||||
without it):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# open http://localhost:5678/setup in a browser, create any owner account
|
|
||||||
# (8+ chars, 1 number, 1 capital letter — no email verification required)
|
|
||||||
make n8n-setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8128/health # {"status":"ok",...}
|
|
||||||
curl -o /dev/null -w "%{http_code}\n" http://localhost:1228/ # 200
|
|
||||||
make test # 66 backend tests
|
|
||||||
make lint # ruff + mypy, zero errors
|
|
||||||
make e2e # 12 Playwright tests (stack must be running)
|
|
||||||
```
|
|
||||||
|
|
||||||
Full detail, recovery expectations, and required operational checks: `docs/17-runbook.md`.
|
|
||||||
|
|
||||||
## Five-minute demonstration flow
|
|
||||||
|
|
||||||
1. Open `http://localhost:1228` → **Open as Operations Manager**.
|
|
||||||
2. **Dashboard**: point out the metrics are live counts (available/rented/cleaning/
|
|
||||||
maintenance/blocked vehicles, open quality issues, pending/failed workflows), and the
|
|
||||||
Attention Required list linking straight to the underlying issues.
|
|
||||||
3. **Vehicles → MO-024** → open the active booking `BK-DEMO-RETURN`, register a return
|
|
||||||
with an odometer reading below MO-024's canonical value → the result panel shows the
|
|
||||||
inspection, the derived vehicle status, the automatically-created data-quality issue,
|
|
||||||
and the queued automation event — canonical odometer is confirmed unchanged.
|
|
||||||
4. **Data Quality → DQ-DEMO-DUPLICATE**: the two-column CUS-0012/CUS-0178 comparison,
|
|
||||||
merge with the inline confirmation step, issue flips to `resolved`.
|
|
||||||
5. **Knowledge**: ask "What must I do when a vehicle returns with damage?" → grounded
|
|
||||||
answer citing both the return and damage-handling procedures with real excerpts.
|
|
||||||
6. **Automation**: filter to `failed`, retry the seeded delivery, watch it succeed within
|
|
||||||
a few seconds via the live n8n instance.
|
|
||||||
7. **Audit**: filter by `return_registered` or `customer_merged` to show every action from
|
|
||||||
this walkthrough is recorded with actor, timestamp, and correlation ID.
|
|
||||||
8. Resize the browser to 360px width to show the responsive layout (nav wraps, tables
|
|
||||||
become cards) — or run `make e2e` and point at the passing responsive assertion.
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -1,16 +1,33 @@
|
|||||||
FROM python:3.12-slim
|
FROM python:3.12-slim-bookworm@sha256:a116514e19457bcb7af7efe9c3dd0b9b71e85b317694e7882a1c52aa15a78134 AS runtime-base
|
||||||
|
ARG VCS_REF=development
|
||||||
|
ARG BUILD_DATE=unknown
|
||||||
|
LABEL org.opencontainers.image.title="Fleet Ops API" \
|
||||||
|
org.opencontainers.image.revision="$VCS_REF" \
|
||||||
|
org.opencontainers.image.created="$BUILD_DATE" \
|
||||||
|
org.opencontainers.image.source="https://fleetops.itworx.tech"
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
|
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY backend/requirements.lock ./
|
COPY backend/requirements-prod.lock ./
|
||||||
RUN pip install --no-cache-dir -r requirements.lock
|
RUN pip install --no-cache-dir -r requirements-prod.lock
|
||||||
COPY backend/pyproject.toml ./
|
COPY backend/pyproject.toml ./
|
||||||
COPY backend/app ./app
|
COPY backend/app ./app
|
||||||
COPY backend/alembic ./alembic
|
COPY backend/alembic ./alembic
|
||||||
COPY backend/alembic.ini ./
|
COPY backend/alembic.ini ./
|
||||||
COPY backend/tests ./tests
|
|
||||||
COPY seed ./seed
|
COPY seed ./seed
|
||||||
COPY knowledge ./knowledge
|
COPY knowledge ./knowledge
|
||||||
COPY backend/entrypoint.sh ./entrypoint.sh
|
COPY backend/entrypoint.sh ./entrypoint.sh
|
||||||
RUN pip install --no-cache-dir --no-deps -e . && chmod +x ./entrypoint.sh
|
RUN pip install --no-cache-dir --no-deps -e . && chmod +x ./entrypoint.sh \
|
||||||
|
&& addgroup --system app && adduser --system --ingroup app --home /app app \
|
||||||
|
&& chown -R app:app /app
|
||||||
|
|
||||||
|
FROM runtime-base AS test
|
||||||
|
COPY backend/requirements.lock ./requirements.lock
|
||||||
|
RUN pip install --no-cache-dir -r requirements.lock
|
||||||
|
COPY backend/tests ./tests
|
||||||
|
USER app
|
||||||
|
|
||||||
|
FROM runtime-base AS runtime
|
||||||
|
# Run migrations and the API as an unprivileged user; nothing here needs root.
|
||||||
|
USER app
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD ["./entrypoint.sh"]
|
CMD ["./entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[alembic]
|
[alembic]
|
||||||
script_location = alembic
|
script_location = alembic
|
||||||
prepend_sys_path = .
|
prepend_sys_path = .
|
||||||
version_path_separator = os
|
path_separator = os
|
||||||
|
|
||||||
[loggers]
|
[loggers]
|
||||||
keys = root,sqlalchemy,alembic
|
keys = root,sqlalchemy,alembic
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""idempotency request fingerprint
|
||||||
|
|
||||||
|
Revision ID: 0a4c1d2e3f5b
|
||||||
|
Revises: c24f6a9d013e
|
||||||
|
Create Date: 2026-08-16 22:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "0a4c1d2e3f5b"
|
||||||
|
down_revision: Union[str, None] = "c24f6a9d013e"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"idempotency_records",
|
||||||
|
sa.Column("request_fingerprint", sa.String(length=64), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("idempotency_records", "request_fingerprint")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""enforce one open issue per detected condition
|
||||||
|
|
||||||
|
Revision ID: 4f2b9c8d7e61
|
||||||
|
Revises: 0a4c1d2e3f5b
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "4f2b9c8d7e61"
|
||||||
|
down_revision = "0a4c1d2e3f5b"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_index(
|
||||||
|
"uq_data_quality_one_open_condition",
|
||||||
|
"data_quality_issues",
|
||||||
|
["rule_type", "entity_type", "entity_id"],
|
||||||
|
unique=True,
|
||||||
|
postgresql_where=sa.text("status = 'open'"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("uq_data_quality_one_open_condition", table_name="data_quality_issues")
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""outbox last_error_code
|
||||||
|
|
||||||
|
Revision ID: 799d8800e241
|
||||||
|
Revises: e7b08389f47f
|
||||||
|
Create Date: 2026-08-03 10:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '799d8800e241'
|
||||||
|
down_revision: Union[str, None] = 'e7b08389f47f'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('outbox_events', sa.Column('last_error_code', sa.String(length=60), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('outbox_events', 'last_error_code')
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""add optional external OIDC identity
|
||||||
|
|
||||||
|
Revision ID: a81d0ce9f662
|
||||||
|
Revises: f43d829ab610
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "a81d0ce9f662"
|
||||||
|
down_revision = "f43d829ab610"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("users", sa.Column("identity_provider", sa.String(80), nullable=True))
|
||||||
|
op.add_column("users", sa.Column("external_subject", sa.String(255), nullable=True))
|
||||||
|
op.create_unique_constraint(
|
||||||
|
"uq_user_external_identity", "users", ["identity_provider", "external_subject"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_constraint("uq_user_external_identity", "users", type_="unique")
|
||||||
|
op.drop_column("users", "external_subject")
|
||||||
|
op.drop_column("users", "identity_provider")
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""operational user credentials
|
||||||
|
|
||||||
|
Revision ID: b7c7b536df85
|
||||||
|
Revises: 799d8800e241
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "b7c7b536df85"
|
||||||
|
down_revision = "799d8800e241"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("users", sa.Column("email", sa.String(length=320), nullable=True))
|
||||||
|
op.add_column("users", sa.Column("password_hash", sa.String(length=512), nullable=True))
|
||||||
|
op.create_unique_constraint("uq_users_email", "users", ["email"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_constraint("uq_users_email", "users", type_="unique")
|
||||||
|
op.drop_column("users", "password_hash")
|
||||||
|
op.drop_column("users", "email")
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""add explicit customer anonymisation state
|
||||||
|
|
||||||
|
Revision ID: b913a72e8c14
|
||||||
|
Revises: a81d0ce9f662
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "b913a72e8c14"
|
||||||
|
down_revision = "a81d0ce9f662"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("customers", sa.Column("anonymized_at", sa.DateTime(timezone=True)))
|
||||||
|
op.create_index("ix_customers_anonymized_at", "customers", ["anonymized_at"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_customers_anonymized_at", table_name="customers")
|
||||||
|
op.drop_column("customers", "anonymized_at")
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""add domain constraints and operational indexes
|
||||||
|
|
||||||
|
Revision ID: c24f6a9d013e
|
||||||
|
Revises: b913a72e8c14
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "c24f6a9d013e"
|
||||||
|
down_revision = "b913a72e8c14"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
checks = (
|
||||||
|
("bookings", "ck_bookings_status", "status IN ('reserved','active','returned','cancelled','blocked')"),
|
||||||
|
("bookings", "ck_bookings_time_window", "ends_at > starts_at"),
|
||||||
|
("bookings", "ck_bookings_start_odometer", "start_odometer_km IS NULL OR start_odometer_km >= 0"),
|
||||||
|
("bookings", "ck_bookings_end_odometer", "end_odometer_km IS NULL OR end_odometer_km >= 0"),
|
||||||
|
("vehicles", "ck_vehicles_operational_status", "operational_status IN ('available','rented','cleaning','maintenance','blocked')"),
|
||||||
|
("vehicles", "ck_vehicles_model_year", "model_year BETWEEN 1900 AND 2100"),
|
||||||
|
("vehicles", "ck_vehicles_odometer", "odometer_km >= 0"),
|
||||||
|
("vehicles", "ck_vehicles_next_service", "next_service_km >= 0"),
|
||||||
|
("vehicles", "ck_vehicles_version", "version >= 1"),
|
||||||
|
("data_quality_issues", "ck_data_quality_rule_type", "rule_type IN ('possible_duplicate_customer','missing_required_field','odometer_regression','booking_overlap','vehicle_status_conflict')"),
|
||||||
|
("data_quality_issues", "ck_data_quality_severity", "severity IN ('low','medium','high')"),
|
||||||
|
("data_quality_issues", "ck_data_quality_status", "status IN ('open','deferred','resolved','rejected')"),
|
||||||
|
("outbox_events", "ck_outbox_delivery_status", "delivery_status IN ('pending','delivering','succeeded','failed')"),
|
||||||
|
("outbox_events", "ck_outbox_attempts", "attempts >= 0"),
|
||||||
|
("audit_events", "ck_audit_actor_type", "actor_type IN ('user','service','system')"),
|
||||||
|
)
|
||||||
|
for table, name, condition in checks:
|
||||||
|
op.create_check_constraint(name, table, condition)
|
||||||
|
|
||||||
|
op.create_index("ix_bookings_vehicle_status_window", "bookings", ["vehicle_id", "status", "starts_at", "ends_at"])
|
||||||
|
op.create_index("ix_data_quality_work_queue", "data_quality_issues", ["status", "due_at", "severity"])
|
||||||
|
op.create_index("ix_outbox_delivery_next_attempt", "outbox_events", ["delivery_status", "next_attempt_at"])
|
||||||
|
op.create_index("ix_audit_action_occurred", "audit_events", ["action", "occurred_at"])
|
||||||
|
op.create_index("ix_audit_entity", "audit_events", ["entity_type", "entity_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_audit_entity", table_name="audit_events")
|
||||||
|
op.drop_index("ix_audit_action_occurred", table_name="audit_events")
|
||||||
|
op.drop_index("ix_outbox_delivery_next_attempt", table_name="outbox_events")
|
||||||
|
op.drop_index("ix_data_quality_work_queue", table_name="data_quality_issues")
|
||||||
|
op.drop_index("ix_bookings_vehicle_status_window", table_name="bookings")
|
||||||
|
for table, name in (
|
||||||
|
("audit_events", "ck_audit_actor_type"),
|
||||||
|
("outbox_events", "ck_outbox_attempts"),
|
||||||
|
("outbox_events", "ck_outbox_delivery_status"),
|
||||||
|
("data_quality_issues", "ck_data_quality_status"),
|
||||||
|
("data_quality_issues", "ck_data_quality_severity"),
|
||||||
|
("data_quality_issues", "ck_data_quality_rule_type"),
|
||||||
|
("vehicles", "ck_vehicles_version"),
|
||||||
|
("vehicles", "ck_vehicles_next_service"),
|
||||||
|
("vehicles", "ck_vehicles_odometer"),
|
||||||
|
("vehicles", "ck_vehicles_model_year"),
|
||||||
|
("vehicles", "ck_vehicles_operational_status"),
|
||||||
|
("bookings", "ck_bookings_end_odometer"),
|
||||||
|
("bookings", "ck_bookings_start_odometer"),
|
||||||
|
("bookings", "ck_bookings_time_window"),
|
||||||
|
("bookings", "ck_bookings_status"),
|
||||||
|
):
|
||||||
|
op.drop_constraint(name, table, type_="check")
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""persist revoked sessions
|
||||||
|
|
||||||
|
Revision ID: d1f83bc64170
|
||||||
|
Revises: b7c7b536df85
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "d1f83bc64170"
|
||||||
|
down_revision = "b7c7b536df85"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"revoked_sessions",
|
||||||
|
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
||||||
|
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("id", sa.Uuid(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"updated_at",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_revoked_sessions_expires_at", "revoked_sessions", ["expires_at"])
|
||||||
|
op.create_index(
|
||||||
|
"ix_revoked_sessions_token_hash",
|
||||||
|
"revoked_sessions",
|
||||||
|
["token_hash"],
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_revoked_sessions_token_hash", table_name="revoked_sessions")
|
||||||
|
op.drop_index("ix_revoked_sessions_expires_at", table_name="revoked_sessions")
|
||||||
|
op.drop_table("revoked_sessions")
|
||||||