From abd7a485b98ad0a4c85eef71f1ccd9c05187f321 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 13:02:09 +0000 Subject: [PATCH] feat(CC-080): add Ingestion Jobs page (data sources + sync events + manual trigger) --- src/app/dashboard/data/ingestion/page.tsx | 152 ++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/app/dashboard/data/ingestion/page.tsx diff --git a/src/app/dashboard/data/ingestion/page.tsx b/src/app/dashboard/data/ingestion/page.tsx new file mode 100644 index 0000000..a4c64bf --- /dev/null +++ b/src/app/dashboard/data/ingestion/page.tsx @@ -0,0 +1,152 @@ +'use client'; + +import { useMemo } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface DataSource { + id: string; name: string; type: string; status: string; lastSyncAt: string | null; + errorCount: number | null; recordCount: number | null; config: Record | null; + createdAt: string; +} +interface AuditEntry { + id: string; action: string; entityType: string | null; entityId: string | null; + details: Record | null; createdAt: string; +} + +const STATUS_META: Record = { + active: { cls: 'bg-signal-ok/10 text-signal-ok', label: 'Activ' }, + error: { cls: 'bg-signal-danger/10 text-signal-danger', label: 'Eroare' }, + paused: { cls: 'bg-muted text-ink-faint', label: 'Pauzat' }, + pending: { cls: 'bg-warn/10 text-warn', label: 'Pending' }, +}; + +function relTime(s: string) { + const diff = (Date.now() - new Date(s).getTime()) / 1000; + if (diff < 60) return 'acum'; + if (diff < 3600) return `${Math.floor(diff / 60)}min`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h`; + return `${Math.floor(diff / 86400)}z`; +} + +export default function IngestionJobsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const { data: sources = [], isLoading } = useQuery({ + queryKey: ['ingestion-sources', tenantId], + queryFn: () => apiFetch('/v1/data-sources', { tenantId }), + enabled: Boolean(tenantId), staleTime: 30_000, refetchInterval: 60_000, + }); + const { data: auditEntries = [] } = useQuery({ + queryKey: ['ingestion-audit', tenantId], + queryFn: () => apiFetch('/v1/audit-log?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const syncEvents = useMemo(() => + auditEntries.filter((e) => { + const action = e.action.toLowerCase(); + return action.includes('sync') || action.includes('import') || action.includes('ingest') || action.includes('crawl'); + }), + [auditEntries]); + + const triggerMut = useMutation({ + mutationFn: (id: string) => apiFetch(`/v1/data-sources/${id}/sync`, { tenantId, method: 'POST', body: {} }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['ingestion-sources', tenantId] }), + }); + + const healthy = sources.filter((s) => s.status === 'active' && !(s.errorCount ?? 0)).length; + const errors = sources.filter((s) => s.status === 'error' || (s.errorCount ?? 0) > 0).length; + + return ( +
+
+

Ingestion Jobs

+

+ {isLoading ? 'Se încarcă…' : `${sources.length} surse · ${healthy} sănătoase${errors > 0 ? ` · ${errors} cu erori` : ''}`} +

+
+ + {/* Summary cards */} +
+
+

{sources.length}

+

surse totale

+
+
+

{healthy}

+

sănătoase

+
+
+

0 ? 'text-signal-danger' : 'text-ink'}`}>{errors}

+

cu erori

+
+
+

{syncEvents.length}

+

sync events

+
+
+ + {/* Sources table */} + {isLoading ? ( +
Se încarcă…
+ ) : sources.length === 0 ? ( +
+

📡

+

Nicio sursă de date configurată. Adaugă din Integrări.

+
+ ) : ( +
+ {sources.map((source) => { + const sm = STATUS_META[source.status] ?? { cls: 'bg-muted text-ink-faint', label: source.status }; + return ( +
+
+
+

{source.name}

+ {sm.label} + {(source.errorCount ?? 0) > 0 && ( + {source.errorCount} erori + )} +
+
+ Tip: {source.type} + {source.recordCount !== null && {source.recordCount.toLocaleString()} înregistrări} + {source.lastSyncAt && Sync: {relTime(source.lastSyncAt)} în urmă} +
+
+ +
+ ); + })} +
+ )} + + {/* Recent sync events */} + {syncEvents.length > 0 && ( +
+

Evenimente sync recente

+
+ {syncEvents.slice(0, 20).map((e) => ( +
+
+

{e.action}

+ {e.entityType &&

{e.entityType}

} +
+ {relTime(e.createdAt)} +
+ ))} +
+
+ )} +
+ ); +}