From efab8d816fc85c4aaf1f988ca78cad0fe39ed3d2 Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:58:26 +0200 Subject: [PATCH] M31: verify RAG inventory and polish attention queue --- PROJECT_STATE.md | 23 +++++ backend/app/api/routers/knowledge.py | 8 +- backend/app/services/knowledge/ragcore.py | 95 +++++++++++++++++-- backend/tests/test_knowledge.py | 94 +++++++++++++++++- docs/09-ragcore-integration.md | 17 ++-- frontend/e2e/fleet-ops-correction.spec.ts | 9 +- frontend/e2e/recruiter-polish.spec.ts | 52 +++++++++- .../src/i18n/locales/en-GB/dashboard.json | 4 +- .../src/i18n/locales/en-GB/knowledge.json | 5 +- .../src/i18n/locales/fr-BE/dashboard.json | 4 +- .../src/i18n/locales/fr-BE/knowledge.json | 5 +- .../src/i18n/locales/nl-BE/dashboard.json | 4 +- .../src/i18n/locales/nl-BE/knowledge.json | 5 +- frontend/src/pages/Dashboard.tsx | 11 ++- frontend/src/pages/Knowledge.tsx | 1 + frontend/src/styles.css | 15 ++- 16 files changed, 314 insertions(+), 38 deletions(-) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index d13f15a..58a9431 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -2697,3 +2697,26 @@ evidence yet." summary were refreshed in `artifacts/evidence/`. - Exact next action: commit/push this evidence-only hand-off update and refresh the server's source archive/revision marker; no runtime rebuild or database change is needed. + +## M31 — verified RAG inventory and dashboard attention polish (2026-08-10) + +- RAGcore health now verifies every language-specific managed source through the + documented exact `/v1/documents` identity lookup. Only an active document with a + published active version and the authoritative Fleet Ops content hash is counted. + Lookups are bounded, concurrent and cached for five minutes; an unavailable verifier + remains explicitly unknown instead of being presented as zero or as a reported count. +- Persisted n8n sync provenance no longer downgrades the stronger provider-verified + statistics state. The Knowledge Hub shows the verified count as a separate evidence + fact alongside source, sync and recency evidence, in all three supported languages. +- The dashboard's remaining-attention action is now a compact 52 px evidence-backed row + with count badge, legible title/hint and a 14 px directional icon. The attention panel + no longer stretches to the neighbouring full-day timeline; desktop and responsive + layouts remain overflow-free. +- Validation: isolated backend **241 passed**; Ruff clean; mypy clean across 58 files in + the locked container; frontend lint/build passed; focused visual regressions **5 passed**; + localized knowledge regressions **3 passed**; complete Playwright **152 passed in 5.4 + minutes**; full and production npm audits report zero vulnerabilities. In-app visual + inspection confirmed the new action dimensions and presentation. +- Exact next action: commit and push M31, create and verify a live database backup, + redeploy the committed archive, then verify live RAG inventory, create a real post-reset + MCP Hub tool-call audit record, and complete production browser/acceptance evidence. diff --git a/backend/app/api/routers/knowledge.py b/backend/app/api/routers/knowledge.py index 229e31f..662a3ce 100644 --- a/backend/app/api/routers/knowledge.py +++ b/backend/app/api/routers/knowledge.py @@ -124,6 +124,12 @@ def knowledge_status( "reported_synced_document_count": synced if isinstance(synced, int) else None, "reported_failed_document_count": failed if isinstance(failed, int) else None, "last_sync_at": latest_sync.occurred_at, - "statistics_state": "sync_reported", + # A persisted sync callback is useful additional provenance, but must not + # downgrade stronger provider-side verification to merely "reported". + "statistics_state": ( + health.statistics_state + if health.statistics_state == "verified" + else "sync_reported" + ), } ) diff --git a/backend/app/services/knowledge/ragcore.py b/backend/app/services/knowledge/ragcore.py index 34b68d1..944bb0b 100644 --- a/backend/app/services/knowledge/ragcore.py +++ b/backend/app/services/knowledge/ragcore.py @@ -1,12 +1,15 @@ from __future__ import annotations +from concurrent.futures import ThreadPoolExecutor from pathlib import Path +from threading import Lock +from time import monotonic import httpx from app.core.config import get_settings from app.services.knowledge import EvidenceState, GroundedAnswer, KnowledgeHealth, SourceCard -from app.services.knowledge.procedures import iter_procedure_documents +from app.services.knowledge.procedures import ProcedureDocument, iter_procedure_documents _GROUNDED_ANSWERABILITY = {"answerable", "partially_answerable"} @@ -24,6 +27,9 @@ _LEAD_ANSWER_TEMPLATE = { } _DEFAULT_LANGUAGE = "en-GB" _MAX_SOURCE_CARDS = 3 +_INDEX_LOOKUP_TIMEOUT_SECONDS = 2.0 +_INDEX_VERIFICATION_TTL_SECONDS = 300.0 +_INDEX_VERIFICATION_WORKERS = 6 _DOMAIN_CONCEPTS: dict[str, tuple[str, ...]] = { "damage": ("damage", "damaged", "schade", "beschadigd", "dommage", "endommagé"), @@ -118,6 +124,8 @@ class RAGcoreKnowledgeProvider: def __init__(self) -> None: self._settings = get_settings() + self._verification_cache: dict[str, tuple[float, int]] = {} + self._verification_lock = Lock() def _client(self) -> httpx.Client: headers = {} @@ -130,6 +138,12 @@ class RAGcoreKnowledgeProvider: ) def health(self, language: str = "en-GB") -> KnowledgeHealth: + documents = [ + document + for document in iter_procedure_documents(Path(self._settings.knowledge_dir)) + if document.language == language + ] + verified_document_count: int | None = None try: with self._client() as client: response = client.get("/health/ready") @@ -140,13 +154,20 @@ class RAGcoreKnowledgeProvider: if available else f"RAGcore degraded: {body.get('status', 'unknown')}" ) + if available: + verified_document_count = self._verify_indexed_documents( + client, language, documents + ) + if verified_document_count is None: + detail += " Index verification is temporarily unavailable." + else: + detail += ( + f" {verified_document_count}/{len(documents)} managed sources have " + "an active published version with the expected content hash." + ) except (httpx.HTTPError, ValueError) as exc: available = False detail = f"RAGcore unavailable: {type(exc).__name__}: {exc}" - source_document_count = sum( - document.language == language - for document in iter_procedure_documents(Path(self._settings.knowledge_dir)) - ) return KnowledgeHealth( provider=self.name, available=available, @@ -154,16 +175,70 @@ class RAGcoreKnowledgeProvider: tenant=self._settings.ragcore_tenant, workspace=self._settings.ragcore_workspace, collection=self._settings.ragcore_collection, - # RAGcore's retrieval API has no corpus-size endpoint. Unknown is explicit - # so the UI never turns this into the misleading claim "0 procedures". - document_count=None, - source_document_count=source_document_count, + # RAGcore deliberately has no browse/count endpoint. Fleet Ops instead + # verifies each managed source through its exact identity lookup and only + # counts an active published version whose content hash still matches. + document_count=verified_document_count, + source_document_count=len(documents), reported_synced_document_count=None, reported_failed_document_count=None, last_sync_at=None, - statistics_state="not_reported", + statistics_state=( + "verified" if verified_document_count is not None else "not_reported" + ), ) + def _verify_indexed_documents( + self, client: httpx.Client, language: str, documents: list[ProcedureDocument] + ) -> int | None: + if not self._settings.ragcore_space_id or not documents: + return None + + now = monotonic() + with self._verification_lock: + cached = self._verification_cache.get(language) + if cached is not None and now - cached[0] < _INDEX_VERIFICATION_TTL_SECONDS: + return cached[1] + + def is_verified(document: ProcedureDocument) -> bool: + response = client.get( + "/v1/documents", + params={ + "source_id": document.source_id, + "external_id": f"{document.document_id}.md", + }, + timeout=_INDEX_LOOKUP_TIMEOUT_SECONDS, + ) + if response.status_code != 200: + raise RuntimeError("RAGcore document verification failed") + body = response.json() + items = body.get("items") if isinstance(body, dict) else None + if not isinstance(items, list) or len(items) != 1 or not isinstance(items[0], dict): + return False + item = items[0] + active_version = item.get("active_version") + return bool( + item.get("space_id") == self._settings.ragcore_space_id + and item.get("source_id") == document.source_id + and item.get("external_id") == f"{document.document_id}.md" + and item.get("status") == "active" + and isinstance(active_version, dict) + and active_version.get("status") == "published" + and active_version.get("content_sha256") == document.content_hash + ) + + try: + with ThreadPoolExecutor( + max_workers=min(_INDEX_VERIFICATION_WORKERS, len(documents)) + ) as executor: + verified_count = sum(executor.map(is_verified, documents)) + except (httpx.HTTPError, RuntimeError, TypeError, ValueError): + return None + + with self._verification_lock: + self._verification_cache[language] = (monotonic(), verified_count) + return verified_count + def ask(self, question: str, correlation_id: str, language: str = "en-GB") -> GroundedAnswer: unavailable = GroundedAnswer( answer="", diff --git a/backend/tests/test_knowledge.py b/backend/tests/test_knowledge.py index f149646..1859468 100644 --- a/backend/tests/test_knowledge.py +++ b/backend/tests/test_knowledge.py @@ -9,6 +9,7 @@ from app.api.routers import knowledge as knowledge_router from app.core.config import get_settings from app.services.knowledge import KnowledgeHealth from app.services.knowledge.demo import DemoKnowledgeProvider +from app.services.knowledge.procedures import iter_procedure_documents from app.services.knowledge.ragcore import RAGcoreKnowledgeProvider @@ -198,6 +199,39 @@ def test_ragcore_status_separates_sync_report_from_unverifiable_index( assert status["statistics_state"] == "sync_reported" +def test_ragcore_status_preserves_stronger_verified_index_evidence(client, ops_client, monkeypatch): + class VerifiedRagcoreProvider: + def health(self, language="en-GB"): + return KnowledgeHealth( + provider="ragcore", + available=True, + detail="ready and verified", + tenant="fleet-ops", + workspace="operations", + collection="internal-procedures", + document_count=11, + source_document_count=11, + reported_synced_document_count=None, + reported_failed_document_count=None, + last_sync_at=None, + statistics_state="verified", + ) + + monkeypatch.setattr(knowledge_router, "get_knowledge_provider", VerifiedRagcoreProvider) + settings = get_settings() + sync = client.post( + "/api/v1/integrations/n8n/procedures-sync-result", + json={"execution_id": "rag-verified-statistics-test", "synced": 33, "failed": 0}, + headers={"X-Service-Token": settings.n8n_callback_token}, + ) + assert sync.status_code == 200 + + status = ops_client.get("/api/v1/knowledge/status?language=nl-BE").json() + assert status["document_count"] == 11 + assert status["reported_synced_document_count"] == 33 + assert status["statistics_state"] == "verified" + + class _FakeResponse: def __init__(self, status_code: int, body: dict): self.status_code = status_code @@ -208,7 +242,14 @@ class _FakeResponse: class _FakeClient: - def __init__(self, get_response=None, post_response=None, post_responses=None, raise_on=None): + def __init__( + self, + get_response=None, + post_response=None, + post_responses=None, + raise_on=None, + get_handler=None, + ): self._get_response = get_response self._post_response = post_response # Maps a path (e.g. "/v1/search") to its own response, for tests that need @@ -217,6 +258,7 @@ class _FakeClient: # existing single-endpoint test keeps working unchanged. self._post_responses = post_responses or {} self._raise_on = raise_on + self._get_handler = get_handler def __enter__(self): return self @@ -224,9 +266,13 @@ class _FakeClient: def __exit__(self, *args): return False - def get(self, path): + def get(self, path, params=None, timeout=None): if self._raise_on == "get": raise httpx.ConnectError("no ragcore in this environment") + if self._get_handler is not None: + return self._get_handler(path, params, timeout) + if path == "/v1/documents": + return _FakeResponse(503, {}) return self._get_response def post(self, path, json=None): @@ -273,6 +319,50 @@ def test_ragcore_provider_health_reports_ready_status(monkeypatch): assert health.statistics_state == "not_reported" +def test_ragcore_provider_verifies_published_documents_and_caches_count(monkeypatch): + provider = RAGcoreKnowledgeProvider() + monkeypatch.setattr(provider._settings, "ragcore_space_id", "space-1") + procedure_documents = [ + document + for document in iter_procedure_documents(Path(provider._settings.knowledge_dir)) + if document.language == "en-GB" + ] + documents = {document.source_id: document for document in procedure_documents} + lookups: list[str] = [] + + def get_handler(path, params, _timeout): + if path == "/health/ready": + return _FakeResponse(200, {"status": "ok"}) + document = documents[params["source_id"]] + lookups.append(document.source_id) + return _FakeResponse( + 200, + { + "items": [ + { + "space_id": "space-1", + "source_id": document.source_id, + "external_id": f"{document.document_id}.md", + "status": "active", + "active_version": { + "status": "published", + "content_sha256": document.content_hash, + }, + } + ] + }, + ) + + monkeypatch.setattr(provider, "_client", lambda: _FakeClient(get_handler=get_handler)) + + first = provider.health("en-GB") + second = provider.health("en-GB") + assert first.document_count == 11 + assert first.statistics_state == "verified" + assert second.document_count == 11 + assert len(lookups) == 11 + + def test_ragcore_provider_health_reports_degraded_status(monkeypatch): provider = RAGcoreKnowledgeProvider() monkeypatch.setattr( diff --git a/docs/09-ragcore-integration.md b/docs/09-ragcore-integration.md index f222bc3..09332f6 100644 --- a/docs/09-ragcore-integration.md +++ b/docs/09-ragcore-integration.md @@ -26,12 +26,17 @@ metadata. - `reported_synced_document_count`, `reported_failed_document_count` and `last_sync_at`: the latest persisted result reported by the central n8n synchronization workflow; - `document_count`: documents independently verified as indexed by the active provider. -The current RAGcore contract deliberately has no corpus-size or space-browse endpoint. For -the RAGcore provider, `document_count` therefore remains `null`; a successful upload report -is never relabelled as proof that indexing and publishing completed. The deterministic demo -provider can verify its in-memory corpus and reports `statistics_state=verified`. RAGcore -reports `sync_reported` only when a persisted workflow callback exists, otherwise -`not_reported`. +The RAGcore contract deliberately has no corpus-size or space-browse endpoint. It does +provide an exact identity lookup through `GET /v1/documents?source_id=...&external_id=...`. +Fleet Ops uses that documented read contract concurrently and with bounded per-request +timeouts for every managed source in the requested language. A document counts only when +RAGcore returns exactly one active document in the configured space with a published active +version whose `content_sha256` matches Fleet Ops's authoritative file. Results are cached +for five minutes. This produces an independently verified `document_count` without changing +RAGcore or relabelling an upload/sync report as index evidence. If exact verification is +temporarily unavailable, `document_count` remains `null` and the sync report stays visibly +separate. Both the deterministic provider and a successful exact RAGcore check report +`statistics_state=verified`. ## Required adapter interface diff --git a/frontend/e2e/fleet-ops-correction.spec.ts b/frontend/e2e/fleet-ops-correction.spec.ts index 5426c42..500c333 100644 --- a/frontend/e2e/fleet-ops-correction.spec.ts +++ b/frontend/e2e/fleet-ops-correction.spec.ts @@ -233,25 +233,28 @@ test.describe("MO-016 status conflict is order-independent", () => { }); test.describe("knowledge base is grounded in the operator's own language", () => { - const cases: { lang: string; question: string; sourceHint: RegExp }[] = [ + const cases: { lang: string; question: string; sourceHint: RegExp; sourceTitle: string }[] = [ { lang: "nl-BE", question: "Wat moet ik doen wanneer een voertuig beschadigd terugkomt?", sourceHint: /zichtbare of gemelde schade/i, + sourceTitle: "Procedure schadeafhandeling", }, { lang: "en-GB", question: "What should I do when a vehicle returns with damage?", sourceHint: /visible or reported damage/i, + sourceTitle: "Damage handling procedure", }, { lang: "fr-BE", question: "Que dois-je faire lorsqu'un véhicule revient endommagé ?", sourceHint: /dommages visibles ou signalés/i, + sourceTitle: "Procédure de gestion des dommages", }, ]; - for (const { lang, question, sourceHint } of cases) { + for (const { lang, question, sourceHint, sourceTitle } of cases) { test(`grounded ${lang} answer cites a ${lang} source about damage`, async ({ page, request }) => { await resetDemoData(request); await loginAsOpsManager(page, lang); @@ -260,7 +263,7 @@ test.describe("knowledge base is grounded in the operator's own language", () => await page.getByRole("button", { name: /^(Vraag stellen|Ask|Demander)$/ }).click(); const localizedSource = page.locator(".source-card", { hasText: sourceHint }).first(); await expect(localizedSource).toBeVisible({ timeout: 15_000 }); - await expect(localizedSource).toContainText(/Schade bij voertuigretour|Vehicle return damage|Dommages au retour du véhicule/i); + await expect(localizedSource).toContainText(sourceTitle); }); } }); diff --git a/frontend/e2e/recruiter-polish.spec.ts b/frontend/e2e/recruiter-polish.spec.ts index 6e2502f..ab02c41 100644 --- a/frontend/e2e/recruiter-polish.spec.ts +++ b/frontend/e2e/recruiter-polish.spec.ts @@ -1,7 +1,13 @@ -import { expect, test } from "@playwright/test"; +import { expect, test, type Page } from "@playwright/test"; test.describe.configure({ mode: "serial" }); +async function loginAsOperationsManager(page: Page) { + await page.goto("/login"); + await page.getByRole("button", { name: "Verken als Operationsmanager" }).click(); + await expect(page).toHaveURL(/\/dashboard$/); +} + test("90-second recruiter entry exposes three verifiable engineering highlights", async ({ page }) => { await page.goto("/login"); await page.getByRole("button", { name: "Bekijk de highlights in 90 seconden" }).click(); @@ -14,8 +20,7 @@ test("90-second recruiter entry exposes three verifiable engineering highlights" }); test("Engineering Story exposes architecture, reliability and honest integration evidence", async ({ page }) => { - await page.goto("/login"); - await page.getByRole("button", { name: "Verken als Operationsmanager" }).click(); + await loginAsOperationsManager(page); await page.goto("/about"); await expect(page.getByRole("heading", { name: "Controle, betrouwbaarheid en uitlegbaarheid" }).first()).toBeVisible(); @@ -36,3 +41,44 @@ test("mobile recruiter surfaces remain readable without horizontal overflow", as expect(overflow).toBeLessThanOrEqual(1); await expect(page.locator(".highlight-card").first()).toBeVisible(); }); + +test("remaining attention items use a compact, legible queue action", async ({ page, request }) => { + await request.post("/api/v1/demo/login", { data: { role: "operations_manager" } }); + await request.post("/api/v1/demo/reset"); + await loginAsOperationsManager(page); + + const queueAction = page.locator(".queue-more a"); + await expect(queueAction).toContainText("Nog 2 aandachtspunten"); + await expect(queueAction).toContainText("Open de volledige werklijst"); + const actionBox = await queueAction.boundingBox(); + const arrowBox = await queueAction.locator(".queue-more-arrow svg").boundingBox(); + expect(actionBox?.height).toBeLessThanOrEqual(70); + expect(arrowBox?.width).toBeLessThanOrEqual(16); +}); + +test("Knowledge Hub renders independently verified RAGcore index evidence", async ({ page }) => { + await loginAsOperationsManager(page); + await page.route("**/api/v1/knowledge/status**", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ + provider: "ragcore", + available: true, + detail: "11/11 managed sources verified", + tenant: "northstar-mobility-demo", + workspace: "mobilityops", + collection: "internal-procedures", + document_count: 11, + source_document_count: 11, + reported_synced_document_count: 33, + reported_failed_document_count: 0, + last_sync_at: "2026-08-10T16:00:00Z", + statistics_state: "verified", + }), + }); + }); + await page.goto("/knowledge"); + + await expect(page.getByText("11 procedures geïndexeerd")).toBeVisible(); + await expect(page.getByText("actief gepubliceerd en inhoudelijk geverifieerd")).toBeVisible(); +}); diff --git a/frontend/src/i18n/locales/en-GB/dashboard.json b/frontend/src/i18n/locales/en-GB/dashboard.json index 3047233..ca18832 100644 --- a/frontend/src/i18n/locales/en-GB/dashboard.json +++ b/frontend/src/i18n/locales/en-GB/dashboard.json @@ -36,7 +36,9 @@ "title": "Attention queue", "description": "{{openIssues}} open quality issues · {{workflowExceptions}} workflow exceptions", "reviewQueue": "Review queue", - "remaining": "View {{count}} more attention item(s)", + "remaining_one": "One more attention item", + "remaining_other": "{{count}} more attention items", + "remainingHint": "Open the complete work queue", "filterPlaceholder": "Filter issues…", "filterAriaLabel": "Search attention queue", "severityAll": "All severity", diff --git a/frontend/src/i18n/locales/en-GB/knowledge.json b/frontend/src/i18n/locales/en-GB/knowledge.json index cfd92e5..6ea4844 100644 --- a/frontend/src/i18n/locales/en-GB/knowledge.json +++ b/frontend/src/i18n/locales/en-GB/knowledge.json @@ -23,11 +23,12 @@ "checkout-procedure": "Required checkout controls" }, "statistics": { + "verifiedIndexed": "actively published and content-verified", "sourceDocuments": "source documents in this language", "reportedSynced": "sync reported by n8n", "reportedFailed": "sync failures reported", - "lastSync": "Latest sync report: {{when}}. RAGcore does not expose a verifiable index size, so sync counts are not presented as indexed documents.", - "noSyncReport": "No sync report has been received yet. RAGcore does not expose a verifiable index size." + "lastSync": "Latest sync report: {{when}}. Index state is independently checked against each managed source's active RAGcore version and content hash.", + "noSyncReport": "No sync report has been received yet. Index evidence is checked separately through active RAGcore versions." }, "providerNote": "This demo answers from a small, fixed set of indexed procedures — not a live RAGcore connection. A live RAGcore backend will later take over the same interface without changing how this page works.", "askHeading": "Ask a procedure question", diff --git a/frontend/src/i18n/locales/fr-BE/dashboard.json b/frontend/src/i18n/locales/fr-BE/dashboard.json index 66d9acd..839b663 100644 --- a/frontend/src/i18n/locales/fr-BE/dashboard.json +++ b/frontend/src/i18n/locales/fr-BE/dashboard.json @@ -36,7 +36,9 @@ "title": "File d'attention", "description": "{{openIssues}} problèmes de qualité ouverts · {{workflowExceptions}} exceptions de workflow", "reviewQueue": "Examiner la file", - "remaining": "Voir encore {{count}} point(s) d’attention", + "remaining_one": "Encore un point d’attention", + "remaining_other": "Encore {{count}} points d’attention", + "remainingHint": "Ouvrir la file de travail complète", "filterPlaceholder": "Filtrer les problèmes…", "filterAriaLabel": "Rechercher dans la file d'attention", "severityAll": "Toute gravité", diff --git a/frontend/src/i18n/locales/fr-BE/knowledge.json b/frontend/src/i18n/locales/fr-BE/knowledge.json index ea8ded1..ddb09ef 100644 --- a/frontend/src/i18n/locales/fr-BE/knowledge.json +++ b/frontend/src/i18n/locales/fr-BE/knowledge.json @@ -23,11 +23,12 @@ "checkout-procedure": "Contrôles obligatoires au départ" }, "statistics": { + "verifiedIndexed": "publiées activement et contenu vérifié", "sourceDocuments": "documents sources dans cette langue", "reportedSynced": "synchronisation signalée par n8n", "reportedFailed": "échecs de synchronisation signalés", - "lastSync": "Dernier rapport de synchronisation : {{when}}. RAGcore n’expose pas de taille d’index vérifiable ; les nombres synchronisés ne sont donc pas présentés comme des documents indexés.", - "noSyncReport": "Aucun rapport de synchronisation reçu. RAGcore n’expose pas de taille d’index vérifiable." + "lastSync": "Dernier rapport de synchronisation : {{when}}. L’état de l’index est contrôlé indépendamment via la version RAGcore active et l’empreinte de contenu de chaque source gérée.", + "noSyncReport": "Aucun rapport de synchronisation reçu. Les preuves d’indexation sont contrôlées séparément via les versions RAGcore actives." }, "providerNote": "Cette démo répond à partir d'un petit ensemble fixe de procédures indexées — pas d'une connexion RAGcore en direct. Un backend RAGcore en direct reprendra plus tard la même interface sans changer le fonctionnement de cette page.", "askHeading": "Poser une question de procédure", diff --git a/frontend/src/i18n/locales/nl-BE/dashboard.json b/frontend/src/i18n/locales/nl-BE/dashboard.json index aa5a4cd..b04aeb5 100644 --- a/frontend/src/i18n/locales/nl-BE/dashboard.json +++ b/frontend/src/i18n/locales/nl-BE/dashboard.json @@ -36,7 +36,9 @@ "title": "Aandachtspunten", "description": "{{openIssues}} open datakwaliteitsproblemen · {{workflowExceptions}} automatiseringsuitzonderingen", "reviewQueue": "Wachtrij bekijken", - "remaining": "Nog {{count}} aandachtspunt(en) bekijken", + "remaining_one": "Nog één aandachtspunt", + "remaining_other": "Nog {{count}} aandachtspunten", + "remainingHint": "Open de volledige werklijst", "filterPlaceholder": "Filter aandachtspunten…", "filterAriaLabel": "Zoek in aandachtspunten", "severityAll": "Alle ernst", diff --git a/frontend/src/i18n/locales/nl-BE/knowledge.json b/frontend/src/i18n/locales/nl-BE/knowledge.json index 88adc00..ae0c33a 100644 --- a/frontend/src/i18n/locales/nl-BE/knowledge.json +++ b/frontend/src/i18n/locales/nl-BE/knowledge.json @@ -23,11 +23,12 @@ "checkout-procedure": "Verplichte vertrekcontroles" }, "statistics": { + "verifiedIndexed": "actief gepubliceerd en inhoudelijk geverifieerd", "sourceDocuments": "brondocumenten in deze taal", "reportedSynced": "sync door n8n gerapporteerd", "reportedFailed": "syncfouten gerapporteerd", - "lastSync": "Laatste syncrapport: {{when}}. RAGcore stelt geen verifieerbare indexomvang beschikbaar; syncaantallen worden daarom niet als geïndexeerde documenten voorgesteld.", - "noSyncReport": "Nog geen syncrapport ontvangen. RAGcore stelt geen verifieerbare indexomvang beschikbaar." + "lastSync": "Laatste syncrapport: {{when}}. De indexstand wordt onafhankelijk gecontroleerd via de actieve RAGcore-versie en inhoudshash van elke beheerde bron.", + "noSyncReport": "Nog geen syncrapport ontvangen. Indexbewijs wordt afzonderlijk via de actieve RAGcore-versies gecontroleerd." }, "providerNote": "Deze demo beantwoordt vanuit een kleine, vaste set geïndexeerde procedures — geen live RAGcore-koppeling. Een live RAGcore-backend zal later dezelfde interface overnemen, zonder dat deze pagina verandert.", "askHeading": "Stel een procedurevraag", diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index ea35d69..f1138d2 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -190,7 +190,16 @@ export function Dashboard() { ))} {isUnfiltered && attention.length > 6 && canSeeQuality && ( -

