fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,419 @@
'use client'
import React, { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'
import type { Canvas, Object as FabricObject } from 'fabric'
import type {
EditorTool,
WorksheetDocument,
WorksheetPage,
PageFormat,
HistoryEntry,
DEFAULT_PAGE_FORMAT
} from '@/app/worksheet-editor/types'
// Context Types
interface WorksheetContextType {
// Canvas
canvas: Canvas | null
setCanvas: (canvas: Canvas | null) => void
// Document
document: WorksheetDocument | null
setDocument: (doc: WorksheetDocument | null) => void
// Tool State
activeTool: EditorTool
setActiveTool: (tool: EditorTool) => void
// Selection
selectedObjects: FabricObject[]
setSelectedObjects: (objects: FabricObject[]) => void
// Zoom
zoom: number
setZoom: (zoom: number) => void
zoomIn: () => void
zoomOut: () => void
zoomToFit: () => void
// Grid
showGrid: boolean
setShowGrid: (show: boolean) => void
snapToGrid: boolean
setSnapToGrid: (snap: boolean) => void
gridSize: number
setGridSize: (size: number) => void
// Pages
currentPageIndex: number
setCurrentPageIndex: (index: number) => void
addPage: () => void
deletePage: (index: number) => void
// History
canUndo: boolean
canRedo: boolean
undo: () => void
redo: () => void
saveToHistory: (action: string) => void
// Save/Load
saveDocument: () => Promise<void>
loadDocument: (id: string) => Promise<void>
// Export
exportToPDF: () => Promise<Blob>
exportToImage: (format: 'png' | 'jpg') => Promise<Blob>
// Dirty State
isDirty: boolean
setIsDirty: (dirty: boolean) => void
}
const WorksheetContext = createContext<WorksheetContextType | null>(null)
// Provider Props
interface WorksheetProviderProps {
children: ReactNode
}
// Generate unique ID
const generateId = () => `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
// Default Page Format
const defaultPageFormat: PageFormat = {
width: 210,
height: 297,
orientation: 'portrait',
margins: { top: 15, right: 15, bottom: 15, left: 15 }
}
export function WorksheetProvider({ children }: WorksheetProviderProps) {
// Canvas State
const [canvas, setCanvas] = useState<Canvas | null>(null)
// Document State
const [document, setDocument] = useState<WorksheetDocument | null>(null)
// Editor State
const [activeTool, setActiveTool] = useState<EditorTool>('select')
const [selectedObjects, setSelectedObjects] = useState<FabricObject[]>([])
const [zoom, setZoom] = useState(1)
const [showGrid, setShowGrid] = useState(true)
const [snapToGrid, setSnapToGrid] = useState(true)
const [gridSize, setGridSize] = useState(10)
const [currentPageIndex, setCurrentPageIndex] = useState(0)
const [isDirty, setIsDirty] = useState(false)
// History State
const historyRef = useRef<HistoryEntry[]>([])
const historyIndexRef = useRef(-1)
const [canUndo, setCanUndo] = useState(false)
const [canRedo, setCanRedo] = useState(false)
// Initialize empty document
useEffect(() => {
if (!document) {
const newDoc: WorksheetDocument = {
id: generateId(),
title: 'Neues Arbeitsblatt',
pages: [{
id: generateId(),
index: 0,
canvasJSON: ''
}],
pageFormat: defaultPageFormat,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
setDocument(newDoc)
}
}, [document])
// Zoom functions
const zoomIn = useCallback(() => {
setZoom(prev => Math.min(prev * 1.2, 4))
}, [])
const zoomOut = useCallback(() => {
setZoom(prev => Math.max(prev / 1.2, 0.25))
}, [])
const zoomToFit = useCallback(() => {
setZoom(1)
}, [])
// Page functions
const addPage = useCallback(() => {
if (!document) return
const newPage: WorksheetPage = {
id: generateId(),
index: document.pages.length,
canvasJSON: ''
}
setDocument({
...document,
pages: [...document.pages, newPage],
updatedAt: new Date().toISOString()
})
setCurrentPageIndex(document.pages.length)
setIsDirty(true)
}, [document])
const deletePage = useCallback((index: number) => {
if (!document || document.pages.length <= 1) return
const newPages = document.pages.filter((_, i) => i !== index)
.map((page, i) => ({ ...page, index: i }))
setDocument({
...document,
pages: newPages,
updatedAt: new Date().toISOString()
})
if (currentPageIndex >= newPages.length) {
setCurrentPageIndex(newPages.length - 1)
}
setIsDirty(true)
}, [document, currentPageIndex])
// History functions
const saveToHistory = useCallback((action: string) => {
if (!canvas) return
try {
const canvasData = canvas.toJSON()
if (!canvasData) return
const entry: HistoryEntry = {
canvasJSON: JSON.stringify(canvasData),
timestamp: Date.now(),
action
}
// Remove any future history if we're not at the end
if (historyIndexRef.current < historyRef.current.length - 1) {
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1)
}
historyRef.current.push(entry)
historyIndexRef.current = historyRef.current.length - 1
// Limit history size
if (historyRef.current.length > 50) {
historyRef.current.shift()
historyIndexRef.current--
}
setCanUndo(historyIndexRef.current > 0)
setCanRedo(false)
setIsDirty(true)
} catch (error) {
console.error('Failed to save history:', error)
}
}, [canvas])
const undo = useCallback(() => {
if (!canvas || historyIndexRef.current <= 0) return
historyIndexRef.current--
const entry = historyRef.current[historyIndexRef.current]
canvas.loadFromJSON(JSON.parse(entry.canvasJSON), () => {
canvas.renderAll()
})
setCanUndo(historyIndexRef.current > 0)
setCanRedo(true)
}, [canvas])
const redo = useCallback(() => {
if (!canvas || historyIndexRef.current >= historyRef.current.length - 1) return
historyIndexRef.current++
const entry = historyRef.current[historyIndexRef.current]
canvas.loadFromJSON(JSON.parse(entry.canvasJSON), () => {
canvas.renderAll()
})
setCanUndo(true)
setCanRedo(historyIndexRef.current < historyRef.current.length - 1)
}, [canvas])
// Save/Load functions
const saveDocument = useCallback(async () => {
if (!canvas || !document) return
// Save current page state
const currentPage = document.pages[currentPageIndex]
currentPage.canvasJSON = JSON.stringify(canvas.toJSON())
// Get API base URL (use same protocol as page)
const { hostname, protocol } = window.location
const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
try {
const response = await fetch(`${apiBase}/api/v1/worksheet/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(document)
})
if (!response.ok) throw new Error('Failed to save')
const result = await response.json()
setDocument({ ...document, id: result.id })
setIsDirty(false)
} catch (error) {
console.error('Save failed:', error)
// Fallback to localStorage
localStorage.setItem(`worksheet_${document.id}`, JSON.stringify(document))
setIsDirty(false)
}
}, [canvas, document, currentPageIndex])
const loadDocument = useCallback(async (id: string) => {
const { hostname, protocol } = window.location
const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
try {
const response = await fetch(`${apiBase}/api/v1/worksheet/${id}`)
if (!response.ok) throw new Error('Failed to load')
const doc = await response.json()
setDocument(doc)
setCurrentPageIndex(0)
if (canvas && doc.pages[0]?.canvasJSON) {
canvas.loadFromJSON(JSON.parse(doc.pages[0].canvasJSON), () => {
canvas.renderAll()
})
}
} catch (error) {
console.error('Load failed:', error)
// Try localStorage fallback
const stored = localStorage.getItem(`worksheet_${id}`)
if (stored) {
const doc = JSON.parse(stored)
setDocument(doc)
setCurrentPageIndex(0)
}
}
}, [canvas])
// Export functions
const exportToPDF = useCallback(async (): Promise<Blob> => {
if (!canvas || !document) throw new Error('No canvas or document')
const { PDFDocument, rgb } = await import('pdf-lib')
const pdfDoc = await PDFDocument.create()
for (const page of document.pages) {
const pdfPage = pdfDoc.addPage([
document.pageFormat.width * 2.83465, // mm to points
document.pageFormat.height * 2.83465
])
// Export canvas as PNG and embed
if (page.canvasJSON) {
// Load the page content
if (currentPageIndex !== page.index) {
await new Promise<void>((resolve) => {
canvas.loadFromJSON(JSON.parse(page.canvasJSON), () => {
canvas.renderAll()
resolve()
})
})
}
const dataUrl = canvas.toDataURL({ format: 'png', multiplier: 2 })
const imageBytes = await fetch(dataUrl).then(res => res.arrayBuffer())
const image = await pdfDoc.embedPng(imageBytes)
pdfPage.drawImage(image, {
x: 0,
y: 0,
width: pdfPage.getWidth(),
height: pdfPage.getHeight()
})
}
}
const pdfBytes = await pdfDoc.save()
// Convert Uint8Array to ArrayBuffer for Blob compatibility
const arrayBuffer = pdfBytes.slice().buffer as ArrayBuffer
return new Blob([arrayBuffer], { type: 'application/pdf' })
}, [canvas, document, currentPageIndex])
const exportToImage = useCallback(async (format: 'png' | 'jpg'): Promise<Blob> => {
if (!canvas) throw new Error('No canvas')
// Fabric.js uses 'jpeg' not 'jpg'
const fabricFormat = format === 'jpg' ? 'jpeg' : format
const dataUrl = canvas.toDataURL({
format: fabricFormat as 'png' | 'jpeg',
quality: format === 'jpg' ? 0.9 : undefined,
multiplier: 2
})
const response = await fetch(dataUrl)
return response.blob()
}, [canvas])
const value: WorksheetContextType = {
canvas,
setCanvas,
document,
setDocument,
activeTool,
setActiveTool,
selectedObjects,
setSelectedObjects,
zoom,
setZoom,
zoomIn,
zoomOut,
zoomToFit,
showGrid,
setShowGrid,
snapToGrid,
setSnapToGrid,
gridSize,
setGridSize,
currentPageIndex,
setCurrentPageIndex,
addPage,
deletePage,
canUndo,
canRedo,
undo,
redo,
saveToHistory,
saveDocument,
loadDocument,
exportToPDF,
exportToImage,
isDirty,
setIsDirty
}
return (
<WorksheetContext.Provider value={value}>
{children}
</WorksheetContext.Provider>
)
}
export function useWorksheet() {
const context = useContext(WorksheetContext)
if (!context) {
throw new Error('useWorksheet must be used within a WorksheetProvider')
}
return context
}

