Closes out the demo-scenario fix: pushed the two pending commits, deployed
6f77a30 to Unraid, and verified all four live checks (integration status,
failed-workflow listing, retry via API and UI, audit trail, Automation page).
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.
The demo manifest still reported the MCP Hub integration as operational purely
because MCP_HUB_REGISTRATION_ENABLED was set, while the integration status page
had already moved to evidence-based status in Batch 4. Registration is
catalog-driven on the Hub's side, so the flag alone proves nothing; reuse
derive_mcp_hub_status() so "operational" requires real recorded mcp_tool_request
calls.
No change to the MCP integration contract: the four read-only routes, service
token and client id handling, inbound X-Correlation-Id preservation, the locale
field on search-knowledge and the provider/correlation_id response fields were
verified as already correct at deployed revision 727c19a and left untouched.
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>
Pushed feat/live-n8n-ragcore-integration to origin, then redeployed
the live Fleet Ops instance from 0da5251 to 0571a40 following the
deployment directory's own established source-archive convention.
Verified live: /health OK, the new n8n procedures endpoint (added
this branch) is reachable and correctly auth-gated, KNOWLEDGE_PROVIDER
still demo as intended.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause found via the live n8n container's own logs: N8N_PROXY_HOPS=0
in the Unraid template didn't match the real reverse-proxy in front of
it, breaking the browserId/CSRF check on every workflow save while
leaving the UI looking fully signed in -- the user's pushback that it
"shows logged in" was correct and prompted digging into server logs
instead of continuing to guess client-side.
Fixed by editing the Unraid template (N8N_PROXY_HOPS 0->1, backed up
first) and recreating the container with every other setting preserved
exactly. Verified by reproducing the exact save action that used to
fail; it now works, and node persistence survives a full reload.
Built and saved both remaining workflow-3 nodes (Summarize sync
result, Report sync result to Fleet Ops) after recovering from an
errant Ctrl+A that deleted a node mid-verification (caught via node
count, restored via n8n's own Version History, redone carefully). Not
published -- that starts real daily production runs and is left for a
separate decision.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Issued a fresh scoped credential via the RAGcore admin UI (separate,
working OIDC session, unaffected by the n8n auth problem) and made a
real authenticated /v1/answers call. Got a clean 200 with a real
answer_id/retrieval_run_id, not degraded -- but not_answerable, 0
citations.
Confirmed this isn't a regression: the same query through RAGcore's
own pre-existing Query Lab tool (untouched this session) returns the
identical result down to zero dense/sparse candidates at the raw
retrieval stage. Ruled out the obvious causes via direct Qdrant/
Postgres checks -- workspace_id, space_id, status, and embedding
digest all correctly match real indexed content. Root cause not yet
found; flagged as a follow-up rather than pursued further to avoid
scope creep on what was a deployment-verification task.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deployed a2905cc to the live RAGcore instance (approved). Verified
the fix directly: POST /v1/search now returns 401
AUTHENTICATION_REQUIRED instead of the old permanent 503
SEARCH_UNAVAILABLE, proving the endpoint reaches real request
handling. A full authenticated /v1/answers call with a real grounded
answer is still outstanding -- the previously-issued production
credential's raw token was never persisted anywhere retrievable, and
issuing a fresh one needs the RAGcore admin UI, not attempted this
round.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The live "Fleet Ops -- RAGcore Procedure Sync" workflow's canvas was
found at zero nodes -- the earlier session's abandoned direct n8n
REST API attempt had gone far enough to wipe it before hitting its
401. Recovered via n8n's own Version History "Restore version"
action back to the last good 4-node save; verified via DOM node
count before and after.
Adding the two remaining nodes then hit the same failure mode: n8n's
own first-party autosave reported "Unauthorized" moments after a
fresh, successful interactive sign-in. Stopped deliberately rather
than retrying against a live instance that already caused one data
loss incident this session -- this looks like an n8n-side session/auth
problem, not something fixable from browser automation.
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>
With explicit owner approval, traced the persistent credential-issuance
rejection to a cross-transaction race in RAGcore's own dependency
injection (two independent DB transactions per request instead of one
shared transaction), fixed and deployed it in RAGcore, and verified a
working "RAGcore Sync Token" n8n credential now exists. Unblocks
workflow 3 and the RAGcoreKnowledgeProvider adapter rewrite.
User explicitly authorized issuing the RAGcore credential directly this
round. Retried via the admin UI (Platform Admin role) after the earlier
raw-API attempt; both fail with an opaque server-side rejection carrying
a trace ID. Documents this as a RAGcore-side blocker, not a Fleet Ops gap.