[split-required] Split final batch of monoliths >1000 LOC

Python (6 files in klausur-service):
- rbac.py (1,132 → 4), admin_api.py (1,012 → 4)
- routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5)

Python (2 files in backend-lehrer):
- unit_api.py (1,226 → 6), game_api.py (1,129 → 5)

Website (6 page files):
- 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components
  in website/components/klausur-korrektur/ (17 shared files)
- companion (1,057 → 10), magic-help (1,017 → 8)

All re-export barrels preserve backward compatibility.
Zero import errors verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 23:17:30 +02:00
parent b2a0126f14
commit 6811264756
67 changed files with 12270 additions and 13651 deletions

View File

@@ -0,0 +1,139 @@
'use client'
/**
* Document Viewer with annotation overlay and page navigation.
* Left panel (2/3 width) in the Korrektur-Workspace.
*/
import type { Annotation, AnnotationType, AnnotationPosition, StudentWork } from '../../app/admin/klausur-korrektur/types'
// Re-use existing annotation components from the klausur-korrektur route
interface DocumentViewerProps {
student: StudentWork | null
documentUrl: string | null
zoom: number
currentPage: number
totalPages: number
annotations: Annotation[]
selectedTool: AnnotationType | null
selectedAnnotation: Annotation | null
annotationCounts: Record<AnnotationType, number>
onZoomChange: (zoom: number) => void
onPageChange: (page: number) => void
onSelectTool: (tool: AnnotationType | null) => void
onCreateAnnotation: (position: AnnotationPosition, type: AnnotationType) => void
onSelectAnnotation: (ann: Annotation) => void
// Render props for toolbar and annotation layer since they are imported from route-local components
AnnotationToolbarComponent: React.ComponentType<{
selectedTool: AnnotationType | null
onSelectTool: (tool: AnnotationType | null) => void
zoom: number
onZoomChange: (zoom: number) => void
annotationCounts: Record<AnnotationType, number>
}>
AnnotationLayerComponent: React.ComponentType<{
annotations: Annotation[]
selectedTool: AnnotationType | null
onCreateAnnotation: (position: AnnotationPosition, type: AnnotationType) => void
onSelectAnnotation: (ann: Annotation) => void
selectedAnnotationId?: string
}>
}
export default function DocumentViewer({
student, documentUrl, zoom, currentPage, totalPages,
annotations, selectedTool, selectedAnnotation, annotationCounts,
onZoomChange, onPageChange, onSelectTool,
onCreateAnnotation, onSelectAnnotation,
AnnotationToolbarComponent, AnnotationLayerComponent,
}: DocumentViewerProps) {
return (
<div className="w-2/3 bg-white rounded-lg border border-slate-200 overflow-hidden flex flex-col">
{/* Toolbar */}
<AnnotationToolbarComponent
selectedTool={selectedTool}
onSelectTool={onSelectTool}
zoom={zoom}
onZoomChange={onZoomChange}
annotationCounts={annotationCounts}
/>
{/* Document display with annotation overlay */}
<div className="flex-1 overflow-auto p-4 bg-slate-100">
{documentUrl ? (
<div
className="mx-auto bg-white shadow-lg relative"
style={{ transform: `scale(${zoom / 100})`, transformOrigin: 'top center' }}
>
{student?.file_path?.endsWith('.pdf') ? (
<iframe
src={documentUrl}
className="w-full h-[800px] border-0"
title="Studentenarbeit"
/>
) : (
<div className="relative">
<img
src={documentUrl}
alt="Studentenarbeit"
className="max-w-full"
onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder-document.png'
}}
/>
<AnnotationLayerComponent
annotations={annotations.filter((ann) => ann.page === currentPage)}
selectedTool={selectedTool}
onCreateAnnotation={onCreateAnnotation}
onSelectAnnotation={onSelectAnnotation}
selectedAnnotationId={selectedAnnotation?.id}
/>
</div>
)}
</div>
) : (
<div className="flex items-center justify-center h-full text-slate-400">
Kein Dokument verfuegbar
</div>
)}
</div>
{/* Page navigation */}
<div className="border-t border-slate-200 p-2 flex items-center justify-center gap-2 bg-slate-50">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage <= 1}
className="p-1 rounded hover:bg-slate-200 disabled:opacity-50"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="text-sm">
Seite {currentPage} / {totalPages}
</span>
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage >= totalPages}
className="p-1 rounded hover:bg-slate-200 disabled:opacity-50"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
{/* OCR Text (collapsible) */}
{student?.ocr_text && (
<details className="border-t border-slate-200">
<summary className="p-3 bg-slate-50 cursor-pointer text-sm font-medium text-slate-600 hover:bg-slate-100">
OCR-Text anzeigen
</summary>
<div className="p-4 max-h-48 overflow-auto text-sm text-slate-700 bg-slate-50">
<pre className="whitespace-pre-wrap font-sans">{student.ocr_text}</pre>
</div>
</details>
)}
</div>
)
}