From 34df66d28c9a1d449e7f0f018f391e1a1affe777 Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:05:02 +0200 Subject: [PATCH] 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 --- .gitignore | 2 + README.md | 20 +- backend/app/api/routers/dashboard.py | 10 +- backend/app/api/routers/integration_status.py | 15 +- backend/app/schemas.py | 6 +- backend/app/services/integration_status.py | 52 +++- backend/tests/test_seed.py | 17 ++ .../final-integrations/current-state-audit.md | 152 +++++++++++ frontend/src/api/types.ts | 6 +- frontend/src/data/integrationLabels.ts | 3 +- .../src/i18n/locales/en-GB/dashboard.json | 3 + frontend/src/i18n/locales/en-GB/quality.json | 4 + .../src/i18n/locales/fr-BE/dashboard.json | 3 + frontend/src/i18n/locales/fr-BE/quality.json | 4 + .../src/i18n/locales/nl-BE/dashboard.json | 3 + frontend/src/i18n/locales/nl-BE/quality.json | 4 + frontend/src/pages/AboutDemo.tsx | 42 +-- frontend/src/pages/Dashboard.tsx | 70 +++-- frontend/src/pages/DataQualityIssueDetail.tsx | 33 ++- frontend/src/styles.css | 16 ++ n8n/workflows/MANIFEST.md | 50 ++-- n8n/workflows/check_drift.py | 1 + .../fleet-ops-ragcore-procedure-sync.json | 246 ++++++++++++++++++ n8n/workflows/fleet-ops-vehicle-return.json | 1 + seed/bookings.csv | 6 +- 25 files changed, 669 insertions(+), 100 deletions(-) create mode 100644 docs/final-integrations/current-state-audit.md create mode 100644 n8n/workflows/fleet-ops-ragcore-procedure-sync.json diff --git a/.gitignore b/.gitignore index 041afbf..0be42cf 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ test-results/ .idea/ .vscode/ *.tsbuildinfo +*.zip +*.tar.gz diff --git a/README.md b/README.md index f022810..5164a31 100644 --- a/README.md +++ b/README.md @@ -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), diff --git a/backend/app/api/routers/dashboard.py b/backend/app/api/routers/dashboard.py index 15039c8..f3b0639 100644 --- a/backend/app/api/routers/dashboard.py +++ b/backend/app/api/routers/dashboard.py @@ -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() diff --git a/backend/app/api/routers/integration_status.py b/backend/app/api/routers/integration_status.py index c1f08f8..dac7aca 100644 --- a/backend/app/api/routers/integration_status.py +++ b/backend/app/api/routers/integration_status.py @@ -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), ) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index af3775a..9188908 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -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): diff --git a/backend/app/services/integration_status.py b/backend/app/services/integration_status.py index ff3e646..fbee2e7 100644 --- a/backend/app/services/integration_status.py +++ b/backend/app/services/integration_status.py @@ -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, + ) diff --git a/backend/tests/test_seed.py b/backend/tests/test_seed.py index 199cf66..1c9aeeb 100644 --- a/backend/tests/test_seed.py +++ b/backend/tests/test_seed.py @@ -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() diff --git a/docs/final-integrations/current-state-audit.md b/docs/final-integrations/current-state-audit.md new file mode 100644 index 0000000..14ceb1d --- /dev/null +++ b/docs/final-integrations/current-state-audit.md @@ -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. diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index aedaa0d..3ed3963 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -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 { diff --git a/frontend/src/data/integrationLabels.ts b/frontend/src/data/integrationLabels.ts index 6aed855..7912836 100644 --- a/frontend/src/data/integrationLabels.ts +++ b/frontend/src/data/integrationLabels.ts @@ -15,7 +15,8 @@ export const N8N_STATE_META: Record = { 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 diff --git a/frontend/src/i18n/locales/en-GB/dashboard.json b/frontend/src/i18n/locales/en-GB/dashboard.json index c3a314b..e0899a0 100644 --- a/frontend/src/i18n/locales/en-GB/dashboard.json +++ b/frontend/src/i18n/locales/en-GB/dashboard.json @@ -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}}" }, diff --git a/frontend/src/i18n/locales/en-GB/quality.json b/frontend/src/i18n/locales/en-GB/quality.json index 6a6531b..ef0e5f8 100644 --- a/frontend/src/i18n/locales/en-GB/quality.json +++ b/frontend/src/i18n/locales/en-GB/quality.json @@ -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", diff --git a/frontend/src/i18n/locales/fr-BE/dashboard.json b/frontend/src/i18n/locales/fr-BE/dashboard.json index a3a757d..53b37de 100644 --- a/frontend/src/i18n/locales/fr-BE/dashboard.json +++ b/frontend/src/i18n/locales/fr-BE/dashboard.json @@ -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}}" }, diff --git a/frontend/src/i18n/locales/fr-BE/quality.json b/frontend/src/i18n/locales/fr-BE/quality.json index 9693782..d5468dd 100644 --- a/frontend/src/i18n/locales/fr-BE/quality.json +++ b/frontend/src/i18n/locales/fr-BE/quality.json @@ -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", diff --git a/frontend/src/i18n/locales/nl-BE/dashboard.json b/frontend/src/i18n/locales/nl-BE/dashboard.json index 774829b..086152e 100644 --- a/frontend/src/i18n/locales/nl-BE/dashboard.json +++ b/frontend/src/i18n/locales/nl-BE/dashboard.json @@ -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}}" }, diff --git a/frontend/src/i18n/locales/nl-BE/quality.json b/frontend/src/i18n/locales/nl-BE/quality.json index c5ec0db..b16451f 100644 --- a/frontend/src/i18n/locales/nl-BE/quality.json +++ b/frontend/src/i18n/locales/nl-BE/quality.json @@ -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", diff --git a/frontend/src/pages/AboutDemo.tsx b/frontend/src/pages/AboutDemo.tsx index 3fde557..4964452 100644 --- a/frontend/src/pages/AboutDemo.tsx +++ b/frontend/src/pages/AboutDemo.tsx @@ -67,30 +67,32 @@ export function AboutDemo() {

{t("about.scopeBody", { productName: PRODUCT_NAME })}

-
-

{t("about.realTitle")}

-

{t("about.realBody")}

-
+
+
+

{t("about.realTitle")}

+

{t("about.realBody")}

+
-
-

{t("about.syntheticTitle")}

-

{t("about.syntheticBody", { testDomain: ".test" })}

-
+
+

{t("about.syntheticTitle")}

+

{t("about.syntheticBody", { testDomain: ".test" })}

+
-
-

{t("about.architectureTitle")}

-

{t("about.architectureBody")}

-
+
+ {t("about.architectureTitle")} +

{t("about.architectureBody")}

+
-
-

{t("about.securityTitle")}

-

{t("about.securityBody")}

-
+
+ {t("about.securityTitle")} +

{t("about.securityBody")}

+
-
-

{t("about.testingTitle")}

-

{t("about.testingBody")}

-
+
+ {t("about.testingTitle")} +

{t("about.testingBody")}

+
+
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 4c80768..c5f9c03 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -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 ; if (!data) return ; @@ -129,32 +140,39 @@ export function Dashboard() { {attention.length === 0 ?

{t("attention.empty")}

: ( -
    - {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 ( -
  • - -
    -

    {title}

    -

    {item.detail}

    -
    - {item.link_ref} - - {href && ( - - {title} - - )} -
  • - ); - })} -
+ <> + {attentionTiers.map((tier) => ( +
+ {attentionTiers.length > 1 &&

{t(tier.labelKey)}

} +
    + {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 ( +
  • + +
    +

    {title}

    +

    {item.detail}

    +
    + {item.link_ref} + + {href && ( + + {title} + + )} +
  • + ); + })} +
+
+ ))} + )}
diff --git a/frontend/src/pages/DataQualityIssueDetail.tsx b/frontend/src/pages/DataQualityIssueDetail.tsx index 090dd2d..0970257 100644 --- a/frontend/src/pages/DataQualityIssueDetail.tsx +++ b/frontend/src/pages/DataQualityIssueDetail.tsx @@ -128,6 +128,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR const [error, setError] = useState(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

{t("detail.duplicateCustomer.bothMissing")}

; @@ -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 +

+ {t("detail.duplicateCustomer.summaryCounts", { matchCount: matchingFields.length, conflictCount: conflictingFields.length })} +

+
{t("detail.duplicateCustomer.keepAsSurvivor")}