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,74 @@
"""
Klausur-Service Models
Data models for exams, students, grading, and Erwartungshorizont.
"""
from .enums import KlausurModus, StudentKlausurStatus, EHStatus
from .exam import Klausur, StudentKlausur
from .grading import AuditLogEntry, GRADE_THRESHOLDS, GRADE_LABELS, DEFAULT_CRITERIA
from .eh import (
Erwartungshorizont,
EHRightsConfirmation,
EHAuditLogEntry,
EHKeyShare,
EHKlausurLink,
EHShareInvitation,
)
from .requests import (
KlausurCreate,
KlausurUpdate,
StudentUpload,
CriterionScoreUpdate,
GutachtenUpdate,
ExaminerAssignment,
ExaminerResult,
GutachtenGenerateRequest,
EHMetadata,
EHUploadMetadata,
EHRAGQuery,
EHIndexRequest,
EHShareRequest,
EHLinkKlausurRequest,
EHInviteRequest,
EHAcceptInviteRequest,
)
__all__ = [
# Enums
"KlausurModus",
"StudentKlausurStatus",
"EHStatus",
# Exam Models
"Klausur",
"StudentKlausur",
# Grading
"AuditLogEntry",
"GRADE_THRESHOLDS",
"GRADE_LABELS",
"DEFAULT_CRITERIA",
# EH Models
"Erwartungshorizont",
"EHRightsConfirmation",
"EHAuditLogEntry",
"EHKeyShare",
"EHKlausurLink",
"EHShareInvitation",
# Request Models
"KlausurCreate",
"KlausurUpdate",
"StudentUpload",
"CriterionScoreUpdate",
"GutachtenUpdate",
"ExaminerAssignment",
"ExaminerResult",
"GutachtenGenerateRequest",
"EHMetadata",
"EHUploadMetadata",
"EHRAGQuery",
"EHIndexRequest",
"EHShareRequest",
"EHLinkKlausurRequest",
"EHInviteRequest",
"EHAcceptInviteRequest",
]

View File

