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,19 @@
"""
GeoEdu Service - Pydantic Models
"""
from .aoi import AOIRequest, AOIResponse, AOIStatus, AOIManifest
from .learning_node import LearningNode, LearningNodeRequest, LearningTheme, NodeType
from .attribution import Attribution, AttributionSource
__all__ = [
"AOIRequest",
"AOIResponse",
"AOIStatus",
"AOIManifest",
"LearningNode",
"LearningNodeRequest",
"LearningTheme",
"NodeType",
"Attribution",
"AttributionSource",
]

162
geo-service/models/aoi.py Normal file
View File

@@ -0,0 +1,162 @@
"""
AOI (Area of Interest) Models
Pydantic models for AOI requests and responses
"""
from enum import Enum
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field
class AOIStatus(str, Enum):
"""Status of an AOI processing job."""
QUEUED = "queued"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
class AOIQuality(str, Enum):
"""Quality level for AOI bundle generation."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class AOITheme(str, Enum):
"""Learning theme for AOI."""
TOPOGRAPHIE = "topographie"
LANDNUTZUNG = "landnutzung"
ORIENTIERUNG = "orientierung"
GEOLOGIE = "geologie"
HYDROLOGIE = "hydrologie"
VEGETATION = "vegetation"
class GeoJSONPolygon(BaseModel):
"""GeoJSON Polygon geometry."""
type: str = Field("Polygon", const=True)
coordinates: list[list[list[float]]] = Field(
...,
description="Polygon coordinates as [[[lon, lat], ...]]",
)
class AOIRequest(BaseModel):
"""Request model for creating an AOI."""
polygon: dict = Field(
...,
description="GeoJSON Polygon geometry",
example={
"type": "Polygon",
"coordinates": [[[9.19, 47.71], [9.20, 47.71], [9.20, 47.70], [9.19, 47.70], [9.19, 47.71]]]
},
)
theme: str = Field(
"topographie",
description="Learning theme for the AOI",
)
quality: str = Field(
"medium",
pattern="^(low|medium|high)$",
description="Bundle quality level",
)
class Config:
json_schema_extra = {
"example": {
"polygon": {
"type": "Polygon",
"coordinates": [[[9.1875, 47.7055], [9.1975, 47.7055], [9.1975, 47.7115], [9.1875, 47.7115], [9.1875, 47.7055]]]
},
"theme": "topographie",
"quality": "medium",
}
}
class AOIResponse(BaseModel):
"""Response model for AOI operations."""
aoi_id: str = Field(..., description="Unique AOI identifier")
status: AOIStatus = Field(..., description="Current processing status")
area_km2: float = Field(0, description="Area in square kilometers")
estimated_size_mb: float = Field(0, description="Estimated bundle size in MB")
message: Optional[str] = Field(None, description="Status message")
download_url: Optional[str] = Field(None, description="Bundle download URL")
manifest_url: Optional[str] = Field(None, description="Manifest URL")
created_at: Optional[str] = Field(None, description="Creation timestamp")
completed_at: Optional[str] = Field(None, description="Completion timestamp")
class Config:
json_schema_extra = {
"example": {
"aoi_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"area_km2": 0.45,
"estimated_size_mb": 25.5,
"message": "AOI processing complete",
"download_url": "/api/v1/aoi/550e8400-e29b-41d4-a716-446655440000/bundle.zip",
"manifest_url": "/api/v1/aoi/550e8400-e29b-41d4-a716-446655440000/manifest.json",
}
}
class AOIBounds(BaseModel):
"""Geographic bounding box."""
west: float = Field(..., description="Western longitude")
south: float = Field(..., description="Southern latitude")
east: float = Field(..., description="Eastern longitude")
north: float = Field(..., description="Northern latitude")
class AOICenter(BaseModel):
"""Geographic center point."""
longitude: float
latitude: float
class AOIManifest(BaseModel):
"""Unity bundle manifest for an AOI."""
version: str = Field("1.0.0", description="Manifest version")
aoi_id: str = Field(..., description="AOI identifier")
created_at: str = Field(..., description="Creation timestamp")
bounds: AOIBounds = Field(..., description="Geographic bounds")
center: AOICenter = Field(..., description="Geographic center")
area_km2: float = Field(..., description="Area in km²")
theme: str = Field(..., description="Learning theme")
quality: str = Field(..., description="Quality level")
assets: dict = Field(..., description="Asset file references")
unity: dict = Field(..., description="Unity-specific configuration")
class Config:
json_schema_extra = {
"example": {
"version": "1.0.0",
"aoi_id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2024-01-15T12:00:00Z",
"bounds": {
"west": 9.1875,
"south": 47.7055,
"east": 9.1975,
"north": 47.7115,
},
"center": {
"longitude": 9.1925,
"latitude": 47.7085,
},
"area_km2": 0.45,
"theme": "topographie",
"quality": "medium",
"assets": {
"terrain": {"file": "terrain.heightmap.png", "config": "terrain.json"},
"osm_features": {"file": "osm_features.json"},
"learning_positions": {"file": "learning_positions.json"},
"attribution": {"file": "attribution.json"},
},
"unity": {
"coordinate_system": "Unity (Y-up, left-handed)",
"scale": 1.0,
"terrain_resolution": 256,
},
}
}

View File

@@ -0,0 +1,97 @@
"""
Attribution Models
Models for license and attribution tracking
"""
from typing import Optional
from pydantic import BaseModel, Field
class AttributionSource(BaseModel):
"""
Attribution information for a data source.
All geographic data requires proper attribution per their licenses.
"""
name: str = Field(..., description="Source name")
license: str = Field(..., description="License name")
url: str = Field(..., description="License or source URL")
attribution: str = Field(..., description="Required attribution text")
required: bool = Field(True, description="Whether attribution is legally required")
logo_url: Optional[str] = Field(None, description="Optional logo URL")
class Attribution(BaseModel):
"""
Complete attribution information for an AOI bundle.
Ensures DSGVO/GDPR compliance and proper data source attribution.
"""
sources: list[AttributionSource] = Field(
...,
description="List of data sources requiring attribution",
)
generated_at: str = Field(..., description="Timestamp when attribution was generated")
notice: str = Field(
"This data must be attributed according to the licenses above when used publicly.",
description="General attribution notice",
)
class Config:
json_schema_extra = {
"example": {
"sources": [
{
"name": "OpenStreetMap",
"license": "Open Database License (ODbL) v1.0",
"url": "https://www.openstreetmap.org/copyright",
"attribution": "© OpenStreetMap contributors",
"required": True,
},
{
"name": "Copernicus DEM",
"license": "Copernicus Data License",
"url": "https://spacedata.copernicus.eu/",
"attribution": "© Copernicus Service Information 2024",
"required": True,
},
],
"generated_at": "2024-01-15T12:00:00Z",
"notice": "This data must be attributed according to the licenses above when used publicly.",
}
}
# Predefined attribution sources
OSM_ATTRIBUTION = AttributionSource(
name="OpenStreetMap",
license="Open Database License (ODbL) v1.0",
url="https://www.openstreetmap.org/copyright",
attribution="© OpenStreetMap contributors",
required=True,
)
COPERNICUS_ATTRIBUTION = AttributionSource(
name="Copernicus DEM",
license="Copernicus Data License",
url="https://spacedata.copernicus.eu/",
attribution="© Copernicus Service Information 2024",
required=True,
)
OPENAERIAL_ATTRIBUTION = AttributionSource(
name="OpenAerialMap",
license="CC-BY 4.0",
url="https://openaerialmap.org/",
attribution="© OpenAerialMap contributors",
required=True,
)
def get_default_attribution() -> Attribution:
"""Get default attribution with standard sources."""
from datetime import datetime
return Attribution(
sources=[OSM_ATTRIBUTION, COPERNICUS_ATTRIBUTION],
generated_at=datetime.utcnow().isoformat(),
)

View File

@@ -0,0 +1,120 @@
"""
Learning Node Models
Pydantic models for educational content nodes
"""
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
class LearningTheme(str, Enum):
"""Available learning themes for geographic education."""
TOPOGRAPHIE = "topographie"
LANDNUTZUNG = "landnutzung"
ORIENTIERUNG = "orientierung"
GEOLOGIE = "geologie"
HYDROLOGIE = "hydrologie"
VEGETATION = "vegetation"
class NodeType(str, Enum):
"""Type of learning node interaction."""
QUESTION = "question" # Multiple choice or open question
OBSERVATION = "observation" # Guided observation task
EXPLORATION = "exploration" # Free exploration with hints
class Position(BaseModel):
"""Geographic position for a learning node."""
latitude: float = Field(..., ge=-90, le=90, description="Latitude in degrees")
longitude: float = Field(..., ge=-180, le=180, description="Longitude in degrees")
altitude: Optional[float] = Field(None, description="Altitude in meters")
class LearningNode(BaseModel):
"""
A learning node (station) within a geographic area.
Contains educational content tied to a specific location,
including questions, hints, and explanations.
"""
id: str = Field(..., description="Unique node identifier")
aoi_id: str = Field(..., description="Parent AOI identifier")
title: str = Field(..., min_length=1, max_length=100, description="Node title")
theme: LearningTheme = Field(..., description="Learning theme")
position: dict = Field(..., description="Geographic position")
question: str = Field(..., description="Learning question or task")
hints: list[str] = Field(default_factory=list, description="Progressive hints")
answer: str = Field(..., description="Correct answer or expected observation")
explanation: str = Field(..., description="Didactic explanation")
node_type: NodeType = Field(NodeType.QUESTION, description="Interaction type")
points: int = Field(10, ge=1, le=100, description="Points awarded for completion")
approved: bool = Field(False, description="Teacher-approved for student use")
media: Optional[list[dict]] = Field(None, description="Associated media files")
tags: Optional[list[str]] = Field(None, description="Content tags")
difficulty: Optional[str] = Field(None, description="Difficulty level")
grade_level: Optional[str] = Field(None, description="Target grade level")
class Config:
json_schema_extra = {
"example": {
"id": "node-001",
"aoi_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Höhenbestimmung",
"theme": "topographie",
"position": {"latitude": 47.7085, "longitude": 9.1925},
"question": "Schätze die Höhe dieses Punktes über dem Meeresspiegel.",
"hints": [
"Schau dir die Vegetation an.",
"Vergleiche mit dem Seespiegel des Bodensees (395m).",
],
"answer": "Ca. 430 Meter über NN",
"explanation": "Die Höhe lässt sich aus der Vegetation und der relativen Position zum Bodensee abschätzen.",
"node_type": "question",
"points": 10,
"approved": True,
}
}
class LearningNodeRequest(BaseModel):
"""Request model for creating a learning node manually."""
title: str = Field(..., min_length=1, max_length=100)
theme: LearningTheme
position: Position
question: str
hints: list[str] = Field(default_factory=list)
answer: str
explanation: str
node_type: NodeType = NodeType.QUESTION
points: int = Field(10, ge=1, le=100)
tags: Optional[list[str]] = None
difficulty: Optional[str] = Field(None, pattern="^(leicht|mittel|schwer)$")
grade_level: Optional[str] = None
class LearningNodeBatch(BaseModel):
"""Batch of learning nodes for bulk operations."""
nodes: list[LearningNode]
total_points: int = 0
def calculate_total_points(self) -> int:
"""Calculate total points from all nodes."""
self.total_points = sum(node.points for node in self.nodes)
return self.total_points
class LearningProgress(BaseModel):
"""Student progress through learning nodes."""
student_id: str
aoi_id: str
completed_nodes: list[str] = Field(default_factory=list)
total_points: int = 0
started_at: Optional[str] = None
completed_at: Optional[str] = None
@property
def completion_percentage(self) -> float:
"""Calculate completion percentage."""
# Would need total node count for accurate calculation
return len(self.completed_nodes) * 10 # Placeholder