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

@@ -1,6 +1,40 @@
'use client'
import { useMemo, useState } from 'react'
import { useMemo } from 'react'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import {
Database,
FileText,
List,
Table,
AlertTriangle,
Shield,
Clock,
Users,
Globe,
} from 'lucide-react'
import {
DataPoint,
DataPointCategory,
@@ -22,70 +56,79 @@ const PLACEHOLDERS = [
{
placeholder: '[DATENPUNKTE_TABLE]',
label: { de: 'Tabelle', en: 'Table' },
description: { de: 'Markdown-Tabelle mit allen Datenpunkten', en: 'Markdown table with all data points' },
description: { de: 'Fügt eine Markdown-Tabelle mit allen Datenpunkten ein', en: 'Inserts a markdown table with all data points' },
icon: Table,
},
{
placeholder: '[DATENPUNKTE_LIST]',
label: { de: 'Liste', en: 'List' },
description: { de: 'Kommaseparierte Liste der Namen', en: 'Comma-separated list of names' },
description: { de: 'Kommaseparierte Liste der Datenpunkt-Namen', en: 'Comma-separated list of data point names' },
icon: List,
},
{
placeholder: '[VERARBEITUNGSZWECKE]',
label: { de: 'Zwecke', en: 'Purposes' },
description: { de: 'Alle Verarbeitungszwecke', en: 'All processing purposes' },
description: { de: 'Alle Verarbeitungszwecke (dedupliziert)', en: 'All processing purposes (deduplicated)' },
icon: FileText,
},
{
placeholder: '[RECHTSGRUNDLAGEN]',
label: { de: 'Rechtsgrundlagen', en: 'Legal Bases' },
description: { de: 'DSGVO-Artikel', en: 'GDPR articles' },
description: { de: 'Verwendete DSGVO-Artikel', en: 'Used GDPR articles' },
icon: Shield,
},
{
placeholder: '[SPEICHERFRISTEN]',
label: { de: 'Speicherfristen', en: 'Retention' },
description: { de: 'Fristen nach Kategorie', en: 'Periods by category' },
description: { de: 'Fristen gruppiert nach Kategorie', en: 'Periods grouped by category' },
icon: Clock,
},
{
placeholder: '[EMPFAENGER]',
label: { de: 'Empfänger', en: 'Recipients' },
description: { de: 'Liste aller Drittparteien', en: 'List of third parties' },
description: { de: 'Liste aller Drittparteien', en: 'List of all third parties' },
icon: Users,
},
{
placeholder: '[BESONDERE_KATEGORIEN]',
label: { de: 'Art. 9', en: 'Art. 9' },
description: { de: 'Abschnitt für sensible Daten', en: 'Section for sensitive data' },
label: { de: 'Art. 9 Abschnitt', en: 'Art. 9 Section' },
description: { de: 'DSGVO-konformer Abschnitt für sensible Daten', en: 'GDPR-compliant section for sensitive data' },
icon: AlertTriangle,
},
{
placeholder: '[DRITTLAND_TRANSFERS]',
label: { de: 'Drittländer', en: 'Third Countries' },
description: { de: 'Datenübermittlung außerhalb EU', en: 'Data transfers outside EU' },
description: { de: 'Abschnitt zu Datenübermittlung außerhalb EU', en: 'Section about data transfers outside EU' },
icon: Globe,
},
]
/**
* Risiko-Badge Farben
* Risiko-Badge Varianten mapping
*/
function getRiskBadgeColor(riskLevel: RiskLevel): string {
function getRiskBadgeVariant(riskLevel: RiskLevel): 'default' | 'secondary' | 'destructive' | 'outline' {
switch (riskLevel) {
case 'HIGH':
return 'bg-red-100 text-red-700 border-red-200'
return 'destructive'
case 'MEDIUM':
return 'bg-yellow-100 text-yellow-700 border-yellow-200'
return 'secondary'
case 'LOW':
default:
return 'bg-green-100 text-green-700 border-green-200'
return 'outline'
}
}
/**
* DataPointsPreview Komponente
*
* Zeigt eine Vorschau der ausgewählten Einwilligungen-Datenpunkte im Dokumentengenerator.
* Ermöglicht das schnelle Einfügen von Platzhaltern.
*/
export function DataPointsPreview({
dataPoints,
onInsertPlaceholder,
language = 'de',
}: DataPointsPreviewProps) {
const [expandedCategories, setExpandedCategories] = useState<string[]>([])
// Gruppiere Datenpunkte nach Kategorie
const byCategory = useMemo(() => {
return dataPoints.reduce((acc, dp) => {
@@ -101,15 +144,21 @@ export function DataPointsPreview({
const stats = useMemo(() => {
const riskCounts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
let specialCategoryCount = 0
let explicitConsentCount = 0
const recipients = new Set<string>()
dataPoints.forEach(dp => {
riskCounts[dp.riskLevel]++
if (dp.isSpecialCategory) specialCategoryCount++
if (dp.requiresExplicitConsent) explicitConsentCount++
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
})
return {
riskCounts,
specialCategoryCount,
explicitConsentCount,
recipientCount: recipients.size,
categoryCount: Object.keys(byCategory).length,
}
}, [dataPoints, byCategory])
@@ -123,173 +172,195 @@ export function DataPointsPreview({
})
}, [byCategory])
const toggleCategory = (category: string) => {
setExpandedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
)
}
if (dataPoints.length === 0) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="font-semibold text-gray-900 flex items-center gap-2 mb-3">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
</svg>
{language === 'de' ? 'Einwilligungen' : 'Consents'}
</h4>
<p className="text-sm text-gray-500">
{language === 'de'
? 'Keine Datenpunkte ausgewählt. Wählen Sie Datenpunkte im Einwilligungs-Schritt aus.'
: 'No data points selected. Select data points in the consent step.'}
</p>
</div>
<Card className="h-full">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Database className="h-4 w-4" />
{language === 'de' ? 'Einwilligungen' : 'Consents'}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{language === 'de'
? 'Keine Datenpunkte ausgewählt. Wählen Sie Datenpunkte im Einwilligungs-Schritt aus, um sie hier zu sehen.'
: 'No data points selected. Select data points in the consent step to see them here.'}
</p>
</CardContent>
</Card>
)
}
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 h-full flex flex-col">
{/* Header */}
<div className="mb-4">
<h4 className="font-semibold text-gray-900 flex items-center gap-2">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
</svg>
<Card className="h-full flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Database className="h-4 w-4" />
{language === 'de' ? 'Einwilligungen' : 'Consents'}
</h4>
<p className="text-sm text-gray-500 mt-1">
</CardTitle>
<CardDescription>
{dataPoints.length} {language === 'de' ? 'Datenpunkte aus' : 'data points from'}{' '}
{stats.categoryCount} {language === 'de' ? 'Kategorien' : 'categories'}
</p>
</div>
</CardDescription>
</CardHeader>
{/* Statistik-Badges */}
<div className="flex flex-wrap gap-2 mb-4">
{stats.riskCounts.HIGH > 0 && (
<span className="px-2 py-1 text-xs rounded-full bg-red-100 text-red-700">
{stats.riskCounts.HIGH} {language === 'de' ? 'Hoch' : 'High'}
</span>
)}
{stats.riskCounts.MEDIUM > 0 && (
<span className="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-700">
{stats.riskCounts.MEDIUM} {language === 'de' ? 'Mittel' : 'Medium'}
</span>
)}
{stats.riskCounts.LOW > 0 && (
<span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-700">
{stats.riskCounts.LOW} {language === 'de' ? 'Niedrig' : 'Low'}
</span>
)}
{stats.specialCategoryCount > 0 && (
<span className="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700 flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
{stats.specialCategoryCount} Art. 9
</span>
)}
</div>
<CardContent className="flex-1 flex flex-col gap-4 overflow-hidden">
{/* Statistik-Badges */}
<div className="flex flex-wrap gap-2">
{stats.riskCounts.HIGH > 0 && (
<Badge variant="destructive">
{stats.riskCounts.HIGH} {language === 'de' ? 'Hoch' : 'High'}
</Badge>
)}
{stats.riskCounts.MEDIUM > 0 && (
<Badge variant="secondary">
{stats.riskCounts.MEDIUM} {language === 'de' ? 'Mittel' : 'Medium'}
</Badge>
)}
{stats.riskCounts.LOW > 0 && (
<Badge variant="outline">
{stats.riskCounts.LOW} {language === 'de' ? 'Niedrig' : 'Low'}
</Badge>
)}
{stats.specialCategoryCount > 0 && (
<Badge variant="destructive" className="bg-orange-500 hover:bg-orange-600">
<AlertTriangle className="h-3 w-3 mr-1" />
{stats.specialCategoryCount} Art. 9
</Badge>
)}
</div>
<div className="border-t border-gray-200 my-3"></div>
<Separator />
{/* Datenpunkte nach Kategorie */}
<div className="flex-1 overflow-y-auto space-y-2 max-h-64">
{sortedCategories.map(([category, points]) => {
const metadata = CATEGORY_METADATA[category as DataPointCategory]
if (!metadata) return null
const isExpanded = expandedCategories.includes(category)
{/* Datenpunkte nach Kategorie */}
<ScrollArea className="flex-1 -mr-4 pr-4">
<Accordion type="multiple" className="w-full">
{sortedCategories.map(([category, points]) => {
const metadata = CATEGORY_METADATA[category as DataPointCategory]
if (!metadata) return null
return (
<div key={category} className="border border-gray-100 rounded-lg">
<button
onClick={() => toggleCategory(category)}
className="w-full flex items-center justify-between p-2 text-sm hover:bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-gray-400">{metadata.code}</span>
<span className="font-medium text-gray-900">
{language === 'de' ? metadata.name.de : metadata.name.en}
</span>
</div>
<div className="flex items-center gap-2">
<span className="px-1.5 py-0.5 text-xs rounded bg-gray-100 text-gray-600">
{points.length}
</span>
<svg
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{isExpanded && (
<ul className="px-2 pb-2 space-y-1">
{points.map(dp => (
<li
key={dp.id}
className="flex items-center justify-between text-sm py-1 pl-6"
>
<span className="truncate max-w-[160px] text-gray-700">
{language === 'de' ? dp.name.de : dp.name.en}
return (
<AccordionItem key={category} value={category}>
<AccordionTrigger className="text-sm hover:no-underline">
<div className="flex items-center gap-2">
<span className="font-mono text-xs text-muted-foreground">
{metadata.code}
</span>
<div className="flex items-center gap-1">
{dp.isSpecialCategory && (
<svg className="w-3 h-3 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
)}
<span className={`px-1.5 py-0.5 text-[10px] rounded border ${getRiskBadgeColor(dp.riskLevel)}`}>
{RISK_LEVEL_STYLING[dp.riskLevel].label[language]}
</span>
</div>
</li>
))}
</ul>
)}
</div>
)
})}
</div>
<span>
{language === 'de' ? metadata.name.de : metadata.name.en}
</span>
<Badge variant="secondary" className="ml-auto mr-2">
{points.length}
</Badge>
</div>
</AccordionTrigger>
<AccordionContent>
<ul className="space-y-1 pl-6">
{points.map(dp => (
<li
key={dp.id}
className="flex items-center justify-between text-sm py-1"
>
<span className="truncate max-w-[180px]">
{language === 'de' ? dp.name.de : dp.name.en}
</span>
<div className="flex items-center gap-1">
{dp.isSpecialCategory && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<AlertTriangle className="h-3 w-3 text-orange-500" />
</TooltipTrigger>
<TooltipContent>
{language === 'de'
? 'Besondere Kategorie (Art. 9 DSGVO)'
: 'Special Category (Art. 9 GDPR)'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Badge
variant={getRiskBadgeVariant(dp.riskLevel)}
className="text-xs px-1.5 py-0"
>
{RISK_LEVEL_STYLING[dp.riskLevel].label[language]}
</Badge>
</div>
</li>
))}
</ul>
</AccordionContent>
</AccordionItem>
)
})}
</Accordion>
</ScrollArea>
<div className="border-t border-gray-200 my-3"></div>
<Separator />
{/* Schnell-Einfügen Buttons */}
<div>
<p className="text-xs font-medium text-gray-500 mb-2">
{language === 'de' ? 'Platzhalter einfügen:' : 'Insert placeholder:'}
</p>
<div className="flex flex-wrap gap-1.5">
{PLACEHOLDERS.slice(0, 4).map(({ placeholder, label }) => (
<button
key={placeholder}
onClick={() => onInsertPlaceholder(placeholder)}
className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded hover:bg-purple-100 transition-colors"
title={placeholder}
>
{language === 'de' ? label.de : label.en}
</button>
))}
{/* Schnell-Einfügen Buttons */}
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
{language === 'de' ? 'Platzhalter einfügen:' : 'Insert placeholder:'}
</p>
<div className="flex flex-wrap gap-1.5">
<TooltipProvider>
{PLACEHOLDERS.slice(0, 4).map(({ placeholder, label, description, icon: Icon }) => (
<Tooltip key={placeholder}>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="h-7 text-xs px-2"
onClick={() => onInsertPlaceholder(placeholder)}
>
<Icon className="h-3 w-3 mr-1" />
{language === 'de' ? label.de : label.en}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="text-xs">
{language === 'de' ? description.de : description.en}
</p>
<p className="text-xs text-muted-foreground font-mono mt-1">
{placeholder}
</p>
</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-1.5">
<TooltipProvider>
{PLACEHOLDERS.slice(4).map(({ placeholder, label, description, icon: Icon }) => (
<Tooltip key={placeholder}>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="h-7 text-xs px-2"
onClick={() => onInsertPlaceholder(placeholder)}
>
<Icon className="h-3 w-3 mr-1" />
{language === 'de' ? label.de : label.en}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="text-xs">
{language === 'de' ? description.de : description.en}
</p>
<p className="text-xs text-muted-foreground font-mono mt-1">
{placeholder}
</p>
</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
</div>
<div className="flex flex-wrap gap-1.5 mt-1.5">
{PLACEHOLDERS.slice(4).map(({ placeholder, label }) => (
<button
key={placeholder}
onClick={() => onInsertPlaceholder(placeholder)}
className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded hover:bg-purple-100 transition-colors"
title={placeholder}
>
{language === 'de' ? label.de : label.en}
</button>
))}
</div>
</div>
</div>
</CardContent>
</Card>
)
}