@@ -0,0 +1,197 @@
"""
Klausur-Service Erwartungshorizont Models
Data classes for BYOEH (Bring-Your-Own-Expectation-Horizon).
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Dict, Any
from .enums import EHStatus
@dataclass
class Erwartungshorizont:
"""An encrypted Erwartungshorizont (expectation horizon)."""
id: str
tenant_id: str
teacher_id: str
title: str
subject: str
niveau: str # 'eA' or 'gA'
year: int
aufgaben_nummer: Optional[str]
encryption_key_hash: str
salt: str
encrypted_file_path: str
file_size_bytes: int
original_filename: str
rights_confirmed: bool
rights_confirmed_at: Optional[datetime]
status: EHStatus
chunk_count: int
indexed_at: Optional[datetime]
error_message: Optional[str]
training_allowed: bool # ALWAYS FALSE
created_at: datetime
deleted_at: Optional[datetime]
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
status_value = self.status.value if hasattr(self.status, 'value') else self.status
return {
'id': self.id,
'tenant_id': self.tenant_id,
'teacher_id': self.teacher_id,
'title': self.title,
'subject': self.subject,
'niveau': self.niveau,
'year': self.year,
'aufgaben_nummer': self.aufgaben_nummer,
'status': status_value,
'chunk_count': self.chunk_count,
'rights_confirmed': self.rights_confirmed,
'rights_confirmed_at': self.rights_confirmed_at.isoformat() if self.rights_confirmed_at else None,
'indexed_at': self.indexed_at.isoformat() if self.indexed_at else None,
'file_size_bytes': self.file_size_bytes,
'original_filename': self.original_filename,
'training_allowed': self.training_allowed,
'created_at': self.created_at.isoformat(),
'deleted_at': self.deleted_at.isoformat() if self.deleted_at else None
}
@dataclass
class EHRightsConfirmation:
"""Rights confirmation for an Erwartungshorizont upload."""
id: str
eh_id: str
teacher_id: str
confirmation_type: str # 'upload' | 'annual'
confirmation_text: str
ip_address: Optional[str]
user_agent: Optional[str]
confirmed_at: datetime
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
'id': self.id,
'eh_id': self.eh_id,
'teacher_id': self.teacher_id,
'confirmation_type': self.confirmation_type,
'confirmation_text': self.confirmation_text,
'confirmed_at': self.confirmed_at.isoformat()
}
@dataclass
class EHAuditLogEntry:
"""Audit log entry for EH operations."""
id: str
eh_id: Optional[str]
tenant_id: str
user_id: str
action: str # upload, index, rag_query, download, delete
details: Optional[Dict]
ip_address: Optional[str]
user_agent: Optional[str]
created_at: datetime
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
'id': self.id,
'eh_id': self.eh_id,
'tenant_id': self.tenant_id,
'user_id': self.user_id,
'action': self.action,
'details': self.details,
'created_at': self.created_at.isoformat()
}
@dataclass
class EHKeyShare:
"""Encrypted passphrase share for authorized users."""
id: str
eh_id: str
user_id: str
encrypted_passphrase: str # Passphrase encrypted with recipient's public key
passphrase_hint: str # Optional hint for the passphrase
granted_by: str
granted_at: datetime
role: str # 'second_examiner', 'third_examiner', 'supervisor'
klausur_id: Optional[str] # Link to specific Klausur if applicable
active: bool
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
'id': self.id,
'eh_id': self.eh_id,
'user_id': self.user_id,
'passphrase_hint': self.passphrase_hint,
'granted_by': self.granted_by,
'granted_at': self.granted_at.isoformat(),
'role': self.role,
'klausur_id': self.klausur_id,
'active': self.active
}
@dataclass
class EHKlausurLink:
"""Link between an EH and a Klausur."""
id: str
eh_id: str
klausur_id: str
linked_by: str
linked_at: datetime
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
'id': self.id,
'eh_id': self.eh_id,
'klausur_id': self.klausur_id,
'linked_by': self.linked_by,
'linked_at': self.linked_at.isoformat()
}
@dataclass
class EHShareInvitation:
"""Invitation to share an EH with another user."""
id: str
eh_id: str
inviter_id: str # User who sent the invitation
invitee_id: str # User receiving the invitation
invitee_email: str # Email for notification
role: str # Target role for the invitee
klausur_id: Optional[str] # Optional link to specific Klausur
message: Optional[str] # Optional message from inviter
status: str # 'pending', 'accepted', 'declined', 'expired', 'revoked'
expires_at: datetime # Invitation expiration
created_at: datetime
accepted_at: Optional[datetime]
declined_at: Optional[datetime]
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
'id': self.id,
'eh_id': self.eh_id,
'inviter_id': self.inviter_id,
'invitee_id': self.invitee_id,
'invitee_email': self.invitee_email,
'role': self.role,
'klausur_id': self.klausur_id,
'message': self.message,
'status': self.status,
'expires_at': self.expires_at.isoformat(),
'created_at': self.created_at.isoformat(),
'accepted_at': self.accepted_at.isoformat() if self.accepted_at else None,
'declined_at': self.declined_at.isoformat() if self.declined_at else None
}

View File

@@ -0,0 +1,33 @@
"""
Klausur-Service Enums
Status and type enumerations.
"""
from enum import Enum
class KlausurModus(str, Enum):
"""Klausur mode: Landes-Abitur or Vorabitur."""
LANDES_ABITUR = "landes_abitur"
VORABITUR = "vorabitur"
class StudentKlausurStatus(str, Enum):
"""Processing status of a student's work."""
UPLOADED = "uploaded"
OCR_PROCESSING = "ocr_processing"
OCR_COMPLETE = "ocr_complete"
ANALYZING = "analyzing"
FIRST_EXAMINER = "first_examiner"
SECOND_EXAMINER = "second_examiner"
COMPLETED = "completed"
ERROR = "error"
class EHStatus(str, Enum):
"""Status of an Erwartungshorizont."""
PENDING_RIGHTS = "pending_rights"
PROCESSING = "processing"
INDEXED = "indexed"
ERROR = "error"

