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:
250
backend/compliance/api/module_routes.py
Normal file
250
backend/compliance/api/module_routes.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
FastAPI routes for Service Module Registry.
|
||||
|
||||
Endpoints:
|
||||
- /modules: Module listing and management
|
||||
- /modules/overview: Module compliance overview
|
||||
- /modules/{module_id}: Module details
|
||||
- /modules/seed: Seed modules from data
|
||||
- /modules/{module_id}/regulations: Add regulation mapping
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from classroom_engine.database import get_db
|
||||
|
||||
from ..db import RegulationRepository
|
||||
from .schemas import (
|
||||
ServiceModuleResponse, ServiceModuleListResponse, ServiceModuleDetailResponse,
|
||||
ModuleRegulationMappingCreate, ModuleRegulationMappingResponse,
|
||||
ModuleSeedRequest, ModuleSeedResponse, ModuleComplianceOverview,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["compliance-modules"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Service Module Registry
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/modules", response_model=ServiceModuleListResponse)
|
||||
async def list_modules(
|
||||
service_type: Optional[str] = None,
|
||||
criticality: Optional[str] = None,
|
||||
processes_pii: Optional[bool] = None,
|
||||
ai_components: Optional[bool] = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List all service modules with optional filters."""
|
||||
from ..db.repository import ServiceModuleRepository
|
||||
|
||||
repo = ServiceModuleRepository(db)
|
||||
modules = repo.get_all(
|
||||
service_type=service_type,
|
||||
criticality=criticality,
|
||||
processes_pii=processes_pii,
|
||||
ai_components=ai_components,
|
||||
)
|
||||
|
||||
# Count regulations and risks for each module
|
||||
results = []
|
||||
for m in modules:
|
||||
reg_count = len(m.regulation_mappings) if m.regulation_mappings else 0
|
||||
risk_count = len(m.module_risks) if m.module_risks else 0
|
||||
|
||||
results.append(ServiceModuleResponse(
|
||||
id=m.id,
|
||||
name=m.name,
|
||||
display_name=m.display_name,
|
||||
description=m.description,
|
||||
service_type=m.service_type.value if m.service_type else None,
|
||||
port=m.port,
|
||||
technology_stack=m.technology_stack or [],
|
||||
repository_path=m.repository_path,
|
||||
docker_image=m.docker_image,
|
||||
data_categories=m.data_categories or [],
|
||||
processes_pii=m.processes_pii,
|
||||
processes_health_data=m.processes_health_data,
|
||||
ai_components=m.ai_components,
|
||||
criticality=m.criticality,
|
||||
owner_team=m.owner_team,
|
||||
owner_contact=m.owner_contact,
|
||||
is_active=m.is_active,
|
||||
compliance_score=m.compliance_score,
|
||||
last_compliance_check=m.last_compliance_check,
|
||||
created_at=m.created_at,
|
||||
updated_at=m.updated_at,
|
||||
regulation_count=reg_count,
|
||||
risk_count=risk_count,
|
||||
))
|
||||
|
||||
return ServiceModuleListResponse(modules=results, total=len(results))
|
||||
|
||||
|
||||
@router.get("/modules/overview", response_model=ModuleComplianceOverview)
|
||||
async def get_modules_overview(db: Session = Depends(get_db)):
|
||||
"""Get overview statistics for all modules."""
|
||||
from ..db.repository import ServiceModuleRepository
|
||||
|
||||
repo = ServiceModuleRepository(db)
|
||||
overview = repo.get_overview()
|
||||
|
||||
return ModuleComplianceOverview(**overview)
|
||||
|
||||
|
||||
@router.get("/modules/{module_id}", response_model=ServiceModuleDetailResponse)
|
||||
async def get_module(module_id: str, db: Session = Depends(get_db)):
|
||||
"""Get a specific module with its regulations and risks."""
|
||||
from ..db.repository import ServiceModuleRepository
|
||||
|
||||
repo = ServiceModuleRepository(db)
|
||||
module = repo.get_with_regulations(module_id)
|
||||
|
||||
if not module:
|
||||
# Try by name
|
||||
module = repo.get_by_name(module_id)
|
||||
if module:
|
||||
module = repo.get_with_regulations(module.id)
|
||||
|
||||
if not module:
|
||||
raise HTTPException(status_code=404, detail=f"Module {module_id} not found")
|
||||
|
||||
# Build regulation list
|
||||
regulations = []
|
||||
for mapping in (module.regulation_mappings or []):
|
||||
reg = mapping.regulation
|
||||
if reg:
|
||||
regulations.append({
|
||||
"code": reg.code,
|
||||
"name": reg.name,
|
||||
"relevance_level": mapping.relevance_level.value if mapping.relevance_level else "medium",
|
||||
"notes": mapping.notes,
|
||||
})
|
||||
|
||||
# Build risk list
|
||||
risks = []
|
||||
for mr in (module.module_risks or []):
|
||||
risk = mr.risk
|
||||
if risk:
|
||||
risks.append({
|
||||
"risk_id": risk.risk_id,
|
||||
"title": risk.title,
|
||||
"inherent_risk": risk.inherent_risk.value if risk.inherent_risk else None,
|
||||
"module_risk_level": mr.module_risk_level.value if mr.module_risk_level else None,
|
||||
})
|
||||
|
||||
return ServiceModuleDetailResponse(
|
||||
id=module.id,
|
||||
name=module.name,
|
||||
display_name=module.display_name,
|
||||
description=module.description,
|
||||
service_type=module.service_type.value if module.service_type else None,
|
||||
port=module.port,
|
||||
technology_stack=module.technology_stack or [],
|
||||
repository_path=module.repository_path,
|
||||
docker_image=module.docker_image,
|
||||
data_categories=module.data_categories or [],
|
||||
processes_pii=module.processes_pii,
|
||||
processes_health_data=module.processes_health_data,
|
||||
ai_components=module.ai_components,
|
||||
criticality=module.criticality,
|
||||
owner_team=module.owner_team,
|
||||
owner_contact=module.owner_contact,
|
||||
is_active=module.is_active,
|
||||
compliance_score=module.compliance_score,
|
||||
last_compliance_check=module.last_compliance_check,
|
||||
created_at=module.created_at,
|
||||
updated_at=module.updated_at,
|
||||
regulation_count=len(regulations),
|
||||
risk_count=len(risks),
|
||||
regulations=regulations,
|
||||
risks=risks,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/modules/seed", response_model=ModuleSeedResponse)
|
||||
async def seed_modules(
|
||||
request: ModuleSeedRequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Seed service modules from predefined data."""
|
||||
from classroom_engine.database import engine
|
||||
from ..db.models import ServiceModuleDB, ModuleRegulationMappingDB, ModuleRiskDB
|
||||
from ..db.repository import ServiceModuleRepository
|
||||
from ..data.service_modules import BREAKPILOT_SERVICES
|
||||
|
||||
try:
|
||||
# Ensure tables exist
|
||||
ServiceModuleDB.__table__.create(engine, checkfirst=True)
|
||||
ModuleRegulationMappingDB.__table__.create(engine, checkfirst=True)
|
||||
ModuleRiskDB.__table__.create(engine, checkfirst=True)
|
||||
|
||||
repo = ServiceModuleRepository(db)
|
||||
result = repo.seed_from_data(BREAKPILOT_SERVICES, force=request.force)
|
||||
|
||||
return ModuleSeedResponse(
|
||||
success=True,
|
||||
message=f"Seeded {result['modules_created']} modules with {result['mappings_created']} regulation mappings",
|
||||
modules_created=result["modules_created"],
|
||||
mappings_created=result["mappings_created"],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Module seeding failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/modules/{module_id}/regulations", response_model=ModuleRegulationMappingResponse)
|
||||
async def add_module_regulation(
|
||||
module_id: str,
|
||||
mapping: ModuleRegulationMappingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Add a regulation mapping to a module."""
|
||||
from ..db.repository import ServiceModuleRepository
|
||||
|
||||
repo = ServiceModuleRepository(db)
|
||||
module = repo.get_by_id(module_id)
|
||||
|
||||
if not module:
|
||||
module = repo.get_by_name(module_id)
|
||||
|
||||
if not module:
|
||||
raise HTTPException(status_code=404, detail=f"Module {module_id} not found")
|
||||
|
||||
# Verify regulation exists
|
||||
reg_repo = RegulationRepository(db)
|
||||
regulation = reg_repo.get_by_id(mapping.regulation_id)
|
||||
if not regulation:
|
||||
regulation = reg_repo.get_by_code(mapping.regulation_id)
|
||||
if not regulation:
|
||||
raise HTTPException(status_code=404, detail=f"Regulation {mapping.regulation_id} not found")
|
||||
|
||||
try:
|
||||
new_mapping = repo.add_regulation_mapping(
|
||||
module_id=module.id,
|
||||
regulation_id=regulation.id,
|
||||
relevance_level=mapping.relevance_level,
|
||||
notes=mapping.notes,
|
||||
applicable_articles=mapping.applicable_articles,
|
||||
)
|
||||
|
||||
return ModuleRegulationMappingResponse(
|
||||
id=new_mapping.id,
|
||||
module_id=new_mapping.module_id,
|
||||
regulation_id=new_mapping.regulation_id,
|
||||
relevance_level=new_mapping.relevance_level.value if new_mapping.relevance_level else "medium",
|
||||
notes=new_mapping.notes,
|
||||
applicable_articles=new_mapping.applicable_articles,
|
||||
module_name=module.name,
|
||||
regulation_code=regulation.code,
|
||||
regulation_name=regulation.name,
|
||||
created_at=new_mapping.created_at,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add regulation mapping: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user