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,191 @@
"""
Pydantic Schemas für Content Service API
"""
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
from models import ContentType, CCLicense, ContentCategory, ContentStatus
# ============= REQUEST SCHEMAS =============
class ContentCreate(BaseModel):
"""Schema für Content Creation"""
title: str = Field(..., min_length=3, max_length=500)
description: Optional[str] = Field(None, max_length=5000)
content_type: ContentType
category: ContentCategory
license: CCLicense = CCLicense.CC_BY_SA
age_min: int = Field(6, ge=3, le=18)
age_max: int = Field(18, ge=3, le=18)
embed_url: Optional[str] = None
tags: List[str] = []
@validator('age_max')
def age_max_must_be_greater(cls, v, values):
if 'age_min' in values and v < values['age_min']:
raise ValueError('age_max must be >= age_min')
return v
class ContentUpdate(BaseModel):
"""Schema für Content Update"""
title: Optional[str] = Field(None, min_length=3, max_length=500)
description: Optional[str] = Field(None, max_length=5000)
category: Optional[ContentCategory] = None
license: Optional[CCLicense] = None
age_min: Optional[int] = Field(None, ge=3, le=18)
age_max: Optional[int] = Field(None, ge=3, le=18)
embed_url: Optional[str] = None
tags: Optional[List[str]] = None
status: Optional[ContentStatus] = None
class RatingCreate(BaseModel):
"""Schema für Rating Creation"""
stars: int = Field(..., ge=1, le=5)
comment: Optional[str] = Field(None, max_length=2000)
# ============= RESPONSE SCHEMAS =============
class TagResponse(BaseModel):
"""Tag Response"""
id: str
name: str
category: Optional[str]
class Config:
from_attributes = True
class RatingResponse(BaseModel):
"""Rating Response"""
id: str
user_id: str
user_name: Optional[str]
stars: int
comment: Optional[str]
created_at: datetime
class Config:
from_attributes = True
class ContentResponse(BaseModel):
"""Content Response (Full)"""
id: str
creator_id: str
creator_name: str
creator_email: Optional[str]
title: str
description: Optional[str]
content_type: ContentType
category: ContentCategory
license: CCLicense
age_min: int
age_max: int
files: List[str]
thumbnail_url: Optional[str]
embed_url: Optional[str]
h5p_content_id: Optional[str]
matrix_room_id: Optional[str]
matrix_event_id: Optional[str]
status: ContentStatus
downloads: int
views: int
avg_rating: float
rating_count: int
impact_score: float
created_at: datetime
updated_at: datetime
published_at: Optional[datetime]
tags: List[TagResponse] = []
class Config:
from_attributes = True
class ContentListItem(BaseModel):
"""Content Response (List View - simplified)"""
id: str
creator_name: str
title: str
description: Optional[str]
content_type: ContentType
category: ContentCategory
license: CCLicense
thumbnail_url: Optional[str]
avg_rating: float
rating_count: int
downloads: int
created_at: datetime
tags: List[str] = []
class Config:
from_attributes = True
class ContentWithRatings(ContentResponse):
"""Content with ratings included"""
ratings: List[RatingResponse] = []
class Config:
from_attributes = True
# ============= FILTER SCHEMAS =============
class ContentFilter(BaseModel):
"""Search/Filter Parameters"""
search: Optional[str] = None
category: Optional[ContentCategory] = None
content_type: Optional[ContentType] = None
license: Optional[CCLicense] = None
age_min: Optional[int] = None
age_max: Optional[int] = None
tags: Optional[List[str]] = None
min_rating: Optional[float] = None
status: Optional[ContentStatus] = ContentStatus.PUBLISHED
creator_id: Optional[str] = None
# Pagination
skip: int = Field(0, ge=0)
limit: int = Field(20, ge=1, le=100)
# Sorting
sort_by: str = Field("created_at", pattern="^(created_at|avg_rating|downloads|title)$")
sort_desc: bool = True
# ============= ANALYTICS SCHEMAS =============
class ContentStats(BaseModel):
"""Content Statistics"""
total_contents: int
total_downloads: int
total_views: int
avg_rating: float
by_category: dict
by_type: dict
by_license: dict
class CreatorStats(BaseModel):
"""Creator Statistics"""
creator_id: str
creator_name: str
total_contents: int
total_downloads: int
total_views: int
avg_rating: float
impact_score: float
content_breakdown: dict
# ============= UPLOAD SCHEMAS =============
class FileUploadResponse(BaseModel):
"""File Upload Response"""
file_url: str
file_name: str
file_size: int
content_type: str