[split-required] [guardrail-change] Enforce 500 LOC budget across all services

Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-27 00:09:30 +02:00
parent 5ef039a6bc
commit 92c86ec6ba
162 changed files with 23853 additions and 23034 deletions

View File

@@ -355,531 +355,6 @@ func (h *SchoolHandlers) ListSubjects(c *gin.Context) {
c.JSON(http.StatusOK, subjects)
}
// ========================================
// Attendance Handlers
// ========================================
// RecordAttendance records attendance for a student
// POST /api/v1/attendance
func (h *SchoolHandlers) RecordAttendance(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req models.RecordAttendanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
record, err := h.attendanceService.RecordAttendance(c.Request.Context(), req, userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, record)
}
// RecordBulkAttendance records attendance for multiple students
// POST /api/v1/classes/:id/attendance
func (h *SchoolHandlers) RecordBulkAttendance(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
classIDStr := c.Param("id")
classID, err := uuid.Parse(classIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
return
}
var req struct {
Date string `json:"date" binding:"required"`
SlotID string `json:"slot_id" binding:"required"`
Records []struct {
StudentID string `json:"student_id"`
Status string `json:"status"`
Note *string `json:"note"`
} `json:"records" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
slotID, err := uuid.Parse(req.SlotID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid slot ID"})
return
}
// Convert to the expected type (without JSON tags)
records := make([]struct {
StudentID string
Status string
Note *string
}, len(req.Records))
for i, r := range req.Records {
records[i] = struct {
StudentID string
Status string
Note *string
}{
StudentID: r.StudentID,
Status: r.Status,
Note: r.Note,
}
}
err = h.attendanceService.RecordBulkAttendance(c.Request.Context(), classID, req.Date, slotID, records, userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "attendance recorded"})
}
// GetClassAttendance gets attendance for a class on a specific date
// GET /api/v1/classes/:id/attendance?date=...
func (h *SchoolHandlers) GetClassAttendance(c *gin.Context) {
classIDStr := c.Param("id")
classID, err := uuid.Parse(classIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
return
}
date := c.Query("date")
if date == "" {
date = time.Now().Format("2006-01-02")
}
overview, err := h.attendanceService.GetAttendanceByClass(c.Request.Context(), classID, date)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, overview)
}
// GetStudentAttendance gets attendance history for a student
// GET /api/v1/students/:id/attendance?start_date=...&end_date=...
func (h *SchoolHandlers) GetStudentAttendance(c *gin.Context) {
studentIDStr := c.Param("id")
studentID, err := uuid.Parse(studentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
return
}
startDateStr := c.Query("start_date")
endDateStr := c.Query("end_date")
var startDate, endDate time.Time
if startDateStr == "" {
startDate = time.Now().AddDate(0, -1, 0) // Last month
} else {
startDate, _ = time.Parse("2006-01-02", startDateStr)
}
if endDateStr == "" {
endDate = time.Now()
} else {
endDate, _ = time.Parse("2006-01-02", endDateStr)
}
records, err := h.attendanceService.GetStudentAttendance(c.Request.Context(), studentID, startDate, endDate)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, records)
}
// ========================================
// Absence Report Handlers
// ========================================
// ReportAbsence allows parents to report absence
// POST /api/v1/absence/report
func (h *SchoolHandlers) ReportAbsence(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req models.ReportAbsenceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
report, err := h.attendanceService.ReportAbsence(c.Request.Context(), req, userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, report)
}
// ConfirmAbsence allows teachers to confirm absence
// PUT /api/v1/absence/:id/confirm
func (h *SchoolHandlers) ConfirmAbsence(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
reportIDStr := c.Param("id")
reportID, err := uuid.Parse(reportIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
return
}
var req struct {
Status string `json:"status" binding:"required"` // "excused" or "unexcused"
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = h.attendanceService.ConfirmAbsence(c.Request.Context(), reportID, userID.(uuid.UUID), req.Status)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "absence confirmed"})
}
// GetPendingAbsenceReports gets pending absence reports for a class
// GET /api/v1/classes/:id/absence/pending
func (h *SchoolHandlers) GetPendingAbsenceReports(c *gin.Context) {
classIDStr := c.Param("id")
classID, err := uuid.Parse(classIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
return
}
reports, err := h.attendanceService.GetPendingAbsenceReports(c.Request.Context(), classID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, reports)
}
// ========================================
// Grade Handlers
// ========================================
// CreateGrade creates a new grade
// POST /api/v1/grades
func (h *SchoolHandlers) CreateGrade(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req models.CreateGradeRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get teacher ID from user ID
teacher, err := h.schoolService.GetTeacherByUserID(c.Request.Context(), userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": "user is not a teacher"})
return
}
grade, err := h.gradeService.CreateGrade(c.Request.Context(), req, teacher.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, grade)
}
// GetStudentGrades gets all grades for a student
// GET /api/v1/students/:id/grades?school_year_id=...
func (h *SchoolHandlers) GetStudentGrades(c *gin.Context) {
studentIDStr := c.Param("id")
studentID, err := uuid.Parse(studentIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
return
}
schoolYearIDStr := c.Query("school_year_id")
if schoolYearIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "school_year_id is required"})
return
}
schoolYearID, err := uuid.Parse(schoolYearIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
return
}
grades, err := h.gradeService.GetStudentGrades(c.Request.Context(), studentID, schoolYearID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, grades)
}
// GetClassGrades gets grades for all students in a class for a subject (Notenspiegel)
// GET /api/v1/classes/:id/grades/:subjectId?school_year_id=...&semester=...
func (h *SchoolHandlers) GetClassGrades(c *gin.Context) {
classIDStr := c.Param("id")
classID, err := uuid.Parse(classIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
return
}
subjectIDStr := c.Param("subjectId")
subjectID, err := uuid.Parse(subjectIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid subject ID"})
return
}
schoolYearIDStr := c.Query("school_year_id")
if schoolYearIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "school_year_id is required"})
return
}
schoolYearID, err := uuid.Parse(schoolYearIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
return
}
semesterStr := c.DefaultQuery("semester", "1")
var semester int
if semesterStr == "1" {
semester = 1
} else {
semester = 2
}
overviews, err := h.gradeService.GetClassGradesBySubject(c.Request.Context(), classID, subjectID, schoolYearID, semester)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, overviews)
}
// GetGradeStatistics gets grade statistics for a class/subject
// GET /api/v1/classes/:id/grades/:subjectId/stats?school_year_id=...&semester=...
func (h *SchoolHandlers) GetGradeStatistics(c *gin.Context) {
classIDStr := c.Param("id")
classID, err := uuid.Parse(classIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
return
}
subjectIDStr := c.Param("subjectId")
subjectID, err := uuid.Parse(subjectIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid subject ID"})
return
}
schoolYearIDStr := c.Query("school_year_id")
if schoolYearIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "school_year_id is required"})
return
}
schoolYearID, err := uuid.Parse(schoolYearIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
return
}
semesterStr := c.DefaultQuery("semester", "1")
var semester int
if semesterStr == "1" {
semester = 1
} else {
semester = 2
}
stats, err := h.gradeService.GetSubjectGradeStatistics(c.Request.Context(), classID, subjectID, schoolYearID, semester)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// ========================================
// Parent Onboarding Handlers
// ========================================
// GenerateOnboardingToken generates a QR code token for parent onboarding
// POST /api/v1/onboarding/tokens
func (h *SchoolHandlers) GenerateOnboardingToken(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req struct {
SchoolID string `json:"school_id" binding:"required"`
ClassID string `json:"class_id" binding:"required"`
StudentID string `json:"student_id" binding:"required"`
Role string `json:"role"` // "parent" or "parent_representative"
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
schoolID, err := uuid.Parse(req.SchoolID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
return
}
classID, err := uuid.Parse(req.ClassID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
return
}
studentID, err := uuid.Parse(req.StudentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
return
}
role := req.Role
if role == "" {
role = "parent"
}
token, err := h.schoolService.GenerateParentOnboardingToken(c.Request.Context(), schoolID, classID, studentID, userID.(uuid.UUID), role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Generate QR code URL
qrURL := "/onboard-parent?token=" + token.Token
c.JSON(http.StatusCreated, gin.H{
"token": token.Token,
"qr_url": qrURL,
"expires_at": token.ExpiresAt,
})
}
// ValidateOnboardingToken validates an onboarding token
// GET /api/v1/onboarding/validate?token=...
func (h *SchoolHandlers) ValidateOnboardingToken(c *gin.Context) {
token := c.Query("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
return
}
onboardingToken, err := h.schoolService.ValidateOnboardingToken(c.Request.Context(), token)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "invalid or expired token"})
return
}
// Get student and school info
student, err := h.schoolService.GetStudent(c.Request.Context(), onboardingToken.StudentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
class, err := h.schoolService.GetClass(c.Request.Context(), onboardingToken.ClassID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
school, err := h.schoolService.GetSchool(c.Request.Context(), onboardingToken.SchoolID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"valid": true,
"role": onboardingToken.Role,
"student_name": student.FirstName + " " + student.LastName,
"class_name": class.Name,
"school_name": school.Name,
"expires_at": onboardingToken.ExpiresAt,
})
}
// RedeemOnboardingToken redeems a token and creates parent account
// POST /api/v1/onboarding/redeem
func (h *SchoolHandlers) RedeemOnboardingToken(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req struct {
Token string `json:"token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.schoolService.RedeemOnboardingToken(c.Request.Context(), req.Token, userID.(uuid.UUID))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "token redeemed successfully"})
}
// ========================================
// Register Routes
// ========================================