[split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)

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>
This commit is contained in:
Benjamin Admin
2026-04-24 17:28:57 +02:00
parent 9ba420fa91
commit b681ddb131
251 changed files with 30016 additions and 25037 deletions

View File

@@ -7,193 +7,32 @@
* nach Zeitplan. Manuelles Starten/Stoppen ebenfalls moeglich.
*/
import { useEffect, useState, useCallback } from 'react'
interface NightModeConfig {
enabled: boolean
shutdown_time: string
startup_time: string
last_action: string | null
last_action_time: string | null
excluded_services: string[]
}
interface NightModeStatus {
config: NightModeConfig
current_time: string
next_action: string | null
next_action_time: string | null
time_until_next_action: string | null
services_status: Record<string, string>
}
interface ServicesInfo {
all_services: string[]
excluded_services: string[]
status: Record<string, string>
}
import { useNightMode } from './_components/useNightMode'
import { MainControl } from './_components/MainControl'
import { StatusCards } from './_components/StatusCards'
import { TimeConfig } from './_components/TimeConfig'
import { ServiceList } from './_components/ServiceList'
import { InfoBox } from './_components/InfoBox'
export default function NightModePage() {
const [status, setStatus] = useState<NightModeStatus | null>(null)
const [services, setServices] = useState<ServicesInfo | null>(null)
const [loading, setLoading] = useState(true)
const [actionLoading, setActionLoading] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [successMessage, setSuccessMessage] = useState<string | null>(null)
// Lokale Konfiguration fuer Bearbeitung
const [editMode, setEditMode] = useState(false)
const [editConfig, setEditConfig] = useState<NightModeConfig | null>(null)
const fetchData = useCallback(async () => {
setError(null)
try {
const [statusRes, servicesRes] = await Promise.all([
fetch('/api/admin/night-mode'),
fetch('/api/admin/night-mode/services'),
])
if (statusRes.ok) {
const data = await statusRes.json()
setStatus(data)
if (!editMode) {
setEditConfig(data.config)
}
} else {
const errData = await statusRes.json()
setError(errData.error || 'Fehler beim Laden des Status')
}
if (servicesRes.ok) {
setServices(await servicesRes.json())
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Verbindung zum Night-Scheduler fehlgeschlagen')
} finally {
setLoading(false)
}
}, [editMode])
useEffect(() => {
fetchData()
}, [fetchData])
// Auto-Refresh alle 30 Sekunden
useEffect(() => {
const interval = setInterval(fetchData, 30000)
return () => clearInterval(interval)
}, [fetchData])
const saveConfig = async () => {
if (!editConfig) return
setActionLoading('save')
setError(null)
setSuccessMessage(null)
try {
const response = await fetch('/api/admin/night-mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editConfig),
})
if (!response.ok) {
const errData = await response.json()
throw new Error(errData.error || 'Fehler beim Speichern')
}
setEditMode(false)
setSuccessMessage('Konfiguration gespeichert')
setTimeout(() => setSuccessMessage(null), 3000)
fetchData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Speichern fehlgeschlagen')
} finally {
setActionLoading(null)
}
}
const executeAction = async (action: 'start' | 'stop') => {
setActionLoading(action)
setError(null)
setSuccessMessage(null)
try {
const response = await fetch('/api/admin/night-mode/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
})
if (!response.ok) {
const errData = await response.json()
throw new Error(errData.error || `Fehler bei ${action}`)
}
const data = await response.json()
setSuccessMessage(data.message || `${action === 'start' ? 'Gestartet' : 'Gestoppt'}`)
setTimeout(() => setSuccessMessage(null), 5000)
fetchData()
} catch (err) {
setError(err instanceof Error ? err.message : `${action} fehlgeschlagen`)
} finally {
setActionLoading(null)
}
}
const toggleEnabled = async () => {
if (!editConfig) return
const newConfig = { ...editConfig, enabled: !editConfig.enabled }
setEditConfig(newConfig)
setActionLoading('toggle')
setError(null)
try {
const response = await fetch('/api/admin/night-mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newConfig),
})
if (!response.ok) {
throw new Error('Fehler beim Umschalten')
}
setSuccessMessage(newConfig.enabled ? 'Nachtmodus aktiviert' : 'Nachtmodus deaktiviert')
setTimeout(() => setSuccessMessage(null), 3000)
fetchData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Umschalten fehlgeschlagen')
// Zuruecksetzen bei Fehler
setEditConfig({ ...editConfig })
} finally {
setActionLoading(null)
}
}
const getServiceStatusColor = (state: string) => {
const lower = state.toLowerCase()
if (lower === 'running' || lower.includes('up')) {
return 'bg-green-100 text-green-800'
}
if (lower === 'exited' || lower.includes('exit')) {
return 'bg-slate-100 text-slate-600'
}
if (lower === 'paused' || lower.includes('pause')) {
return 'bg-yellow-100 text-yellow-800'
}
return 'bg-slate-100 text-slate-600'
}
const runningCount = Object.values(status?.services_status || {}).filter(
s => s.toLowerCase() === 'running' || s.toLowerCase().includes('up')
).length
const stoppedCount = Object.values(status?.services_status || {}).filter(
s => s.toLowerCase() === 'exited' || s.toLowerCase().includes('exit')
).length
const {
status,
services,
loading,
actionLoading,
error,
successMessage,
editMode,
setEditMode,
editConfig,
setEditConfig,
saveConfig,
executeAction,
toggleEnabled,
cancelEdit,
runningCount,
stoppedCount,
} = useNightMode()
return (
<div className="p-6">
@@ -226,334 +65,33 @@ export default function NightModePage() {
</div>
) : (
<>
{/* Haupt-Steuerung */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6">
{/* Toggle */}
<div className="flex items-center gap-6">
<div
onClick={toggleEnabled}
className={`relative inline-flex h-10 w-20 cursor-pointer items-center rounded-full transition-colors ${
editConfig?.enabled ? 'bg-orange-600' : 'bg-slate-300'
} ${actionLoading === 'toggle' ? 'opacity-50 cursor-wait' : ''}`}
>
<span
className={`inline-block h-8 w-8 transform rounded-full bg-white shadow-lg transition-transform ${
editConfig?.enabled ? 'translate-x-11' : 'translate-x-1'
}`}
/>
</div>
<div>
<h2 className="text-xl font-semibold text-slate-900">
Nachtmodus: {editConfig?.enabled ? 'Aktiv' : 'Inaktiv'}
</h2>
<p className="text-sm text-slate-500">
{editConfig?.enabled
? `Abschaltung um ${editConfig.shutdown_time}, Start um ${editConfig.startup_time}`
: 'Zeitgesteuerte Abschaltung ist deaktiviert'}
</p>
</div>
</div>
<MainControl
editConfig={editConfig}
actionLoading={actionLoading}
onToggle={toggleEnabled}
onExecute={executeAction}
/>
{/* Manuelle Aktionen */}
<div className="flex gap-3">
<button
onClick={() => executeAction('stop')}
disabled={actionLoading !== null}
className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 transition-colors"
>
{actionLoading === 'stop' ? (
<span className="animate-spin">&#9696;</span>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
</svg>
)}
Jetzt abschalten
</button>
<button
onClick={() => executeAction('start')}
disabled={actionLoading !== null}
className="flex items-center gap-2 px-5 py-2.5 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 transition-colors"
>
{actionLoading === 'start' ? (
<span className="animate-spin">&#9696;</span>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
Jetzt starten
</button>
</div>
</div>
</div>
<StatusCards
status={status}
runningCount={runningCount}
stoppedCount={stoppedCount}
/>
{/* Status-Karten */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-5">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<div className="text-2xl font-bold text-slate-900">{status?.current_time || '--:--'}</div>
<div className="text-sm text-slate-500">Aktuelle Zeit</div>
</div>
</div>
</div>
<TimeConfig
editMode={editMode}
editConfig={editConfig}
actionLoading={actionLoading}
status={status}
onSetEditMode={setEditMode}
onSetEditConfig={setEditConfig}
onSave={saveConfig}
onCancel={cancelEdit}
/>
<div className="bg-white rounded-xl border border-slate-200 p-5">
<div className="flex items-center gap-3">
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
status?.next_action === 'shutdown' ? 'bg-red-100' : 'bg-green-100'
}`}>
{status?.next_action === 'shutdown' ? (
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</div>
<div>
<div className="text-2xl font-bold text-slate-900">
{status?.next_action_time || '--:--'}
</div>
<div className="text-sm text-slate-500">
{status?.next_action === 'shutdown' ? 'Naechste Abschaltung' : 'Naechster Start'}
</div>
</div>
</div>
</div>
<ServiceList services={services} status={status} />
<div className="bg-white rounded-xl border border-slate-200 p-5">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center">
<svg className="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<div className="text-2xl font-bold text-slate-900">
{status?.time_until_next_action || '-'}
</div>
<div className="text-sm text-slate-500">Countdown</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-5">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center">
<svg className="w-6 h-6 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<div>
<div className="flex items-center gap-2">
<span className="text-lg font-bold text-green-600">{runningCount}</span>
<span className="text-slate-400">/</span>
<span className="text-lg font-bold text-slate-600">{stoppedCount}</span>
</div>
<div className="text-sm text-slate-500">Aktiv / Gestoppt</div>
</div>
</div>
</div>
</div>
{/* Zeitkonfiguration */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-900">Zeitkonfiguration</h3>
{editMode ? (
<div className="flex gap-2">
<button
onClick={() => {
setEditMode(false)
setEditConfig(status?.config || null)
}}
className="px-4 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
>
Abbrechen
</button>
<button
onClick={saveConfig}
disabled={actionLoading === 'save'}
className="px-4 py-2 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 transition-colors"
>
{actionLoading === 'save' ? 'Speichern...' : 'Speichern'}
</button>
</div>
) : (
<button
onClick={() => setEditMode(true)}
className="px-4 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
>
Bearbeiten
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
<span className="flex items-center gap-2">
<svg className="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
Abschaltung um
</span>
</label>
{editMode ? (
<input
type="time"
value={editConfig?.shutdown_time || '22:00'}
onChange={e => setEditConfig(prev => prev ? { ...prev, shutdown_time: e.target.value } : null)}
className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
) : (
<div className="text-3xl font-mono font-bold text-slate-900 bg-slate-50 px-4 py-3 rounded-lg">
{editConfig?.shutdown_time || '22:00'}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
<span className="flex items-center gap-2">
<svg className="w-4 h-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Start um
</span>
</label>
{editMode ? (
<input
type="time"
value={editConfig?.startup_time || '06:00'}
onChange={e => setEditConfig(prev => prev ? { ...prev, startup_time: e.target.value } : null)}
className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
) : (
<div className="text-3xl font-mono font-bold text-slate-900 bg-slate-50 px-4 py-3 rounded-lg">
{editConfig?.startup_time || '06:00'}
</div>
)}
</div>
</div>
{/* Letzte Aktion */}
{status?.config.last_action && (
<div className="mt-6 pt-6 border-t border-slate-200">
<div className="text-sm text-slate-500">
Letzte Aktion:{' '}
<span className={`font-semibold ${status.config.last_action === 'startup' ? 'text-green-600' : 'text-red-600'}`}>
{status.config.last_action === 'startup' ? 'Gestartet' : 'Abgeschaltet'}
</span>
{status.config.last_action_time && (
<span className="ml-2">
am {new Date(status.config.last_action_time).toLocaleString('de-DE')}
</span>
)}
</div>
</div>
)}
</div>
{/* Service-Liste */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-200">
<h3 className="text-lg font-semibold text-slate-900">Services</h3>
<p className="text-sm text-slate-500 mt-1">
Gruen = wird verwaltet, Grau = ausgeschlossen (laeuft immer)
</p>
</div>
<div className="p-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{services?.all_services.map(service => {
const serviceStatus = status?.services_status[service] || 'unknown'
const isExcluded = services.excluded_services.includes(service)
return (
<div
key={service}
className={`flex items-center justify-between px-4 py-3 rounded-lg border ${
isExcluded
? 'bg-slate-50 border-slate-200'
: 'bg-orange-50 border-orange-200'
}`}
>
<div className="flex items-center gap-2">
{isExcluded ? (
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
) : (
<svg className="w-4 h-4 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
<span className={`text-sm font-medium ${isExcluded ? 'text-slate-500' : 'text-slate-900'}`}>
{service}
</span>
</div>
<span className={`px-2 py-0.5 rounded text-xs font-semibold ${getServiceStatusColor(serviceStatus)}`}>
{serviceStatus}
</span>
</div>
)
})}
{/* Auch excluded Services anzeigen, die nicht in all_services sind */}
{services?.excluded_services
.filter(s => !services.all_services.includes(s))
.map(service => (
<div
key={service}
className="flex items-center justify-between px-4 py-3 rounded-lg border bg-slate-50 border-slate-200"
>
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span className="text-sm font-medium text-slate-500">
{service}
</span>
</div>
<span className="px-2 py-0.5 rounded text-xs font-semibold bg-slate-100 text-slate-600">
excluded
</span>
</div>
))}
</div>
</div>
</div>
{/* Info Box */}
<div className="mt-6 bg-orange-50 border border-orange-200 rounded-xl p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-semibold text-orange-900">Hinweise zur Nachtabschaltung</h4>
<ul className="text-sm text-orange-800 mt-1 space-y-1 list-disc list-inside">
<li>Der <strong>night-scheduler</strong> und <strong>nginx</strong> bleiben immer aktiv</li>
<li>Services werden mit <code>docker compose stop</code> angehalten (Daten bleiben erhalten)</li>
<li>Bei manuellem Start/Stop wird die letzte Aktion gespeichert</li>
<li>Der Scheduler prueft jede Minute, ob eine Aktion faellig ist</li>
</ul>
</div>
</div>
</div>
<InfoBox />
</>
)}
</div>