/** * AutomationsDashboardPage * * System-level dashboard for workflow runs across all features and mandates. * Uses /api/system/workflow-runs endpoints with RBAC scoping. */ import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { FaSync, FaPlay, FaCog, FaChartBar, FaDownload } from 'react-icons/fa'; import { FormGeneratorTable, type ColumnConfig } from '../components/FormGenerator'; import { useToast } from '../contexts/ToastContext'; import { formatUnixTimestamp } from '../utils/time'; import api from '../api'; import { useLanguage } from '../providers/language/LanguageContext'; import styles from './admin/Admin.module.css'; interface WorkflowRunMetrics { totalRuns: number; runsByStatus: Record; totalTokens: number; totalCredits: number; workflowCount: number; activeWorkflows: number; } interface WorkflowRun { id: string; workflowId: string; workflowLabel?: string; mandateId?: string; ownerId?: string; status: string; costTokens?: number; costCredits?: number; sysCreatedAt?: number; sysModifiedAt?: number; } function _formatTs(ts?: number): string { if (ts == null || ts <= 0) return '—'; const sec = ts < 1e12 ? ts : ts / 1000; const { time } = formatUnixTimestamp(sec, undefined, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); return time; } const _STATUS_COLORS: Record = { completed: 'var(--success-color, #28a745)', failed: 'var(--danger-color, #dc3545)', running: 'var(--primary-color, #007bff)', paused: 'var(--warning-color, #ffc107)', cancelled: 'var(--text-secondary, #666)', }; interface MetricCardProps { icon: React.ReactNode; label: string; value: string | number; color?: string; } const MetricCard: React.FC = ({ icon, label, value, color }) => (
{icon}
{label}
{value}
); export const AutomationsDashboardPage: React.FC = () => { const { t } = useLanguage(); const { showError } = useToast(); const [metrics, setMetrics] = useState(null); const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(true); const _load = useCallback(async () => { setLoading(true); try { const [metricsResp, runsResp] = await Promise.all([ api.get('/api/system/workflow-runs/metrics'), api.get('/api/system/workflow-runs', { params: { limit: 50 } }), ]); setMetrics(metricsResp.data); setRuns(runsResp.data?.runs || []); } catch (e) { console.error('[automations] dashboard load failed', e); showError(t('Fehler beim Laden des Automations-Dashboards')); } finally { setLoading(false); } }, [showError, t]); useEffect(() => { _load(); }, [_load]); const _downloadRunTracing = useCallback(async (run: WorkflowRun) => { if (!run.id) return; try { const resp = await api.get(`/api/system/workflow-runs/${run.id}/steps`); const steps = resp.data?.steps || []; const report = { runId: run.id, workflowId: run.workflowId, workflowLabel: run.workflowLabel, status: run.status, startedAt: _formatTs(run.sysCreatedAt), endedAt: _formatTs(run.sysModifiedAt), steps, }; const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `run-tracing-${run.id.slice(0, 8)}.json`; a.click(); URL.revokeObjectURL(url); } catch (e) { console.error('[automations] download tracing failed', e); showError(t('Download fehlgeschlagen')); } }, [showError, t]); const _runColumns: ColumnConfig[] = useMemo(() => [ { key: 'workflowLabel', label: t('Workflow'), type: 'string', width: 200, sortable: true, formatter: (v: string, row: WorkflowRun) => v || row.workflowId || t('—'), }, { key: 'mandateId', label: t('Mandant'), type: 'string', width: 120, sortable: true, filterable: true, formatter: (v: string) => v ? v.slice(0, 8) + '…' : t('—'), }, { key: 'status', label: t('Status'), type: 'string', width: 110, sortable: true, filterable: true, formatter: (v: string) => ( {v} ), }, { key: 'sysCreatedAt', label: t('Gestartet'), type: 'number', width: 150, sortable: true, formatter: (v: number) => _formatTs(v), }, { key: 'sysModifiedAt', label: t('Beendet'), type: 'number', width: 150, formatter: (v: number) => _formatTs(v), }, { key: 'id', label: '', type: 'string', width: 50, sortable: false, formatter: (_v: string, row: WorkflowRun) => ( ), }, ], [t, _downloadRunTracing]); return (

{t('Automations')}

{t('Workflow-Runs über alle Features und Mandanten')}

} label={t('Workflows')} value={metrics?.workflowCount ?? t('—')} /> } label={t('Aktive Workflows')} value={metrics?.activeWorkflows ?? t('—')} color="var(--success-color, #28a745)" /> } label={t('Runs gesamt')} value={metrics?.totalRuns ?? t('—')} />
{metrics?.runsByStatus && Object.keys(metrics.runsByStatus).length > 0 && (

{t('Läufe nach Status')}

{Object.entries(metrics.runsByStatus).map(([status, count]) => ( {status}: {count} ))}
)} {metrics && (metrics.totalTokens > 0 || metrics.totalCredits > 0) && (
{metrics.totalTokens > 0 && (
{t('Tokens gesamt:')} {metrics.totalTokens.toLocaleString('de-DE')}
)} {metrics.totalCredits > 0 && (
{t('Credits gesamt:')} {metrics.totalCredits.toLocaleString('de-DE', { minimumFractionDigits: 2 })}
)}
)}

{t('Letzte Runs')}

data={runs} columns={_runColumns} loading={loading} pagination={true} pageSize={15} searchable={true} filterable={true} sortable={true} selectable={false} emptyMessage={t('Noch keine Workflow-Runs vorhanden.')} />
); }; export default AutomationsDashboardPage;