[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

@@ -0,0 +1,208 @@
'use client'
import { RefObject } from 'react'
import type { GridResult, GridCell } from '@/app/(admin)/ai/ocr-kombi/types'
import { colTypeLabel, colTypeColor, confColor, CellCrop } from './WordRecognitionUtils'
const KLAUSUR_API = '/klausur-api'
interface WordRecognitionLabelingProps {
sessionId: string
gridResult: GridResult | null
editedCells: GridCell[]
activeIndex: number
setActiveIndex: (idx: number) => void
columnsUsed: GridResult['columns_used']
getUniqueRowCount: () => number
getRowCells: (rowPosition: number) => GridCell[]
updateCell: (cellId: string, value: string) => void
confirmEntry: () => void
skipEntry: () => void
enRef: RefObject<HTMLInputElement | null>
/** cellsByRow map + sorted row indices, pre-computed by parent */
cellsByRow: Map<number, GridCell[]>
sortedRowIndices: number[]
}
export function WordRecognitionLabeling({
sessionId,
gridResult,
editedCells,
activeIndex,
setActiveIndex,
columnsUsed,
getUniqueRowCount,
getRowCells,
updateCell,
confirmEntry,
skipEntry,
enRef,
cellsByRow,
sortedRowIndices,
}: WordRecognitionLabelingProps) {
if (editedCells.length === 0) return null
const overlayUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/words-overlay`
const dewarpedUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped`
return (
<div className="grid grid-cols-3 gap-4">
{/* Left 2/3: Image with highlighted active row */}
<div className="col-span-2">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Zeile {activeIndex + 1} von {getUniqueRowCount()}
</div>
<div className="border rounded-lg overflow-hidden dark:border-gray-700 bg-gray-50 dark:bg-gray-900 relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`${overlayUrl}?t=${Date.now()}`}
alt="Wort-Overlay"
className="w-full h-auto"
/>
{/* Highlight overlay for active row */}
{(() => {
const rowCells = getRowCells(activeIndex)
return rowCells.map(cell => (
<div
key={cell.cell_id}
className="absolute border-2 border-yellow-400 bg-yellow-400/10 pointer-events-none"
style={{
left: `${cell.bbox_pct.x}%`,
top: `${cell.bbox_pct.y}%`,
width: `${cell.bbox_pct.w}%`,
height: `${cell.bbox_pct.h}%`,
}}
/>
))
})()}
</div>
</div>
{/* Right 1/3: Editable fields */}
<div className="space-y-3">
{/* Navigation */}
<div className="flex items-center justify-between">
<button
onClick={() => setActiveIndex(Math.max(0, activeIndex - 1))}
disabled={activeIndex === 0}
className="px-2 py-1 text-xs border rounded hover:bg-gray-50 dark:hover:bg-gray-700 dark:border-gray-600 disabled:opacity-30"
>
Zurueck
</button>
<span className="text-xs text-gray-500">
{activeIndex + 1} / {getUniqueRowCount()}
</span>
<button
onClick={() => setActiveIndex(Math.min(
getUniqueRowCount() - 1,
activeIndex + 1
))}
disabled={activeIndex >= getUniqueRowCount() - 1}
className="px-2 py-1 text-xs border rounded hover:bg-gray-50 dark:hover:bg-gray-700 dark:border-gray-600 disabled:opacity-30"
>
Weiter
</button>
</div>
{/* Status badge */}
<div className="flex items-center gap-2">
{(() => {
const rowCells = getRowCells(activeIndex)
const avgConf = rowCells.length
? Math.round(rowCells.reduce((s, c) => s + c.confidence, 0) / rowCells.length)
: 0
return (
<span className={`text-xs font-mono ${confColor(avgConf)}`}>
{avgConf}% Konfidenz
</span>
)
})()}
</div>
{/* Editable fields -- one per column, driven by columns_used */}
<div className="space-y-2">
{(() => {
const rowCells = getRowCells(activeIndex)
return columnsUsed.map((col, colIdx) => {
const cell = rowCells.find(c => c.col_index === col.index)
if (!cell) return null
return (
<div key={col.index}>
<div className="flex items-center gap-1 mb-0.5">
<label className={`text-[10px] font-medium ${colTypeColor(col.type)}`}>
{colTypeLabel(col.type)}
</label>
<span className="text-[9px] text-gray-400">{cell.cell_id}</span>
</div>
{/* Cell crop */}
<div className="border rounded dark:border-gray-700 overflow-hidden bg-white dark:bg-gray-900 h-10 relative mb-1">
<CellCrop imageUrl={dewarpedUrl} bbox={cell.bbox_pct} />
</div>
<textarea
ref={colIdx === 0 ? enRef as any : undefined}
rows={Math.max(1, (cell.text || '').split('\n').length)}
value={cell.text || ''}
onChange={(e) => updateCell(cell.cell_id, e.target.value)}
className="w-full px-2 py-1.5 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 font-mono resize-none"
/>
</div>
)
})
})()}
</div>
{/* Action buttons */}
<div className="flex gap-2">
<button
onClick={confirmEntry}
className="flex-1 px-3 py-1.5 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium"
>
Bestaetigen (Enter)
</button>
<button
onClick={skipEntry}
className="px-3 py-1.5 text-xs border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 dark:border-gray-600"
>
Skip
</button>
</div>
{/* Shortcuts hint */}
<div className="text-[10px] text-gray-400 space-y-0.5">
<div>Enter = Bestaetigen & weiter</div>
<div>Ctrl+Down = Ueberspringen</div>
<div>Ctrl+Up = Zurueck</div>
</div>
{/* Row list (compact) */}
<div className="border-t dark:border-gray-700 pt-2 mt-2">
<div className="text-[10px] font-medium text-gray-500 dark:text-gray-400 mb-1">
Alle Zeilen
</div>
<div className="max-h-48 overflow-y-auto space-y-0.5">
{sortedRowIndices.map((rowIdx, posIdx) => {
const rowCells = cellsByRow.get(rowIdx) || []
const textParts = rowCells.filter(c => c.text).map(c => c.text.replace(/\n/g, ' '))
return (
<div
key={rowIdx}
onClick={() => setActiveIndex(posIdx)}
className={`flex items-center gap-1 px-2 py-1 rounded text-[10px] cursor-pointer transition-colors ${
posIdx === activeIndex
? 'bg-teal-50 dark:bg-teal-900/30 border border-teal-200 dark:border-teal-700'
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
<span className="w-6 text-right text-gray-400 font-mono">R{String(rowIdx).padStart(2, '0')}</span>
<span className="truncate text-gray-600 dark:text-gray-400 font-mono">
{textParts.join(' \u2192 ') || '\u2014'}
</span>
</div>
)
})}
</div>
</div>
</div>
</div>
)
}