- ScopeExportTab: 11 Feldnamen-Mismatches gegen ScopeDecision Interface korrigiert (level→determinedLevel, riskScore→risk_score, hardTriggers→triggeredHardTriggers, depthDescription→depth, effortEstimate→estimatedEffort, isMandatory→required, triggeredByHardTrigger→triggeredBy, effortDays→estimatedEffort) - Company Profile: GET vom Backend beim Mount, snake_case→camelCase, SDK State Fallback - Modules: Aktivierung/Deaktivierung ans Backend schreiben (activate/deactivate Endpoints) - Obligations: Explizites Fehler-Banner statt stiller Fallback bei Backend-Fehler - Source Policy: BlockedContentDB Model + GET /api/v1/admin/blocked-content Endpoint - Import: Offline-Modus Label fuer Backend-Fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
287 lines
10 KiB
Python
287 lines
10 KiB
Python
"""
|
|
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}/activate")
|
|
async def activate_module(module_id: str, db: Session = Depends(get_db)):
|
|
"""Activate a service 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")
|
|
|
|
module.is_active = True
|
|
db.commit()
|
|
|
|
return {"status": "activated", "id": module.id, "name": module.name}
|
|
|
|
|
|
@router.post("/modules/{module_id}/deactivate")
|
|
async def deactivate_module(module_id: str, db: Session = Depends(get_db)):
|
|
"""Deactivate a service 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")
|
|
|
|
module.is_active = False
|
|
db.commit()
|
|
|
|
return {"status": "deactivated", "id": module.id, "name": module.name}
|
|
|
|
|
|
@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))
|