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:
co-authored by
Claude Sonnet 5
parent
e39c0a1dd6
commit
4049c0c6b1
@@ -223,6 +223,18 @@ class SearchResponse(BaseModel):
|
|||||||
results: list[SearchResultItem]
|
results: list[SearchResultItem]
|
||||||
|
|
||||||
|
|
||||||
|
class N8nWorkflowEvidence(BaseModel):
|
||||||
|
name: str
|
||||||
|
built: bool
|
||||||
|
last_seen_at: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class N8nErrorHandlerStatus(BaseModel):
|
||||||
|
total_failures_registered: int
|
||||||
|
latest_failure_at: datetime | None
|
||||||
|
latest_failure_workflow: str | None
|
||||||
|
|
||||||
|
|
||||||
class N8nIntegrationStatus(BaseModel):
|
class N8nIntegrationStatus(BaseModel):
|
||||||
configured: bool
|
configured: bool
|
||||||
dispatch_enabled: bool
|
dispatch_enabled: bool
|
||||||
@@ -233,6 +245,10 @@ class N8nIntegrationStatus(BaseModel):
|
|||||||
succeeded: int
|
succeeded: int
|
||||||
latest_success_at: datetime | None
|
latest_success_at: datetime | None
|
||||||
latest_failure_at: datetime | None
|
latest_failure_at: datetime | None
|
||||||
|
expected_workflow_count: int
|
||||||
|
known_workflow_count: int
|
||||||
|
workflows: list[N8nWorkflowEvidence]
|
||||||
|
error_handler: N8nErrorHandlerStatus
|
||||||
|
|
||||||
|
|
||||||
class McpHubIntegrationStatus(BaseModel):
|
class McpHubIntegrationStatus(BaseModel):
|
||||||
|
|||||||
@@ -6,11 +6,21 @@ from sqlalchemy import func, select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
|
from app.models.audit import AuditEvent
|
||||||
from app.models.outbox import OutboxEvent
|
from app.models.outbox import OutboxEvent
|
||||||
from app.schemas import N8nIntegrationStatus
|
from app.schemas import N8nErrorHandlerStatus, N8nIntegrationStatus, N8nWorkflowEvidence
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
|
# The 4 canonical Fleet Ops n8n workflows (see n8n/workflows/MANIFEST.md). Workflow 3
|
||||||
|
# (RAGcore Procedure Sync) is not built yet, so it always reports no evidence.
|
||||||
|
_CANONICAL_WORKFLOWS = (
|
||||||
|
"Fleet Ops — Vehicle Return Orchestration",
|
||||||
|
"Fleet Ops — Scheduled Data Quality Scan",
|
||||||
|
"Fleet Ops — RAGcore Procedure Sync",
|
||||||
|
"Fleet Ops — Workflow Error Handler",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def derive_n8n_status(db: Session) -> N8nIntegrationStatus:
|
def derive_n8n_status(db: Session) -> N8nIntegrationStatus:
|
||||||
counts: dict[str, int] = dict(
|
counts: dict[str, int] = dict(
|
||||||
@@ -42,6 +52,52 @@ def derive_n8n_status(db: Session) -> N8nIntegrationStatus:
|
|||||||
else:
|
else:
|
||||||
state = "no_evidence"
|
state = "no_evidence"
|
||||||
|
|
||||||
|
# Scheduled scan evidence: only service-triggered runs count as n8n evidence, not
|
||||||
|
# runs an operator triggered manually from the Data Quality page.
|
||||||
|
latest_scan_at = db.scalar(
|
||||||
|
select(func.max(AuditEvent.occurred_at)).where(
|
||||||
|
AuditEvent.action == "data_quality_scan_run",
|
||||||
|
AuditEvent.actor_type == "service",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Error handler evidence: registrations posted by the "Fleet Ops — Workflow Error
|
||||||
|
# Handler" n8n workflow itself, which also doubles as proof that workflow is wired
|
||||||
|
# up and firing correctly.
|
||||||
|
total_failures_registered = (
|
||||||
|
db.scalar(
|
||||||
|
select(func.count(AuditEvent.id)).where(
|
||||||
|
AuditEvent.action == "n8n_workflow_failure_registered"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
latest_failure_row = db.execute(
|
||||||
|
select(AuditEvent.occurred_at, AuditEvent.after_json)
|
||||||
|
.where(AuditEvent.action == "n8n_workflow_failure_registered")
|
||||||
|
.order_by(AuditEvent.occurred_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
).first()
|
||||||
|
latest_handler_failure_at = latest_failure_row[0] if latest_failure_row else None
|
||||||
|
latest_handler_failure_workflow = (
|
||||||
|
(latest_failure_row[1] or {}).get("workflow_name") if latest_failure_row else None
|
||||||
|
)
|
||||||
|
|
||||||
|
evidence_by_workflow = {
|
||||||
|
"Fleet Ops — Vehicle Return Orchestration": latest_success_at,
|
||||||
|
"Fleet Ops — Scheduled Data Quality Scan": latest_scan_at,
|
||||||
|
"Fleet Ops — RAGcore Procedure Sync": None,
|
||||||
|
"Fleet Ops — Workflow Error Handler": latest_handler_failure_at,
|
||||||
|
}
|
||||||
|
workflows = [
|
||||||
|
N8nWorkflowEvidence(
|
||||||
|
name=name,
|
||||||
|
built=name != "Fleet Ops — RAGcore Procedure Sync",
|
||||||
|
last_seen_at=evidence_by_workflow[name],
|
||||||
|
)
|
||||||
|
for name in _CANONICAL_WORKFLOWS
|
||||||
|
]
|
||||||
|
|
||||||
return N8nIntegrationStatus(
|
return N8nIntegrationStatus(
|
||||||
configured=bool(settings.n8n_webhook_url),
|
configured=bool(settings.n8n_webhook_url),
|
||||||
dispatch_enabled=settings.n8n_dispatch_enabled,
|
dispatch_enabled=settings.n8n_dispatch_enabled,
|
||||||
@@ -52,4 +108,12 @@ def derive_n8n_status(db: Session) -> N8nIntegrationStatus:
|
|||||||
succeeded=succeeded,
|
succeeded=succeeded,
|
||||||
latest_success_at=latest_success_at,
|
latest_success_at=latest_success_at,
|
||||||
latest_failure_at=latest_failure_at,
|
latest_failure_at=latest_failure_at,
|
||||||
|
expected_workflow_count=len(_CANONICAL_WORKFLOWS),
|
||||||
|
known_workflow_count=sum(1 for w in workflows if w.last_seen_at is not None),
|
||||||
|
workflows=workflows,
|
||||||
|
error_handler=N8nErrorHandlerStatus(
|
||||||
|
total_failures_registered=total_failures_registered,
|
||||||
|
latest_failure_at=latest_handler_failure_at,
|
||||||
|
latest_failure_workflow=latest_handler_failure_workflow,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,3 +39,82 @@ def test_integration_status_is_operational_once_all_failed_events_resolved(ops_c
|
|||||||
body = response.json()["n8n"]
|
body = response.json()["n8n"]
|
||||||
assert body["failed"] == 0
|
assert body["failed"] == 0
|
||||||
assert body["state"] == "operational"
|
assert body["state"] == "operational"
|
||||||
|
|
||||||
|
|
||||||
|
def test_integration_status_lists_all_four_canonical_workflows(ops_client):
|
||||||
|
body = ops_client.get("/api/v1/integrations/status").json()["n8n"]
|
||||||
|
assert body["expected_workflow_count"] == 4
|
||||||
|
names = {w["name"] for w in body["workflows"]}
|
||||||
|
assert names == {
|
||||||
|
"Fleet Ops — Vehicle Return Orchestration",
|
||||||
|
"Fleet Ops — Scheduled Data Quality Scan",
|
||||||
|
"Fleet Ops — RAGcore Procedure Sync",
|
||||||
|
"Fleet Ops — Workflow Error Handler",
|
||||||
|
}
|
||||||
|
ragcore_sync = next(w for w in body["workflows"] if "RAGcore" in w["name"])
|
||||||
|
assert ragcore_sync["built"] is False
|
||||||
|
assert ragcore_sync["last_seen_at"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_integration_status_scheduled_scan_evidence_only_counts_service_runs(client, ops_client):
|
||||||
|
from app.core.config import get_settings
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
before = ops_client.get("/api/v1/integrations/status").json()["n8n"]
|
||||||
|
scan_workflow = next(
|
||||||
|
w for w in before["workflows"] if w["name"].endswith("Scheduled Data Quality Scan")
|
||||||
|
)
|
||||||
|
assert scan_workflow["last_seen_at"] is None
|
||||||
|
|
||||||
|
scan = client.post(
|
||||||
|
"/api/v1/integrations/n8n/scheduled-scan",
|
||||||
|
headers={"X-Service-Token": settings.n8n_callback_token},
|
||||||
|
)
|
||||||
|
assert scan.status_code == 200
|
||||||
|
|
||||||
|
after = ops_client.get("/api/v1/integrations/status").json()["n8n"]
|
||||||
|
scan_workflow = next(
|
||||||
|
w for w in after["workflows"] if w["name"].endswith("Scheduled Data Quality Scan")
|
||||||
|
)
|
||||||
|
assert scan_workflow["last_seen_at"] is not None
|
||||||
|
assert after["known_workflow_count"] > before["known_workflow_count"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_integration_status_reflects_error_handler_registrations(client, ops_client):
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from app.core.config import get_settings
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
before = ops_client.get("/api/v1/integrations/status").json()["n8n"]
|
||||||
|
|
||||||
|
execution_id = str(uuid.uuid4())
|
||||||
|
report = client.post(
|
||||||
|
"/api/v1/integrations/n8n/workflow-error",
|
||||||
|
json={
|
||||||
|
"workflow_id": "mobilityops-return-processing",
|
||||||
|
"workflow_name": "Fleet Ops — Vehicle Return Orchestration",
|
||||||
|
"execution_id": execution_id,
|
||||||
|
"failed_at": "2026-08-04T10:15:00Z",
|
||||||
|
"error_category": "httpError",
|
||||||
|
"error_summary": "Simulated failure for status test",
|
||||||
|
"trigger_context": "webhook",
|
||||||
|
"attempt": 1,
|
||||||
|
},
|
||||||
|
headers={"X-Service-Token": settings.n8n_callback_token},
|
||||||
|
)
|
||||||
|
assert report.status_code == 200
|
||||||
|
|
||||||
|
after = ops_client.get("/api/v1/integrations/status").json()["n8n"]
|
||||||
|
assert (
|
||||||
|
after["error_handler"]["total_failures_registered"]
|
||||||
|
== before["error_handler"]["total_failures_registered"] + 1
|
||||||
|
)
|
||||||
|
assert after["error_handler"]["latest_failure_workflow"] == (
|
||||||
|
"Fleet Ops — Vehicle Return Orchestration"
|
||||||
|
)
|
||||||
|
handler_workflow = next(
|
||||||
|
w for w in after["workflows"] if w["name"].endswith("Workflow Error Handler")
|
||||||
|
)
|
||||||
|
assert handler_workflow["last_seen_at"] is not None
|
||||||
|
|||||||
@@ -254,6 +254,18 @@ export interface KnowledgeHealth {
|
|||||||
document_count: number;
|
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 {
|
export interface N8nIntegrationStatus {
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
dispatch_enabled: boolean;
|
dispatch_enabled: boolean;
|
||||||
@@ -264,6 +276,10 @@ export interface N8nIntegrationStatus {
|
|||||||
succeeded: number;
|
succeeded: number;
|
||||||
latest_success_at: string | null;
|
latest_success_at: string | null;
|
||||||
latest_failure_at: string | null;
|
latest_failure_at: string | null;
|
||||||
|
expected_workflow_count: number;
|
||||||
|
known_workflow_count: number;
|
||||||
|
workflows: N8nWorkflowEvidence[];
|
||||||
|
error_handler: N8nErrorHandlerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface McpHubIntegrationStatus {
|
export interface McpHubIntegrationStatus {
|
||||||
|
|||||||
@@ -17,3 +17,13 @@ export const MCP_STATE_META: Record<IntegrationStatus["mcp_hub"]["state"], { sta
|
|||||||
not_configured: { statusClass: "not_configured", labelKey: "notConnected" },
|
not_configured: { statusClass: "not_configured", labelKey: "notConnected" },
|
||||||
configured: { statusClass: "no_events", labelKey: "prepared" },
|
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",
|
"demoMode": "Demo mode",
|
||||||
"unavailable": "Unavailable"
|
"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": {
|
"ledger": {
|
||||||
"title": "Automation jobs",
|
"title": "Automation jobs",
|
||||||
"description": "Persisted automation attempts with the latest failure evidence.",
|
"description": "Persisted automation attempts with the latest failure evidence.",
|
||||||
|
|||||||
@@ -26,6 +26,28 @@
|
|||||||
"demoMode": "Mode démo",
|
"demoMode": "Mode démo",
|
||||||
"unavailable": "Indisponible"
|
"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": {
|
"ledger": {
|
||||||
"title": "Tâches d'automatisation",
|
"title": "Tâches d'automatisation",
|
||||||
"description": "Tentatives d'automatisation enregistrées avec les dernières preuves d'échec.",
|
"description": "Tentatives d'automatisation enregistrées avec les dernières preuves d'échec.",
|
||||||
|
|||||||
@@ -26,6 +26,28 @@
|
|||||||
"demoMode": "Demomodus",
|
"demoMode": "Demomodus",
|
||||||
"unavailable": "Niet beschikbaar"
|
"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": {
|
"ledger": {
|
||||||
"title": "Automatiseringsopdrachten",
|
"title": "Automatiseringsopdrachten",
|
||||||
"description": "Vastgelegde automatiseringspogingen met de recentste foutevidentie.",
|
"description": "Vastgelegde automatiseringspogingen met de recentste foutevidentie.",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { StatusBadge } from "../components/Badge";
|
|||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useLocaleFormat } from "../i18n/format";
|
import { useLocaleFormat } from "../i18n/format";
|
||||||
import { ApiErrorNotice, ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
|
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";
|
type ViewFilter = "attention" | "recent" | "succeeded" | "all";
|
||||||
|
|
||||||
@@ -218,6 +218,73 @@ export function Automation() {
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</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")} />
|
<SectionHeading title={t("ledger.title")} description={t("ledger.description")} />
|
||||||
|
|
||||||
<form className="filters" aria-label={t("ledger.title")}>
|
<form className="filters" aria-label={t("ledger.title")}>
|
||||||
|
|||||||
Reference in New Issue
Block a user