M31: verify RAG inventory and polish attention queue
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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="",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -190,7 +190,16 @@ export function Dashboard() {
|
||||
</div>
|
||||
))}
|
||||
{isUnfiltered && attention.length > 6 && canSeeQuality && (
|
||||
<p className="queue-more"><Link to="/data-quality">{t("attention.remaining", { count: attention.length - 6 })} <Icon name="chevron" /></Link></p>
|
||||
<p className="queue-more">
|
||||
<Link to="/data-quality">
|
||||
<span className="queue-more-count" aria-hidden="true">{attention.length - 6}</span>
|
||||
<span className="queue-more-copy">
|
||||
<strong>{t("attention.remaining", { count: attention.length - 6 })}</strong>
|
||||
<small>{t("attention.remainingHint")}</small>
|
||||
</span>
|
||||
<span className="queue-more-arrow"><Icon name="chevron" /></span>
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -154,6 +154,7 @@ export function Knowledge() {
|
||||
)}
|
||||
{statusSettled && status?.provider === "ragcore" && (
|
||||
<div className="knowledge-index-evidence" role="status">
|
||||
<div><strong>{status.document_count ?? "—"}</strong><span>{t("statistics.verifiedIndexed")}</span></div>
|
||||
<div><strong>{status.source_document_count}</strong><span>{t("statistics.sourceDocuments")}</span></div>
|
||||
<div>
|
||||
<strong>{status.reported_synced_document_count ?? "—"}</strong>
|
||||
|
||||
+12
-3
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user