From f7a0b11e4107f063773e3567bc6a32e99151ad67 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 2 Mar 2026 12:02:40 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Vorbereitung-Module=20auf=20100%=20?= =?UTF-8?q?=E2=80=94=20Feld-Fixes,=20Backend-Persistenz,=20Endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../app/(sdk)/sdk/company-profile/page.tsx | 63 ++++++++++++++++--- .../app/(sdk)/sdk/import/page.tsx | 4 +- .../app/(sdk)/sdk/modules/page.tsx | 20 +++++- .../app/(sdk)/sdk/obligations/page.tsx | 11 +++- .../v1/modules/[moduleId]/activate/route.ts | 36 +++++++++++ .../v1/modules/[moduleId]/deactivate/route.ts | 36 +++++++++++ .../sdk/compliance-scope/ScopeExportTab.tsx | 51 +++++++-------- .../compliance/api/module_routes.py | 36 +++++++++++ .../compliance/api/source_policy_router.py | 38 +++++++++++ .../compliance/db/source_policy_models.py | 22 +++++++ 10 files changed, 278 insertions(+), 39 deletions(-) create mode 100644 admin-compliance/app/api/sdk/v1/modules/[moduleId]/activate/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/modules/[moduleId]/deactivate/route.ts diff --git a/admin-compliance/app/(sdk)/sdk/company-profile/page.tsx b/admin-compliance/app/(sdk)/sdk/company-profile/page.tsx index 5be1a13..1ed0ee6 100644 --- a/admin-compliance/app/(sdk)/sdk/company-profile/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/company-profile/page.tsx @@ -1130,16 +1130,65 @@ export default function CompanyProfilePage() { const totalSteps = wizardSteps.length const lastStep = wizardSteps[wizardSteps.length - 1].id - // Load existing profile + // Load existing profile: first try backend, then SDK state as fallback useEffect(() => { - if (state.companyProfile) { - setFormData(state.companyProfile) - // If profile is complete, show last step - if (state.companyProfile.isComplete) { - setCurrentStep(5) + let cancelled = false + + async function loadFromBackend() { + try { + const response = await fetch('/api/sdk/v1/company-profile?tenant_id=default') + if (response.ok) { + const data = await response.json() + if (data && !cancelled) { + const backendProfile: Partial = { + companyName: data.company_name || '', + legalForm: data.legal_form || undefined, + industry: data.industry || '', + foundedYear: data.founded_year || undefined, + businessModel: data.business_model || undefined, + offerings: data.offerings || [], + companySize: data.company_size || undefined, + employeeCount: data.employee_count || '', + annualRevenue: data.annual_revenue || '', + headquartersCountry: data.headquarters_country || 'DE', + headquartersCity: data.headquarters_city || '', + hasInternationalLocations: data.has_international_locations || false, + internationalCountries: data.international_countries || [], + targetMarkets: data.target_markets || [], + primaryJurisdiction: data.primary_jurisdiction || 'DE', + isDataController: data.is_data_controller ?? true, + isDataProcessor: data.is_data_processor ?? false, + usesAI: data.uses_ai ?? false, + aiUseCases: data.ai_use_cases || [], + dpoName: data.dpo_name || '', + dpoEmail: data.dpo_email || '', + isComplete: data.is_complete || false, + } + setFormData(backendProfile) + if (backendProfile.isComplete) { + setCurrentStep(5) + } + return + } + } + } catch { + // Backend not available, fall through to SDK state + } + + // Fallback: use SDK state + if (!cancelled && state.companyProfile) { + setFormData(state.companyProfile) + if (state.companyProfile.isComplete) { + setCurrentStep(5) + } } } - }, [state.companyProfile]) + + loadFromBackend() + + return () => { cancelled = true } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) const updateFormData = (updates: Partial) => { setFormData(prev => ({ ...prev, ...updates })) diff --git a/admin-compliance/app/(sdk)/sdk/import/page.tsx b/admin-compliance/app/(sdk)/sdk/import/page.tsx index c8848de..855746b 100644 --- a/admin-compliance/app/(sdk)/sdk/import/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/import/page.tsx @@ -425,7 +425,7 @@ export default function ImportPage() { setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, status: 'error' as const, error: 'Analyse fehlgeschlagen' } : f))) } } catch { - // Fallback: create basic document without backend analysis + // Offline-Modus: create basic document without backend analysis const doc: ImportedDocument = { id: file.id, name: file.file.name, @@ -438,7 +438,7 @@ export default function ImportPage() { confidence: 0.5, extractedEntities: [], gaps: [], - recommendations: ['Backend nicht erreichbar — manuelle Pruefung empfohlen'], + recommendations: ['Offline-Modus — Backend nicht erreichbar, manuelle Pruefung empfohlen'], }, } addImportedDocument(doc) diff --git a/admin-compliance/app/(sdk)/sdk/modules/page.tsx b/admin-compliance/app/(sdk)/sdk/modules/page.tsx index 58fa6af..62269b6 100644 --- a/admin-compliance/app/(sdk)/sdk/modules/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/modules/page.tsx @@ -301,7 +301,7 @@ export default function ModulesPage() { .filter(m => state.modules.some(sm => sm.id === m.id)) .reduce((sum, m) => sum + m.controlsCount, 0) - const handleActivateModule = (module: DisplayModule) => { + const handleActivateModule = async (module: DisplayModule) => { const serviceModule: ServiceModule = { id: module.id, name: module.name, @@ -312,11 +312,27 @@ export default function ModulesPage() { hasAIComponents: module.hasAIComponents, } dispatch({ type: 'ADD_MODULE', payload: serviceModule }) + + try { + await fetch(`/api/sdk/v1/modules/${encodeURIComponent(module.id)}/activate`, { + method: 'POST', + }) + } catch { + console.warn('Could not persist module activation to backend') + } } - const handleDeactivateModule = (moduleId: string) => { + const handleDeactivateModule = async (moduleId: string) => { const updatedModules = state.modules.filter(m => m.id !== moduleId) dispatch({ type: 'SET_STATE', payload: { modules: updatedModules } }) + + try { + await fetch(`/api/sdk/v1/modules/${encodeURIComponent(moduleId)}/deactivate`, { + method: 'POST', + }) + } catch { + console.warn('Could not persist module deactivation to backend') + } } const stepInfo = STEP_EXPLANATIONS['modules'] diff --git a/admin-compliance/app/(sdk)/sdk/obligations/page.tsx b/admin-compliance/app/(sdk)/sdk/obligations/page.tsx index 4e061bb..f3aa4f1 100644 --- a/admin-compliance/app/(sdk)/sdk/obligations/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/obligations/page.tsx @@ -198,6 +198,7 @@ export default function ObligationsPage() { const [filter, setFilter] = useState('all') const [loading, setLoading] = useState(true) const [backendAvailable, setBackendAvailable] = useState(false) + const [backendError, setBackendError] = useState(null) useEffect(() => { async function loadObligations() { @@ -215,7 +216,7 @@ export default function ObligationsPage() { } } } catch { - // Backend unavailable, use SDK state obligations + setBackendError('Backend nicht erreichbar — Pflichten aus lokalem State geladen (Offline-Modus)') } // Fallback: use obligations from SDK state @@ -275,6 +276,14 @@ export default function ObligationsPage() { Pflichten aus UCCA-Assessments geladen (Live-Daten) )} + {backendError && !backendAvailable && ( +
+ + + + {backendError} +
+ )} {/* Loading */} {loading && ( diff --git a/admin-compliance/app/api/sdk/v1/modules/[moduleId]/activate/route.ts b/admin-compliance/app/api/sdk/v1/modules/[moduleId]/activate/route.ts new file mode 100644 index 0000000..8188cb9 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/modules/[moduleId]/activate/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ moduleId: string }> } +) { + try { + const { moduleId } = await params + const response = await fetch( + `${BACKEND_URL}/api/modules/${encodeURIComponent(moduleId)}/activate`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + } + ) + + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json( + { error: 'Backend error', details: errorText }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Failed to activate module:', error) + return NextResponse.json( + { error: 'Failed to connect to backend' }, + { status: 503 } + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/modules/[moduleId]/deactivate/route.ts b/admin-compliance/app/api/sdk/v1/modules/[moduleId]/deactivate/route.ts new file mode 100644 index 0000000..852efbb --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/modules/[moduleId]/deactivate/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ moduleId: string }> } +) { + try { + const { moduleId } = await params + const response = await fetch( + `${BACKEND_URL}/api/modules/${encodeURIComponent(moduleId)}/deactivate`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + } + ) + + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json( + { error: 'Backend error', details: errorText }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Failed to deactivate module:', error) + return NextResponse.json( + { error: 'Failed to connect to backend' }, + { status: 503 } + ) + } +} diff --git a/admin-compliance/components/sdk/compliance-scope/ScopeExportTab.tsx b/admin-compliance/components/sdk/compliance-scope/ScopeExportTab.tsx index 32104fe..bdfc1fa 100644 --- a/admin-compliance/components/sdk/compliance-scope/ScopeExportTab.tsx +++ b/admin-compliance/components/sdk/compliance-scope/ScopeExportTab.tsx @@ -34,10 +34,10 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s const headers = ['Typ', 'Tiefe', 'Aufwand (Tage)', 'Pflicht', 'Hard-Trigger'] const rows = decision.requiredDocuments.map((doc) => [ DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType, - doc.depthDescription, - doc.effortEstimate?.days?.toString() || '0', - doc.isMandatory ? 'Ja' : 'Nein', - doc.triggeredByHardTrigger ? 'Ja' : 'Nein', + doc.depth, + doc.estimatedEffort || '0', + doc.required ? 'Ja' : 'Nein', + doc.triggeredBy.length > 0 ? 'Ja' : 'Nein', ]) const csvContent = [headers, ...rows].map((row) => row.map((cell) => `"${cell}"`).join(',')).join('\n') @@ -57,32 +57,29 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s let markdown = `# Compliance Scope Entscheidung\n\n` markdown += `**Datum:** ${new Date().toLocaleDateString('de-DE')}\n\n` markdown += `## Einstufung\n\n` - markdown += `**Level:** ${decision.level} - ${DEPTH_LEVEL_LABELS[decision.level]}\n\n` + markdown += `**Level:** ${decision.determinedLevel} - ${DEPTH_LEVEL_LABELS[decision.determinedLevel]}\n\n` if (decision.reasoning) { markdown += `**Begründung:** ${decision.reasoning}\n\n` } if (decision.scores) { markdown += `## Scores\n\n` - markdown += `- **Risiko-Score:** ${decision.scores.riskScore}/100\n` - markdown += `- **Komplexitäts-Score:** ${decision.scores.complexityScore}/100\n` - markdown += `- **Assurance-Score:** ${decision.scores.assuranceScore}/100\n` - markdown += `- **Gesamt-Score:** ${decision.scores.compositeScore}/100\n\n` + markdown += `- **Risiko-Score:** ${decision.scores.risk_score}/100\n` + markdown += `- **Komplexitäts-Score:** ${decision.scores.complexity_score}/100\n` + markdown += `- **Assurance-Score:** ${decision.scores.assurance_need}/100\n` + markdown += `- **Gesamt-Score:** ${decision.scores.composite_score}/100\n\n` } - if (decision.hardTriggers && decision.hardTriggers.length > 0) { - const matchedTriggers = decision.hardTriggers.filter((ht) => ht.matched) - if (matchedTriggers.length > 0) { - markdown += `## Aktive Hard-Trigger\n\n` - matchedTriggers.forEach((trigger) => { - markdown += `- **${trigger.label}**\n` - markdown += ` - ${trigger.description}\n` - if (trigger.legalReference) { - markdown += ` - Rechtsgrundlage: ${trigger.legalReference}\n` - } - }) - markdown += `\n` - } + if (decision.triggeredHardTriggers && decision.triggeredHardTriggers.length > 0) { + markdown += `## Aktive Hard-Trigger\n\n` + decision.triggeredHardTriggers.forEach((trigger) => { + markdown += `- **${trigger.rule.label}**\n` + markdown += ` - ${trigger.rule.description}\n` + if (trigger.rule.legalReference) { + markdown += ` - Rechtsgrundlage: ${trigger.rule.legalReference}\n` + } + }) + markdown += `\n` } if (decision.requiredDocuments && decision.requiredDocuments.length > 0) { @@ -90,9 +87,9 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s markdown += `| Typ | Tiefe | Aufwand | Pflicht | Hard-Trigger |\n` markdown += `|-----|-------|---------|---------|-------------|\n` decision.requiredDocuments.forEach((doc) => { - markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.depthDescription} | ${ - doc.effortEstimate?.days || 0 - } Tage | ${doc.isMandatory ? 'Ja' : 'Nein'} | ${doc.triggeredByHardTrigger ? 'Ja' : 'Nein'} |\n` + markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.depth} | ${ + doc.estimatedEffort || '0' + } | ${doc.required ? 'Ja' : 'Nein'} | ${doc.triggeredBy.length > 0 ? 'Ja' : 'Nein'} |\n` }) markdown += `\n` } @@ -111,8 +108,8 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s decision.nextActions.forEach((action) => { markdown += `${action.priority}. **${action.title}**\n` markdown += ` ${action.description}\n` - if (action.effortDays) { - markdown += ` Aufwand: ${action.effortDays} Tage\n` + if (action.estimatedEffort) { + markdown += ` Aufwand: ${action.estimatedEffort}\n` } markdown += `\n` }) diff --git a/backend-compliance/compliance/api/module_routes.py b/backend-compliance/compliance/api/module_routes.py index 05df076..521ed2d 100644 --- a/backend-compliance/compliance/api/module_routes.py +++ b/backend-compliance/compliance/api/module_routes.py @@ -198,6 +198,42 @@ async def seed_modules( 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, diff --git a/backend-compliance/compliance/api/source_policy_router.py b/backend-compliance/compliance/api/source_policy_router.py index f1739ba..cea91f8 100644 --- a/backend-compliance/compliance/api/source_policy_router.py +++ b/backend-compliance/compliance/api/source_policy_router.py @@ -32,6 +32,7 @@ from sqlalchemy.orm import Session from database import get_db from compliance.db.source_policy_models import ( AllowedSourceDB, + BlockedContentDB, SourceOperationDB, PIIRuleDB, SourcePolicyAuditDB, @@ -398,6 +399,43 @@ async def delete_pii_rule(rule_id: str, db: Session = Depends(get_db)): return {"status": "deleted", "id": rule_id} +# ============================================================================= +# Blocked Content +# ============================================================================= + +@router.get("/blocked-content") +async def list_blocked_content( + limit: int = Query(50, ge=1, le=500), + offset: int = Query(0, ge=0), + domain: Optional[str] = None, + db: Session = Depends(get_db), +): + """List blocked content entries.""" + query = db.query(BlockedContentDB) + + if domain: + query = query.filter(BlockedContentDB.domain == domain) + + total = query.count() + entries = query.order_by(BlockedContentDB.created_at.desc()).offset(offset).limit(limit).all() + + return { + "blocked": [ + { + "id": str(e.id), + "url": e.url, + "domain": e.domain, + "block_reason": e.block_reason, + "rule_id": str(e.rule_id) if e.rule_id else None, + "details": e.details, + "created_at": e.created_at.isoformat() if e.created_at else None, + } + for e in entries + ], + "total": total, + } + + # ============================================================================= # Audit Trail # ============================================================================= diff --git a/backend-compliance/compliance/db/source_policy_models.py b/backend-compliance/compliance/db/source_policy_models.py index 05977a4..5eebfde 100644 --- a/backend-compliance/compliance/db/source_policy_models.py +++ b/backend-compliance/compliance/db/source_policy_models.py @@ -85,6 +85,28 @@ class PIIRuleDB(Base): ) +class BlockedContentDB(Base): + """Blocked content entries tracked by source policy enforcement.""" + + __tablename__ = 'compliance_blocked_content' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + url = Column(Text, nullable=True) + domain = Column(String(255), nullable=False) + block_reason = Column(String(100), nullable=False) # unlicensed, pii, blacklisted, etc. + rule_id = Column(UUID(as_uuid=True), nullable=True) # PII rule or source that triggered block + details = Column(JSON, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + + __table_args__ = ( + Index('idx_blocked_content_domain', 'domain'), + Index('idx_blocked_content_created', 'created_at'), + ) + + def __repr__(self): + return f"" + + class SourcePolicyAuditDB(Base): """Audit trail for source policy changes."""