{t("attention.remaining", { count: attention.length - 6 })}

+

+ + + + {t("attention.remaining", { count: attention.length - 6 })} + {t("attention.remainingHint")} + + + +

)} )} diff --git a/frontend/src/pages/Knowledge.tsx b/frontend/src/pages/Knowledge.tsx index 875c687..7f8c8b6 100644 --- a/frontend/src/pages/Knowledge.tsx +++ b/frontend/src/pages/Knowledge.tsx @@ -154,6 +154,7 @@ export function Knowledge() { )} {statusSettled && status?.provider === "ragcore" && (
+
{status.document_count ?? "—"}{t("statistics.verifiedIndexed")}
{status.source_document_count}{t("statistics.sourceDocuments")}
{status.reported_synced_document_count ?? "—"} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 5ac6705..fe1072a 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -182,7 +182,7 @@ a:hover { color: var(--teal); } .inline-action { display: inline-flex; align-items: center; gap: 5px; padding: 0 18px; color: var(--teal-dark); text-decoration: none; font-size: .72rem; font-weight: 700; white-space: nowrap; } .inline-action svg { width: 14px; } -.operations-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(320px, .75fr); gap: 16px; margin-bottom: 16px; } +.operations-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(320px, .75fr); align-items: start; gap: 16px; margin-bottom: 16px; } .secondary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } .work-panel, .panel, .record-surface, .table-shell { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); } .panel { padding: 20px; } @@ -195,7 +195,15 @@ a:hover { color: var(--teal); } .attention-list, .integration-list, .recent-list, .record-list, .automation-list, .today-list { list-style: none; margin: 0; padding: 0; } .attention-list li { min-height: 68px; display: grid; grid-template-columns: auto minmax(0,1fr) auto 16px; align-items: center; gap: 11px; padding: 10px 18px; border-bottom: 1px solid #e8edf2; transition: background .14s ease; } .attention-list li:last-child { border-bottom: 0; }.attention-list li:hover { background: #f9fbfc; } -.queue-more { margin: 0; padding: 13px 18px; border-top: 1px solid var(--line); font-size: var(--type-meta); font-weight: 700; }.queue-more a { display: inline-flex; align-items: center; gap: 5px; } +.queue-more { margin: 0; padding: 10px 12px; border-top: 1px solid var(--line); background: var(--surface-subtle); } +.queue-more a { display: grid; grid-template-columns: 34px minmax(0, 1fr) 30px; align-items: center; gap: 10px; min-height: 52px; padding: 7px 8px; color: var(--ink); background: white; border: 1px solid var(--line); border-radius: var(--radius); text-decoration: none; transition: border-color .14s ease, box-shadow .14s ease, transform .14s ease; } +.queue-more a:hover { border-color: var(--teal); box-shadow: 0 5px 16px rgba(13, 126, 116, .09); transform: translateY(-1px); } +.queue-more-count { width: 34px; height: 34px; display: grid; place-items: center; color: var(--teal-dark); background: var(--teal-pale); border-radius: 50%; font-size: .78rem; font-weight: 800; } +.queue-more-copy { min-width: 0; display: grid; gap: 2px; } +.queue-more-copy strong { font-size: .75rem; line-height: 1.25; } +.queue-more-copy small { color: var(--muted); font-size: .64rem; font-weight: 500; } +.queue-more-arrow { width: 30px; height: 30px; display: grid; place-items: center; color: var(--teal-dark); background: var(--teal-pale); border-radius: 50%; } +.queue-more-arrow svg { width: 14px; height: 14px; } .attention-title { margin: 0; font-size: .78rem; font-weight: 700; }.attention-title a { color: var(--ink); text-decoration: none; } .attention-detail { max-width: 58ch; margin: 4px 0 0; color: var(--muted); font-size: .69rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .queue-ref { color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .65rem; }.row-chevron { width: 14px; color: var(--muted-light); } @@ -475,11 +483,12 @@ details summary { cursor: pointer; color: var(--teal-dark); }.data-table details .knowledge-provider-note { display: flex; align-items: flex-start; gap: 8px; margin: -6px 0 18px; padding: 10px 14px; color: var(--ink-soft); background: var(--info-pale); border: 1px solid #cfe3ee; border-radius: var(--radius); font-size: .72rem; line-height: 1.5; } .knowledge-provider-note svg { width: 15px; flex-shrink: 0; margin-top: 1px; color: var(--info); } -.knowledge-index-evidence { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin: -6px 0 18px; overflow: hidden; background: var(--line); border: 1px solid var(--line); border-radius: var(--radius); } +.knowledge-index-evidence { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: -6px 0 18px; overflow: hidden; background: var(--line); border: 1px solid var(--line); border-radius: var(--radius); } .knowledge-index-evidence > div { display: grid; gap: 3px; padding: 11px 14px; background: white; } .knowledge-index-evidence strong { font-size: .86rem; } .knowledge-index-evidence span, .knowledge-index-evidence p { color: var(--muted); font-size: .66rem; line-height: 1.45; } .knowledge-index-evidence p { grid-column: 1 / -1; margin: 0; padding: 9px 14px; background: var(--surface-subtle); } +@media (max-width: 640px) { .knowledge-index-evidence { grid-template-columns: repeat(2, minmax(0, 1fr)); } } .knowledge-suggestions { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-top: 12px; } .knowledge-suggestions > span { color: var(--muted); font-size: .68rem; font-weight: 700; } .suggestion-chip { padding: 6px 11px; color: var(--teal-dark); background: var(--teal-pale); border: 1px solid #bfe6df; border-radius: 999px; font-size: .68rem; font-weight: 600; cursor: pointer; }