/** * Aggregates data for the Integrations architecture page. * Primary payload: GET /api/system/integrations-overview (no fictitious diagram data). */ import { useState, useEffect, useCallback } from 'react'; import api from '../api'; import type { FeatureInstance, NavigationMandate } from './useNavigation'; export interface AicoreModuleRow { connectorType: string; label: string; modelCount: number; } export interface InfraToolRow { id: string; label: string; } export type DataLayerItemKind = | 'userConnection' | 'dataSource' | 'featureDataSource' | 'trusteeAccounting'; export interface DataLayerItem { kind: DataLayerItemKind; id: string; /** userConnection */ displayLabel?: string; connectionReference?: string; authority?: string; /** dataSource */ label?: string; sourceType?: string; connectionId?: string; /** shared */ featureInstanceId?: string | null; mandateId?: string | null; /** featureDataSource */ featureCode?: string; tableName?: string; /** trusteeAccounting */ instanceLabel?: string; connectorType?: string; } export interface LiveStats { aiCallCount: number; aiCallPeriodDays: number; totalWorkflows: number; activeWorkflows: number; totalRuns: number; totalTokens: number; } export interface ExtractorClassRow { className: string; extensions: string[]; } export interface RendererClassRow { className: string; formats: string[]; } export interface IntegrationsDiagramPayload { aicoreModules: AicoreModuleRow[]; infraTools: InfraToolRow[]; extractorExtensions: string[]; extractorClasses: ExtractorClassRow[]; rendererFormats: string[]; rendererClasses: RendererClassRow[]; dataLayerItems: DataLayerItem[]; liveStats: LiveStats; errors?: string[]; } export interface MandateCardData { id: string; uiLabel: string; dotColor: string; /** "Feature: Instanzbezeichnung" per instance */ moduleChips: string[]; } export interface UseIntegrationsOverviewResult { loading: boolean; error: string | null; refetch: () => Promise; diagram: IntegrationsDiagramPayload | null; mandateCards: MandateCardData[]; workflowChips: string[]; hasNeutralization: boolean; } function _dotColorForIndex(index: number): string { const palette = ['#378ADD', '#1D9E75', '#D85A30', '#8B5CF6', '#EC4899', '#0EA5E9']; return palette[index % palette.length]; } function _collectGraphicalEditorInstanceIds(mandates: NavigationMandate[]): string[] { const ids: string[] = []; for (const mandate of mandates) { for (const feature of mandate.features) { if (feature.uiComponent === 'feature.graphicalEditor') { for (const inst of feature.instances) { if (inst.id && !ids.includes(inst.id)) { ids.push(inst.id); } } } } } return ids; } function _hasFeatureCode(mandates: NavigationMandate[], code: string): boolean { for (const mandate of mandates) { for (const feature of mandate.features) { if (feature.uiComponent === `feature.${code}`) { return true; } } } return false; } function _featureCodeFromUiComponent(uiComponent: string): string { return uiComponent.startsWith('feature.') ? uiComponent.slice(8) : uiComponent; } function _instanceChipLine(inst: FeatureInstance, featureUiComponent: string): string { const label = (inst.uiLabel || '').trim(); const code = (inst.featureCode || _featureCodeFromUiComponent(featureUiComponent)).trim(); if (label && code) { return `${label} (${code})`; } if (label) { return label; } return code; } function _buildMandateCards(mandates: NavigationMandate[]): MandateCardData[] { return mandates.map((m, i) => { const moduleChips: string[] = []; for (const f of m.features) { for (const inst of f.instances) { const line = _instanceChipLine(inst, f.uiComponent); if (line && !moduleChips.includes(line)) { moduleChips.push(line); } } } return { id: m.id, uiLabel: m.uiLabel, dotColor: _dotColorForIndex(i), moduleChips: moduleChips.slice(0, 24), }; }); } const _DEFAULT_LIVE_STATS: LiveStats = { aiCallCount: 0, aiCallPeriodDays: 30, totalWorkflows: 0, activeWorkflows: 0, totalRuns: 0, totalTokens: 0, }; function _normalizeExtractorClasses(raw: unknown): ExtractorClassRow[] { if (!Array.isArray(raw)) return []; const out: ExtractorClassRow[] = []; for (const row of raw) { if (!row || typeof row !== 'object') continue; const r = row as Record; const className = typeof r.className === 'string' ? r.className : ''; const extensions = Array.isArray(r.extensions) ? (r.extensions as string[]) : []; if (className && extensions.length) out.push({ className, extensions }); } return out; } function _normalizeRendererClasses(raw: unknown): RendererClassRow[] { if (!Array.isArray(raw)) return []; const out: RendererClassRow[] = []; for (const row of raw) { if (!row || typeof row !== 'object') continue; const r = row as Record; const className = typeof r.className === 'string' ? r.className : ''; const formats = Array.isArray(r.formats) ? (r.formats as string[]) : []; if (className && formats.length) out.push({ className, formats }); } return out; } function _normalizeDiagramPayload(raw: unknown): IntegrationsDiagramPayload { const o = raw && typeof raw === 'object' ? (raw as Record) : {}; const rawStats = o.liveStats && typeof o.liveStats === 'object' ? (o.liveStats as Record) : {}; return { aicoreModules: Array.isArray(o.aicoreModules) ? (o.aicoreModules as AicoreModuleRow[]) : [], infraTools: Array.isArray(o.infraTools) ? (o.infraTools as InfraToolRow[]) : [], extractorExtensions: Array.isArray(o.extractorExtensions) ? (o.extractorExtensions as string[]) : [], extractorClasses: _normalizeExtractorClasses(o.extractorClasses), rendererFormats: Array.isArray(o.rendererFormats) ? (o.rendererFormats as string[]) : [], rendererClasses: _normalizeRendererClasses(o.rendererClasses), dataLayerItems: Array.isArray(o.dataLayerItems) ? (o.dataLayerItems as DataLayerItem[]) : [], liveStats: { aiCallCount: typeof rawStats.aiCallCount === 'number' ? rawStats.aiCallCount : _DEFAULT_LIVE_STATS.aiCallCount, aiCallPeriodDays: typeof rawStats.aiCallPeriodDays === 'number' ? rawStats.aiCallPeriodDays : _DEFAULT_LIVE_STATS.aiCallPeriodDays, totalWorkflows: typeof rawStats.totalWorkflows === 'number' ? rawStats.totalWorkflows : _DEFAULT_LIVE_STATS.totalWorkflows, activeWorkflows: typeof rawStats.activeWorkflows === 'number' ? rawStats.activeWorkflows : _DEFAULT_LIVE_STATS.activeWorkflows, totalRuns: typeof rawStats.totalRuns === 'number' ? rawStats.totalRuns : _DEFAULT_LIVE_STATS.totalRuns, totalTokens: typeof rawStats.totalTokens === 'number' ? rawStats.totalTokens : _DEFAULT_LIVE_STATS.totalTokens, }, errors: Array.isArray(o.errors) ? (o.errors as string[]) : undefined, }; } export function useIntegrationsOverview(): UseIntegrationsOverviewResult { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [diagram, setDiagram] = useState(null); const [mandateCards, setMandateCards] = useState([]); const [workflowChips, setWorkflowChips] = useState([]); const [hasNeutralization, setHasNeutralization] = useState(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const [navResult, diagramResult] = await Promise.allSettled([ api.get('/api/navigation'), api.get('/api/system/integrations-overview'), ]); let mandatesForWorkflows: NavigationMandate[] = []; if (navResult.status === 'fulfilled') { const blocks = navResult.value.data?.blocks ?? []; const dynamicBlock = blocks.find((b: { type: string }) => b.type === 'dynamic'); mandatesForWorkflows = dynamicBlock?.mandates ?? []; setMandateCards(_buildMandateCards(mandatesForWorkflows)); setHasNeutralization(_hasFeatureCode(mandatesForWorkflows, 'neutralization')); } else { setMandateCards([]); setHasNeutralization(false); setError( navResult.reason instanceof Error ? navResult.reason.message : String(navResult.reason), ); } if (diagramResult.status === 'fulfilled') { setDiagram(_normalizeDiagramPayload(diagramResult.value.data)); } else { setDiagram(_normalizeDiagramPayload({})); const msg = diagramResult.reason instanceof Error ? diagramResult.reason.message : String(diagramResult.reason); setError((prev) => (prev ? `${prev} | ${msg}` : msg)); } const geIds = _collectGraphicalEditorInstanceIds(mandatesForWorkflows); const wfLabels: string[] = []; const seenWf = new Set(); for (const instanceId of geIds.slice(0, 4)) { try { const wfRes = await api.get(`/api/workflows/${instanceId}/workflows`, { params: { active: 'true' }, }); const wfData = wfRes.data; const list = Array.isArray(wfData) ? wfData : (wfData as { items?: { label?: string }[]; workflows?: { label?: string }[] })?.items ?? (wfData as { workflows?: { label?: string }[] })?.workflows ?? []; for (const w of list) { const lab = (w as { label?: string }).label; if (lab && !seenWf.has(lab)) { seenWf.add(lab); wfLabels.push(lab); } if (wfLabels.length >= 8) break; } } catch { /* ignore */ } if (wfLabels.length >= 8) break; } setWorkflowChips(wfLabels.slice(0, 8)); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Laden fehlgeschlagen'); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); return { loading, error, refetch: load, diagram, mandateCards, workflowChips, hasNeutralization, }; }