View File

@@ -0,0 +1,272 @@
/**
* Worksheet Cleanup Service
*
* API client for the worksheet cleanup endpoints:
* - Handwriting detection
* - Handwriting removal (inpainting)
* - Layout reconstruction
*
* All processing happens on the local Mac Mini server.
*/
export interface CleanupCapabilities {
opencv_available: boolean
lama_available: boolean
paddleocr_available: boolean
}
export interface DetectionResult {
has_handwriting: boolean
confidence: number
handwriting_ratio: number
detection_method: string
mask_base64?: string
}
export interface PreviewResult {
has_handwriting: boolean
confidence: number
handwriting_ratio: number
image_width: number
image_height: number
estimated_times_ms: {
detection: number
inpainting: number
reconstruction: number
total: number
}
capabilities: {
lama_available: boolean
}
}
export interface PipelineResult {
success: boolean
handwriting_detected: boolean
handwriting_removed: boolean
layout_reconstructed: boolean
cleaned_image_base64?: string
fabric_json?: any
metadata: {
detection?: {
confidence: number
handwriting_ratio: number
method: string
}
inpainting?: {
method_used: string
processing_time_ms: number
}
layout?: {
element_count: number
table_count: number
page_width: number
page_height: number
}
}
}
export type InpaintingMethod = 'auto' | 'opencv_telea' | 'opencv_ns' | 'lama'
export interface CleanupOptions {
removeHandwriting: boolean
reconstructLayout: boolean
inpaintingMethod: InpaintingMethod
}
/**
* Get the API base URL for the klausur-service
*/
function getApiUrl(): string {
if (typeof window === 'undefined') return 'http://localhost:8086'
const { hostname, protocol } = window.location
return hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
}
/**
* Get available cleanup capabilities on the server
*/
export async function getCapabilities(): Promise<CleanupCapabilities> {
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/capabilities`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
}
/**
* Quick preview of cleanup without full processing
*/
export async function previewCleanup(file: File): Promise<PreviewResult> {
const formData = new FormData()
formData.append('image', file)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/preview-cleanup`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
return response.json()
}
/**
* Detect handwriting in an image
*/
export async function detectHandwriting(
file: File,
options: { returnMask?: boolean; minConfidence?: number } = {}
): Promise<DetectionResult> {
const formData = new FormData()
formData.append('image', file)
formData.append('return_mask', String(options.returnMask ?? true))
formData.append('min_confidence', String(options.minConfidence ?? 0.3))
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/detect-handwriting`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
return response.json()
}
/**
* Get the handwriting detection mask as an image blob
*/
export async function getHandwritingMask(file: File): Promise<Blob> {
const formData = new FormData()
formData.append('image', file)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/detect-handwriting/mask`, {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.blob()
}
/**
* Remove handwriting from an image
*/
export async function removeHandwriting(
file: File,
options: {
mask?: File
method?: InpaintingMethod
returnBase64?: boolean
} = {}
): Promise<{ imageBlob?: Blob; imageBase64?: string; metadata: any }> {
const formData = new FormData()
formData.append('image', file)
formData.append('method', options.method ?? 'auto')
formData.append('return_base64', String(options.returnBase64 ?? false))
if (options.mask) {
formData.append('mask', options.mask)
}
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/remove-handwriting`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
if (options.returnBase64) {
const data = await response.json()
return {
imageBase64: data.image_base64,
metadata: data.metadata
}
} else {
const imageBlob = await response.blob()
const methodUsed = response.headers.get('X-Method-Used') || 'unknown'
const processingTime = parseFloat(response.headers.get('X-Processing-Time-Ms') || '0')
return {
imageBlob,
metadata: {
method_used: methodUsed,
processing_time_ms: processingTime
}
}
}
}
/**
* Run the full cleanup pipeline
*/
export async function runCleanupPipeline(
file: File,
options: CleanupOptions = {
removeHandwriting: true,
reconstructLayout: true,
inpaintingMethod: 'auto'
}
): Promise<PipelineResult> {
const formData = new FormData()
formData.append('image', file)
formData.append('remove_handwriting', String(options.removeHandwriting))
formData.append('reconstruct', String(options.reconstructLayout))
formData.append('inpainting_method', options.inpaintingMethod)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/cleanup-pipeline`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
return response.json()
}
/**
* Helper to convert base64 to blob
*/
export function base64ToBlob(base64: string, mimeType: string = 'image/png'): Blob {
const byteCharacters = atob(base64)
const byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512)
const byteNumbers = new Array(slice.length)
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
byteArrays.push(byteArray)
}
return new Blob(byteArrays, { type: mimeType })
}
/**
* Helper to create object URL from base64
*/
export function base64ToObjectUrl(base64: string, mimeType: string = 'image/png'): string {
const blob = base64ToBlob(base64, mimeType)
return URL.createObjectURL(blob)
}

View File

@@ -0,0 +1,10 @@
/**
* Worksheet Editor Library
*
* Export context and utilities
*/
export { WorksheetProvider, useWorksheet } from './WorksheetContext'
// Cleanup Service for handwriting removal
export * from './cleanup-service'