M31: verify RAG inventory and polish attention queue
MobilityOps acceptance / backend (push) Canceled after 0s
MobilityOps acceptance / frontend (push) Canceled after 0s

This commit is contained in:
NuklearRabbit
2026-08-10 20:58:26 +02:00
parent cfffb1ce54
commit efab8d816f
16 changed files with 314 additions and 38 deletions
+6 -3
View File
@@ -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);
});
}
});
+49 -3
View File
@@ -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) dattention",
"remaining_one": "Encore un point dattention",
"remaining_other": "Encore {{count}} points dattention",
"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 nexpose pas de taille dindex 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 nexpose pas de taille dindex vérifiable."
"lastSync": "Dernier rapport de synchronisation : {{when}}. L’état de lindex est contrôlé indépendamment via la version RAGcore active et lempreinte de contenu de chaque source gérée.",
"noSyncReport": "Aucun rapport de synchronisation reçu. Les preuves dindexation 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",
+10 -1
View File
@@ -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>
)}
</>
)}
+1
View File
@@ -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
View File
@@ -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; }