View File

@@ -0,0 +1,68 @@
"""
Klausur-Service Exam Models
Data classes for Klausur and StudentKlausur.
"""
from dataclasses import dataclass, asdict
from datetime import datetime
from typing import Optional, List, Dict, Any
from .enums import KlausurModus, StudentKlausurStatus
@dataclass
class StudentKlausur:
"""A student's exam work."""
id: str
klausur_id: str
student_name: str
student_id: Optional[str]
file_path: Optional[str]
ocr_text: Optional[str]
status: StudentKlausurStatus
criteria_scores: Dict[str, Dict]
gutachten: Optional[Dict]
raw_points: int
grade_points: int
created_at: datetime
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
d = asdict(self)
d['status'] = self.status.value if hasattr(self.status, 'value') else self.status
d['created_at'] = self.created_at.isoformat()
return d
@dataclass
class Klausur:
"""An exam/Klausur with associated student works."""
id: str
title: str
subject: str
modus: KlausurModus
class_id: Optional[str]
year: int
semester: str
erwartungshorizont: Optional[Dict]
students: List[StudentKlausur]
created_at: datetime
teacher_id: str
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
'id': self.id,
'title': self.title,
'subject': self.subject,
'modus': self.modus.value,
'class_id': self.class_id,
'year': self.year,
'semester': self.semester,
'erwartungshorizont': self.erwartungshorizont,
'student_count': len(self.students),
'students': [s.to_dict() for s in self.students],
'created_at': self.created_at.isoformat(),
'teacher_id': self.teacher_id
}

View File

