Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
201 lines
6.8 KiB
TypeScript
201 lines
6.8 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState, useCallback } from 'react'
|
|
import type {
|
|
ToolStatus,
|
|
Finding,
|
|
SeveritySummary,
|
|
MonitoringMetric,
|
|
ActiveAlert,
|
|
ScanType,
|
|
TabId,
|
|
} from './types'
|
|
import { SCAN_TYPE_LABELS } from './types'
|
|
|
|
const DEFAULT_METRICS: MonitoringMetric[] = [
|
|
{ name: 'API Latency', value: 45, unit: 'ms', status: 'ok', trend: 'stable' },
|
|
{ name: 'Auth Failures', value: 3, unit: '/h', status: 'ok', trend: 'down' },
|
|
{ name: 'Rate Limit Hits', value: 12, unit: '/h', status: 'warning', trend: 'up' },
|
|
{ name: 'Failed Logins', value: 0, unit: '/24h', status: 'ok', trend: 'stable' },
|
|
{ name: 'SSL Expiry', value: 45, unit: 'days', status: 'ok', trend: 'down' },
|
|
{ name: 'Open Ports', value: 8, unit: '', status: 'ok', trend: 'stable' },
|
|
]
|
|
|
|
export function useSecurityDashboard() {
|
|
const [tools, setTools] = useState<ToolStatus[]>([])
|
|
const [findings, setFindings] = useState<Finding[]>([])
|
|
const [summary, setSummary] = useState<SeveritySummary>({ critical: 0, high: 0, medium: 0, low: 0, info: 0, total: 0 })
|
|
const [history, setHistory] = useState<import('./types').HistoryItem[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [scanning, setScanning] = useState<string | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
const [monitoringMetrics, setMonitoringMetrics] = useState<MonitoringMetric[]>([])
|
|
const [activeAlerts, setActiveAlerts] = useState<ActiveAlert[]>([])
|
|
const [severityFilter, setSeverityFilter] = useState<string | null>(null)
|
|
const [toolFilter, setToolFilter] = useState<string | null>(null)
|
|
const [showFullDocs, setShowFullDocs] = useState(false)
|
|
const [scanMessage, setScanMessage] = useState<string | null>(null)
|
|
const [lastScanTime, setLastScanTime] = useState<string | null>(null)
|
|
|
|
const fetchData = useCallback(async (showLoadingSpinner = false) => {
|
|
if (showLoadingSpinner) {
|
|
setLoading(true)
|
|
}
|
|
setError(null)
|
|
|
|
try {
|
|
const [toolsRes, findingsRes, summaryRes, historyRes] = await Promise.all([
|
|
fetch('/api/v1/security/tools'),
|
|
fetch('/api/v1/security/findings'),
|
|
fetch('/api/v1/security/summary'),
|
|
fetch('/api/v1/security/history'),
|
|
])
|
|
|
|
if (toolsRes.ok) setTools(await toolsRes.json())
|
|
if (findingsRes.ok) setFindings(await findingsRes.json())
|
|
if (summaryRes.ok) setSummary(await summaryRes.json())
|
|
if (historyRes.ok) setHistory(await historyRes.json())
|
|
|
|
const [metricsRes, alertsRes] = await Promise.all([
|
|
fetch('/api/v1/security/monitoring/metrics'),
|
|
fetch('/api/v1/security/monitoring/alerts'),
|
|
])
|
|
|
|
if (metricsRes.ok) {
|
|
setMonitoringMetrics(await metricsRes.json())
|
|
} else {
|
|
setMonitoringMetrics(DEFAULT_METRICS)
|
|
}
|
|
if (alertsRes.ok) {
|
|
setActiveAlerts(await alertsRes.json())
|
|
} else {
|
|
setActiveAlerts([])
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Verbindung zum Backend fehlgeschlagen')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
// Initial load
|
|
useEffect(() => {
|
|
fetchData(true)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
// Auto-refresh every 60 seconds
|
|
useEffect(() => {
|
|
const interval = setInterval(() => fetchData(false), 60000)
|
|
return () => clearInterval(interval)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
const runScan = async (scanType: ScanType) => {
|
|
console.log(`Starting scan: ${scanType}`)
|
|
setScanning(scanType)
|
|
setError(null)
|
|
setScanMessage(`${SCAN_TYPE_LABELS[scanType]} wird gestartet...`)
|
|
|
|
try {
|
|
const response = await fetch(`/api/v1/security/scan/${scanType}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
console.log(`Scan response status: ${response.status}`)
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
console.error(`Scan error: ${errorText}`)
|
|
throw new Error(`Scan fehlgeschlagen: ${response.status} - ${errorText}`)
|
|
}
|
|
|
|
const result = await response.json()
|
|
console.log('Scan result:', result)
|
|
|
|
setScanMessage(`${SCAN_TYPE_LABELS[scanType]} laeuft im Hintergrund. Ergebnisse werden in wenigen Sekunden aktualisiert.`)
|
|
setLastScanTime(new Date().toLocaleTimeString('de-DE'))
|
|
|
|
setTimeout(() => { fetchData(false); setScanMessage(null) }, 5000)
|
|
setTimeout(() => fetchData(false), 15000)
|
|
setTimeout(() => fetchData(false), 30000)
|
|
} catch (err) {
|
|
console.error('Scan error:', err)
|
|
setError(err instanceof Error ? err.message : 'Scan fehlgeschlagen - Pruefe Browser-Konsole')
|
|
setScanMessage(null)
|
|
} finally {
|
|
setScanning(null)
|
|
}
|
|
}
|
|
|
|
const filteredFindings = findings.filter(f => {
|
|
if (severityFilter && f.severity.toUpperCase() !== severityFilter.toUpperCase()) return false
|
|
if (toolFilter && f.tool.toLowerCase() !== toolFilter.toLowerCase()) return false
|
|
return true
|
|
})
|
|
|
|
const overallStatus = getOverallStatus(summary)
|
|
|
|
return {
|
|
tools,
|
|
findings,
|
|
filteredFindings,
|
|
summary,
|
|
history,
|
|
loading,
|
|
scanning,
|
|
error,
|
|
activeTab,
|
|
setActiveTab,
|
|
monitoringMetrics,
|
|
activeAlerts,
|
|
severityFilter,
|
|
setSeverityFilter,
|
|
toolFilter,
|
|
setToolFilter,
|
|
showFullDocs,
|
|
setShowFullDocs,
|
|
scanMessage,
|
|
lastScanTime,
|
|
overallStatus,
|
|
runScan,
|
|
}
|
|
}
|
|
|
|
// --- Helper functions ---
|
|
|
|
function getOverallStatus(summary: SeveritySummary) {
|
|
if (summary.critical > 0) return { label: 'Critical Issues', color: 'bg-red-100 text-red-800' }
|
|
if (summary.high > 0) return { label: 'High Issues', color: 'bg-orange-100 text-orange-800' }
|
|
if (summary.medium > 0) return { label: 'Warnings', color: 'bg-yellow-100 text-yellow-800' }
|
|
return { label: 'Secure', color: 'bg-green-100 text-green-800' }
|
|
}
|
|
|
|
export function getSeverityBadge(severity: string) {
|
|
const base = 'px-3 py-1 rounded-full text-xs font-semibold uppercase'
|
|
switch (severity.toUpperCase()) {
|
|
case 'CRITICAL': return `${base} bg-red-100 text-red-800`
|
|
case 'HIGH': return `${base} bg-orange-100 text-orange-800`
|
|
case 'MEDIUM': return `${base} bg-yellow-100 text-yellow-800`
|
|
case 'LOW': return `${base} bg-green-100 text-green-800`
|
|
default: return `${base} bg-blue-100 text-blue-800`
|
|
}
|
|
}
|
|
|
|
export function getStatusBadge(installed: boolean) {
|
|
return installed
|
|
? 'px-2 py-1 rounded text-xs font-semibold bg-green-100 text-green-800'
|
|
: 'px-2 py-1 rounded text-xs font-semibold bg-red-100 text-red-800'
|
|
}
|
|
|
|
export function getHistoryStatusColor(status: string) {
|
|
switch (status) {
|
|
case 'success': return 'bg-green-500'
|
|
case 'warning': return 'bg-yellow-500'
|
|
case 'error': return 'bg-red-500'
|
|
default: return 'bg-slate-400'
|
|
}
|
|
}
|