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,501 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import dynamic from 'next/dynamic'
import {
AOIResponse,
AOITheme,
AOIQuality,
Difficulty,
GeoJSONPolygon,
LearningNode,
GeoServiceHealth,
DemoTemplate,
} from './types'
// Dynamic imports for map components (no SSR)
const AOISelector = dynamic(
() => import('@/components/geo-lernwelt/AOISelector'),
{ ssr: false, loading: () => <MapLoadingPlaceholder /> }
)
const UnityViewer = dynamic(
() => import('@/components/geo-lernwelt/UnityViewer'),
{ ssr: false }
)
// API base URL
const GEO_SERVICE_URL = process.env.NEXT_PUBLIC_GEO_SERVICE_URL || 'http://localhost:8088'
// Loading placeholder for map
function MapLoadingPlaceholder() {
return (
<div className="w-full h-[400px] bg-slate-800 rounded-xl flex items-center justify-center">
<div className="text-white/60 flex flex-col items-center gap-2">
<div className="w-8 h-8 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Karte wird geladen...</span>
</div>
</div>
)
}
// Theme icons and colors
const THEME_CONFIG: Record<AOITheme, { icon: string; color: string; label: string }> = {
topographie: { icon: '🏔️', color: 'bg-amber-500', label: 'Topographie' },
landnutzung: { icon: '🏘️', color: 'bg-green-500', label: 'Landnutzung' },
orientierung: { icon: '🧭', color: 'bg-blue-500', label: 'Orientierung' },
geologie: { icon: '🪨', color: 'bg-stone-500', label: 'Geologie' },
hydrologie: { icon: '💧', color: 'bg-cyan-500', label: 'Hydrologie' },
vegetation: { icon: '🌲', color: 'bg-emerald-500', label: 'Vegetation' },
}
export default function GeoLernweltPage() {
// State
const [serviceHealth, setServiceHealth] = useState<GeoServiceHealth | null>(null)
const [currentAOI, setCurrentAOI] = useState<AOIResponse | null>(null)
const [drawnPolygon, setDrawnPolygon] = useState<GeoJSONPolygon | null>(null)
const [selectedTheme, setSelectedTheme] = useState<AOITheme>('topographie')
const [quality, setQuality] = useState<AOIQuality>('medium')
const [difficulty, setDifficulty] = useState<Difficulty>('mittel')
const [learningNodes, setLearningNodes] = useState<LearningNode[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'map' | 'unity'>('map')
const [demoTemplate, setDemoTemplate] = useState<DemoTemplate | null>(null)
// Check service health on mount
useEffect(() => {
checkServiceHealth()
loadMainauTemplate()
}, [])
const checkServiceHealth = async () => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/health`)
if (res.ok) {
const health = await res.json()
setServiceHealth(health)
}
} catch (e) {
console.error('Service health check failed:', e)
setServiceHealth(null)
}
}
const loadMainauTemplate = async () => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi/templates/mainau`)
if (res.ok) {
const template = await res.json()
setDemoTemplate(template)
}
} catch (e) {
console.error('Failed to load Mainau template:', e)
}
}
const handlePolygonDrawn = useCallback((polygon: GeoJSONPolygon) => {
setDrawnPolygon(polygon)
setError(null)
}, [])
const handleCreateAOI = async () => {
if (!drawnPolygon) {
setError('Bitte zeichne zuerst ein Gebiet auf der Karte.')
return
}
setIsLoading(true)
setError(null)
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
polygon: drawnPolygon,
theme: selectedTheme,
quality,
}),
})
if (!res.ok) {
const errorData = await res.json()
throw new Error(errorData.detail || 'Fehler beim Erstellen des Gebiets')
}
const aoi = await res.json()
setCurrentAOI(aoi)
// Poll for completion
pollAOIStatus(aoi.aoi_id)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const pollAOIStatus = async (aoiId: string) => {
const poll = async () => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi/${aoiId}`)
if (res.ok) {
const aoi = await res.json()
setCurrentAOI(aoi)
if (aoi.status === 'completed') {
// Load learning nodes
generateLearningNodes(aoiId)
} else if (aoi.status === 'failed') {
setError('Verarbeitung fehlgeschlagen')
} else {
// Continue polling
setTimeout(poll, 2000)
}
}
} catch (e) {
console.error('Polling error:', e)
}
}
poll()
}
const generateLearningNodes = async (aoiId: string) => {
try {
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/learning/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
aoi_id: aoiId,
theme: selectedTheme,
difficulty,
node_count: 5,
language: 'de',
}),
})
if (res.ok) {
const data = await res.json()
setLearningNodes(data.nodes)
}
} catch (e) {
console.error('Failed to generate learning nodes:', e)
}
}
const handleLoadDemo = () => {
if (demoTemplate) {
setDrawnPolygon(demoTemplate.polygon)
setSelectedTheme(demoTemplate.suggested_themes[0] || 'topographie')
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-indigo-950 to-slate-900">
{/* Header */}
<header className="border-b border-white/10 bg-black/20 backdrop-blur-lg">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-3xl">🌍</span>
<div>
<h1 className="text-xl font-semibold text-white">Geo-Lernwelt</h1>
<p className="text-sm text-white/60">Interaktive Erdkunde-Lernplattform</p>
</div>
</div>
{/* Service Status */}
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
serviceHealth?.status === 'healthy'
? 'bg-green-500'
: 'bg-yellow-500 animate-pulse'
}`}
/>
<span className="text-sm text-white/60">
{serviceHealth?.status === 'healthy' ? 'Verbunden' : 'Verbinde...'}
</span>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-6">
{/* Tab Navigation */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setActiveTab('map')}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
activeTab === 'map'
? 'bg-white/10 text-white'
: 'text-white/60 hover:text-white hover:bg-white/5'
}`}
>
🗺 Gebiet waehlen
</button>
<button
onClick={() => setActiveTab('unity')}
disabled={!currentAOI || currentAOI.status !== 'completed'}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
activeTab === 'unity'
? 'bg-white/10 text-white'
: 'text-white/60 hover:text-white hover:bg-white/5'
} disabled:opacity-40 disabled:cursor-not-allowed`}
>
🎮 3D-Lernwelt
</button>
</div>
{/* Error Message */}
{error && (
<div className="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-xl text-red-200">
{error}
</div>
)}
{activeTab === 'map' ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Map Section (2/3) */}
<div className="lg:col-span-2 space-y-4">
{/* Map Card */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 overflow-hidden">
<div className="p-4 border-b border-white/10">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium text-white">Gebiet auf der Karte waehlen</h2>
{demoTemplate && (
<button
onClick={handleLoadDemo}
className="px-3 py-1.5 text-sm bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 rounded-lg transition-colors"
>
📍 Demo: Insel Mainau
</button>
)}
</div>
<p className="text-sm text-white/60 mt-1">
Zeichne ein Polygon (max. 4 km²) um das gewuenschte Lerngebiet
</p>
</div>
<div className="h-[500px]">
<AOISelector
onPolygonDrawn={handlePolygonDrawn}
initialPolygon={drawnPolygon}
maxAreaKm2={4}
geoServiceUrl={GEO_SERVICE_URL}
/>
</div>
</div>
{/* Attribution */}
<div className="text-xs text-white/40 text-center">
Kartendaten: © OpenStreetMap contributors (ODbL) | Hoehenmodell: © Copernicus DEM
</div>
</div>
{/* Settings Panel (1/3) */}
<div className="space-y-4">
{/* Theme Selection */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Lernthema</h3>
<div className="grid grid-cols-2 gap-2">
{(Object.keys(THEME_CONFIG) as AOITheme[]).map((theme) => {
const config = THEME_CONFIG[theme]
return (
<button
key={theme}
onClick={() => setSelectedTheme(theme)}
className={`p-3 rounded-xl text-left transition-all ${
selectedTheme === theme
? 'bg-white/15 border border-white/30'
: 'bg-white/5 border border-transparent hover:bg-white/10'
}`}
>
<span className="text-2xl">{config.icon}</span>
<div className="text-sm text-white mt-1">{config.label}</div>
</button>
)
})}
</div>
</div>
{/* Quality Selection */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Qualitaet</h3>
<div className="flex gap-2">
{(['low', 'medium', 'high'] as AOIQuality[]).map((q) => (
<button
key={q}
onClick={() => setQuality(q)}
className={`flex-1 py-2 rounded-lg text-sm transition-all ${
quality === q
? 'bg-white/15 text-white border border-white/30'
: 'bg-white/5 text-white/60 hover:bg-white/10'
}`}
>
{q === 'low' ? 'Schnell' : q === 'medium' ? 'Standard' : 'Hoch'}
</button>
))}
</div>
</div>
{/* Difficulty Selection */}
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Schwierigkeitsgrad</h3>
<div className="flex gap-2">
{(['leicht', 'mittel', 'schwer'] as Difficulty[]).map((d) => (
<button
key={d}
onClick={() => setDifficulty(d)}
className={`flex-1 py-2 rounded-lg text-sm capitalize transition-all ${
difficulty === d
? 'bg-white/15 text-white border border-white/30'
: 'bg-white/5 text-white/60 hover:bg-white/10'
}`}
>
{d}
</button>
))}
</div>
</div>
{/* Area Info */}
{drawnPolygon && (
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-2">Ausgewaehltes Gebiet</h3>
<div className="text-sm text-white/60">
<p>Polygon gezeichnet </p>
<p className="text-white/40 text-xs mt-1">
Klicke &quot;Lernwelt erstellen&quot; um fortzufahren
</p>
</div>
</div>
)}
{/* Create Button */}
<button
onClick={handleCreateAOI}
disabled={!drawnPolygon || isLoading}
className={`w-full py-4 rounded-xl font-medium text-lg transition-all ${
drawnPolygon && !isLoading
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:from-blue-600 hover:to-purple-600'
: 'bg-white/10 text-white/40 cursor-not-allowed'
}`}
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Wird erstellt...
</span>
) : (
'🚀 Lernwelt erstellen'
)}
</button>
{/* AOI Status */}
{currentAOI && (
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-2">Status</h3>
<div className="space-y-2">
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
currentAOI.status === 'completed'
? 'bg-green-500'
: currentAOI.status === 'failed'
? 'bg-red-500'
: 'bg-yellow-500 animate-pulse'
}`}
/>
<span className="text-sm text-white/80 capitalize">
{currentAOI.status === 'queued'
? 'In Warteschlange...'
: currentAOI.status === 'processing'
? 'Wird verarbeitet...'
: currentAOI.status === 'completed'
? 'Fertig!'
: 'Fehlgeschlagen'}
</span>
</div>
{currentAOI.area_km2 > 0 && (
<p className="text-xs text-white/50">
Flaeche: {currentAOI.area_km2.toFixed(2)} km²
</p>
)}
</div>
</div>
)}
</div>
</div>
) : (
/* Unity 3D Viewer Tab */
<div className="space-y-4">
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 overflow-hidden">
<div className="p-4 border-b border-white/10 flex items-center justify-between">
<div>
<h2 className="text-lg font-medium text-white">3D-Lernwelt</h2>
<p className="text-sm text-white/60">
Erkunde das Gebiet und bearbeite die Lernstationen
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-white/60">
{learningNodes.length} Lernstationen
</span>
</div>
</div>
<div className="h-[600px]">
{currentAOI && currentAOI.status === 'completed' ? (
<UnityViewer
aoiId={currentAOI.aoi_id}
manifestUrl={currentAOI.manifest_url}
learningNodes={learningNodes}
geoServiceUrl={GEO_SERVICE_URL}
/>
) : (
<div className="h-full flex items-center justify-center text-white/60">
Erstelle zuerst ein Lerngebiet im Tab &quot;Gebiet waehlen&quot;
</div>
)}
</div>
</div>
{/* Learning Nodes List */}
{learningNodes.length > 0 && (
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
<h3 className="text-white font-medium mb-3">Lernstationen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{learningNodes.map((node, idx) => (
<div
key={node.id}
className="p-3 bg-white/5 rounded-xl border border-white/10"
>
<div className="flex items-center gap-2 mb-2">
<span className="w-6 h-6 bg-blue-500/30 rounded-full flex items-center justify-center text-xs text-white">
{idx + 1}
</span>
<span className="text-white font-medium text-sm">{node.title}</span>
</div>
<p className="text-xs text-white/60 line-clamp-2">{node.question}</p>
<div className="mt-2 flex items-center gap-2">
<span className="text-xs bg-white/10 px-2 py-0.5 rounded text-white/60">
{node.points} Punkte
</span>
{node.approved && (
<span className="text-xs text-green-400"> Freigegeben</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</main>
</div>
)
}