n8n: surface real per-workflow evidence on the integration status page

Fleet Ops integration status no longer depends only on a config
boolean or the most recent outbox event: N8nIntegrationStatus now
reports per-canonical-workflow evidence (last successful outbox
delivery for the return workflow, latest service-triggered
data_quality_scan_run for the scan workflow, latest
n8n_workflow_failure_registered for the error handler, and "not built"
for the still-blocked RAGcore sync), plus an error-handler summary
(total failures registered, latest failure + which workflow).

Automation page renders this as a localized workflow table (EN/NL/FR)
with technical workflow names tucked under a "Technical details"
disclosure, matching the existing progressive-disclosure pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-04 16:15:09 +02:00
co-authored by Claude Sonnet 5
parent e39c0a1dd6
commit 4049c0c6b1
9 changed files with 320 additions and 2 deletions
+16
View File
@@ -254,6 +254,18 @@ export interface KnowledgeHealth {
document_count: number;
}
export interface N8nWorkflowEvidence {
name: string;
built: boolean;
last_seen_at: string | null;
}
export interface N8nErrorHandlerStatus {
total_failures_registered: number;
latest_failure_at: string | null;
latest_failure_workflow: string | null;
}
export interface N8nIntegrationStatus {
configured: boolean;
dispatch_enabled: boolean;
@@ -264,6 +276,10 @@ export interface N8nIntegrationStatus {
succeeded: number;
latest_success_at: string | null;
latest_failure_at: string | null;
expected_workflow_count: number;
known_workflow_count: number;
workflows: N8nWorkflowEvidence[];
error_handler: N8nErrorHandlerStatus;
}
export interface McpHubIntegrationStatus {
+10
View File
@@ -17,3 +17,13 @@ export const MCP_STATE_META: Record<IntegrationStatus["mcp_hub"]["state"], { sta
not_configured: { statusClass: "not_configured", labelKey: "notConnected" },
configured: { statusClass: "no_events", labelKey: "prepared" },
};
// Maps the backend's canonical n8n workflow name (see n8n/workflows/MANIFEST.md) to a
// stable slug for localized display names in `integrations:workflows.names.*` — the
// backend name itself is a technical identifier, not something to show untranslated.
export const N8N_WORKFLOW_SLUGS: Record<string, string> = {
"Fleet Ops — Vehicle Return Orchestration": "vehicleReturn",
"Fleet Ops — Scheduled Data Quality Scan": "scheduledScan",
"Fleet Ops — RAGcore Procedure Sync": "ragcoreSync",
"Fleet Ops — Workflow Error Handler": "errorHandler",
};
@@ -26,6 +26,28 @@
"demoMode": "Demo mode",
"unavailable": "Unavailable"
},
"workflows": {
"title": "Automation workflows",
"description": "{{known}} of {{expected}} canonical n8n workflows have live evidence of running.",
"notBuilt": "Not built yet",
"noEvidence": "No evidence yet",
"columns": {
"name": "Workflow",
"status": "Status",
"lastSeen": "Last evidence",
"technicalId": "Details"
},
"names": {
"vehicleReturn": "Vehicle return orchestration",
"scheduledScan": "Scheduled data quality scan",
"ragcoreSync": "Knowledge procedure sync",
"errorHandler": "Workflow error handler"
},
"errorHandler": {
"summary": "{{count}} automation failure(s) registered — latest from {{workflow}} at {{when}}.",
"summaryEmpty": "No automation failures have been registered."
}
},
"ledger": {
"title": "Automation jobs",
"description": "Persisted automation attempts with the latest failure evidence.",
@@ -26,6 +26,28 @@
"demoMode": "Mode démo",
"unavailable": "Indisponible"
},
"workflows": {
"title": "Workflows d'automatisation",
"description": "{{known}} workflows n8n canoniques sur {{expected}} disposent de preuves actuelles de fonctionnement.",
"notBuilt": "Pas encore créé",
"noEvidence": "Aucune preuve pour l'instant",
"columns": {
"name": "Workflow",
"status": "Statut",
"lastSeen": "Dernière preuve",
"technicalId": "Détails"
},
"names": {
"vehicleReturn": "Orchestration du retour de véhicule",
"scheduledScan": "Analyse planifiée de la qualité des données",
"ragcoreSync": "Synchronisation des procédures de connaissances",
"errorHandler": "Gestionnaire d'erreurs de workflow"
},
"errorHandler": {
"summary": "{{count}} échec(s) d'automatisation enregistré(s) — le dernier provient de {{workflow}} à {{when}}.",
"summaryEmpty": "Aucun échec d'automatisation n'a été enregistré."
}
},
"ledger": {
"title": "Tâches d'automatisation",
"description": "Tentatives d'automatisation enregistrées avec les dernières preuves d'échec.",
@@ -26,6 +26,28 @@
"demoMode": "Demomodus",
"unavailable": "Niet beschikbaar"
},
"workflows": {
"title": "Automatiseringsworkflows",
"description": "{{known}} van {{expected}} canonieke n8n-workflows hebben actuele evidentie van werking.",
"notBuilt": "Nog niet gebouwd",
"noEvidence": "Nog geen evidentie",
"columns": {
"name": "Workflow",
"status": "Status",
"lastSeen": "Laatste evidentie",
"technicalId": "Details"
},
"names": {
"vehicleReturn": "Voertuigretour-orkestratie",
"scheduledScan": "Geplande datakwaliteitsscan",
"ragcoreSync": "Synchronisatie kennisprocedures",
"errorHandler": "Workflowfoutafhandelaar"
},
"errorHandler": {
"summary": "{{count}} automatiseringsfout(en) geregistreerd — laatste van {{workflow}} om {{when}}.",
"summaryEmpty": "Er zijn geen automatiseringsfouten geregistreerd."
}
},
"ledger": {
"title": "Automatiseringsopdrachten",
"description": "Vastgelegde automatiseringspogingen met de recentste foutevidentie.",
+68 -1
View File
@@ -7,7 +7,7 @@ import { StatusBadge } from "../components/Badge";
import { useAuth } from "../context/AuthContext";
import { useLocaleFormat } from "../i18n/format";
import { ApiErrorNotice, ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
import { N8N_STATE_META, MCP_STATE_META } from "../data/integrationLabels";
import { N8N_STATE_META, MCP_STATE_META, N8N_WORKFLOW_SLUGS } from "../data/integrationLabels";
type ViewFilter = "attention" | "recent" | "succeeded" | "all";
@@ -218,6 +218,73 @@ export function Automation() {
</article>
</section>
<SectionHeading
title={t("workflows.title")}
description={t("workflows.description", {
known: integrationStatus?.n8n.known_workflow_count ?? 0,
expected: integrationStatus?.n8n.expected_workflow_count ?? 4,
})}
/>
{integrationStatus && (
<div className="table-shell">
<table className="data-table">
<caption className="visually-hidden">{t("workflows.title")}</caption>
<thead>
<tr>
<th scope="col">{t("workflows.columns.name")}</th>
<th scope="col">{t("workflows.columns.status")}</th>
<th scope="col">{t("workflows.columns.lastSeen")}</th>
<th scope="col">{t("workflows.columns.technicalId")}</th>
</tr>
</thead>
<tbody>
{integrationStatus.n8n.workflows.map((w) => {
const slug = N8N_WORKFLOW_SLUGS[w.name] ?? "";
const displayName = slug ? t(`workflows.names.${slug}`) : w.name;
const badge = !w.built
? { status: "not_configured", label: t("workflows.notBuilt") }
: w.last_seen_at
? { status: "available", label: t("statusLabels.operational") }
: { status: "no_events", label: t("workflows.noEvidence") };
return (
<tr key={w.name}>
<td data-label={t("workflows.columns.name")}>{displayName}</td>
<td data-label={t("workflows.columns.status")}>
<StatusBadge status={badge.status} label={badge.label} />
</td>
<td data-label={t("workflows.columns.lastSeen")}>
{w.last_seen_at ? (
<time dateTime={w.last_seen_at}>{formatDateTime(w.last_seen_at)}</time>
) : (
"—"
)}
</td>
<td data-label={t("workflows.columns.technicalId")}>
<details className="evidence-disclosure">
<summary>{t("common:actions.technicalDetails")}</summary>
<span className="table-subtext">{w.name}</span>
</details>
</td>
</tr>
);
})}
</tbody>
</table>
<p className="table-meta">
{integrationStatus.n8n.error_handler.total_failures_registered > 0
? t("workflows.errorHandler.summary", {
count: integrationStatus.n8n.error_handler.total_failures_registered,
workflow: integrationStatus.n8n.error_handler.latest_failure_workflow ?? "",
when: integrationStatus.n8n.error_handler.latest_failure_at
? formatDateTime(integrationStatus.n8n.error_handler.latest_failure_at)
: "",
})
: t("workflows.errorHandler.summaryEmpty")}
</p>
</div>
)}
<SectionHeading title={t("ledger.title")} description={t("ledger.description")} />
<form className="filters" aria-label={t("ledger.title")}>