MCP_HUB_BASE_URL had the same wrong-hostname bug as RAGCORE_BASE_URL earlier
this session (itworx-mcp-hub:8000 doesn't resolve; the real container is
reachable at the host's own 192.168.10.150:1100) -- fixed live, resolving the
Automation page showing "Operationeel" and "Hub Onbereikbaar" simultaneously.
Went on to actually publish the "Fleet Ops -- RAGcore Procedure Sync" n8n
workflow now that RAGcore is reachable: its own RAGcore Sync Token credential
had gone stale from the same rotation as the earlier one, so minted a fresh,
dedicated, minimally-scoped (sources:sync only) credential, verified a real
manual run (33 synced, 0 failed, result registered) before publishing.
That exposed a real, now-stale bug: derive_n8n_status() hardcoded this
workflow's evidence to None with a comment explaining it was unpublished --
true when written, false now. The workflow's own result-report callback
already writes a real n8n_procedures_synced audit event; wired that in as its
evidence source, the same pattern the scheduled scan and error handler already
use, instead of a value that could never update itself once the workflow went
live.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RAGcore's /v1/answers (generation + citation validation) is currently returning
a consistent 503 VALIDATION_RETRIES_EXHAUSTED live -- a RAGcore-side bug in its
own generation/validation step, out of scope to fix here (CLAUDE.md forbids
modifying the RAGcore repo). Its retrieval pipeline (/v1/search) is a materially
different, simpler stage with no generation step, and returns real, correctly
cited results right now.
RAGcoreKnowledgeProvider.ask() tries /v1/answers first (unchanged behavior once
RAGcore's generation is fixed), and only when that endpoint itself is
unavailable -- non-2xx or unreachable, never a real 200 classifying the
question as insufficiently answerable -- falls back to /v1/search and builds
the shown "answer" as an extractive citation-wrapped excerpt, mirroring
DemoKnowledgeProvider's own existing template exactly. Never invents an answer
to the question; only ever shows a real, cited excerpt RAGcore's own search
actually found.
Also fixed two real config bugs found while wiring this up live: RAGCORE_BASE_URL
pointed at a non-existent internal hostname (ragcore-api:8000 -- the real
container is reachable at the host's own address on port 1237), and the
previous test credential had been invalidated with nothing to replace it. Minted
a fresh, correctly-scoped service-account credential via RAGcore's own admin
control plane (the documented, legitimate way to obtain one).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- .form-grid labels (missing-field form, odometer-regression correction fields)
and the odometer/overlap note textareas had no stacked label-above-input
styling at all -- the shared rule only covered .filters/.return-form, so these
fell back to default inline browser layout with mismatched input widths.
Extended the existing rule to cover .form-grid and label:has(> textarea).
- Vehicle maintenance list showed the raw, untranslated seed text
("Synthetic scheduled service record") regardless of locale -- purely
decorative and 1:1 redundant with the (already-translated) category. Replaced
it with the record's real odometer reading, mirroring the sibling
Inspections tab's pattern.
- "Today's movements" was always the same fixed 4 bookings (2 returns, 2
departures) on every reset, reading as a static mockup rather than live
fleet activity. Added 8 more bookings anchored to land on "today" across 8
additional vehicles, spread through the day.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three content defects found by a live reviewer:
- Dashboard attention subtext was raw, untranslated evidence.summary text, and for
11 of 15 seeded issues that text was literally "Synthetic deterministic seed
issue". AttentionItem now exposes evidence_signals (stable code + params, same
shape as the issue detail page) instead of a detail string; the frontend renders
them through a shared describeEvidenceSignal() used by both the dashboard and the
issue detail page. Every previously-placeholder seed row now cites a real,
per-rule-type fact (a genuinely crossed service threshold, a genuinely blank
field, or a real pair of booking odometer readings) instead of invented prose.
- 5 of 7 blocked vehicles had no quality issue at all and one had only a resolved
one, so "needs attention" led nowhere. Each now has a real open
missing_required_field issue backed by a genuinely blank field (no schema change,
no migration -- reuses the existing data-quality pipeline).
- Booking odometer fields showing a bare "-" for 25 reserved + 1 active booking now
show a localized explanation ("trip hasn't started yet" / "not yet closed").
MO-024's rented-but-service-overdue contradiction was already caught by the
vehicle-status evaluator (DQ-SCAN, vehicle.manual_review_required) -- added a
regression test rather than new logic.
Also fixed a related bug the above exposed: the vehicle entity_snapshot omitted
registration_number entirely, so the "provide missing fields" form always showed
it blank regardless of the real value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The demo seed plants exactly one failed delivery (BK-H-0020) to demonstrate
retry and audit. Because derive_n8n_status() counted any failure, every fresh
reset pinned the n8n integration to "degraded" -- the demo showed a warning
about a prop, which tells a viewer something untrue about the automation.
The seeded failure now carries its own error code, demoScenarioTimeout, rather
than the generic connectionError a real timeout produces. No column and no
migration: last_error_code already existed, is already surfaced to the UI and is
already localizable.
- integration status splits failed into unexpected_failed and
demo_scenario_failed; only unexpected failures may move the state. A staged
failure alone leaves n8n operational.
- latest_failure_at is a health signal and now ignores the staged failure;
latest_demo_scenario_at reports it separately.
- /api/v1/workflows exposes is_demo_scenario. The Automation page labels the run
as a prepared demo scenario, explains that it is a simulated temporary failure
that does not affect automation health, and offers a distinct "retry demo
scenario" action. Translated in nl-BE, en-GB and fr-BE.
- the carve-out stays narrow: a real failure still degrades n8n, and a genuine
later failure of the same event overwrites the demo code with the real one.
- the retry itself is unchanged and real: the event goes back on the outbox and
the dispatcher delivers it to n8n like any other, so 19+1 becomes 20+0 only on
an actual round trip. The audit records which kind of failure was retried.
Tests that assert on the seeded scenario now reseed first, since earlier test
files legitimately mutate the outbox and the suite shares one database.
Verified locally against a real PostgreSQL 16: 181 passed, ruff clean, mypy
clean (50 files), tsc clean, frontend build clean. Not deployed and not
browser-verified.
Fixed two concrete gaps in the MCP knowledge-search endpoint: no locale field
existed at all (now nl-BE/en-GB/fr-BE, wired to the knowledge provider's
existing language param), and the correlation ID was always freshly minted,
ignoring any inbound X-Correlation-Id header. Added a shared dependency and
applied it to all four MCP endpoints so Fleet Ops's own audit log preserves
the Hub's real correlation ID end to end.
MCP_HUB_BASE_URL/MCP_PROVIDER_ID were declared in .env.example but never read
anywhere. Since the Hub's own registration is catalog-driven (it never needs
Fleet Ops to push a registration call), wired mcp_hub_base_url for a real Hub
reachability health check instead of an unneeded self-registration call.
Renamed Fleet Ops's own internal audit tool labels mobilityops_* -> fleet_ops_*
(mirrored in contracts/mcp-tools.json with mobilityops_* kept as deprecated
aliases); documented that the live Hub connector's own dotted tool namespace
is a separate, Hub-owned naming layer, deliberately not touched.
Automation page's MCP card now shows real evidence (last tool/client/count/
timestamp, honest no-evidence state) instead of just the registration flag.
Also fixed a real methodology gap found mid-session: compose.yaml's api
service has no bind mount, so `docker compose run --rm api` silently tests a
stale image until rebuilt. Re-ran every local gate after rebuilding; fixed one
genuinely stale test assertion and two lint line-length errors surfaced by
that rebuild. 176 tests passing, ruff clean, mypy clean (50 files).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GUI: dashboard Attention Queue presents a curated severity mix instead of pure
severity-sort (grouped Now/Today/Later headers); Today's Movements seed data
curated so a fresh reset shows a credible day (2+ departures, 2+ returns), with
a new seed-integrity test; About Demo restructured into a compact grid with
progressive disclosure for technical sections; Duplicate Merge shows match/conflict
counts, hides matching fields by default, and previews the final merged record
before confirmation.
Repo hygiene: removed a stray empty `backend;C` directory and an untracked 31MB
zip export; `.gitignore` now excludes future archive exports.
n8n: fixed invalid JSON (a missing `},` between two node objects) in the committed
`fleet-ops-vehicle-return.json` -- the file could not be parsed. Live-validated
workflow 3 (RAGcore Procedure Sync): found and fixed a real defect (three body
parameters had a stray trailing `}}`) and a missing Error Workflow wiring, both
via the safe `n8n import:workflow` CLI path; exported the corrected, still-
inactive workflow as the new source of truth and updated MANIFEST.md/check_drift.py.
Publishing it (starts real daily unattended runs) remains a separate decision.
RAGcore: root-caused and fixed (live, approved) the "zero retrieval candidates"
bug -- a filesystem permission bug (`embedding_profiles.json` unreadable by the
app's own runtime user) that broke every retrieval call before it reached Qdrant.
Every other suspect (grants, scope resolution, Qdrant filters, embeddings) was
verified healthy first. Found a second, deeper gap: the reranker adapter calls
an Ollama HTTP route that does not exist on the deployed Ollama version, so
`/v1/answers` still returns `not_answerable`. `KNOWLEDGE_PROVIDER` stays `demo`
until that is resolved on the RAGcore side. Evidence-based MCP Hub integration
status (real tool-call audit history, not just a boolean flag) replaces the old
`configured`/`not_configured` guess. Full findings in
`docs/final-integrations/current-state-audit.md`.
Backend: 172 tests passing, ruff clean, mypy clean (50 files). Frontend: tsc
clean, production build clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous adapter targeted an endpoint shape RAGcore never actually
exposed. health() now checks /health/ready and ask() posts to the real
POST /v1/answers with Bearer auth and requested_space_ids, matching
RAGcore's actual contract after this session's Bearer-auth and
search/answer wiring work.
Adds RAGCORE_SPACE_ID config/env plumbing (a question is meaningless
without a knowledge space to scope it to) and 12 new adapter tests
covering degradation paths: missing space id, connection errors,
non-200 responses, malformed responses, not-answerable, and
answerable-without-citations all fail closed to "insufficient
evidence" rather than fabricating an answer.
KNOWLEDGE_PROVIDER stays "demo" in production for now -- switching
requires RAGcore's own search/answer application to actually be
deployed and live-verified, tracked separately in PROJECT_STATE.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /api/v1/integrations/n8n/procedures lists every procedure Markdown
file Fleet Ops ships (all languages) with a stable per-document id and
content hash, ready for workflow 3 to push into RAGcore. POST
.../procedures-sync-result records the sync outcome as an idempotent
audit event, matching the existing return-callback/workflow-error
pattern. Extracted frontmatter parsing out of the demo knowledge
provider into a shared module so both read the same source of truth.
Fleet Ops integration status no longer depends only on a config
boolean or the most recent outbox event: N8nIntegrationStatus now
reports per-canonical-workflow evidence (last successful outbox
delivery for the return workflow, latest service-triggered
data_quality_scan_run for the scan workflow, latest
n8n_workflow_failure_registered for the error handler, and "not built"
for the still-blocked RAGcore sync), plus an error-handler summary
(total failures registered, latest failure + which workflow).
Automation page renders this as a localized workflow table (EN/NL/FR)
with technical workflow names tucked under a "Technical details"
disclosure, matching the existing progressive-disclosure pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New POST /api/v1/integrations/n8n/workflow-error, service-token
authenticated, for the central "Fleet Ops — Workflow Error Handler"
n8n workflow to report a bounded, secret-free failure (workflow id/
name, execution id, safe error category, trigger context, correlation
id, attempt, retry action). Idempotent on execution_id via the same
audit-event precheck pattern used by /return-callback, so a
redelivered error report is not registered twice.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Inspected the shared n8n instance (n8n.itworx.tech) live: both existing
Fleet Ops workflows are genuinely active and structurally match the repo,
but the shared X-Service-Token secret was stored as plaintext literal
text in both HTTP Request nodes (exportable in the clear), and the
production return webhook had n8n-level Authentication set to "None"
(publicly callable by anyone who discovered the URL). Findings recorded
in docs/live-ai-integration/n8n-current-state.md.
Fixed on the n8n side (both workflows published): the shared token now
lives in a single Header Auth credential instead of two literal copies;
the return webhook now requires a second, distinct Header Auth
credential.
Fixed on the Fleet Ops side to match: the outbox dispatcher now sends
the new X-Fleet-Ops-Trigger-Token header (new
MOBILITYOPS_WEBHOOK_TRIGGER_TOKEN setting) when calling the webhook.
Live-verified against the real webhook: a request with no header is now
rejected (403); a request with the correct header passes n8n's auth and
reaches Fleet Ops's own business logic.
That same live test also surfaced a real robustness gap: an n8n
execution that errors before its "Respond to Webhook" node runs can
still answer with a 2xx status and an empty body, which made
response.json() raise an uncaught exception, potentially leaving the
outbox event stuck in "delivering". Now treated as an explicit,
retryable failure (error_code=malformedResponse), with a regression
test reproducing the exact case.
New frontend/e2e/fleet-ops-correction.spec.ts covers section 12 of the brief:
branding (Fleet Ops visible, no MobilityOps/PoC leaks, in all 3 languages), the
language switcher persisting across reload, the full status-recommendation flow
(non-mutating preview, exact-status confirm button, manual review with no apply
button, stale-token rejection), MO-016 order independence at the browser level, the
knowledge base grounding the exact brief question in its own language, and localized
audit/automation content with raw codes only under "Technical details".
Writing these tests surfaced two real bugs:
- DataQualityIssueDetail.tsx conflated "no conflict" with "manual review required"
because both carry safe_to_apply: false (a no_conflict recommendation has nothing to
apply, so it's trivially "not safe to apply" without being unsafe). This showed a
false "manual review required" panel for MO-016 after its overlap was resolved,
instead of the correct "no change needed" state. Fixed by keying the branch on
manual_review_required alone.
- test_mo_016_status_conflict_recommendation_is_order_independent never actually
exercised MO-016: _first_open() returned whichever vehicle_status_conflict issue was
most recently detected (there are ~14 open after a reset), not necessarily
DQ-DEMO-STATUS, so the test's MO-016 assertions were trivially true regardless of
what the code under test did. Added _first_open_for_vehicle() and rewrote the test
to explicitly target MO-016, and to assert the behaviour order independence actually
requires: resolving the overlap first must correctly leave nothing to apply (the
vehicle already matches the facts), not literally the same end status as resolving
the conflict first.
151 backend tests, Ruff, mypy green; full 108-test Playwright suite green (two
transient, non-reproducible flakes confirmed to pass in isolation and unrelated to
this change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fix the demo knowledge provider's tokenizer: a plain [a-z0-9]+ regex silently
dropped accented characters, splitting French words like "véhicule" into "v" +
"hicule" and mangling retrieval for nearly every French query. Now matches the
Latin-1 accented range too.
- Reweight section scoring so the body match (the actual substance of a section)
outranks a heading/title match (a shallow structural hint) rather than the reverse
-- confirmed via the brief's exact validation question that the old weighting
misranked the damage procedure behind a topically-adjacent document in all three
languages (nl-BE: a checkout section; en-GB/fr-BE: the return procedure), purely
because a generic word like "vehicle"/"voertuig" happened to sit in a heading/title.
- Remove leftover "MobilityOps" and "PoC" mentions from 5 English and 4 NL/FR
procedure documents -- knowledge-base prose is visible UI content and was missed by
the earlier rebrand.
- Add regression tests: the brief's exact NL/EN/FR damage question must ground on the
damage procedure as the *primary* source (not just appear in the top 3), and no
procedure file may contain "MobilityOps" or "PoC".
151 backend tests, Ruff, mypy green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add a single shared, pure vehicle-status evaluator (app/services/vehicle_status.py)
used identically by the data-quality scanner, a new non-mutating status-recommendation
preview endpoint, and a transactional apply endpoint with optimistic-concurrency token
revalidation -- eliminates the old opaque "calculate and apply" action and the unsafe
"maintenance + active booking -> auto rented" shortcut. Frontend
DataQualityIssueDetail.tsx now shows a review/decide/confirm panel with localized
why/evidence/consequence text in nl-BE/en-GB/fr-BE, with an exact "Change status to
<status>" confirm action per the brief.
- Fix MO-016 issue-order dependency: resolving the booking-overlap issue before vs.
after the status-conflict issue now converges on the same final vehicle status,
proven by test_mo_016_status_conflict_recommendation_is_order_independent.
- Make "Fleet Ops" a non-localizable brand constant (frontend/src/product.ts,
backend PRODUCT_NAME) via {{productName}} interpolation everywhere the brand name
appeared in locale prose; add a permanent test guarding against a translation file
ever defining the brand name or an "appName" key again.
- Convert dynamic backend prose to stable message codes + params: return status
reasons, audit field/actor-type labels, automation last_error, and search
section/vehicle/booking/issue results all now carry codes the frontend localizes,
with raw technical text demoted to a "Technical details" disclosure.
- docs/fleet-ops-correction/: gap audit, i18n inventory, and the vehicle-status
decision table documenting the evaluator's rules and safe-status principles.
148 backend tests + Ruff + mypy green; Alembic migration verified upgrade/downgrade;
frontend tsc/build and the i18n-coverage Playwright suite green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rebrands the product from MobilityOps to Fleet Ops across the UI, backend defaults and
knowledge base, and makes nl-BE/en-GB/fr-BE full first-class languages: i18next with
eager-bundled per-namespace resources, a persisted accessible language switcher (topbar
and mobile drawer), locale-aware date/number formatting, and a coverage test that fails
the build on any missing or empty translation key.
Backend dynamic content (demo scenarios, blocked-reason text, integration status) moves
from fixed English/Dutch prose to stable message codes + params so the frontend can
localize it; the demo knowledge base gains a fully translated NL/EN/FR procedure corpus
(11 documents each) with per-language retrieval and localized evidence-state messages.
The Demo Guide becomes breakpoint-adaptive: a docked rail on extra-wide desktop, a
floating panel that auto-collapses to a persistent, closable progress chip on standard
desktop/tablet, and a collapsed/half/full bottom sheet on mobile -- with scroll+focus+
highlight on "go to this step", Escape handling, and reduced-motion support.
The Data Quality Workbench gets accessible choice-card decisions with a clear primary/
secondary/tertiary action hierarchy; the Automation ledger groups repeated successes and
uses meaningful short refs; the Audit trail groups events by correlation id with human
action labels and readable before/after diffs. Attention Queue, Today's movements,
Vehicles, Bookings and Data Quality rows are fully clickable (stretched-link pattern)
with independent secondary links, keyboard support and mobile touch targets.
Fixes a topbar overflow on mobile caused by the new language switcher (moved into the
mobile drawer at <=960px) and two dangling aria-labelledby references introduced this
session. Updates all affected Playwright specs for the new nl-BE default and the new
Audit/DemoGuide DOM structure, and adds new i18n-coverage, demo-guide-adaptive and
clickable-rows specs. 131 backend tests, Ruff and mypy, and 71 Playwright tests pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Integration status badges across Dashboard/Automation now show honest
plain-language labels instead of raw backend state strings (and fix a
few states that had no matching CSS colour class at all). Audit trail
gets a "view related events" action reusing the existing correlation_id
filter. About page gains scope/architecture/security/testing sections
and a guided-demo entry point. POST /api/v1/demo/reset now runs and
records a server-side scenario-integrity check. Also fixes a second real
race condition (caught by the return-review e2e test): the odometer
scenario pre-fill now resolves before ReturnForm mounts instead of
patching its value in after the fact.
Adds GET /api/v1/demo/manifest as a single source of truth for the demo's
fictional org identity (Northstar Mobility -- surfacing the project's
already-locked tenant name), synthetic-data/reset state, and live scenario
readiness. Rewrites the login screen in Dutch with an honest, no-password
demo entry and a guided-demo entry point, replaces the loud full-width
demo banner with a subtle badge + popover, and adds a compact About page
explaining what's real vs. synthetic vs. not yet connected.
Booking/inspection/maintenance/outbox dates were authored as absolute
timestamps around a fixed 2026-08-01 anchor and never re-anchored at
seed/reset time, so demo scenarios (e.g. BK-DEMO-RETURN) silently drifted
into the past. Every reset now shifts seeded dates by (today - authored
anchor); dashboard's "today" filter uses real wall-clock time instead of
the now-removed frozen demo_today setting. Adds seed-validation tests
proving scenarios S1/S2/S4/S5 are present and internally consistent after
every reset.
The original docs described two n8n workflows but the repository only ever
shipped one (return-processing); the sketched second workflow (knowledge
sync) depends on RAGcore, which isn't connected here, so it stays deferred.
Add POST /api/v1/integrations/n8n/scheduled-scan (X-Service-Token
protected, same pattern as the return callback), calling the same
run_scan() the manual "Run quality scan" UI action uses and recording a
service-actor data_quality_scan_run audit event. run_scan() already only
creates an issue for a condition without one open, so overlapping triggers
do no duplicate domain work.
n8n/mobilityops-scheduled-quality-scan.json (hourly schedule + manual test
trigger, both feeding the same HTTP call) ships "active": false so it can't
fire anywhere until deliberately published. Verified live against the
local n8n instance via the Manual test trigger: full green execution, and
the resulting data_quality_scan_run audit event (actor_type=service,
actor_label="n8n scheduled scan") confirms the real round trip, not just a
contract test. deploy/unraid/setup-scheduled-scan.sh mirrors the existing
return-workflow publish script for the shared Unraid n8n.