import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from 'react'; import type { Assessment, AssessmentProject, CanonicalAssessmentModel, ImportFileRecord, ScanRun } from '../../domain/canonical-model'; import type { GpoBackupStatusRecord, ProjectAssessmentHistoryResponse } from '../../api/assessor-api-contract'; import { AssessorApiClient } from '../../api/assessor-api-client'; import { defaultSecurityScoringPolicy } from '../../domain/security-scoring'; import { Dialog } from '../../components/Dialog/Dialog'; import { OperationProgressDialog } from '../../components/OperationProgress/OperationProgress'; import { FileImport } from '../file-import/FileImport'; import { stableAssessmentId, stableProjectId } from '../../shared/identifiers'; import { groupProjectImportFiles, type ScanImportGroup } from './datahub-import-groups'; import { GpoFolderImport } from './GpoFolderImport'; import { GpoStatusRow } from './GpoStatusRow'; import { PolicyAnalysisPanel } from './PolicyAnalysisPanel'; import { PanelLoadingState } from '../../components/PanelLoadingState/PanelLoadingState'; import { BackendConnectionDialog } from '../../components/BackendConnectionDialog/BackendConnectionDialog'; import { MicrosoftBaselineLibrary } from './MicrosoftBaselineLibrary'; import { IsoCorpusLibrary } from './IsoCorpusLibrary'; import { MitreMatrixLibrary } from './MitreMatrixLibrary'; import { ReferenceRecognitionPanel } from './ReferenceRecognitionPanel'; import { activeMitreMatrix, clearStoredMitreMatrix, loadStoredMitreMatrix, MITRE_MATRIX_CHANGED_EVENT, saveStoredMitreMatrix, setServerMitreMatrix, validateMitreMatrixData } from '../mitre-graph/mitre-graph'; import { downloadBrowserBlob } from '../../platform/browser/browser-download-adapter'; import { TriangleIcon } from '../../components/TriangleIcon'; import { DatahubHistoryOverview, type DatahubTimeslotSelection } from './DatahubHistoryOverview'; import { comparableTimeslotSources, type DatahubTimeslotResultMetrics } from './datahub-scan-analysis'; import { datahubScanReadiness, isDatahubScanViewable, type DatahubScanReadiness } from './datahub-scan-readiness'; import { summarizeScanProcessingReadiness } from './datahub-processing-readiness'; import { compareReimportSourceHashes } from '../../domain/reimport-source'; import { loadTimeslotOpenReadiness, type TimeslotOpenReadiness } from './datahub-open-readiness'; import { DatahubMetricsCache } from './datahub-metrics-cache'; import { useReferenceRecognitionStatus } from './use-reference-recognition-status'; import { datahubScanLabel } from './datahub-scan-labels'; const assessorApi = new AssessorApiClient(); const metricsCache = new DatahubMetricsCache(scope => assessorApi.getTimeslotResultMetrics( scope.projectId, scope.scanRunIds, scope.assessmentIds)); type BackendStatus = 'connected' | 'checking' | 'unavailable'; interface DataHubProps { activeProjectId?: string; backendStatus: BackendStatus; onProjectSelected(project?: AssessmentProject, scanRunIds?: string[], assessmentIds?: string[]): void; onProjectDataChanged?(projectId: string): void | Promise; onDetailViewChanged?(isDetailView: boolean, clientName?: string): void; } interface ProjectFormState { mode: 'create' | 'edit'; project?: AssessmentProject; name: string; description: string; consultant: string; } interface SaveFeedback { tone: 'success' | 'error'; message: string; } type ScanReadinessState = DatahubScanReadiness | 'checking' | 'unavailable'; type TenantDetailSection = 'client-information' | 'gpo-analysis'; function projectConsultant(project: AssessmentProject): string { return typeof project.attributes.consultant === 'string' ? project.attributes.consultant : ''; } function formatTenantDate(value: string): string { return new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(value)); } function formatHistoryDate(value: string): string { return new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeStyle: 'medium' }).format(new Date(value)); } function gpoStatusRevision(records: GpoBackupStatusRecord[]): string { return JSON.stringify(records.map(record => [ record.artifactId, record.jobId, record.state, record.updatedAt, record.gpoCount, record.notLinkedGpoCount ?? null, record.evaluationUpdates?.map(update => [update.evaluationRunId, update.previousVersion, update.currentVersion]) ?? [] ]).sort((left, right) => String(left[0]).localeCompare(String(right[0])))); } function groupScanReports(files: ImportFileRecord[], scanRuns: ScanRun[] = []): ScanImportGroup[] { const runById = new Map(scanRuns.map(run => [run.id, run])); const filesByRun = new Map(); for (const file of files) filesByRun.set(file.scanRunId, [...(filesByRun.get(file.scanRunId) ?? []), file]); return [...filesByRun.entries()].map(([scanRunId, runFiles]) => { const run = runById.get(scanRunId); const fileDate = run?.completedAt ?? run?.startedAt ?? runFiles[0].lastModifiedAt ?? runFiles[0].importedAt; const importedAt = runFiles.reduce((latest, file) => Date.parse(file.importedAt) > Date.parse(latest) ? file.importedAt : latest, runFiles[0].importedAt); return { id: scanRunId, scanRunId, scanRunIds: [scanRunId], assessmentIds: run ? [run.assessmentId] : [], tool: { ...(run?.tool ?? runFiles[0].detectedTool), name: datahubScanLabel(run?.tool ?? runFiles[0].detectedTool, scanRunId, runFiles) }, files: runFiles, gpoBackups: [], sourceCount: 1, fileCount: runFiles.length, fileNames: runFiles.map(file => file.fileName), formats: [...new Set(runFiles.map(file => file.format))], sizeBytes: runFiles.reduce((sum, file) => sum + file.sizeBytes, 0), fileDate, importedAt, possibleDuplicateGroupIds: [] }; }).sort((left, right) => Date.parse(right.fileDate) - Date.parse(left.fileDate)); } export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChanged, onDetailViewChanged, backendStatus }: DataHubProps) { const [projects, setProjects] = useState([]); const [referenceRefreshKey, setReferenceRefreshKey] = useState(0); const { states: projectReferenceReadiness, reportStatus: reportReferenceStatus } = useReferenceRecognitionStatus( projects, backendStatus === 'connected', referenceRefreshKey); const [projectData, setProjectData] = useState>({}); const [projectHistory, setProjectHistory] = useState>({}); const [projectGpoData, setProjectGpoData] = useState>({}); const [form, setForm] = useState(null); const [masterDataEditing, setMasterDataEditing] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [scanDeleteTarget, setScanDeleteTarget] = useState(null); const [gpoDeleteTarget, setGpoDeleteTarget] = useState(null); const [timeslotDeleteTarget, setTimeslotDeleteTarget] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [operationProgress, setOperationProgress] = useState(); const [processingGpoJob, setProcessingGpoJob] = useState<{ projectId: string; artifactId: string; jobId: string }>(); const [error, setError] = useState(null); const [saveFeedback, setSaveFeedback] = useState(null); const [mitreStatus, setMitreStatus] = useState(() => loadStoredMitreMatrix()); const [mitreActiveVersion, setMitreActiveVersion] = useState(() => activeMitreMatrix().version); const [mitreBusy, setMitreBusy] = useState<'refresh' | 'download' | 'upload' | null>(null); const [mitreError, setMitreError] = useState(null); const [mitreFeedback, setMitreFeedback] = useState(null); const [scanSelection, setScanSelection] = useState(null); const [timeslotMetrics, setTimeslotMetrics] = useState(null); const [timeslotMetricsBusy, setTimeslotMetricsBusy] = useState(false); const [timeslotMetricsError, setTimeslotMetricsError] = useState(null); const [metricsRevision, setMetricsRevision] = useState(null); const [selectedTimeslotIds, setSelectedTimeslotIds] = useState([]); const [excludedScanRunIds, setExcludedScanRunIds] = useState([]); const [expandedTimeslotIds, setExpandedTimeslotIds] = useState([]); const [timeslotDialogOpen, setTimeslotDialogOpen] = useState(false); const [timeslotDate, setTimeslotDate] = useState(''); const [scanReadiness, setScanReadiness] = useState>({}); const [scanLabels, setScanLabels] = useState>({}); const [projectOpenReadiness, setProjectOpenReadiness] = useState>>({}); const [detailSection, setDetailSection] = useState('client-information'); const projectModelsCache = useRef(new Map()); const projectLoadGeneration = useRef(0); const preferredProjectId = useRef(activeProjectId); preferredProjectId.current = activeProjectId; useEffect(() => { onDetailViewChanged?.(form !== null, form?.mode === 'edit' ? form.name.trim() || form.project?.name : undefined); }, [form, onDetailViewChanged]); useEffect(() => () => onDetailViewChanged?.(false), [onDetailViewChanged]); useEffect(() => { let mounted = true; void assessorApi.getMitreCatalog().then(state => { if (!mounted) return; setServerMitreMatrix(state.catalog); setMitreActiveVersion(activeMitreMatrix().version); }).catch(() => undefined); return () => { mounted = false; }; }, []); const handleMitreRefresh = async () => { setMitreBusy('refresh'); setMitreError(null); setMitreFeedback(null); try { const result = await assessorApi.refreshMitreCatalog(); clearStoredMitreMatrix(); setMitreStatus(null); setServerMitreMatrix(result.catalog); setMitreActiveVersion(result.catalog.version); setReferenceRefreshKey(current => current + 1); setMitreFeedback(result.updated ? `Enterprise ATT&CK v${result.catalog.version} is active in ASSESSOR.` : `Enterprise ATT&CK v${result.catalog.version} is already current.`); } catch (error) { setMitreError(error instanceof Error ? error.message : 'MITRE could not be updated.'); } finally { setMitreBusy(null); } }; const handleMitreDownload = async () => { setMitreBusy('download'); setMitreError(null); setMitreFeedback(null); try { const source = await assessorApi.downloadLatestMitreSource(); downloadBrowserBlob(source.blob, source.fileName); setMitreFeedback(`Official MITRE file ${source.fileName} downloaded.`); } catch (error) { setMitreError(error instanceof Error ? error.message : 'MITRE could not be downloaded.'); } finally { setMitreBusy(null); } }; const handleMitreUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setMitreError(null); setMitreFeedback(null); setMitreBusy('upload'); setOperationProgress('Importing MITRE Matrix'); try { const text = await file.text(); const parsed = JSON.parse(text); const validated = validateMitreMatrixData(parsed); saveStoredMitreMatrix(validated); setMitreStatus(validated); setMitreActiveVersion(validated.version); setMitreFeedback(`MITRE Matrix version ${validated.version} (${validated.techniques.length} techniques, ${validated.tacticOrder.length} tactics) updated successfully.`); globalThis.dispatchEvent(new Event(MITRE_MATRIX_CHANGED_EVENT)); } catch (err) { setMitreError(err instanceof Error ? err.message : 'Error loading the MITRE Matrix JSON.'); } finally { setOperationProgress(undefined); setMitreBusy(null); event.target.value = ''; } }; const handleMitreReset = () => { clearStoredMitreMatrix(); setMitreStatus(null); setMitreActiveVersion(activeMitreMatrix().version); setMitreFeedback(`Active catalog v${activeMitreMatrix().version} restored.`); setMitreError(null); globalThis.dispatchEvent(new Event(MITRE_MATRIX_CHANGED_EVENT)); }; const loadProjects = useCallback(async () => { const generation = ++projectLoadGeneration.current; setMetricsRevision(null); setTimeslotMetrics(null); if (backendStatus !== 'connected') { setProjects([]); setProjectData({}); setLoading(false); return; } setLoading(true); setProjectOpenReadiness({}); setError(null); try { projectModelsCache.current.clear(); const [list, health] = await Promise.all([assessorApi.listProjects(), assessorApi.health()]); const prioritized = [...list].sort((left, right) => Number(right.id === preferredProjectId.current) - Number(left.id === preferredProjectId.current)); const data = await Promise.all(prioritized.map(async project => { const [files, history, gpoStatus] = await Promise.all([ assessorApi.listProjectImportFiles(project.id), assessorApi.listProjectAssessmentHistory(project.id), assessorApi.listProjectGpoBackupStatus(project.id) ]); if (generation === projectLoadGeneration.current) { const latest = groupProjectImportFiles(files, history.assessments, history.scanRuns, gpoStatus.records) .find(group => group.scanRunIds.length); if (latest) metricsCache.prefetch({ revision: health.repositoryRevision, projectId: project.id, scanRunIds: latest.scanRunIds, assessmentIds: latest.assessmentIds }); } return { projectId: project.id, files, history, gpoRecords: gpoStatus.records }; })); if (generation !== projectLoadGeneration.current) return; setProjects(list); setProjectData(Object.fromEntries(data.map(item => [item.projectId, item.files]))); setProjectHistory(Object.fromEntries(data.map(item => [item.projectId, item.history]))); setProjectGpoData(Object.fromEntries(data.map(item => [item.projectId, item.gpoRecords]))); setReferenceRefreshKey(current => current + 1); setMetricsRevision(health.repositoryRevision); // Open-readiness checks are independent of the six result metrics. Start // them after the overview is available so they cannot hold back the rings. globalThis.setTimeout(() => { if (generation !== projectLoadGeneration.current) return; void Promise.all(data.map(async item => ({ projectId: item.projectId, openReadiness: await loadTimeslotOpenReadiness(query => assessorApi.queryUnifiedResultFacets(query), item.projectId, groupProjectImportFiles(item.files, item.history.assessments, item.history.scanRuns, item.gpoRecords)) }))).then(entries => { if (generation === projectLoadGeneration.current) setProjectOpenReadiness(Object.fromEntries(entries.map(item => [item.projectId, item.openReadiness]))); }); }, 0); } catch (reason) { if (generation === projectLoadGeneration.current) setError(reason instanceof Error ? reason.message : 'Projects could not be loaded.'); } finally { if (generation === projectLoadGeneration.current) setLoading(false); } }, [backendStatus]); useEffect(() => { void loadProjects(); }, [loadProjects]); useEffect(() => () => { projectLoadGeneration.current++; }, []); const pendingGpoProjectIds = projects .filter(project => projectGpoData[project.id]?.some(record => record.state === 'queued' || record.state === 'running')) .map(project => project.id); const pendingGpoProjectKey = pendingGpoProjectIds.join('\u0000'); useEffect(() => { if (backendStatus !== 'connected' || !pendingGpoProjectKey) return; const projectIds = pendingGpoProjectKey.split('\u0000'); let cancelled = false; let refreshing = false; const timer = globalThis.setInterval(async () => { if (refreshing) return; refreshing = true; try { const entries = (await Promise.all(projectIds.map(async projectId => { const response = await assessorApi.listProjectGpoBackupStatus(projectId); if (gpoStatusRevision(response.records) === gpoStatusRevision(projectGpoData[projectId] ?? [])) return null; const history = projectHistory[projectId]; const openReadiness = await loadTimeslotOpenReadiness(query => assessorApi.queryUnifiedResultFacets(query), projectId, groupProjectImportFiles(projectData[projectId] ?? [], history?.assessments, history?.scanRuns, response.records)); return { projectId, records: response.records, openReadiness }; }))).filter((entry): entry is NonNullable => entry !== null); if (!cancelled && entries.length) { metricsCache.invalidate(); void loadProjects(); setProjectGpoData(current => ({ ...current, ...Object.fromEntries(entries.map(entry => [entry.projectId, entry.records])) })); setProjectOpenReadiness(current => ({ ...current, ...Object.fromEntries(entries.map(entry => [entry.projectId, entry.openReadiness])) })); } } catch { if (!cancelled) { setError('GPO processing status could not be refreshed.'); } } finally { refreshing = false; } }, 5_000); return () => { cancelled = true; globalThis.clearInterval(timer); }; }, [backendStatus, pendingGpoProjectKey, projectData, projectGpoData, projectHistory, loadProjects]); useEffect(() => { if (!processingGpoJob) return; const record = projectGpoData[processingGpoJob.projectId]?.find(item => item.artifactId === processingGpoJob.artifactId && item.jobId === processingGpoJob.jobId); if (record && !['queued', 'running'].includes(record.state)) { setProcessingGpoJob(undefined); setOperationProgress(undefined); } }, [processingGpoJob, projectGpoData]); useEffect(() => { const refresh = () => { metricsCache.invalidate(); void loadProjects(); }; globalThis.addEventListener('assessor:datahub-changed', refresh); globalThis.addEventListener('assessor:gpo-changed', refresh); return () => { globalThis.removeEventListener('assessor:datahub-changed', refresh); globalThis.removeEventListener('assessor:gpo-changed', refresh); }; }, [loadProjects]); const groupsForProject = useCallback((projectId: string): ScanImportGroup[] => { const history = projectHistory[projectId]; return groupProjectImportFiles( projectData[projectId] ?? [], history?.assessments, history?.scanRuns, projectGpoData[projectId] ?? [] ); }, [projectData, projectGpoData, projectHistory]); useEffect(() => { if (loading || !projects.length) return; setScanSelection(current => { if (current) { const available = new Set(groupsForProject(current.projectId).map(group => group.id)); if (projects.some(project => project.id === current.projectId) && current.slots.length <= 1 && current.slots.every(slot => available.has(slot.id))) return current; } const preferred = projects.find(project => project.id === activeProjectId); if (!preferred) return null; const latest = groupsForProject(preferred.id).find(group => group.scanRunIds.length); return { projectId: preferred.id, slots: latest ? [{ id: latest.id, runIds: latest.scanRunIds, assessmentIds: latest.assessmentIds, effectiveAt: latest.fileDate }] : [] }; }); }, [activeProjectId, groupsForProject, loading, projects]); const loadProjectModels = useCallback(async (projectId: string): Promise => { const cached = projectModelsCache.current.get(projectId); if (cached) return cached; const assessments = await assessorApi.listProjectAssessments(projectId); const models = await Promise.all(assessments.map(assessment => assessorApi.loadAssessmentModel(assessment.id))); projectModelsCache.current.set(projectId, models); return models; }, []); useEffect(() => { const selectedTimeslot = scanSelection?.slots[scanSelection.slots.length - 1]; if (!selectedTimeslot || metricsRevision === null) { setTimeslotMetrics(null); setTimeslotMetricsError(null); setTimeslotMetricsBusy(Boolean(selectedTimeslot)); return; } let cancelled = false; const scope = { revision: metricsRevision, projectId: scanSelection.projectId, scanRunIds: selectedTimeslot.runIds, assessmentIds: selectedTimeslot.assessmentIds }; const cached = metricsCache.peek(scope); setTimeslotMetrics(cached ?? null); setTimeslotMetricsBusy(cached === undefined); setTimeslotMetricsError(null); void metricsCache.load(scope) .then(metrics => { if (cancelled) return; setTimeslotMetrics(metrics); }) .catch(reason => { if (cancelled) return; setTimeslotMetrics(null); setTimeslotMetricsError(reason instanceof Error ? reason.message : 'The selected timeslot status could not be loaded.'); }) .finally(() => { if (!cancelled) setTimeslotMetricsBusy(false); }); return () => { cancelled = true; }; }, [scanSelection, metricsRevision]); const selectProject = useCallback((projectId: string) => { const latest = groupsForProject(projectId).find(group => group.scanRunIds.length); const cached = latest && metricsRevision !== null ? metricsCache.peek({ revision: metricsRevision, projectId, scanRunIds: latest.scanRunIds, assessmentIds: latest.assessmentIds }) : undefined; setTimeslotMetrics(cached ?? null); setTimeslotMetricsError(null); setTimeslotMetricsBusy(Boolean(latest) && cached === undefined); setScanSelection({ projectId, slots: latest ? [{ id: latest.id, runIds: latest.scanRunIds, assessmentIds: latest.assessmentIds, effectiveAt: latest.fileDate }] : [] }); }, [groupsForProject, metricsRevision]); const editedGroups = useMemo(() => form?.project ? groupsForProject(form.project.id) : [], [form?.project, groupsForProject]); const editedReports = useMemo(() => form?.project ? groupScanReports(projectData[form.project.id] ?? [], projectHistory[form.project.id]?.scanRuns) : [], [form?.project, projectData, projectHistory]); useEffect(() => { const projectId = form?.project?.id; if (!projectId || editedReports.length === 0) { setScanReadiness({}); setScanLabels({}); return; } let cancelled = false; setScanLabels({}); setScanReadiness(Object.fromEntries(editedReports.map(report => [report.scanRunId, 'checking']))); void loadProjectModels(projectId).then(models => { if (cancelled) return; setScanLabels(Object.fromEntries(editedReports.map(report => [report.scanRunId, datahubScanLabel(report.tool, report.scanRunId, report.files, models)]))); setScanReadiness(Object.fromEntries(editedReports.map(report => [ report.scanRunId, datahubScanReadiness(models, report.scanRunId, report.tool.id) ]))); }).catch(() => { if (!cancelled) setScanReadiness(Object.fromEntries(editedReports.map(report => [report.scanRunId, 'unavailable']))); }); return () => { cancelled = true; }; }, [editedReports, form?.project?.id, loadProjectModels]); const selectedTimeslots = useMemo(() => editedGroups.filter(group => selectedTimeslotIds.includes(group.id)), [editedGroups, selectedTimeslotIds]); const selectedSources = useMemo(() => comparableTimeslotSources( selectedTimeslots.map(group => ({ id: group.id, runIds: group.scanRunIds.filter(runId => !excludedScanRunIds.includes(runId) && isDatahubScanViewable(scanReadiness[runId])), hasGpo: group.gpoBackups.length > 0 })), form?.project ? projectHistory[form.project.id]?.scanRuns ?? [] : [] ), [excludedScanRunIds, form?.project, projectHistory, scanReadiness, selectedTimeslots]); const openCreate = () => { setError(null); setSaveFeedback(null); setMasterDataEditing(true); setExcludedScanRunIds([]); setExpandedTimeslotIds([]); setDetailSection('client-information'); setForm({ mode: 'create', name: '', description: '', consultant: '' }); }; const openEdit = (project: AssessmentProject) => { const groups = groupsForProject(project.id); const latestTimeslot = groups[0]; const files = projectData[project.id] ?? []; const affectedTimeslotIds = groups .filter(group => summarizeScanProcessingReadiness(files.filter(file => group.scanRunIds.includes(file.scanRunId))).state !== 'ready' || group.gpoBackups.some(record => record.state === 'dead-letter' || record.state === 'recovery-required')) .map(group => group.id); setError(null); setSaveFeedback(null); setMasterDataEditing(false); setSelectedTimeslotIds(latestTimeslot ? [latestTimeslot.id] : []); setExcludedScanRunIds([]); setExpandedTimeslotIds(affectedTimeslotIds); setTimeslotDialogOpen(false); setTimeslotDeleteTarget(null); setDetailSection('client-information'); setForm({ mode: 'edit', project, name: project.name, description: project.description ?? '', consultant: projectConsultant(project) }); }; const updateForm = (patch: Partial) => { setSaveFeedback(null); setForm(current => current ? { ...current, ...patch } : current); }; const toggleTimeslot = (timeslotId: string, checked: boolean) => { const group = editedGroups.find(candidate => candidate.id === timeslotId); setSelectedTimeslotIds(current => checked ? current.includes(timeslotId) ? current : [...current, timeslotId] : current.filter(id => id !== timeslotId)); if (group) setExcludedScanRunIds(current => current.filter(runId => !group.scanRunIds.includes(runId))); setError(null); }; const toggleScanReport = (timeslotId: string, scanRunId: string, checked: boolean) => { setExcludedScanRunIds(current => checked ? current.filter(id => id !== scanRunId) : current.includes(scanRunId) ? current : [...current, scanRunId]); if (checked) { setSelectedTimeslotIds(current => current.includes(timeslotId) ? current : [...current, timeslotId]); setExpandedTimeslotIds(current => current.includes(timeslotId) ? current : [...current, timeslotId]); } setError(null); }; const openSelectedTimeslots = () => { if (!form?.project || !selectedTimeslots.length) return; const runIds = selectedTimeslots.length === 1 ? selectedTimeslots[0].scanRunIds.filter(runId => !excludedScanRunIds.includes(runId) && isDatahubScanViewable(scanReadiness[runId])) : selectedSources.runIds; if (!runIds.length) { if (selectedTimeslots.length === 1 && projectOpenReadiness[form.project.id]?.[selectedTimeslots[0].id]?.hasPolicyResults) { onProjectSelected(form.project, [], selectedTimeslots[0].assessmentIds); return; } setError('The selected timeslots do not contain a common comparable scanner source.'); return; } onProjectSelected(form.project, runIds, [...new Set(selectedTimeslots.flatMap(timeslot => timeslot.assessmentIds))]); }; const createTimeslot = async (event: FormEvent) => { event.preventDefault(); if (!form?.project || !timeslotDate) return; const timeslotId = `timeslot-${timeslotDate}`; if (editedGroups.some(group => group.id === timeslotId)) { setError('A timeslot already exists for this date.'); return; } setBusy(true); setError(null); try { const now = new Date().toISOString(); const effectiveAt = `${timeslotDate}T12:00:00.000Z`; const assessment: Assessment = { id: stableAssessmentId(form.project.id, effectiveAt), projectId: form.project.id, name: `Timeslot ${formatTenantDate(effectiveAt)}`, effectiveAt, scoringPolicy: defaultSecurityScoringPolicy(), createdAt: now, updatedAt: now }; await assessorApi.saveAssessment(assessment); await loadProjects(); setSelectedTimeslotIds([timeslotId]); setExcludedScanRunIds([]); setExpandedTimeslotIds([timeslotId]); setTimeslotDialogOpen(false); setSaveFeedback({ tone: 'success', message: 'Timeslot created and selected.' }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'The timeslot could not be created.'); } finally { setBusy(false); } }; const saveProject = async (event: FormEvent) => { event.preventDefault(); if (!form?.name.trim() || (form.mode === 'edit' && !masterDataEditing)) return; setBusy(true); setError(null); setSaveFeedback(null); try { const now = new Date().toISOString(); const project: AssessmentProject = form.project ? { ...form.project, name: form.name.trim(), description: form.description.trim(), updatedAt: now, attributes: { ...form.project.attributes, consultant: form.consultant.trim() } } : { id: stableProjectId(form.name, Date.now(), Date.now()), name: form.name.trim(), description: form.description.trim(), createdAt: now, updatedAt: now, attributes: { consultant: form.consultant.trim() } }; const response = await assessorApi.saveProject(project); const confirmed = await assessorApi.getProject(response.project.id); const persisted = confirmed.updatedAt === project.updatedAt && confirmed.name === project.name && (confirmed.description ?? '') === (project.description ?? '') && projectConsultant(confirmed) === projectConsultant(project); if (!persisted) throw new Error('The database did not confirm the saved state.'); await loadProjects(); setForm({ mode: 'edit', project: confirmed, name: confirmed.name, description: confirmed.description ?? '', consultant: projectConsultant(confirmed) }); setMasterDataEditing(false); setSaveFeedback({ tone: 'success', message: 'Saved to the database.' }); } catch (reason) { setSaveFeedback({ tone: 'error', message: reason instanceof Error ? reason.message : 'Saving to the database failed.' }); } finally { setBusy(false); } }; const deleteProject = async () => { if (!deleteTarget) return; setBusy(true); setError(null); try { await assessorApi.deleteProject(deleteTarget.id); if (activeProjectId === deleteTarget.id) onProjectSelected(undefined); if (form?.project?.id === deleteTarget.id) { setMasterDataEditing(false); setForm(null); } setDeleteTarget(null); await loadProjects(); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Project could not be deleted.'); } finally { setBusy(false); } }; const deleteScanRun = async () => { const projectId = form?.project?.id; if (!projectId || !scanDeleteTarget) return; setBusy(true); setError(null); setSaveFeedback(null); try { const result = await assessorApi.deleteScanRun(projectId, scanDeleteTarget.scanRunId); if (!result.deleted) throw new Error('The scan report was no longer present in the database.'); setExcludedScanRunIds(current => current.filter(id => id !== scanDeleteTarget.scanRunId)); setScanDeleteTarget(null); await loadProjects(); if (activeProjectId === projectId) await onProjectDataChanged?.(projectId); globalThis.dispatchEvent(new CustomEvent('assessor:datahub-changed', { detail: { projectId } })); setSaveFeedback({ tone: 'success', message: 'Scan report and its records were deleted from the database.' }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Scan report could not be deleted.'); } finally { setBusy(false); } }; const adoptReplacement = async (replacedReport: ScanImportGroup, replacementReport: ScanImportGroup) => { const projectId = form?.project?.id; if (!projectId) return; setBusy(true); setError(null); setSaveFeedback(null); try { const adopted = await assessorApi.adoptScanRunReplacement(projectId, replacedReport.scanRunId, replacementReport.scanRunId); await loadProjects(); if (activeProjectId === projectId) await onProjectDataChanged?.(projectId); globalThis.dispatchEvent(new CustomEvent('assessor:datahub-changed', { detail: { projectId } })); const transfer = adopted.reimportReviewTransfer; const preservation = transfer.unresolved > 0 ? ` ${transfer.transferred} client records were transferred; ${transfer.unresolved} remain retained on the previous scan and require review.` : ` ${transfer.transferred} client records were transferred safely.`; setSaveFeedback({ tone: 'success', message: `${replacementReport.tool.name} was adopted as the replacement scan. The previous scan remains auditably retained.${preservation}` }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'The imported scan could not be adopted as the replacement.'); } finally { setBusy(false); } }; const deleteTimeslot = async () => { const projectId = form?.project?.id; if (!projectId || !timeslotDeleteTarget) return; if (!timeslotDeleteTarget.assessmentIds.length) { setTimeslotDeleteTarget(null); setError('This timeslot has no persisted assessment and cannot be deleted safely.'); return; } setBusy(true); setError(null); setSaveFeedback(null); try { for (const assessmentId of timeslotDeleteTarget.assessmentIds) { const result = await assessorApi.deleteAssessment(projectId, assessmentId); if (!result.deleted) throw new Error('The timeslot was no longer present in the database.'); } const deletedId = timeslotDeleteTarget.id; setSelectedTimeslotIds(current => current.filter(id => id !== deletedId)); setExcludedScanRunIds(current => current.filter(runId => !timeslotDeleteTarget.scanRunIds.includes(runId))); setExpandedTimeslotIds(current => current.filter(id => id !== deletedId)); setTimeslotDeleteTarget(null); await loadProjects(); if (activeProjectId === projectId) await onProjectDataChanged?.(projectId); globalThis.dispatchEvent(new CustomEvent('assessor:datahub-changed', { detail: { projectId } })); setSaveFeedback({ tone: 'success', message: 'Timeslot and its assigned data were deleted from the database.' }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Timeslot could not be deleted.'); } finally { setBusy(false); } }; const reevaluateGpoBackup = async (record: GpoBackupStatusRecord) => { const projectId = form?.project?.id; if (!projectId || !record.evaluationUpdates?.length) return; setBusy(true); setOperationProgress('Re-evaluating GPO baseline comparisons'); setError(null); setSaveFeedback(null); let failure: string | undefined; try { for (const update of record.evaluationUpdates) { await assessorApi.evaluatePolicy(projectId, { aggregationRunId: update.aggregationRunId, baselineAnalysisIds: update.baselineAnalysisIds }); } } catch (reason) { failure = reason instanceof Error ? reason.message : 'GPO re-evaluation failed. Review the processing controls in GPO Analysis. Previous results remain retained.'; } finally { await loadProjects(); if (failure) setError(failure); else setSaveFeedback({ tone: 'success', message: 'GPO re-evaluation completed. Previous runs remain retained as history. Reopen the timeslot to load the current results.' }); setOperationProgress(undefined); setBusy(false); } }; const reprocessGpoBackup = async (record: GpoBackupStatusRecord) => { const projectId = form?.project?.id; if (!projectId || !record.analysisUpdate) return; setBusy(true); setOperationProgress('Reprocessing the stored GPO source'); setError(null); setSaveFeedback(null); try { const result = await assessorApi.reprocessGpoBackup(projectId, record.artifactId, record.analysisUpdate.expectedLatestProcessingRunId); setProcessingGpoJob({ projectId, artifactId: result.artifactId, jobId: result.jobId }); setOperationProgress('Processing GPO backup'); await loadProjects(); setSaveFeedback({ tone: 'success', message: 'GPO reprocessing was queued. Previous runs remain retained as history; re-evaluate once processing completes.' }); } catch (reason) { setOperationProgress(undefined); setError(reason instanceof Error ? reason.message : 'GPO reprocessing could not be started. Previous results remain retained.'); } finally { setBusy(false); } }; const retryGpoBackup = async (record: GpoBackupStatusRecord) => { const projectId = form?.project?.id; if (!projectId) return; setBusy(true); setOperationProgress('Starting GPO processing'); setError(null); setSaveFeedback(null); try { const result = await assessorApi.retryGpoBackup(projectId, record.artifactId); setProcessingGpoJob({ projectId, artifactId: result.artifactId, jobId: result.jobId }); setOperationProgress('Processing GPO backup'); await loadProjects(); setSaveFeedback({ tone: 'success', message: 'The GPO analysis job was requeued.' }); } catch (reason) { setOperationProgress(undefined); setError(reason instanceof Error ? reason.message : 'The GPO analysis job could not be restarted.'); } finally { setBusy(false); } }; const deleteGpoBackup = async () => { const projectId = form?.project?.id; if (!projectId || !gpoDeleteTarget) return; setBusy(true); setError(null); setSaveFeedback(null); try { await assessorApi.deleteGpoBackup(projectId, gpoDeleteTarget.artifactId); setGpoDeleteTarget(null); await loadProjects(); if (activeProjectId === projectId) await onProjectDataChanged?.(projectId); globalThis.dispatchEvent(new CustomEvent('assessor:datahub-changed', { detail: { projectId } })); setSaveFeedback({ tone: 'success', message: 'GPO backup and its derived records were deleted from the database.' }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'The GPO backup could not be deleted.'); } finally { setBusy(false); } }; if (backendStatus !== 'connected') { return
{/* Kein Eyebrow, kein Inhaltstitel: den Seitennamen traegt die Topbar (siehe Work als visuelle Referenz). Es bleibt die Einordnungszeile. */}

Datahub is the control center for clients, scans, and findings.

; } const editedProject = form?.project; const singleTimeslotRunIds = selectedTimeslots.length === 1 ? selectedTimeslots[0].scanRunIds.filter(runId => !excludedScanRunIds.includes(runId) && isDatahubScanViewable(scanReadiness[runId])) : []; const openDisabled = !selectedTimeslots.length || (selectedTimeslots.length === 1 ? !projectOpenReadiness[form?.project?.id ?? '']?.[selectedTimeslots[0].id]?.canOpen || (!singleTimeslotRunIds.length && !projectOpenReadiness[form?.project?.id ?? '']?.[selectedTimeslots[0].id]?.hasPolicyResults) : !selectedSources.runIds.length); const filesSection = editedProject &&

Timeslots

{loading ? : editedGroups.length === 0 ?

No timeslots yet

Create the first dated assessment timeslot.

: editedGroups.map(group => { const reports = editedReports.filter(report => group.scanRunIds.includes(report.scanRunId)); const processingByRun = new Map(reports.map(report => [report.scanRunId, summarizeScanProcessingReadiness(report.files)])); const affectedReports = reports.filter(report => processingByRun.get(report.scanRunId)?.state !== 'ready'); const replacementByRun = new Map(affectedReports.flatMap(affected => { const candidate = reports .filter(report => report.scanRunId !== affected.scanRunId && report.tool.id === affected.tool.id && compareReimportSourceHashes(affected.files.map(file => file.sha256), report.files.map(file => file.sha256)) === 'match' && Date.parse(report.importedAt) > Date.parse(affected.importedAt) && processingByRun.get(report.scanRunId)?.state === 'ready' && scanReadiness[report.scanRunId] === 'recognized') .sort((left, right) => Date.parse(right.importedAt) - Date.parse(left.importedAt))[0]; return candidate ? [[affected.scanRunId, candidate] as const] : []; })); const viewableReports = reports.filter(report => isDatahubScanViewable(scanReadiness[report.scanRunId])); const selected = selectedTimeslotIds.includes(group.id); const selectedReportCount = selected ? viewableReports.filter(report => !excludedScanRunIds.includes(report.scanRunId)).length : 0; const timeslotChecked = selected && (viewableReports.length === 0 || selectedReportCount === viewableReports.length); const timeslotIndeterminate = selected && viewableReports.length > 0 && selectedReportCount < viewableReports.length; return
{ const open = event.currentTarget.open; setExpandedTimeslotIds(current => open ? current.includes(group.id) ? current : [...current, group.id] : current.filter(id => id !== group.id)); }} > { if (element) element.indeterminate = timeslotIndeterminate; }} type="checkbox" checked={timeslotChecked} aria-label={`Select timeslot ${formatTenantDate(group.fileDate)}`} onClick={event => event.stopPropagation()} onChange={event => toggleTimeslot(group.id, event.target.checked)}/> {formatTenantDate(group.fileDate)} {(reports.length + group.gpoBackups.length).toLocaleString('de-DE')} {reports.length === 0 && group.gpoBackups.length === 0 ?

No sources

This timeslot does not contain a scan report or GPO backup yet.

:
{reports.map(sourceReport => { const report = scanLabels[sourceReport.scanRunId] ? { ...sourceReport, tool: { ...sourceReport.tool, name: scanLabels[sourceReport.scanRunId] } } : sourceReport; const readiness = scanReadiness[report.scanRunId] ?? 'checking'; const processing = processingByRun.get(report.scanRunId)!; const viewable = isDatahubScanViewable(readiness); const statusLabel = processing.state === 'update' ? 'Reimport recommended' : processing.state === 'review' ? 'Review required' : processing.state === 'error' ? 'Processing error' : readiness === 'recognized' ? 'Ready' : readiness === 'generic' ? 'Generic' : readiness === 'not-viewable' ? 'Not viewable' : readiness === 'unavailable' ? 'Unavailable' : 'Checking'; const statusTitle = processing.state !== 'ready' ? processing.explanation : readiness === 'recognized' ? 'Tool recognized, Work records available, and processing contract ready.' : readiness === 'generic' ? 'Work records are available, but no specific scanner profile was recognized.' : readiness === 'not-viewable' ? 'The upload is stored, but it produced no records that Work can display.' : readiness === 'unavailable' ? 'Display readiness could not be checked.' : 'Display readiness is being checked.'; const replacement = replacementByRun.get(report.scanRunId); const processingHeading = processing.state === 'error' ? 'ASSESSOR processing issue' : processing.state === 'review' ? 'Review required' : 'New ASSESSOR processing profile available'; return
toggleScanReport(group.id, report.scanRunId, event.target.checked)}/> {report.tool.name}{report.fileCount.toLocaleString('de-DE')} {report.fileCount === 1 ? 'file' : 'files'} · {report.formats.join(', ') || '—'} {formatHistoryDate(report.importedAt)} {statusLabel}
{processing.state !== 'ready' &&
{processingHeading}

{replacement ? 'A newer ready scan of the same tool is present. It can be adopted as the replacement without deleting or uploading it again.' : processing.explanation}

{replacement ? : processing.state !== 'review' && }
}
; })} {group.gpoBackups.map(record => void reevaluateGpoBackup(record)} onReprocess={() => void reprocessGpoBackup(record)} onRetry={() => void retryGpoBackup(record)} onDelete={() => setGpoDeleteTarget(record)} />)}
}

Add Entry

Entire timeslot
; })}
; return
{form ?
{detailSection === 'client-information' ?
Client Information
{form.project && projectGpoData[form.project.id]?.length > 0 && !masterDataEditing && }{form.project && !masterDataEditing && }{form.mode === 'edit' && !masterDataEditing && }{(form.mode === 'create' || masterDataEditing) && }
{error &&
{error}
} {saveFeedback &&
{saveFeedback.message}
}