@@ -0,0 +1,71 @@
"""
Klausur-Service Grading Models
Grade thresholds, labels, criteria, and audit logging.
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, Dict, Any
# =============================================
# GRADE CONSTANTS
# =============================================
GRADE_THRESHOLDS = {
15: 95, 14: 90, 13: 85, 12: 80, 11: 75, 10: 70,
9: 65, 8: 60, 7: 55, 6: 50, 5: 45, 4: 40,
3: 33, 2: 27, 1: 20, 0: 0
}
GRADE_LABELS = {
15: "1+ (sehr gut plus)", 14: "1 (sehr gut)", 13: "1- (sehr gut minus)",
12: "2+ (gut plus)", 11: "2 (gut)", 10: "2- (gut minus)",
9: "3+ (befriedigend plus)", 8: "3 (befriedigend)", 7: "3- (befriedigend minus)",
6: "4+ (ausreichend plus)", 5: "4 (ausreichend)", 4: "4- (ausreichend minus)",
3: "5+ (mangelhaft plus)", 2: "5 (mangelhaft)", 1: "5- (mangelhaft minus)",
0: "6 (ungenuegend)"
}
DEFAULT_CRITERIA = {
"rechtschreibung": {"weight": 0.15, "label": "Rechtschreibung"},
"grammatik": {"weight": 0.15, "label": "Grammatik"},
"inhalt": {"weight": 0.40, "label": "Inhalt"},
"struktur": {"weight": 0.15, "label": "Struktur"},
"stil": {"weight": 0.15, "label": "Stil"},
}
# =============================================
# AUDIT LOG
# =============================================
@dataclass
class AuditLogEntry:
"""Audit log entry for tracking changes."""
id: str
timestamp: datetime
user_id: str
action: str # score_update, gutachten_update, status_change, examiner_assign
entity_type: str # klausur, student
entity_id: str
field: Optional[str]
old_value: Optional[str]
new_value: Optional[str]
details: Optional[Dict]
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
'id': self.id,
'timestamp': self.timestamp.isoformat(),
'user_id': self.user_id,
'action': self.action,
'entity_type': self.entity_type,
'entity_id': self.entity_id,
'field': self.field,
'old_value': self.old_value,
'new_value': self.new_value,
'details': self.details
}

View File

@@ -0,0 +1,152 @@
"""
Klausur-Service Request Models
Pydantic models for API request/response validation.
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel
from .enums import KlausurModus
# =============================================
# KLAUSUR REQUESTS
# =============================================
class KlausurCreate(BaseModel):
"""Request to create a new Klausur."""
title: str
subject: str
modus: KlausurModus = KlausurModus.VORABITUR
class_id: Optional[str] = None
year: int = 2025
semester: str = "Q1"
class KlausurUpdate(BaseModel):
"""Request to update a Klausur."""
title: Optional[str] = None
subject: Optional[str] = None
erwartungshorizont: Optional[Dict[str, Any]] = None
# =============================================
# STUDENT REQUESTS
# =============================================
class StudentUpload(BaseModel):
"""Request for student work upload metadata."""
student_name: str
student_id: Optional[str] = None
# =============================================
# GRADING REQUESTS
# =============================================
class CriterionScoreUpdate(BaseModel):
"""Request to update a criterion score."""
criterion: str
score: int
annotations: Optional[List[str]] = None
class GutachtenUpdate(BaseModel):
"""Request to update the Gutachten."""
einleitung: str
hauptteil: str
fazit: str
staerken: Optional[List[str]] = None
schwaechen: Optional[List[str]] = None
class GutachtenGenerateRequest(BaseModel):
"""Request to generate a KI-based Gutachten."""
include_strengths: bool = True
include_weaknesses: bool = True
tone: str = "formal" # formal, friendly, constructive
# BYOEH RAG Integration
use_eh: bool = False # Whether to use Erwartungshorizont for context
eh_passphrase: Optional[str] = None # Passphrase for EH decryption
# =============================================
# EXAMINER REQUESTS
# =============================================
class ExaminerAssignment(BaseModel):
"""Request to assign an examiner."""
examiner_id: str
examiner_role: str # first_examiner, second_examiner
notes: Optional[str] = None
class ExaminerResult(BaseModel):
"""Request to submit an examiner's result."""
grade_points: int
notes: Optional[str] = None
# =============================================
# BYOEH REQUESTS
# =============================================
class EHMetadata(BaseModel):
"""Metadata for an Erwartungshorizont."""
title: str
subject: str
niveau: str # 'eA' | 'gA'
year: int
aufgaben_nummer: Optional[str] = None
class EHUploadMetadata(BaseModel):
"""Metadata for EH upload including encryption info."""
metadata: EHMetadata
encryption_key_hash: str
salt: str
rights_confirmed: bool
original_filename: str
class EHRAGQuery(BaseModel):
"""Request for RAG query against Erwartungshorizonte."""
query_text: str
passphrase: str
subject: Optional[str] = None
limit: int = 5
class EHIndexRequest(BaseModel):
"""Request to index an Erwartungshorizont."""
passphrase: str
class EHShareRequest(BaseModel):
"""Request to share EH with another examiner."""
user_id: str # User to share with
role: str # second_examiner, third_examiner, supervisor
encrypted_passphrase: str # Passphrase encrypted for recipient
passphrase_hint: Optional[str] = None
klausur_id: Optional[str] = None # Optional: link to specific Klausur
class EHLinkKlausurRequest(BaseModel):
"""Request to link EH to a Klausur."""
klausur_id: str
class EHInviteRequest(BaseModel):
"""Request to invite another user to access an EH."""
invitee_email: str # Email of the recipient
invitee_id: Optional[str] = None # User ID if known
role: str # second_examiner, third_examiner, supervisor, department_head
klausur_id: Optional[str] = None # Optional: link to specific Klausur
message: Optional[str] = None # Optional message for the invitee
expires_in_days: int = 14 # Default 14 days expiration
class EHAcceptInviteRequest(BaseModel):
"""Request to accept an invitation and receive the encrypted passphrase."""
encrypted_passphrase: str # The passphrase encrypted for the invitee