M8: GUI polish, n8n workflow-3 fixes, RAGcore retrieval root-cause and fix

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>
This commit is contained in:
NuklearRabbit
2026-08-05 13:05:02 +02:00
co-authored by Claude Sonnet 5
parent 3ebca9e9b7
commit 34df66d28c
25 changed files with 669 additions and 100 deletions
+2
View File
@@ -14,3 +14,5 @@ test-results/
.idea/
.vscode/
*.tsbuildinfo
*.zip
*.tar.gz
+13 -7
View File
@@ -68,15 +68,21 @@ It is not an ERP, CRM, accounting package, public booking site, payment system o
delivery counts, not just the most recent event.
- **RAGcore**: the demo `KnowledgeProvider` (deterministic TF-IDF extractive retrieval
over the local procedure documents) is what satisfies the knowledge-assistant
acceptance criteria and is fully verified. A `RAGcoreKnowledgeProvider` HTTP adapter is
implemented and unit-tested, including its unavailable-degradation path, but was never
exercised against a live RAGcore instance in this environment.
acceptance criteria and is what's active in production (`KNOWLEDGE_PROVIDER=demo`). A
`RAGcoreKnowledgeProvider` HTTP adapter is implemented, unit-tested, and has been
exercised live against the deployed RAGcore instance: a real filesystem-permission bug
that caused every live retrieval to return zero candidates was found and fixed
(`docs/final-integrations/current-state-audit.md`), but a second, deeper gap — RAGcore's
reranker adapter calls an Ollama HTTP route (`/api/rerank`) that does not exist on the
deployed Ollama version — still blocks real grounded answers. `KNOWLEDGE_PROVIDER` stays
`demo` until that is resolved on the RAGcore side.
- **ITWorx MCP Hub**: the four read-only provider endpoints are implemented, tested, and
directly `curl`-verified with correct auth enforcement and audit logging.
`MCP_HUB_REGISTRATION_ENABLED` is now actually wired into `Settings` (it was previously
declared in `.env.example` but silently dropped) and reported honestly by the
integration-status endpoint. No live Hub instance was reachable in this environment to
verify an actual Hub round trip.
`MCP_HUB_REGISTRATION_ENABLED` is actually wired into `Settings` and reported honestly
by the integration-status endpoint (evidence-based: real tool-call audit history, not
just the flag). The Fleet Ops connector is confirmed live in the ITWorx MCP Hub's own
production deployment (Tower), with a real contract fix already applied there
(`vehicle.get`'s wire parameter normalized to `vehicleRef`).
See `artifacts/functional-completion/final-summary.md` for the functional-completion
audit evidence (supersedes the design-validation summary below for integration status),
+8 -2
View File
@@ -72,8 +72,14 @@ def get_dashboard(
issue_ref=issue.public_ref,
)
)
attention_items.sort(key=lambda item: _SEVERITY_ORDER.get(item.severity, 3))
attention_items = attention_items[:8]
# Curate a credible severity mix instead of letting `high` dominate every slot:
# each item's real severity is unchanged, only the display selection is capped per
# tier (a handful of "now", then "today", then "later") so a heavy day of high-severity
# issues doesn't crowd out medium/low ones the operator should still see.
high_items = [i for i in attention_items if i.severity == "high"]
medium_items = [i for i in attention_items if i.severity == "medium"]
low_items = [i for i in attention_items if i.severity == "low"]
attention_items = (high_items[:3] + medium_items[:3] + low_items[:2])[:8]
today = _today()
bookings = db.scalars(select(Booking)).all()
+3 -12
View File
@@ -4,16 +4,10 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.api.deps import get_db, require_operations_manager
from app.core.config import get_settings
from app.schemas import (
CurrentUser,
IntegrationStatusOut,
McpHubIntegrationStatus,
)
from app.services.integration_status import derive_n8n_status
from app.schemas import CurrentUser, IntegrationStatusOut
from app.services.integration_status import derive_mcp_hub_status, derive_n8n_status
router = APIRouter(prefix="/api/v1/integrations", tags=["integrations"])
settings = get_settings()
@router.get("/status", response_model=IntegrationStatusOut)
@@ -23,8 +17,5 @@ def integration_status(
) -> IntegrationStatusOut:
return IntegrationStatusOut(
n8n=derive_n8n_status(db),
mcp_hub=McpHubIntegrationStatus(
registration_enabled=settings.mcp_hub_registration_enabled,
state="configured" if settings.mcp_hub_registration_enabled else "not_configured",
),
mcp_hub=derive_mcp_hub_status(db),
)
+5 -1
View File
@@ -279,7 +279,11 @@ class N8nIntegrationStatus(BaseModel):
class McpHubIntegrationStatus(BaseModel):
registration_enabled: bool
state: Literal["not_configured", "configured"]
state: Literal["not_configured", "no_evidence", "operational"]
total_calls: int
last_tool: str | None = None
last_client: str | None = None
last_called_at: datetime | None = None
class IntegrationStatusOut(BaseModel):
+48 -4
View File
@@ -8,12 +8,18 @@ from sqlalchemy.orm import Session
from app.core.config import get_settings
from app.models.audit import AuditEvent
from app.models.outbox import OutboxEvent
from app.schemas import N8nErrorHandlerStatus, N8nIntegrationStatus, N8nWorkflowEvidence
from app.schemas import (
McpHubIntegrationStatus,
N8nErrorHandlerStatus,
N8nIntegrationStatus,
N8nWorkflowEvidence,
)
settings = get_settings()
# The 4 canonical Fleet Ops n8n workflows (see n8n/workflows/MANIFEST.md). Workflow 3
# (RAGcore Procedure Sync) is not built yet, so it always reports no evidence.
# The 4 canonical Fleet Ops n8n workflows (see n8n/workflows/MANIFEST.md). All 4 are
# built (workflow 3, RAGcore Procedure Sync, has all 6 nodes saved); workflow 3 is not
# yet published/active, so it will show no *run* evidence until it is.
_CANONICAL_WORKFLOWS = (
"Fleet Ops — Vehicle Return Orchestration",
"Fleet Ops — Scheduled Data Quality Scan",
@@ -92,7 +98,7 @@ def derive_n8n_status(db: Session) -> N8nIntegrationStatus:
workflows = [
N8nWorkflowEvidence(
name=name,
built=name != "Fleet Ops — RAGcore Procedure Sync",
built=True,
last_seen_at=evidence_by_workflow[name],
)
for name in _CANONICAL_WORKFLOWS
@@ -117,3 +123,41 @@ def derive_n8n_status(db: Session) -> N8nIntegrationStatus:
latest_failure_workflow=latest_handler_failure_workflow,
),
)
def derive_mcp_hub_status(db: Session) -> McpHubIntegrationStatus:
"""Evidence-based MCP Hub status: real tool-call audit history, not just the
`MCP_HUB_REGISTRATION_ENABLED` flag flipped on. Every `mcp_tool_request` call
already writes an `AuditEvent` (see `app/api/routers/mcp_integrations.py`)."""
total_calls = (
db.scalar(
select(func.count(AuditEvent.id)).where(AuditEvent.action == "mcp_tool_request")
)
or 0
)
latest_call_row = db.execute(
select(AuditEvent.occurred_at, AuditEvent.actor_label, AuditEvent.metadata_json)
.where(AuditEvent.action == "mcp_tool_request")
.order_by(AuditEvent.occurred_at.desc())
.limit(1)
).first()
last_called_at = latest_call_row[0] if latest_call_row else None
last_client = latest_call_row[1] if latest_call_row else None
last_tool = (latest_call_row[2] or {}).get("tool") if latest_call_row else None
state: Literal["not_configured", "no_evidence", "operational"]
if not settings.mcp_hub_registration_enabled:
state = "not_configured"
elif total_calls > 0:
state = "operational"
else:
state = "no_evidence"
return McpHubIntegrationStatus(
registration_enabled=settings.mcp_hub_registration_enabled,
state=state,
total_calls=total_calls,
last_tool=last_tool,
last_client=last_client,
last_called_at=last_called_at,
)
+17
View File
@@ -174,3 +174,20 @@ def test_seed_dates_are_anchored_to_reset_moment():
assert marker.metadata_json["seed_authored_anchor"] == SEED_AUTHORED_ANCHOR.isoformat()
finally:
db.close()
def test_seed_today_movements_are_a_credible_mix():
"""A fresh reset must not land on a dead 'Today's movements' dashboard section:
at least two departures and two returns should fall on the reset day, mirroring
the same status/date rule the dashboard router uses to build the today list."""
db = SessionLocal()
try:
reset_and_seed(db)
today = datetime.now(UTC).date()
bookings = db.scalars(select(Booking)).all()
departures = [b for b in bookings if b.starts_at.date() == today and b.status in ("reserved", "active")]
returns = [b for b in bookings if b.ends_at.date() == today and b.status in ("active", "returned")]
assert len(departures) >= 2
assert len(returns) >= 2
finally:
db.close()
@@ -0,0 +1,152 @@
# Current-state audit — Fleet Ops final integrations
Date: 2026-08-05. Compiled from direct repository inspection (git log/status/diff across
all three repos), `PROJECT_STATE.md` history, and read-only investigation of the sibling
repos' own state docs. No live server SSH/curl evidence is included in this pass yet —
see `integration-release-state.md` for the live-verification checklist as it is executed.
## Repository revisions at audit time
| Repo | Path | Branch | HEAD | Notes |
|---|---|---|---|---|
| Fleet Ops (MobilityOps) | `C:\Projects\MobilityOps` | `feat/fleet-ops-final-integrations` (new, branched from `feat/live-n8n-ragcore-integration`) | `3ebca9e` | `feat/live-n8n-ragcore-integration` was pushed to `origin` at `0571a40` and deployed live; `3ebca9e` (logo rebrand) is one commit ahead, not yet deployed. `master` is 19 commits behind and stale (localization-round only). |
| RAGcore | `C:\Projects\RAGcore` | `main` | `64a908a` | Up to date with `origin/main`. Uncommitted local work in progress (see below) — not Fleet-Ops-related, left untouched. |
| ITWorx MCP Hub | `C:\Projects\ITWorx_MCP_Hub` | `feature/wp240-final-acceptance` | `26e6bd8` (+ later `75bb16a`) | Contains `f107544` (MobilityOps connector) as a direct ancestor, plus a real contract fix (`96de385`, vehicleRef camelCase). Already deployed live to Tower at `c4a0f6d`. |
## Branch-name correction (recorded assumption)
The task brief names the working branch `feat/fleet-ops-final-integrations` as already
selected and "branched from the most recently validated, localized, deployed master
branch." That literal branch did not exist. `master` is in fact stale (19 commits behind,
last touched for a localization round only) — the actually-validated, deployed line of
work is `feat/live-n8n-ragcore-integration` (pushed to `origin`, deployed to
`http://192.168.10.150:1236` at `0571a40`, one commit behind current HEAD). Created
`feat/fleet-ops-final-integrations` from that branch's HEAD (`3ebca9e`) instead of from
`master`, since that satisfies the actual intent (continue from the validated/deployed
line) even though the literal branch name in the brief was inaccurate.
## What is actually already done (contradicts "not yet live" framing in places)
- **n8n**: 3 of 4 canonical workflows are live and active in the shared instance
(`n8n.itworx.tech`): Vehicle Return Orchestration, Scheduled Data Quality Scan,
Workflow Error Handler. The 4th, RAGcore Procedure Sync, has all 6 nodes built and
saved but is **not published** (deliberately left for an explicit activation decision,
since publishing starts real unattended daily runs against production). The root cause
of an earlier "auth"-looking failure (`N8N_PROXY_HOPS=0` behind the TLS-terminating
reverse proxy, breaking the browserId CSRF check on every mutating REST call) was found
and fixed at the infrastructure level (Unraid template), not worked around.
- **RAGcore**: deployed to `http://192.168.10.150:1237`, application wiring for
search/context/answer is real (commit `a2905cc`, confirmed present in RAGcore's own
history at `13 commits behind HEAD`). `KNOWLEDGE_PROVIDER` is still `demo` in Fleet Ops
because real queries against the "Fleet Ops Procedures" space return **zero dense and
zero sparse candidates** at the raw retrieval stage — confirmed not a Fleet-Ops-side
wiring bug (RAGcore's own trusted Query Lab tool reproduces the identical zero-candidate
result against the same space). Root cause not yet found as of this audit; ruled out so
far: point count/scoping (83 published, correctly scoped), embedding digest mismatch
(matches), collection alias resolution (resolves correctly). One separate, confirmed,
pre-existing bug: `_DEFAULT_LANGUAGE = "en"` is hardcoded in RAGcore's ingestion handler
— every chunk is stamped `language: "en"` regardless of actual content; RAGcore has never
done real language detection. Not the cause of zero candidates, but must be fixed for
trilingual retrieval (task 6A) once the space is answerable at all.
- **MCP Hub**: the Fleet Ops read-only connector (4 tools, `mobilityops.*`) is **already
live in production** on Tower (commit `c4a0f6d`), reachable via `fleetops.itworx.tech`,
end-to-end verified once already per the Hub's own `CLAUDE.md`/`BUILD_STATE.json`. A
real contract bug was found and fixed there (`vehicle.get`'s input schema disagreed with
the actual wire parameter name — `vehicle_ref` vs `vehicleRef`). What is **not** yet done:
the Hub's own formal production-acceptance checklist row for MobilityOps (`CON-P04`) has
not been executed, and Fleet Ops's own `MCP_HUB_REGISTRATION_ENABLED`/base-URL
configuration has not been confirmed as actually wired and flipped on from the Fleet Ops
side (open item for this audit's Batch 4).
## Confirmed contradictions to resolve (task section 3)
- "Twee versus vier n8n-workflows": resolved above — 3 active + 1 built-but-unpublished.
Canonical set is 4; only 3 are live.
- "Demo-provider versus live RAGcore": Fleet Ops is still on the demo knowledge provider
by deliberate, documented decision (not an oversight) pending the retrieval root cause.
- "MCP Hub-status": prior Fleet Ops docs (`.env.example`, `MCP_HUB_REGISTRATION_ENABLED`)
predate the Hub-side deployment and need reconciling against the fact that the connector
is already live on the Hub side.
- Repo hygiene: removed an untracked, empty `backend;C` directory and an untracked 31 MB
`MobilityOps.zip` stray export; added `*.zip`/`*.tar.gz` to `.gitignore`. No accidentally
committed `__pycache__`/`.pytest_cache`/`test-results` were found in git history.
## RAGcore retrieval root cause — found and partially fixed (2026-08-05, this session)
Investigated live against production (`192.168.10.150`, containers `ragcore-app-1`,
`ragcore-qdrant-1`, `ragcore-postgres-1`, `ollama`), read-only first, then two approved
live changes.
**Root cause #1 (FIXED): filesystem permission bug, not authorization/data.** Verified,
in order, that every earlier suspect was actually healthy: the `control.grants` row
(active, `editor` role, correct application/space), the real
`ControlPlaneAuthorizationInputsProvider` + `RetrievalAuthorizationService.resolve()` code
path run in-process against the live DB (resolves a non-empty `effective_space_ids`), the
exact production Qdrant filter run directly against the live collection (returns real
matching points), and a real ANN vector query under that filter (real hits, sensible
scores). The actual break: `/workspace/.state/models/embedding_profiles.json` — the file
`RetrievalPipeline.run()` reads on every single query to resolve the active embedding
profile — was owned by container-side `root:root` mode `600` on the bind-mounted
`/mnt/cache/appdata/ragcore/state/models` host path, while the real running app process
is uid 10001 (`ragcore`). Every retrieval call hit a `PermissionError` reading its own
state file before ever reaching Qdrant — a plain filesystem-ownership bug, invisible to
every DB/Qdrant-level check. **Fixed live**: `chown 10001:10001` +
`chmod 644`/`755` on that file/directory (approved by the user beforehand). Re-verified
in-process: `RetrievalPipeline.run()` now returns 5 real, relevant hits for an English
damage-procedure question (previously 0).
**Root cause #2 (found, NOT fixed — needs a design decision): reranking is
architecturally unavailable.** `DEFAULT_RERANKER_PROFILE.model_identifier` is
`bge-reranker-v2-m3:v1`, which was never actually present in Ollama's model list (0 of 14
installed models matched). With the user's approval, pulled a working GGUF
(`xitao/bge-reranker-v2-m3:latest`, 1.2 GB) into the shared Ollama instance. **This did
not fix reranking**: `OllamaRerankAdapter` posts to `{ollama_base_url}/api/rerank`, and
this Ollama server (version `0.32.5`) returns a plain `404` for that route — it has no
rerank endpoint at all. This is not a missing-model problem, it is that RAGcore's
reranker adapter was built against an Ollama HTTP API that does not exist in the deployed
version (matches the code's own comment that no reranker-profile registry or live
validation existed yet). The retrieval pipeline degrades gracefully on rerank failure
(RRF-fusion-only hits still returned, confirmed above), but the `/v1/answers` endpoint's
answerability classifier still returns `not_answerable`/0 citations for real NL/EN/FR
questions against real matching content, live-verified after fix #1 with a freshly
minted, correctly-scoped credential.
Options for #2, not decided yet: (a) find/confirm whether a newer Ollama version adds a
real `/api/rerank` route and upgrade the shared instance (affects every other project on
this Ollama — needs its own explicit approval and blast-radius review); (b) change
RAGcore's reranker adapter to call a route Ollama actually supports (e.g. score via
`/api/embed` + a manual similarity/cross-encoder computation, or drop the separate
rerank step and let the answerability classifier trust RRF-fused scores) — a RAGcore
code/design change, out of Fleet Ops's own mandate to decide unilaterally; (c) leave
`KNOWLEDGE_PROVIDER=demo` until RAGcore's own team/session resolves this.
**Side effect to flag**: minting the live-verification credential used `rotate=True` on
the existing "Fleet Ops Knowledge Assistant (production)" service account (a second
credential would have exceeded RAGcore's own 2-active-credential cap), which invalidates
whatever token was previously issued for that account. Since Fleet Ops is still on
`KNOWLEDGE_PROVIDER=demo`, this has no live user-facing impact today, but a fresh
credential must be issued and wired into Fleet Ops's `RAGCORE_API_TOKEN` at actual
cutover time — do not assume the old one still works.
**Concurrency note**: `C:\Projects\RAGcore` had substantial uncommitted local changes
from what appears to be a different, actively-running session (36 modified/untracked
files by the end of this investigation, including files this investigation also read).
No commits or file edits were made in that checkout this session precisely because of
that collision risk — the two live fixes above were applied directly to the running
containers/Ollama instance (approved), not to the RAGcore git repository. **Follow-up
required**: once the concurrent session's work lands, the reranker-profile fix (whichever
option above is chosen) still needs an actual code change + commit + redeploy in
`C:\Projects\RAGcore`, which was not safe to do mid-collision this session.
## Minimal remaining implementation order
1. Root-cause the RAGcore zero-candidate retrieval bug (blocks flipping `KNOWLEDGE_PROVIDER`
and blocks the trilingual live-acceptance and AI Operations Brief tasks).
2. Fix RAGcore's hardcoded `language: "en"` chunk metadata for trilingual retrieval.
3. Decide on and execute n8n workflow 3 publication, with live no-op-on-rerun verification.
4. Confirm/complete Fleet Ops-side MCP Hub registration wiring and run the Hub's own
CON-P04 acceptance row.
5. Build the AI Operations Brief runbook once RAGcore and MCP Hub are both live-green.
6. GUI polish batch (dashboard Today/Attention presentation, duplicate-merge presentation,
About Demo scannability, Demo Guide completion state).
7. Final regression gates and evidence write-up.
+5 -1
View File
@@ -284,7 +284,11 @@ export interface N8nIntegrationStatus {
export interface McpHubIntegrationStatus {
registration_enabled: boolean;
state: "not_configured" | "configured";
state: "not_configured" | "no_evidence" | "operational";
total_calls: number;
last_tool: string | null;
last_client: string | null;
last_called_at: string | null;
}
export interface IntegrationStatus {
+2 -1
View File
@@ -15,7 +15,8 @@ export const N8N_STATE_META: Record<IntegrationStatus["n8n"]["state"], { statusC
export const MCP_STATE_META: Record<IntegrationStatus["mcp_hub"]["state"], { statusClass: string; labelKey: string }> = {
not_configured: { statusClass: "not_configured", labelKey: "notConnected" },
configured: { statusClass: "no_events", labelKey: "prepared" },
no_evidence: { statusClass: "no_events", labelKey: "prepared" },
operational: { statusClass: "available", labelKey: "operational" },
};
// Maps the backend's canonical n8n workflow name (see n8n/workflows/MANIFEST.md) to a
@@ -43,6 +43,9 @@
"severityMedium": "Warning",
"severityLow": "Info",
"severityAriaLabel": "Severity",
"tierNow": "Handle now",
"tierToday": "Follow up today",
"tierLater": "Review later",
"empty": "No issues match this filter.",
"openRecord": "Open {{title}}"
},
@@ -118,6 +118,10 @@
"keepAsSurvivor": "Keep as survivor",
"differs": "Differs",
"match": "Match",
"summaryCounts": "{{matchCount}} matching fields · {{conflictCount}} conflicting fields",
"showMatchingFields": "Also show matching fields",
"hideMatchingFields": "Hide matching fields",
"previewTitle": "Preview of the merged record ({{ref}})",
"mergePreview": "{{loser}} will become a tombstone linked to {{survivor}}; its bookings will be rewired.",
"mergeInto": "Merge into {{ref}}",
"confirmMergeTitle": "Confirm merge",
@@ -43,6 +43,9 @@
"severityMedium": "Avertissement",
"severityLow": "Info",
"severityAriaLabel": "Gravité",
"tierNow": "À traiter maintenant",
"tierToday": "À suivre aujourd'hui",
"tierLater": "À contrôler plus tard",
"empty": "Aucun problème ne correspond à ce filtre.",
"openRecord": "Ouvrir {{title}}"
},
@@ -118,6 +118,10 @@
"keepAsSurvivor": "Conserver comme fiche principale",
"differs": "Diffère",
"match": "Identique",
"summaryCounts": "{{matchCount}} champs identiques · {{conflictCount}} champs en conflit",
"showMatchingFields": "Afficher aussi les champs identiques",
"hideMatchingFields": "Masquer les champs identiques",
"previewTitle": "Aperçu de la fiche fusionnée ({{ref}})",
"mergePreview": "{{loser}} deviendra une fiche archivée liée à {{survivor}} ; ses réservations seront transférées.",
"mergeInto": "Fusionner avec {{ref}}",
"confirmMergeTitle": "Confirmer la fusion",
@@ -43,6 +43,9 @@
"severityMedium": "Waarschuwing",
"severityLow": "Info",
"severityAriaLabel": "Ernst",
"tierNow": "Nu behandelen",
"tierToday": "Vandaag opvolgen",
"tierLater": "Later controleren",
"empty": "Geen aandachtspunten voor dit filter.",
"openRecord": "Open {{title}}"
},
@@ -118,6 +118,10 @@
"keepAsSurvivor": "Behouden als hoofdprofiel",
"differs": "Verschilt",
"match": "Gelijk",
"summaryCounts": "{{matchCount}} overeenkomende velden · {{conflictCount}} conflicterende velden",
"showMatchingFields": "Ook overeenkomende velden tonen",
"hideMatchingFields": "Overeenkomende velden verbergen",
"previewTitle": "Voorbeeld van het samengevoegde record ({{ref}})",
"mergePreview": "{{loser}} wordt een tombstone gekoppeld aan {{survivor}}; de boekingen worden herverbonden.",
"mergeInto": "Samenvoegen met {{ref}}",
"confirmMergeTitle": "Samenvoegen bevestigen",
+22 -20
View File
@@ -67,30 +67,32 @@ export function AboutDemo() {
<p>{t("about.scopeBody", { productName: PRODUCT_NAME })}</p>
</section>
<section className="record-surface about-card">
<h2>{t("about.realTitle")}</h2>
<p>{t("about.realBody")}</p>
</section>
<div className="about-grid">
<div className="record-surface about-card">
<h2>{t("about.realTitle")}</h2>
<p>{t("about.realBody")}</p>
</div>
<section className="record-surface about-card">
<h2>{t("about.syntheticTitle")}</h2>
<p>{t("about.syntheticBody", { testDomain: ".test" })}</p>
</section>
<div className="record-surface about-card">
<h2>{t("about.syntheticTitle")}</h2>
<p>{t("about.syntheticBody", { testDomain: ".test" })}</p>
</div>
<section className="record-surface about-card">
<h2>{t("about.architectureTitle")}</h2>
<p>{t("about.architectureBody")}</p>
</section>
<details className="record-surface about-card about-details">
<summary>{t("about.architectureTitle")}</summary>
<p>{t("about.architectureBody")}</p>
</details>
<section className="record-surface about-card">
<h2>{t("about.securityTitle")}</h2>
<p>{t("about.securityBody")}</p>
</section>
<details className="record-surface about-card about-details">
<summary>{t("about.securityTitle")}</summary>
<p>{t("about.securityBody")}</p>
</details>
<section className="record-surface about-card">
<h2>{t("about.testingTitle")}</h2>
<p>{t("about.testingBody")}</p>
</section>
<details className="record-surface about-card about-details">
<summary>{t("about.testingTitle")}</summary>
<p>{t("about.testingBody")}</p>
</details>
</div>
<section aria-label={t("about.integrationsTitle")} className="record-surface">
<SectionHeading title={t("about.integrationsTitle")} description={t("about.integrationsDescription")} />
+44 -26
View File
@@ -75,6 +75,17 @@ export function Dashboard() {
return matchesSeverity && haystack.includes(query.toLowerCase());
}) ?? [], [data, query, severity, t]);
const isUnfiltered = severity === "all" && query === "";
const attentionTiers = useMemo(() => {
if (!isUnfiltered) return [{ key: "all", labelKey: "", items: attention.slice(0, 6) }];
const bySeverity = (level: string) => attention.filter((item) => item.severity === level);
return [
{ key: "high", labelKey: "attention.tierNow", items: bySeverity("high") },
{ key: "medium", labelKey: "attention.tierToday", items: bySeverity("medium") },
{ key: "low", labelKey: "attention.tierLater", items: bySeverity("low") },
].filter((tier) => tier.items.length > 0);
}, [attention, isUnfiltered]);
if (error) return <ErrorState message={error} />;
if (!data) return <LoadingState label={t("common:status.loading")} />;
@@ -129,32 +140,39 @@ export function Dashboard() {
<label><span className="visually-hidden">{t("attention.severityAriaLabel")}</span><select value={severity} onChange={(event) => setSeverity(event.target.value)}><option value="all">{t("attention.severityAll")}</option><option value="high">{t("attention.severityHigh")}</option><option value="medium">{t("attention.severityMedium")}</option><option value="low">{t("attention.severityLow")}</option></select></label>
</div>
{attention.length === 0 ? <p className="quiet-empty"><Icon name="check" /> {t("attention.empty")}</p> : (
<ul className="attention-list">
{attention.slice(0, 6).map((item, index) => {
const href = item.issue_ref && canSeeQuality
? `/data-quality/${item.issue_ref}`
: item.link_type === "vehicle"
? `/vehicles/${item.link_ref}`
: null;
const title = attentionItemTitle(t, item);
return (
<li key={`${item.link_ref}-${index}`} className={href ? "row-clickable" : ""}>
<SeverityBadge severity={item.severity} />
<div className="queue-copy">
<p className="attention-title">{title}</p>
<p className="attention-detail">{item.detail}</p>
</div>
<span className="queue-ref">{item.link_ref}</span>
<Icon name="chevron" className="row-chevron" />
{href && (
<Link className="row-link" to={href} aria-label={t("attention.openRecord", { title })}>
<span className="visually-hidden">{title}</span>
</Link>
)}
</li>
);
})}
</ul>
<>
{attentionTiers.map((tier) => (
<div key={tier.key} className="attention-tier">
{attentionTiers.length > 1 && <p className="attention-tier-label">{t(tier.labelKey)}</p>}
<ul className="attention-list">
{tier.items.map((item, index) => {
const href = item.issue_ref && canSeeQuality
? `/data-quality/${item.issue_ref}`
: item.link_type === "vehicle"
? `/vehicles/${item.link_ref}`
: null;
const title = attentionItemTitle(t, item);
return (
<li key={`${item.link_ref}-${index}`} className={href ? "row-clickable" : ""}>
<SeverityBadge severity={item.severity} />
<div className="queue-copy">
<p className="attention-title">{title}</p>
<p className="attention-detail">{item.detail}</p>
</div>
<span className="queue-ref">{item.link_ref}</span>
<Icon name="chevron" className="row-chevron" />
{href && (
<Link className="row-link" to={href} aria-label={t("attention.openRecord", { title })}>
<span className="visually-hidden">{title}</span>
</Link>
)}
</li>
);
})}
</ul>
</div>
))}
</>
)}
</section>
+32 -1
View File
@@ -128,6 +128,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
const [error, setError] = useState<ApiErrorInfo | null>(null);
const [submitting, setSubmitting] = useState(false);
const [confirming, setConfirming] = useState(false);
const [showMatching, setShowMatching] = useState(false);
if (!issue.entity_snapshot || !issue.related_snapshots[0]) {
return <p className="error">{t("detail.duplicateCustomer.bothMissing")}</p>;
@@ -137,6 +138,9 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
const survivor = survivorRef === a.public_ref ? a : b;
const loser = survivorRef === a.public_ref ? b : a;
const conflictingFields = MERGE_FIELDS.filter((field) => String(a[field] ?? "") !== String(b[field] ?? ""));
const matchingFields = MERGE_FIELDS.filter((field) => !conflictingFields.includes(field));
const visibleFields = showMatching ? MERGE_FIELDS : conflictingFields;
async function handleMerge() {
setError(null);
@@ -174,6 +178,10 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
<SectionHeading headingId="compare-heading" title={t("detail.duplicateCustomer.heading")} description={t("detail.duplicateCustomer.description")} />
<ApiErrorNotice error={error} />
<p className="merge-summary-counts">
{t("detail.duplicateCustomer.summaryCounts", { matchCount: matchingFields.length, conflictCount: conflictingFields.length })}
</p>
<fieldset className="choice-fieldset">
<legend>{t("detail.duplicateCustomer.keepAsSurvivor")}</legend>
<label className={`choice-card ${survivorRef === a.public_ref ? "is-selected" : ""}`}>
@@ -205,7 +213,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
</tr>
</thead>
<tbody>
{MERGE_FIELDS.map((field) => {
{visibleFields.map((field) => {
const valueA = a[field] ? String(a[field]) : "—";
const valueB = b[field] ? String(b[field]) : "—";
const differ = valueA !== valueB;
@@ -248,10 +256,33 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
</tbody>
</table>
{matchingFields.length > 0 && (
<button type="button" className="button button-tertiary toggle-matching-fields" onClick={() => setShowMatching((v) => !v)}>
{showMatching ? t("detail.duplicateCustomer.hideMatchingFields") : t("detail.duplicateCustomer.showMatchingFields")}
</button>
)}
<p className="merge-preview">
{t("detail.duplicateCustomer.mergePreview", { loser: loser.public_ref, survivor: survivor.public_ref })}
</p>
<div className="merge-record-preview">
<p className="merge-record-preview-title">{t("detail.duplicateCustomer.previewTitle", { ref: survivor.public_ref })}</p>
<dl>
{MERGE_FIELDS.map((field) => {
const choice = fieldChoices[field];
const chosenSide = choice === "a" ? a : choice === "b" ? b : survivor;
const value = (chosenSide[field] ? String(chosenSide[field]) : survivor[field] ? String(survivor[field]) : "—");
return (
<div key={field}>
<dt>{t(`detail.duplicateCustomer.fields.${field}`)}</dt>
<dd>{value || "—"}</dd>
</div>
);
})}
</dl>
</div>
{!confirming && (
<button type="button" className="button button-primary" onClick={() => setConfirming(true)}>
{t("detail.duplicateCustomer.mergeInto", { ref: survivor.public_ref })}
+16
View File
@@ -193,6 +193,8 @@ a:hover { color: var(--teal); }
.row-link { position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.row-link:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.attention-list li.row-clickable:hover .attention-title, .movement-timeline li.row-clickable:hover .movement-ref { color: var(--teal-dark); }
.attention-tier + .attention-tier { border-top: 1px solid var(--line); }
.attention-tier-label { margin: 0; padding: 9px 18px 5px; color: var(--muted); font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
.data-table tr.row-clickable:hover { background: var(--surface-subtle); }
.data-table tr.row-clickable .cell-link { position: relative; z-index: 2; }
@@ -249,6 +251,12 @@ details summary { cursor: pointer; color: var(--teal-dark); }.data-table details
.about-cta { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; background: var(--teal-pale); border-color: #bfe6df; }
.about-cta strong { display: block; color: var(--ink); font-size: .85rem; }
.about-cta p { margin: 2px 0 0; }
.about-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; margin-bottom: 18px; }
.about-grid .about-card { margin-bottom: 0; }
.about-details summary { cursor: pointer; color: var(--ink); font-size: 1rem; letter-spacing: -.015em; font-weight: 700; }
.about-details[open] summary { margin-bottom: 8px; }
.about-details summary::-webkit-details-marker { display: none; }
.about-details summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1px; margin: 0; background: var(--line); border: 1px solid var(--line); }
.detail-grid div { min-height: 76px; padding: 13px 14px; background: white; }
.detail-grid dt { margin: 0; color: var(--muted); font-size: .63rem; font-weight: 700; text-transform: uppercase; letter-spacing: .065em; }.detail-grid dd { margin: 7px 0 0; color: var(--ink); font-size: .82rem; font-weight: 700; }
@@ -288,6 +296,14 @@ details summary { cursor: pointer; color: var(--teal-dark); }.data-table details
.duplicate-compare { margin-top: 18px; }.duplicate-compare fieldset { margin: 15px 0; padding: 13px; border: 1px solid var(--line); }.duplicate-compare legend { padding: 0 5px; color: var(--ink-soft); font-size: .69rem; font-weight: 700; }.duplicate-compare fieldset label { display: inline-flex !important; margin-right: 18px; }
.compare-table th, .compare-table td { vertical-align: top; }.compare-table label { display: inline-flex; flex-direction: row; align-items: center; gap: 6px; }.difference-mark, .match-mark { display: block; width: max-content; margin-top: 4px; padding: 2px 5px; font-size: .52rem; border-radius: 2px; }.difference-mark { color: var(--warning); background: var(--warning-pale); }.match-mark { color: var(--success); background: var(--success-pale); }
.merge-preview { padding: 13px; color: var(--ink-soft); background: var(--surface-subtle); border-left: 3px solid var(--teal); font-size: .75rem; }.duplicate-compare > button { color: white; background: var(--teal-dark); border-color: var(--teal-dark); }
.merge-summary-counts { margin: 0 0 12px; color: var(--muted); font-size: .72rem; font-weight: 700; }
.toggle-matching-fields { display: inline-block; margin: 10px 0; padding: 6px 10px; color: var(--ink-soft); background: transparent; border: 1px solid var(--line-strong); border-radius: var(--radius); font-size: .68rem; font-weight: 700; cursor: pointer; }
.toggle-matching-fields:hover { background: var(--surface-subtle); }
.merge-record-preview { margin: 14px 0; padding: 13px; background: var(--surface-subtle); border: 1px solid var(--line); border-radius: var(--radius); }
.merge-record-preview-title { margin: 0 0 8px; color: var(--ink); font-size: .72rem; font-weight: 700; }
.merge-record-preview dl { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 6px 16px; }
.merge-record-preview dt { margin: 0; color: var(--muted); font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
.merge-record-preview dd { margin: 1px 0 0; color: var(--ink); font-size: .78rem; }
.confirm-bar { margin-top: 13px; padding: 15px; background: var(--warning-pale); border: 1px solid #eed4aa; }.confirm-bar p { margin: 0 0 11px; color: #75420c; font-size: .75rem; font-weight: 700; }.confirm-bar button:first-of-type { color: white; background: var(--critical); border-color: var(--critical); }
.resolution-actions { display: flex; gap: 9px; }.evidence-block { max-width: 100%; padding: 13px; overflow: auto; color: #324054; background: var(--surface-subtle); border: 1px solid var(--line); font-size: .68rem; }
.evidence-disclosure { margin-top: 14px; }.evidence-disclosure summary { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }.evidence-disclosure .evidence-block { margin-top: 8px; }
+28 -22
View File
@@ -18,7 +18,7 @@ credential values are never embedded; nodes reference named n8n credentials inst
| Active status (as of 2026-08-04) | Active / Published |
| Error Workflow | `Fleet Ops — Workflow Error Handler` (wired) |
| Timeouts / bounded retries | `Record follow-up` HTTP node: 15s timeout, retry on fail (3 tries, 1000ms wait) |
| Checksum (sha256) | `a6f399dd77a7203dec7c0ac95e8540abf55f2703da519e06f1c37f2e1220f609` |
| Checksum (sha256) | `e5b6ba02a7824867620ceaf214224521d52de76337604b947f26f1b0b5432358` (updated 2026-08-05 — the committed file had invalid JSON, a missing `},` between two node objects; fixed, no live workflow change) |
## 2. Fleet Ops — Scheduled Data Quality Scan
@@ -37,33 +37,39 @@ credential values are never embedded; nodes reference named n8n credentials inst
## 3. Fleet Ops — RAGcore Procedure Sync
Not yet built, but no longer blocked. Credential issuance for the `fleet-ops` application
previously failed 100% of the time with an opaque rejection ("authoritative service-account
state rejected issuance" via the raw API; a generic error via the admin UI). Root-caused to
a genuine bug in RAGcore itself — a cross-transaction race in
`src/ragcore/api/v1/control/dependencies.py` where `get_control_application` and
`get_credential_service` each opened their own independent database transaction, so a
freshly-created service account was invisible to the immediately-following credential-issue
read. Fixed in RAGcore (with explicit owner approval) by sharing one request-scoped
transaction between both dependencies; verified against RAGcore's own test suite (64
passing) and deployed to the live instance. A working credential now exists: n8n credential
**"RAGcore Sync Token"** (Header Auth, `Authorization: Bearer <token>`), scope
`sources:sync` for `fleet-ops`. Will build
`n8n/workflows/fleet-ops-ragcore-procedure-sync.json` against the real RAGcore contract
(`POST /v1/uploads`, `GET /v1/knowledge-spaces`, etc. — see
`contracts/ragcore-contract-assumptions.md` and the live inspection notes in
`docs/live-ai-integration/n8n-current-state.md`) next.
Fully built and saved live (6 real nodes: Schedule Trigger → List procedures → Prepare
uploads → Upload to RAGcore → Summarize sync result → Report sync result to Fleet Ops),
but **deliberately not published/active** — the Schedule Trigger runs daily at midnight,
so activating it starts real, unattended runs against production RAGcore and Fleet Ops;
that is a separate, explicit go-live decision, not something to flip silently.
While validating this workflow (2026-08-05), found and fixed a real defect: the "Report
sync result to Fleet Ops" node's three body-parameter expressions each had a stray
trailing `}}` (e.g. `={{ $json.execution_id }} }}` instead of `={{ $json.execution_id
}}`), which would have sent malformed values on every real run. Fixed via `n8n
import:workflow` (the safe CLI path — not the REST API, which caused a prior wipe
incident in this environment) against the same live workflow ID, keeping it inactive;
re-exported and verified the fix applied with no other change (still 6 real nodes,
`active: false`). Also found and fixed a second gap: `settings.errorWorkflow` was unset
(workflows 1-2 wire `"errorWorkflow": "Xppn2rAEqUuyiCJF"`, workflow 3 did not) — wired it
the same way via the same CLI import path, re-verified.
| Field | Value |
|---|---|
| File | `fleet-ops-ragcore-procedure-sync.json` (not yet created) |
| Live workflow ID | — |
| Active status | Not built |
| File | `fleet-ops-ragcore-procedure-sync.json` |
| Purpose | Sync the trilingual procedure documents from Fleet Ops into RAGcore as source documents, keeping stable per-document IDs and skipping unchanged content. |
| Trigger | Schedule Trigger (daily at midnight) |
| Event contract | N/A — HTTP-triggered sync. Reads `GET /api/v1/integrations/n8n/procedures`, uploads via RAGcore's `POST /v1/uploads`, reports via `POST /api/v1/integrations/n8n/procedures-sync-result`. |
| Required credentials | `Fleet Ops Service Token` (Header Auth, on the procedures-list and result-report calls); `RAGcore Sync Token` (Header Auth, `sources:sync` scope, on the upload call) |
| Live workflow ID | `6wbkc4d1AouGpmWT` |
| Active status (as of 2026-08-05) | Built / Saved / **Inactive** (not published — daily unattended runs require a separate explicit go-live decision) |
| Error Workflow | `Fleet Ops — Workflow Error Handler` (wired) |
| Checksum (sha256) | `643c0515a50fed3d35e28f0b8cb17841f6f8d91699944980b5c56e619d5bf2a6` |
## 4. Fleet Ops — Workflow Error Handler
Central technical workflow attached to workflows 1-2 via n8n's per-workflow "Error
Workflow" setting (workflow 3 will be wired the same way once it exists). Receives n8n's
Central technical workflow attached to all three other Fleet Ops workflows (1, 2, 3) via
n8n's per-workflow "Error Workflow" setting. Receives n8n's
standard Error Trigger payload, derives a bounded/secret-free failure report (safe error
category, truncated summary, no stack trace, no headers/tokens), and POSTs it to Fleet
Ops, which registers an audit event idempotently keyed on `execution_id`.
+1
View File
@@ -33,6 +33,7 @@ KNOWN_WORKFLOWS = [
("fleet-ops-vehicle-return.json", "mobilityops-return-processing"),
("fleet-ops-data-quality-scan.json", "mobilityops-scheduled-quality-scan"),
("fleet-ops-error-handler.json", "Xppn2rAEqUuyiCJF"),
("fleet-ops-ragcore-procedure-sync.json", "6wbkc4d1AouGpmWT"),
]
# Fields that legitimately differ between a committed definition and the live instance
@@ -0,0 +1,246 @@
{
"id": "6wbkc4d1AouGpmWT",
"name": "Fleet Ops — RAGcore Procedure Sync",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
128,
-144
],
"id": "08f41c46-d5a9-4cb9-beec-0f074a9eb7bc",
"name": "Sticky Note"
},
{
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-144,
-144
],
"id": "6edcd359-08eb-4d88-98e9-5ca0625047cc",
"name": "Schedule Trigger"
},
{
"parameters": {
"url": "http://192.168.10.150:1236/api/v1/integrations/n8n/procedures",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"timeout": 15000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
80,
-144
],
"id": "b66d3ee4-6264-48ab-89fc-aeb4d1597435",
"name": "List procedures",
"retryOnFail": true,
"credentials": {
"httpHeaderAuth": {
"name": "Fleet Ops Service Token"
}
}
},
{
"parameters": {
"jsCode": "const documents = $input.first().json.documents;\nconst results = [];\nfor (const doc of documents) {\n const binary = await this.helpers.prepareBinaryData(Buffer.from(doc.content, 'utf-8'), doc.document_id + '.md', 'text/markdown');\n results.push({json: {id: doc.id, language: doc.language, document_id: doc.document_id, title: doc.title, version: doc.version, content_hash: doc.content_hash}, binary: {file: binary}});\n}\nreturn results;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
304,
-144
],
"id": "e9699a7a-87ff-4603-b89b-553dc7f2f29d",
"name": "Prepare uploads"
},
{
"parameters": {
"method": "POST",
"url": "https://rag.itworx.tech/v1/uploads",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Idempotency-Key",
"value": "={{ $json.id + '-' + $json.content_hash }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"name": "space_id",
"value": "f4c91e49-5cf9-48ba-b3d6-e0e9854ebccc"
},
{
"name": "source_id",
"value": "={{ $json.id }}"
},
{
"parameterType": "formBinaryData",
"name": "file",
"inputDataFieldName": "file"
}
]
},
"options": {
"timeout": 20000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
528,
-144
],
"id": "6d38f228-ed82-44a7-bc59-c785767d4f35",
"name": "Upload to RAGcore",
"retryOnFail": true,
"credentials": {
"httpHeaderAuth": {
"name": "RAGcore Sync Token"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nlet synced = 0;\nlet failed = 0;\nfor (const item of items) {\n if (item.json && item.json.error) {\n failed += 1;\n } else {\n synced += 1;\n }\n}\nreturn [{ json: { execution_id: String($execution.id), synced, failed } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
-144
],
"id": "4b90056e-29f8-4df0-b7b4-695f4b9e0004",
"name": "Summarize sync result"
},
{
"parameters": {
"method": "POST",
"url": "http://192.168.10.150:1236/api/v1/integrations/n8n/procedures-sync-result",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "execution_id",
"value": "={{ $json.execution_id }}"
},
{
"name": "synced",
"value": "={{ $json.synced }}"
},
{
"name": "failed",
"value": "={{ $json.failed }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
848,
-144
],
"id": "953cd587-3d3e-44b5-907c-92b8355b00e3",
"name": "Report sync result to Fleet Ops",
"credentials": {
"httpHeaderAuth": {
"name": "Fleet Ops Service Token"
}
}
}
],
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "List procedures",
"type": "main",
"index": 0
}
]
]
},
"List procedures": {
"main": [
[
{
"node": "Prepare uploads",
"type": "main",
"index": 0
}
]
]
},
"Prepare uploads": {
"main": [
[
{
"node": "Upload to RAGcore",
"type": "main",
"index": 0
}
]
]
},
"Upload to RAGcore": {
"main": [
[
{
"node": "Summarize sync result",
"type": "main",
"index": 0
}
]
]
},
"Summarize sync result": {
"main": [
[
{
"node": "Report sync result to Fleet Ops",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false,
"errorWorkflow": "Xppn2rAEqUuyiCJF",
"callerPolicy": "workflowsFromSameOwner"
},
"active": false,
"meta": {
"templateCredsSetupCompleted": false
},
"tags": []
}
@@ -68,6 +68,7 @@
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000
},
{
"parameters": {
"respondWith": "json",
+3 -3
View File
@@ -1,5 +1,5 @@
public_ref,customer_ref,vehicle_ref,starts_at,ends_at,status,start_odometer_km,end_odometer_km,requirements_complete
BK-H-0001,CUS-0012,MO-008,2026-07-26T08:00:00Z,2026-07-29T08:00:00Z,returned,20932,21133,true
BK-H-0001,CUS-0012,MO-008,2026-07-29T08:00:00Z,2026-08-01T08:00:00Z,returned,20932,21133,true
BK-H-0002,CUS-0023,MO-015,2026-07-24T07:00:00Z,2026-07-28T07:00:00Z,returned,26659,26861,true
BK-H-0003,CUS-0034,MO-022,2026-07-22T06:00:00Z,2026-07-27T06:00:00Z,returned,31407,31610,true
BK-H-0004,CUS-0045,MO-029,2026-07-20T05:00:00Z,2026-07-26T05:00:00Z,returned,36708,36912,true
@@ -220,8 +220,8 @@ BK-H-0218,CUS-0059,MO-027,2025-12-14T07:00:00Z,2025-12-24T07:00:00Z,returned,352
BK-H-0219,CUS-0070,MO-034,2025-12-12T06:00:00Z,2025-12-23T06:00:00Z,returned,39838,40257,true
BK-H-0220,CUS-0081,MO-041,2025-12-20T05:00:00Z,2025-12-22T05:00:00Z,returned,45223,45643,true
BK-DEMO-RETURN,CUS-0042,MO-024,2026-07-28T09:00:00Z,2026-08-01T09:00:00Z,active,53610,,true
BK-F-001,CUS-0014,MO-010,2026-08-03T10:00:00Z,2026-08-07T10:00:00Z,reserved,,,true
BK-F-002,CUS-0027,MO-019,2026-08-04T11:00:00Z,2026-08-09T11:00:00Z,reserved,,,true
BK-F-001,CUS-0014,MO-010,2026-08-01T10:00:00Z,2026-08-05T10:00:00Z,reserved,,,true
BK-F-002,CUS-0027,MO-019,2026-08-01T11:00:00Z,2026-08-06T11:00:00Z,reserved,,,true
BK-F-003,CUS-0040,MO-028,2026-08-05T12:00:00Z,2026-08-11T12:00:00Z,reserved,,,false
BK-F-004,CUS-0053,MO-037,2026-08-06T13:00:00Z,2026-08-13T13:00:00Z,reserved,,,true
BK-F-005,CUS-0066,MO-046,2026-08-07T09:00:00Z,2026-08-15T09:00:00Z,reserved,,,true
1 public_ref customer_ref vehicle_ref starts_at ends_at status start_odometer_km end_odometer_km requirements_complete
2 BK-H-0001 CUS-0012 MO-008 2026-07-26T08:00:00Z 2026-07-29T08:00:00Z 2026-07-29T08:00:00Z 2026-08-01T08:00:00Z returned 20932 21133 true
3 BK-H-0002 CUS-0023 MO-015 2026-07-24T07:00:00Z 2026-07-28T07:00:00Z returned 26659 26861 true
4 BK-H-0003 CUS-0034 MO-022 2026-07-22T06:00:00Z 2026-07-27T06:00:00Z returned 31407 31610 true
5 BK-H-0004 CUS-0045 MO-029 2026-07-20T05:00:00Z 2026-07-26T05:00:00Z returned 36708 36912 true
220 BK-H-0219 CUS-0070 MO-034 2025-12-12T06:00:00Z 2025-12-23T06:00:00Z returned 39838 40257 true
221 BK-H-0220 CUS-0081 MO-041 2025-12-20T05:00:00Z 2025-12-22T05:00:00Z returned 45223 45643 true
222 BK-DEMO-RETURN CUS-0042 MO-024 2026-07-28T09:00:00Z 2026-08-01T09:00:00Z active 53610 true
223 BK-F-001 CUS-0014 MO-010 2026-08-03T10:00:00Z 2026-08-01T10:00:00Z 2026-08-07T10:00:00Z 2026-08-05T10:00:00Z reserved true
224 BK-F-002 CUS-0027 MO-019 2026-08-04T11:00:00Z 2026-08-01T11:00:00Z 2026-08-09T11:00:00Z 2026-08-06T11:00:00Z reserved true
225 BK-F-003 CUS-0040 MO-028 2026-08-05T12:00:00Z 2026-08-11T12:00:00Z reserved false
226 BK-F-004 CUS-0053 MO-037 2026-08-06T13:00:00Z 2026-08-13T13:00:00Z reserved true
227 BK-F-005 CUS-0066 MO-046 2026-08-07T09:00:00Z 2026-08-15T09:00:00Z reserved true