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>
150 lines
5.1 KiB
Python
150 lines
5.1 KiB
Python
"""
|
|
Audit Models - DSGVO-compliant logging
|
|
NO PII in audit logs - only references and metadata
|
|
|
|
Erlaubt: ref_id (truncated), content_type, size_bytes, ttl_hours
|
|
Verboten: user_name, content, transcript, email
|
|
"""
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional, Dict, Any
|
|
from pydantic import BaseModel, Field
|
|
import uuid
|
|
|
|
|
|
class AuditAction(str, Enum):
|
|
"""Audit action types."""
|
|
# Session actions
|
|
SESSION_CREATED = "session_created"
|
|
SESSION_CONNECTED = "session_connected"
|
|
SESSION_CLOSED = "session_closed"
|
|
SESSION_EXPIRED = "session_expired"
|
|
|
|
# Audio actions (no content logged)
|
|
AUDIO_RECEIVED = "audio_received"
|
|
AUDIO_PROCESSED = "audio_processed"
|
|
|
|
# Task actions
|
|
TASK_CREATED = "task_created"
|
|
TASK_QUEUED = "task_queued"
|
|
TASK_STARTED = "task_started"
|
|
TASK_COMPLETED = "task_completed"
|
|
TASK_FAILED = "task_failed"
|
|
TASK_EXPIRED = "task_expired"
|
|
|
|
# Encryption actions
|
|
ENCRYPTION_KEY_VERIFIED = "encryption_key_verified"
|
|
ENCRYPTION_KEY_INVALID = "encryption_key_invalid"
|
|
|
|
# Integration actions
|
|
BREAKPILOT_CALLED = "breakpilot_called"
|
|
PERSONAPLEX_CALLED = "personaplex_called"
|
|
OLLAMA_CALLED = "ollama_called"
|
|
|
|
# Security actions
|
|
RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
|
|
UNAUTHORIZED_ACCESS = "unauthorized_access"
|
|
|
|
|
|
class AuditEntry(BaseModel):
|
|
"""
|
|
Audit log entry - DSGVO compliant.
|
|
NO PII is stored - only truncated references and metadata.
|
|
"""
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
# Action identification
|
|
action: AuditAction
|
|
namespace_id_truncated: str = Field(
|
|
...,
|
|
description="First 8 chars of namespace ID",
|
|
max_length=8,
|
|
)
|
|
|
|
# Reference IDs (truncated for privacy)
|
|
session_id_truncated: Optional[str] = Field(
|
|
default=None,
|
|
description="First 8 chars of session ID",
|
|
max_length=8,
|
|
)
|
|
task_id_truncated: Optional[str] = Field(
|
|
default=None,
|
|
description="First 8 chars of task ID",
|
|
max_length=8,
|
|
)
|
|
|
|
# Metadata (no PII)
|
|
content_type: Optional[str] = Field(default=None, description="Type of content processed")
|
|
size_bytes: Optional[int] = Field(default=None, description="Size in bytes")
|
|
duration_ms: Optional[int] = Field(default=None, description="Duration in milliseconds")
|
|
ttl_hours: Optional[int] = Field(default=None, description="TTL in hours")
|
|
|
|
# Technical metadata
|
|
success: bool = Field(default=True)
|
|
error_code: Optional[str] = Field(default=None)
|
|
latency_ms: Optional[int] = Field(default=None)
|
|
|
|
# Context (no PII)
|
|
device_type: Optional[str] = Field(default=None)
|
|
client_version: Optional[str] = Field(default=None)
|
|
backend_used: Optional[str] = Field(default=None, description="personaplex, ollama, etc.")
|
|
|
|
@staticmethod
|
|
def truncate_id(full_id: str, length: int = 8) -> str:
|
|
"""Truncate ID for privacy."""
|
|
if not full_id:
|
|
return ""
|
|
return full_id[:length]
|
|
|
|
class Config:
|
|
json_schema_extra = {
|
|
"example": {
|
|
"id": "audit-123",
|
|
"timestamp": "2026-01-26T10:30:00Z",
|
|
"action": "task_completed",
|
|
"namespace_id_truncated": "teacher-",
|
|
"session_id_truncated": "session-",
|
|
"task_id_truncated": "task-xyz",
|
|
"content_type": "student_observation",
|
|
"size_bytes": 256,
|
|
"ttl_hours": 168,
|
|
"success": True,
|
|
"latency_ms": 1250,
|
|
"backend_used": "ollama",
|
|
}
|
|
}
|
|
|
|
|
|
class AuditCreate(BaseModel):
|
|
"""Request to create an audit entry."""
|
|
action: AuditAction
|
|
namespace_id: str = Field(..., description="Will be truncated before storage")
|
|
session_id: Optional[str] = Field(default=None, description="Will be truncated")
|
|
task_id: Optional[str] = Field(default=None, description="Will be truncated")
|
|
content_type: Optional[str] = Field(default=None)
|
|
size_bytes: Optional[int] = Field(default=None)
|
|
duration_ms: Optional[int] = Field(default=None)
|
|
success: bool = Field(default=True)
|
|
error_code: Optional[str] = Field(default=None)
|
|
latency_ms: Optional[int] = Field(default=None)
|
|
device_type: Optional[str] = Field(default=None)
|
|
backend_used: Optional[str] = Field(default=None)
|
|
|
|
def to_audit_entry(self) -> AuditEntry:
|
|
"""Convert to AuditEntry with truncated IDs."""
|
|
return AuditEntry(
|
|
action=self.action,
|
|
namespace_id_truncated=AuditEntry.truncate_id(self.namespace_id),
|
|
session_id_truncated=AuditEntry.truncate_id(self.session_id) if self.session_id else None,
|
|
task_id_truncated=AuditEntry.truncate_id(self.task_id) if self.task_id else None,
|
|
content_type=self.content_type,
|
|
size_bytes=self.size_bytes,
|
|
duration_ms=self.duration_ms,
|
|
success=self.success,
|
|
error_code=self.error_code,
|
|
latency_ms=self.latency_ms,
|
|
device_type=self.device_type,
|
|
backend_used=self.backend_used,
|
|
)
|