This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/teacher_dashboard_api.py
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

952 lines
32 KiB
Python

# ==============================================
# Breakpilot Drive - Teacher Dashboard API
# ==============================================
# Lehrer-Dashboard fuer Unit-Zuweisung und Analytics:
# - Units zu Klassen zuweisen
# - Schueler-Fortschritt einsehen
# - Klassen-Analytics
# - H5P und PDF Content verwalten
# - Unit-Einstellungen pro Klasse
from fastapi import APIRouter, HTTPException, Query, Depends, Request
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta
from enum import Enum
import uuid
import os
import logging
import httpx
logger = logging.getLogger(__name__)
# Feature flags
USE_DATABASE = os.getenv("GAME_USE_DATABASE", "true").lower() == "true"
REQUIRE_AUTH = os.getenv("TEACHER_REQUIRE_AUTH", "true").lower() == "true"
SCHOOL_SERVICE_URL = os.getenv("SCHOOL_SERVICE_URL", "http://school-service:8084")
router = APIRouter(prefix="/api/teacher", tags=["Teacher Dashboard"])
# ==============================================
# Pydantic Models
# ==============================================
class UnitAssignmentStatus(str, Enum):
"""Status of a unit assignment"""
DRAFT = "draft"
ACTIVE = "active"
COMPLETED = "completed"
ARCHIVED = "archived"
class TeacherControlSettings(BaseModel):
"""Unit settings that teachers can configure"""
allow_skip: bool = True
allow_replay: bool = True
max_time_per_stop_sec: int = 90
show_hints: bool = True
require_precheck: bool = True
require_postcheck: bool = True
class AssignUnitRequest(BaseModel):
"""Request to assign a unit to a class"""
unit_id: str
class_id: str
due_date: Optional[datetime] = None
settings: Optional[TeacherControlSettings] = None
notes: Optional[str] = None
class UnitAssignment(BaseModel):
"""Unit assignment record"""
assignment_id: str
unit_id: str
class_id: str
teacher_id: str
status: UnitAssignmentStatus
settings: TeacherControlSettings
due_date: Optional[datetime] = None
notes: Optional[str] = None
created_at: datetime
updated_at: datetime
class StudentUnitProgress(BaseModel):
"""Progress of a single student on a unit"""
student_id: str
student_name: str
session_id: Optional[str] = None
status: str # "not_started", "in_progress", "completed"
completion_rate: float = 0.0
precheck_score: Optional[float] = None
postcheck_score: Optional[float] = None
learning_gain: Optional[float] = None
time_spent_minutes: int = 0
last_activity: Optional[datetime] = None
current_stop: Optional[str] = None
stops_completed: int = 0
total_stops: int = 0
class ClassUnitProgress(BaseModel):
"""Overall progress of a class on a unit"""
assignment_id: str
unit_id: str
unit_title: str
class_id: str
class_name: str
total_students: int
started_count: int
completed_count: int
avg_completion_rate: float
avg_precheck_score: Optional[float] = None
avg_postcheck_score: Optional[float] = None
avg_learning_gain: Optional[float] = None
avg_time_minutes: float
students: List[StudentUnitProgress]
class MisconceptionReport(BaseModel):
"""Report of detected misconceptions"""
concept_id: str
concept_label: str
misconception: str
affected_students: List[str]
frequency: int
unit_id: str
stop_id: str
class ClassAnalyticsSummary(BaseModel):
"""Summary analytics for a class"""
class_id: str
class_name: str
total_units_assigned: int
units_completed: int
active_units: int
avg_completion_rate: float
avg_learning_gain: Optional[float]
total_time_hours: float
top_performers: List[str]
struggling_students: List[str]
common_misconceptions: List[MisconceptionReport]
class ContentResource(BaseModel):
"""Generated content resource"""
resource_type: str # "h5p", "pdf", "worksheet"
title: str
url: str
generated_at: datetime
unit_id: str
# ==============================================
# Auth Dependency
# ==============================================
async def get_current_teacher(request: Request) -> Dict[str, Any]:
"""Get current teacher from JWT token."""
if not REQUIRE_AUTH:
# Dev mode: return demo teacher
return {
"user_id": "e9484ad9-32ee-4f2b-a4e1-d182e02ccf20",
"email": "demo@breakpilot.app",
"role": "teacher",
"name": "Demo Lehrer"
}
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing authorization token")
try:
import jwt
token = auth_header[7:]
secret = os.getenv("JWT_SECRET", "dev-secret-key")
payload = jwt.decode(token, secret, algorithms=["HS256"])
if payload.get("role") not in ["teacher", "admin"]:
raise HTTPException(status_code=403, detail="Teacher or admin role required")
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# ==============================================
# Database Integration
# ==============================================
_teacher_db = None
async def get_teacher_database():
"""Get teacher database instance with lazy initialization."""
global _teacher_db
if not USE_DATABASE:
return None
if _teacher_db is None:
try:
from unit.database import get_teacher_db
_teacher_db = await get_teacher_db()
logger.info("Teacher database initialized")
except ImportError:
logger.warning("Teacher database module not available")
except Exception as e:
logger.warning(f"Teacher database not available: {e}")
return _teacher_db
# ==============================================
# School Service Integration
# ==============================================
async def get_classes_for_teacher(teacher_id: str) -> List[Dict[str, Any]]:
"""Get classes assigned to a teacher from school service."""
async with httpx.AsyncClient(timeout=10.0) as client:
try:
response = await client.get(
f"{SCHOOL_SERVICE_URL}/api/v1/school/classes",
headers={"X-Teacher-ID": teacher_id}
)
if response.status_code == 200:
return response.json()
except Exception as e:
logger.error(f"Failed to get classes from school service: {e}")
return []
async def get_students_in_class(class_id: str) -> List[Dict[str, Any]]:
"""Get students in a class from school service."""
async with httpx.AsyncClient(timeout=10.0) as client:
try:
response = await client.get(
f"{SCHOOL_SERVICE_URL}/api/v1/school/classes/{class_id}/students"
)
if response.status_code == 200:
return response.json()
except Exception as e:
logger.error(f"Failed to get students from school service: {e}")
return []
# ==============================================
# In-Memory Storage (Fallback)
# ==============================================
_assignments_store: Dict[str, Dict[str, Any]] = {}
# ==============================================
# API Endpoints - Unit Assignment
# ==============================================
@router.post("/assignments", response_model=UnitAssignment)
async def assign_unit_to_class(
request_data: AssignUnitRequest,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> UnitAssignment:
"""
Assign a unit to a class.
Creates an assignment that allows students in the class to play the unit.
Teacher can configure settings like skip, replay, time limits.
"""
assignment_id = str(uuid.uuid4())
now = datetime.utcnow()
settings = request_data.settings or TeacherControlSettings()
assignment = {
"assignment_id": assignment_id,
"unit_id": request_data.unit_id,
"class_id": request_data.class_id,
"teacher_id": teacher["user_id"],
"status": UnitAssignmentStatus.ACTIVE,
"settings": settings.model_dump(),
"due_date": request_data.due_date,
"notes": request_data.notes,
"created_at": now,
"updated_at": now,
}
db = await get_teacher_database()
if db:
try:
await db.create_assignment(assignment)
except Exception as e:
logger.error(f"Failed to store assignment: {e}")
# Fallback: store in memory
_assignments_store[assignment_id] = assignment
logger.info(f"Unit {request_data.unit_id} assigned to class {request_data.class_id}")
return UnitAssignment(
assignment_id=assignment_id,
unit_id=request_data.unit_id,
class_id=request_data.class_id,
teacher_id=teacher["user_id"],
status=UnitAssignmentStatus.ACTIVE,
settings=settings,
due_date=request_data.due_date,
notes=request_data.notes,
created_at=now,
updated_at=now,
)
@router.get("/assignments", response_model=List[UnitAssignment])
async def list_assignments(
class_id: Optional[str] = Query(None, description="Filter by class"),
status: Optional[UnitAssignmentStatus] = Query(None, description="Filter by status"),
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> List[UnitAssignment]:
"""
List all unit assignments for the teacher.
Optionally filter by class or status.
"""
db = await get_teacher_database()
assignments = []
if db:
try:
assignments = await db.list_assignments(
teacher_id=teacher["user_id"],
class_id=class_id,
status=status.value if status else None
)
except Exception as e:
logger.error(f"Failed to list assignments: {e}")
if not assignments:
# Fallback: filter in-memory store
for assignment in _assignments_store.values():
if assignment["teacher_id"] != teacher["user_id"]:
continue
if class_id and assignment["class_id"] != class_id:
continue
if status and assignment["status"] != status.value:
continue
assignments.append(assignment)
return [
UnitAssignment(
assignment_id=a["assignment_id"],
unit_id=a["unit_id"],
class_id=a["class_id"],
teacher_id=a["teacher_id"],
status=a["status"],
settings=TeacherControlSettings(**a["settings"]),
due_date=a.get("due_date"),
notes=a.get("notes"),
created_at=a["created_at"],
updated_at=a["updated_at"],
)
for a in assignments
]
@router.get("/assignments/{assignment_id}", response_model=UnitAssignment)
async def get_assignment(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> UnitAssignment:
"""Get details of a specific assignment."""
db = await get_teacher_database()
if db:
try:
assignment = await db.get_assignment(assignment_id)
if assignment and assignment["teacher_id"] == teacher["user_id"]:
return UnitAssignment(
assignment_id=assignment["assignment_id"],
unit_id=assignment["unit_id"],
class_id=assignment["class_id"],
teacher_id=assignment["teacher_id"],
status=assignment["status"],
settings=TeacherControlSettings(**assignment["settings"]),
due_date=assignment.get("due_date"),
notes=assignment.get("notes"),
created_at=assignment["created_at"],
updated_at=assignment["updated_at"],
)
except Exception as e:
logger.error(f"Failed to get assignment: {e}")
# Fallback
if assignment_id in _assignments_store:
a = _assignments_store[assignment_id]
if a["teacher_id"] == teacher["user_id"]:
return UnitAssignment(
assignment_id=a["assignment_id"],
unit_id=a["unit_id"],
class_id=a["class_id"],
teacher_id=a["teacher_id"],
status=a["status"],
settings=TeacherControlSettings(**a["settings"]),
due_date=a.get("due_date"),
notes=a.get("notes"),
created_at=a["created_at"],
updated_at=a["updated_at"],
)
raise HTTPException(status_code=404, detail="Assignment not found")
@router.put("/assignments/{assignment_id}")
async def update_assignment(
assignment_id: str,
settings: Optional[TeacherControlSettings] = None,
status: Optional[UnitAssignmentStatus] = None,
due_date: Optional[datetime] = None,
notes: Optional[str] = None,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> UnitAssignment:
"""Update assignment settings or status."""
db = await get_teacher_database()
assignment = None
if db:
try:
assignment = await db.get_assignment(assignment_id)
except Exception as e:
logger.error(f"Failed to get assignment: {e}")
if not assignment and assignment_id in _assignments_store:
assignment = _assignments_store[assignment_id]
if not assignment or assignment["teacher_id"] != teacher["user_id"]:
raise HTTPException(status_code=404, detail="Assignment not found")
# Update fields
if settings:
assignment["settings"] = settings.model_dump()
if status:
assignment["status"] = status.value
if due_date:
assignment["due_date"] = due_date
if notes is not None:
assignment["notes"] = notes
assignment["updated_at"] = datetime.utcnow()
if db:
try:
await db.update_assignment(assignment)
except Exception as e:
logger.error(f"Failed to update assignment: {e}")
_assignments_store[assignment_id] = assignment
return UnitAssignment(
assignment_id=assignment["assignment_id"],
unit_id=assignment["unit_id"],
class_id=assignment["class_id"],
teacher_id=assignment["teacher_id"],
status=assignment["status"],
settings=TeacherControlSettings(**assignment["settings"]),
due_date=assignment.get("due_date"),
notes=assignment.get("notes"),
created_at=assignment["created_at"],
updated_at=assignment["updated_at"],
)
@router.delete("/assignments/{assignment_id}")
async def delete_assignment(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, str]:
"""Delete/archive an assignment."""
db = await get_teacher_database()
if db:
try:
assignment = await db.get_assignment(assignment_id)
if assignment and assignment["teacher_id"] == teacher["user_id"]:
await db.delete_assignment(assignment_id)
if assignment_id in _assignments_store:
del _assignments_store[assignment_id]
return {"status": "deleted", "assignment_id": assignment_id}
except Exception as e:
logger.error(f"Failed to delete assignment: {e}")
if assignment_id in _assignments_store:
a = _assignments_store[assignment_id]
if a["teacher_id"] == teacher["user_id"]:
del _assignments_store[assignment_id]
return {"status": "deleted", "assignment_id": assignment_id}
raise HTTPException(status_code=404, detail="Assignment not found")
# ==============================================
# API Endpoints - Progress & Analytics
# ==============================================
@router.get("/assignments/{assignment_id}/progress", response_model=ClassUnitProgress)
async def get_assignment_progress(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> ClassUnitProgress:
"""
Get detailed progress for an assignment.
Shows each student's status, scores, and time spent.
"""
db = await get_teacher_database()
assignment = None
if db:
try:
assignment = await db.get_assignment(assignment_id)
except Exception as e:
logger.error(f"Failed to get assignment: {e}")
if not assignment and assignment_id in _assignments_store:
assignment = _assignments_store[assignment_id]
if not assignment or assignment["teacher_id"] != teacher["user_id"]:
raise HTTPException(status_code=404, detail="Assignment not found")
# Get students in class
students = await get_students_in_class(assignment["class_id"])
# Get progress for each student
student_progress = []
total_completion = 0.0
total_precheck = 0.0
total_postcheck = 0.0
total_time = 0
precheck_count = 0
postcheck_count = 0
started = 0
completed = 0
for student in students:
student_id = student.get("id", student.get("student_id"))
progress = StudentUnitProgress(
student_id=student_id,
student_name=student.get("name", f"Student {student_id[:8]}"),
status="not_started",
completion_rate=0.0,
stops_completed=0,
total_stops=0,
)
if db:
try:
session_data = await db.get_student_unit_session(
student_id=student_id,
unit_id=assignment["unit_id"]
)
if session_data:
progress.session_id = session_data.get("session_id")
progress.status = "completed" if session_data.get("completed_at") else "in_progress"
progress.completion_rate = session_data.get("completion_rate", 0.0)
progress.precheck_score = session_data.get("precheck_score")
progress.postcheck_score = session_data.get("postcheck_score")
progress.time_spent_minutes = session_data.get("duration_seconds", 0) // 60
progress.last_activity = session_data.get("updated_at")
progress.stops_completed = session_data.get("stops_completed", 0)
progress.total_stops = session_data.get("total_stops", 0)
if progress.precheck_score is not None and progress.postcheck_score is not None:
progress.learning_gain = progress.postcheck_score - progress.precheck_score
# Aggregate stats
total_completion += progress.completion_rate
total_time += progress.time_spent_minutes
if progress.precheck_score is not None:
total_precheck += progress.precheck_score
precheck_count += 1
if progress.postcheck_score is not None:
total_postcheck += progress.postcheck_score
postcheck_count += 1
if progress.status != "not_started":
started += 1
if progress.status == "completed":
completed += 1
except Exception as e:
logger.error(f"Failed to get student progress: {e}")
student_progress.append(progress)
total_students = len(students) or 1 # Avoid division by zero
return ClassUnitProgress(
assignment_id=assignment_id,
unit_id=assignment["unit_id"],
unit_title=f"Unit {assignment['unit_id']}", # Would load from unit definition
class_id=assignment["class_id"],
class_name=f"Class {assignment['class_id'][:8]}", # Would load from school service
total_students=len(students),
started_count=started,
completed_count=completed,
avg_completion_rate=total_completion / total_students,
avg_precheck_score=total_precheck / precheck_count if precheck_count > 0 else None,
avg_postcheck_score=total_postcheck / postcheck_count if postcheck_count > 0 else None,
avg_learning_gain=(total_postcheck / postcheck_count - total_precheck / precheck_count)
if precheck_count > 0 and postcheck_count > 0 else None,
avg_time_minutes=total_time / started if started > 0 else 0,
students=student_progress,
)
@router.get("/classes/{class_id}/analytics", response_model=ClassAnalyticsSummary)
async def get_class_analytics(
class_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> ClassAnalyticsSummary:
"""
Get summary analytics for a class.
Includes all unit assignments, overall progress, and common misconceptions.
"""
db = await get_teacher_database()
# Get all assignments for this class
assignments = []
if db:
try:
assignments = await db.list_assignments(
teacher_id=teacher["user_id"],
class_id=class_id
)
except Exception as e:
logger.error(f"Failed to list assignments: {e}")
if not assignments:
assignments = [
a for a in _assignments_store.values()
if a["class_id"] == class_id and a["teacher_id"] == teacher["user_id"]
]
total_units = len(assignments)
completed_units = sum(1 for a in assignments if a.get("status") == "completed")
active_units = sum(1 for a in assignments if a.get("status") == "active")
# Aggregate student performance
students = await get_students_in_class(class_id)
student_scores = {}
misconceptions = []
if db:
try:
for student in students:
student_id = student.get("id", student.get("student_id"))
analytics = await db.get_student_analytics(student_id)
if analytics:
student_scores[student_id] = {
"name": student.get("name", student_id[:8]),
"avg_score": analytics.get("avg_postcheck_score", 0),
"total_time": analytics.get("total_time_minutes", 0),
}
# Get common misconceptions
misconceptions_data = await db.get_class_misconceptions(class_id)
for m in misconceptions_data:
misconceptions.append(MisconceptionReport(
concept_id=m["concept_id"],
concept_label=m["concept_label"],
misconception=m["misconception"],
affected_students=m["affected_students"],
frequency=m["frequency"],
unit_id=m["unit_id"],
stop_id=m["stop_id"],
))
except Exception as e:
logger.error(f"Failed to aggregate analytics: {e}")
# Identify top and struggling students
sorted_students = sorted(
student_scores.items(),
key=lambda x: x[1]["avg_score"],
reverse=True
)
top_performers = [s[1]["name"] for s in sorted_students[:3]]
struggling_students = [s[1]["name"] for s in sorted_students[-3:] if s[1]["avg_score"] < 0.6]
total_time = sum(s["total_time"] for s in student_scores.values())
avg_scores = [s["avg_score"] for s in student_scores.values() if s["avg_score"] > 0]
avg_completion = sum(avg_scores) / len(avg_scores) if avg_scores else 0
return ClassAnalyticsSummary(
class_id=class_id,
class_name=f"Klasse {class_id[:8]}",
total_units_assigned=total_units,
units_completed=completed_units,
active_units=active_units,
avg_completion_rate=avg_completion,
avg_learning_gain=None, # Would calculate from pre/post scores
total_time_hours=total_time / 60,
top_performers=top_performers,
struggling_students=struggling_students,
common_misconceptions=misconceptions[:5],
)
@router.get("/students/{student_id}/progress")
async def get_student_progress(
student_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, Any]:
"""
Get detailed progress for a specific student.
Shows all units attempted and their performance.
"""
db = await get_teacher_database()
if db:
try:
progress = await db.get_student_full_progress(student_id)
return progress
except Exception as e:
logger.error(f"Failed to get student progress: {e}")
return {
"student_id": student_id,
"units_attempted": 0,
"units_completed": 0,
"avg_score": 0.0,
"total_time_minutes": 0,
"sessions": [],
}
# ==============================================
# API Endpoints - Content Resources
# ==============================================
@router.get("/assignments/{assignment_id}/resources", response_model=List[ContentResource])
async def get_assignment_resources(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher),
request: Request = None
) -> List[ContentResource]:
"""
Get generated content resources for an assignment.
Returns links to H5P activities and PDF worksheets.
"""
db = await get_teacher_database()
assignment = None
if db:
try:
assignment = await db.get_assignment(assignment_id)
except Exception as e:
logger.error(f"Failed to get assignment: {e}")
if not assignment and assignment_id in _assignments_store:
assignment = _assignments_store[assignment_id]
if not assignment or assignment["teacher_id"] != teacher["user_id"]:
raise HTTPException(status_code=404, detail="Assignment not found")
unit_id = assignment["unit_id"]
base_url = str(request.base_url).rstrip("/") if request else "http://localhost:8000"
resources = [
ContentResource(
resource_type="h5p",
title=f"{unit_id} - H5P Aktivitaeten",
url=f"{base_url}/api/units/content/{unit_id}/h5p",
generated_at=datetime.utcnow(),
unit_id=unit_id,
),
ContentResource(
resource_type="worksheet",
title=f"{unit_id} - Arbeitsblatt (HTML)",
url=f"{base_url}/api/units/content/{unit_id}/worksheet",
generated_at=datetime.utcnow(),
unit_id=unit_id,
),
ContentResource(
resource_type="pdf",
title=f"{unit_id} - Arbeitsblatt (PDF)",
url=f"{base_url}/api/units/content/{unit_id}/worksheet.pdf",
generated_at=datetime.utcnow(),
unit_id=unit_id,
),
]
return resources
@router.post("/assignments/{assignment_id}/regenerate-content")
async def regenerate_content(
assignment_id: str,
resource_type: str = Query("all", description="h5p, pdf, or all"),
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, Any]:
"""
Trigger regeneration of content resources.
Useful after updating unit definitions.
"""
db = await get_teacher_database()
assignment = None
if db:
try:
assignment = await db.get_assignment(assignment_id)
except Exception as e:
logger.error(f"Failed to get assignment: {e}")
if not assignment and assignment_id in _assignments_store:
assignment = _assignments_store[assignment_id]
if not assignment or assignment["teacher_id"] != teacher["user_id"]:
raise HTTPException(status_code=404, detail="Assignment not found")
# In production, this would trigger async job to regenerate content
logger.info(f"Content regeneration triggered for {assignment['unit_id']}: {resource_type}")
return {
"status": "queued",
"assignment_id": assignment_id,
"unit_id": assignment["unit_id"],
"resource_type": resource_type,
"message": "Content regeneration has been queued",
}
# ==============================================
# API Endpoints - Available Units
# ==============================================
@router.get("/units/available")
async def list_available_units(
grade: Optional[str] = Query(None, description="Filter by grade level"),
template: Optional[str] = Query(None, description="Filter by template type"),
locale: str = Query("de-DE", description="Locale"),
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> List[Dict[str, Any]]:
"""
List all available units for assignment.
Teachers see all published units matching their criteria.
"""
db = await get_teacher_database()
if db:
try:
units = await db.list_available_units(
grade=grade,
template=template,
locale=locale
)
return units
except Exception as e:
logger.error(f"Failed to list units: {e}")
# Fallback: return demo units
return [
{
"unit_id": "bio_eye_lightpath_v1",
"title": "Auge - Lichtstrahl-Flug",
"template": "flight_path",
"grade_band": ["5", "6", "7"],
"duration_minutes": 8,
"difficulty": "base",
"description": "Reise durch das Auge und folge dem Lichtstrahl",
"learning_objectives": [
"Verstehen des Lichtwegs durch das Auge",
"Funktionen der Augenbestandteile benennen",
],
},
{
"unit_id": "math_pizza_equivalence_v1",
"title": "Pizza-Boxenstopp - Brueche und Prozent",
"template": "station_loop",
"grade_band": ["5", "6"],
"duration_minutes": 10,
"difficulty": "base",
"description": "Entdecke die Verbindung zwischen Bruechen, Dezimalzahlen und Prozent",
"learning_objectives": [
"Brueche in Prozent umrechnen",
"Aequivalenzen erkennen",
],
},
]
# ==============================================
# API Endpoints - Dashboard Overview
# ==============================================
@router.get("/dashboard")
async def get_dashboard(
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, Any]:
"""
Get teacher dashboard overview.
Summary of all classes, active assignments, and alerts.
"""
db = await get_teacher_database()
# Get teacher's classes
classes = await get_classes_for_teacher(teacher["user_id"])
# Get all active assignments
active_assignments = []
if db:
try:
active_assignments = await db.list_assignments(
teacher_id=teacher["user_id"],
status="active"
)
except Exception as e:
logger.error(f"Failed to list assignments: {e}")
if not active_assignments:
active_assignments = [
a for a in _assignments_store.values()
if a["teacher_id"] == teacher["user_id"] and a.get("status") == "active"
]
# Calculate alerts (students falling behind, due dates, etc.)
alerts = []
for assignment in active_assignments:
if assignment.get("due_date") and assignment["due_date"] < datetime.utcnow() + timedelta(days=2):
alerts.append({
"type": "due_soon",
"assignment_id": assignment["assignment_id"],
"message": f"Zuweisung endet in weniger als 2 Tagen",
})
return {
"teacher": {
"id": teacher["user_id"],
"name": teacher.get("name", "Lehrer"),
"email": teacher.get("email"),
},
"classes": len(classes),
"active_assignments": len(active_assignments),
"total_students": sum(c.get("student_count", 0) for c in classes),
"alerts": alerts,
"recent_activity": [], # Would load recent session completions
}
@router.get("/health")
async def health_check() -> Dict[str, Any]:
"""Health check for teacher dashboard API."""
db = await get_teacher_database()
db_status = "connected" if db else "in-memory"
return {
"status": "healthy",
"service": "teacher-dashboard",
"database": db_status,
"auth_required": REQUIRE_AUTH,
}