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
+23
View File
@@ -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.
+7 -1
View File
@@ -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"
),
}
)
+85 -10
View File
@@ -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="",
+92 -2
View File
@@ -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(
+11 -6
View File
@@ -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
+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; }