feat: 7 Vorbereitungs-Module auf 100% — Frontend, Proxy-Routen, Backend-Fixes
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
Profil: machineBuilder-Felder im POST-Body, PATCH-Handler Scope: API-Route (GET/POST), ScopeDecisionTab Props + Buttons, Export-Druckansicht HTML Anwendung: PUT-Handler, Bearbeiten-Button, Pagination/Search Import: Verlauf laden, DELETE-Route, Offline-Badge, ObjectURL Memory-Leak fix Screening: Security-Backlog Button verdrahtet, Scan-Verlauf Module: Detail-Seite, GET-Proxy, Konfigurieren-Button, Modul-erstellen-Modal, Error-Toast Quellen: 10 Proxy-Routen, Tab-Komponenten umgestellt, Dashboard-Tab, blocked_today Bug fix, Datum-Filter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1248,6 +1248,33 @@ export default function CompanyProfilePage() {
|
||||
dpo_name: formData.dpoName || '',
|
||||
dpo_email: formData.dpoEmail || '',
|
||||
is_complete: true,
|
||||
// Machine builder fields (if applicable)
|
||||
...(formData.machineBuilder ? {
|
||||
machine_builder: {
|
||||
product_types: formData.machineBuilder.productTypes || [],
|
||||
product_description: formData.machineBuilder.productDescription || '',
|
||||
product_pride: formData.machineBuilder.productPride || '',
|
||||
contains_software: formData.machineBuilder.containsSoftware || false,
|
||||
contains_firmware: formData.machineBuilder.containsFirmware || false,
|
||||
contains_ai: formData.machineBuilder.containsAI || false,
|
||||
ai_integration_type: formData.machineBuilder.aiIntegrationType || [],
|
||||
has_safety_function: formData.machineBuilder.hasSafetyFunction || false,
|
||||
safety_function_description: formData.machineBuilder.safetyFunctionDescription || '',
|
||||
autonomous_behavior: formData.machineBuilder.autonomousBehavior || false,
|
||||
human_oversight_level: formData.machineBuilder.humanOversightLevel || 'full',
|
||||
is_networked: formData.machineBuilder.isNetworked || false,
|
||||
has_remote_access: formData.machineBuilder.hasRemoteAccess || false,
|
||||
has_ota_updates: formData.machineBuilder.hasOTAUpdates || false,
|
||||
update_mechanism: formData.machineBuilder.updateMechanism || '',
|
||||
export_markets: formData.machineBuilder.exportMarkets || [],
|
||||
critical_sector_clients: formData.machineBuilder.criticalSectorClients || false,
|
||||
critical_sectors: formData.machineBuilder.criticalSectors || [],
|
||||
oem_clients: formData.machineBuilder.oemClients || false,
|
||||
ce_marking_required: formData.machineBuilder.ceMarkingRequired || false,
|
||||
existing_ce_process: formData.machineBuilder.existingCEProcess || false,
|
||||
has_risk_assessment: formData.machineBuilder.hasRiskAssessment || false,
|
||||
},
|
||||
} : {}),
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import type { ImportedDocument, ImportedDocumentType, GapAnalysis, GapItem } from '@/lib/sdk/types'
|
||||
@@ -216,7 +216,15 @@ function FileItem({
|
||||
<span className="text-sm">Analysiere...</span>
|
||||
</div>
|
||||
)}
|
||||
{file.status === 'complete' && (
|
||||
{file.status === 'complete' && file.error === 'offline' && (
|
||||
<div className="flex items-center gap-1 text-amber-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-sm">Offline — nicht analysiert</span>
|
||||
</div>
|
||||
)}
|
||||
{file.status === 'complete' && file.error !== 'offline' && (
|
||||
<div className="flex items-center gap-1 text-green-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
@@ -334,6 +342,42 @@ export default function ImportPage() {
|
||||
const [files, setFiles] = useState<UploadedFile[]>([])
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||
const [analysisResult, setAnalysisResult] = useState<GapAnalysis | null>(null)
|
||||
const [importHistory, setImportHistory] = useState<any[]>([])
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
const [objectUrls, setObjectUrls] = useState<string[]>([])
|
||||
|
||||
// 4.1: Load import history
|
||||
useEffect(() => {
|
||||
const loadHistory = async () => {
|
||||
setHistoryLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/sdk/v1/import?tenant_id=default')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setImportHistory(Array.isArray(data) ? data : data.items || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load import history:', err)
|
||||
} finally {
|
||||
setHistoryLoading(false)
|
||||
}
|
||||
}
|
||||
loadHistory()
|
||||
}, [analysisResult])
|
||||
|
||||
// 4.4: Cleanup ObjectURLs on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
objectUrls.forEach(url => URL.revokeObjectURL(url))
|
||||
}
|
||||
}, [objectUrls])
|
||||
|
||||
// Helper to create and track ObjectURLs
|
||||
const createTrackedObjectURL = useCallback((file: File) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
setObjectUrls(prev => [...prev, url])
|
||||
return url
|
||||
}, [])
|
||||
|
||||
const handleFilesAdded = useCallback((newFiles: File[]) => {
|
||||
const uploadedFiles: UploadedFile[] = newFiles.map(file => ({
|
||||
@@ -391,7 +435,7 @@ export default function ImportPage() {
|
||||
id: result.document_id || file.id,
|
||||
name: file.file.name,
|
||||
type: result.detected_type || file.type,
|
||||
fileUrl: URL.createObjectURL(file.file),
|
||||
fileUrl: createTrackedObjectURL(file.file),
|
||||
uploadedAt: new Date(),
|
||||
analyzedAt: new Date(),
|
||||
analysisResult: {
|
||||
@@ -430,7 +474,7 @@ export default function ImportPage() {
|
||||
id: file.id,
|
||||
name: file.file.name,
|
||||
type: file.type,
|
||||
fileUrl: URL.createObjectURL(file.file),
|
||||
fileUrl: createTrackedObjectURL(file.file),
|
||||
uploadedAt: new Date(),
|
||||
analyzedAt: new Date(),
|
||||
analysisResult: {
|
||||
@@ -442,7 +486,7 @@ export default function ImportPage() {
|
||||
},
|
||||
}
|
||||
addImportedDocument(doc)
|
||||
setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 100, status: 'complete' as const } : f)))
|
||||
setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 100, status: 'complete' as const, error: 'offline' } : f)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +609,56 @@ export default function ImportPage() {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import-Verlauf (4.1) */}
|
||||
{importHistory.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 className="font-semibold text-gray-900">Import-Verlauf</h3>
|
||||
<p className="text-sm text-gray-500">{importHistory.length} fruehere Imports</p>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{importHistory.map((item: any, idx: number) => (
|
||||
<div key={item.id || idx} className="px-6 py-4 flex items-center justify-between hover:bg-gray-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{item.name || item.filename || `Import #${idx + 1}`}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{item.document_type || item.type || 'Unbekannt'} — {item.uploaded_at ? new Date(item.uploaded_at).toLocaleString('de-DE') : 'Unbekannt'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/import/${item.id}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
setImportHistory(prev => prev.filter(h => h.id !== item.id))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete import:', err)
|
||||
}
|
||||
}}
|
||||
className="p-2 text-gray-400 hover:text-red-500 transition-colors"
|
||||
title="Import loeschen"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{historyLoading && (
|
||||
<div className="text-center py-4 text-sm text-gray-500">Import-Verlauf wird geladen...</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
209
admin-compliance/app/(sdk)/sdk/modules/[moduleId]/page.tsx
Normal file
209
admin-compliance/app/(sdk)/sdk/modules/[moduleId]/page.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface ModuleDetail {
|
||||
id: string
|
||||
name: string
|
||||
display_name: string
|
||||
description: string
|
||||
is_active: boolean
|
||||
criticality: string
|
||||
processes_pii: boolean
|
||||
ai_components: boolean
|
||||
compliance_score: number | null
|
||||
regulation_count: number
|
||||
risk_count: number
|
||||
requirements?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
status: string
|
||||
regulation: string
|
||||
}>
|
||||
controls?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
status: string
|
||||
description: string
|
||||
}>
|
||||
regulations?: string[]
|
||||
}
|
||||
|
||||
export default function ModuleDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const moduleId = params.moduleId as string
|
||||
|
||||
const [module, setModule] = useState<ModuleDetail | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/modules/${encodeURIComponent(moduleId)}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Modul nicht gefunden')
|
||||
}
|
||||
const data = await response.json()
|
||||
setModule(data)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
if (moduleId) load()
|
||||
}, [moduleId])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">Lade Modul-Details...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !module) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
|
||||
<h3 className="text-lg font-semibold text-red-800">Fehler</h3>
|
||||
<p className="text-red-600 mt-1">{error || 'Modul nicht gefunden'}</p>
|
||||
</div>
|
||||
<Link href="/sdk/modules" className="text-purple-600 hover:text-purple-700">
|
||||
Zurueck zur Uebersicht
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const criticalityColors: Record<string, string> = {
|
||||
HIGH: 'bg-red-100 text-red-700',
|
||||
MEDIUM: 'bg-yellow-100 text-yellow-700',
|
||||
LOW: 'bg-green-100 text-green-700',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Link href="/sdk/modules" className="hover:text-purple-600">Module</Link>
|
||||
<span>/</span>
|
||||
<span className="text-gray-900">{module.display_name || module.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{module.display_name || module.name}</h1>
|
||||
<p className="text-gray-500 mt-1">{module.description}</p>
|
||||
<div className="flex items-center gap-3 mt-3">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${module.is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}`}>
|
||||
{module.is_active ? 'Aktiv' : 'Inaktiv'}
|
||||
</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${criticalityColors[module.criticality?.toUpperCase()] || 'bg-gray-100 text-gray-700'}`}>
|
||||
{module.criticality}
|
||||
</span>
|
||||
{module.processes_pii && (
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-700">PII</span>
|
||||
)}
|
||||
{module.ai_components && (
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-indigo-100 text-indigo-700">KI</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/sdk/modules')}
|
||||
className="px-4 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
Zurueck
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="text-sm text-gray-500">Compliance Score</div>
|
||||
<div className="text-3xl font-bold text-purple-600">
|
||||
{module.compliance_score != null ? `${module.compliance_score}%` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-blue-200 p-6">
|
||||
<div className="text-sm text-blue-600">Regulierungen</div>
|
||||
<div className="text-3xl font-bold text-blue-600">{module.regulation_count || 0}</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-orange-200 p-6">
|
||||
<div className="text-sm text-orange-600">Risiken</div>
|
||||
<div className="text-3xl font-bold text-orange-600">{module.risk_count || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requirements */}
|
||||
{module.requirements && module.requirements.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 className="font-semibold text-gray-900">Anforderungen</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{module.requirements.map(req => (
|
||||
<div key={req.id} className="px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">{req.title}</p>
|
||||
<p className="text-xs text-gray-500">{req.regulation}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
req.status === 'IMPLEMENTED' || req.status === 'VERIFIED'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: req.status === 'IN_PROGRESS'
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}`}>
|
||||
{req.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controls */}
|
||||
{module.controls && module.controls.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 className="font-semibold text-gray-900">Kontrollen</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{module.controls.map(ctrl => (
|
||||
<div key={ctrl.id} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium text-gray-900">{ctrl.title}</p>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
ctrl.status === 'IMPLEMENTED' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
|
||||
}`}>
|
||||
{ctrl.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">{ctrl.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Regulations */}
|
||||
{module.regulations && module.regulations.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Zugeordnete Regulierungen</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{module.regulations.map(reg => (
|
||||
<span key={reg} className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded-full">{reg}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSDK, ServiceModule } from '@/lib/sdk'
|
||||
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
||||
|
||||
@@ -126,11 +127,13 @@ function ModuleCard({
|
||||
isActive,
|
||||
onActivate,
|
||||
onDeactivate,
|
||||
onConfigure,
|
||||
}: {
|
||||
module: DisplayModule
|
||||
isActive: boolean
|
||||
onActivate: () => void
|
||||
onDeactivate: () => void
|
||||
onConfigure: () => void
|
||||
}) {
|
||||
const categoryColors = {
|
||||
gdpr: 'bg-blue-100 text-blue-700',
|
||||
@@ -208,7 +211,10 @@ function ModuleCard({
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
{isActive ? (
|
||||
<>
|
||||
<button className="flex-1 px-4 py-2 text-sm bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors">
|
||||
<button
|
||||
onClick={onConfigure}
|
||||
className="flex-1 px-4 py-2 text-sm bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
Konfigurieren
|
||||
</button>
|
||||
<button
|
||||
@@ -237,10 +243,16 @@ function ModuleCard({
|
||||
|
||||
export default function ModulesPage() {
|
||||
const { state, dispatch } = useSDK()
|
||||
const router = useRouter()
|
||||
const [filter, setFilter] = useState<string>('all')
|
||||
const [availableModules, setAvailableModules] = useState<Omit<DisplayModule, 'status' | 'completionPercent'>[]>(fallbackModules)
|
||||
const [isLoadingModules, setIsLoadingModules] = useState(true)
|
||||
const [backendError, setBackendError] = useState<string | null>(null)
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [newModuleName, setNewModuleName] = useState('')
|
||||
const [newModuleCategory, setNewModuleCategory] = useState<ModuleCategory>('custom')
|
||||
const [newModuleDescription, setNewModuleDescription] = useState('')
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
|
||||
// Load modules from backend
|
||||
useEffect(() => {
|
||||
@@ -312,26 +324,79 @@ export default function ModulesPage() {
|
||||
hasAIComponents: module.hasAIComponents,
|
||||
}
|
||||
dispatch({ type: 'ADD_MODULE', payload: serviceModule })
|
||||
setActionError(null)
|
||||
|
||||
try {
|
||||
await fetch(`/api/sdk/v1/modules/${encodeURIComponent(module.id)}/activate`, {
|
||||
const res = await fetch(`/api/sdk/v1/modules/${encodeURIComponent(module.id)}/activate`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (!res.ok) throw new Error('Aktivierung fehlgeschlagen')
|
||||
} catch {
|
||||
console.warn('Could not persist module activation to backend')
|
||||
// Rollback optimistic update
|
||||
const rollbackModules = state.modules.filter(m => m.id !== module.id)
|
||||
dispatch({ type: 'SET_STATE', payload: { modules: rollbackModules } })
|
||||
setActionError(`Modul "${module.name}" konnte nicht aktiviert werden.`)
|
||||
setTimeout(() => setActionError(null), 5000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeactivateModule = async (moduleId: string) => {
|
||||
const previousModules = [...state.modules]
|
||||
const updatedModules = state.modules.filter(m => m.id !== moduleId)
|
||||
dispatch({ type: 'SET_STATE', payload: { modules: updatedModules } })
|
||||
setActionError(null)
|
||||
|
||||
try {
|
||||
await fetch(`/api/sdk/v1/modules/${encodeURIComponent(moduleId)}/deactivate`, {
|
||||
const res = await fetch(`/api/sdk/v1/modules/${encodeURIComponent(moduleId)}/deactivate`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (!res.ok) throw new Error('Deaktivierung fehlgeschlagen')
|
||||
} catch {
|
||||
console.warn('Could not persist module deactivation to backend')
|
||||
// Rollback optimistic update
|
||||
dispatch({ type: 'SET_STATE', payload: { modules: previousModules } })
|
||||
setActionError('Modul konnte nicht deaktiviert werden.')
|
||||
setTimeout(() => setActionError(null), 5000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateModule = async () => {
|
||||
if (!newModuleName.trim()) return
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/modules', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: newModuleName,
|
||||
category: newModuleCategory,
|
||||
description: newModuleDescription,
|
||||
}),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const newMod: Omit<DisplayModule, 'status' | 'completionPercent'> = {
|
||||
id: data.id || `custom-${Date.now()}`,
|
||||
name: newModuleName,
|
||||
description: newModuleDescription,
|
||||
category: newModuleCategory,
|
||||
regulations: [],
|
||||
criticality: 'MEDIUM',
|
||||
processesPersonalData: false,
|
||||
hasAIComponents: false,
|
||||
requirementsCount: 0,
|
||||
controlsCount: 0,
|
||||
}
|
||||
setAvailableModules(prev => [...prev, newMod])
|
||||
}
|
||||
|
||||
setShowCreateModal(false)
|
||||
setNewModuleName('')
|
||||
setNewModuleCategory('custom')
|
||||
setNewModuleDescription('')
|
||||
} catch {
|
||||
setActionError('Modul konnte nicht erstellt werden.')
|
||||
setTimeout(() => setActionError(null), 5000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +412,10 @@ export default function ModulesPage() {
|
||||
explanation={stepInfo.explanation}
|
||||
tips={stepInfo.tips}
|
||||
>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
@@ -355,6 +423,78 @@ export default function ModulesPage() {
|
||||
</button>
|
||||
</StepHeader>
|
||||
|
||||
{/* Error Toast */}
|
||||
{actionError && (
|
||||
<div className="fixed top-4 right-4 z-50 bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg max-w-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="w-5 h-5 text-red-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<p className="text-sm text-red-700">{actionError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Module Modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-white rounded-xl p-6 w-full max-w-md shadow-2xl">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Eigenes Modul erstellen</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newModuleName}
|
||||
onChange={e => setNewModuleName(e.target.value)}
|
||||
placeholder="z.B. ISO 42001 AI Management"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
|
||||
<select
|
||||
value={newModuleCategory}
|
||||
onChange={e => setNewModuleCategory(e.target.value as ModuleCategory)}
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
<option value="gdpr">DSGVO</option>
|
||||
<option value="ai-act">AI Act</option>
|
||||
<option value="iso27001">ISO 27001</option>
|
||||
<option value="nis2">NIS2</option>
|
||||
<option value="custom">Eigene</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
value={newModuleDescription}
|
||||
onChange={e => setNewModuleDescription(e.target.value)}
|
||||
placeholder="Was deckt dieses Modul ab?"
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateModule}
|
||||
disabled={!newModuleName.trim()}
|
||||
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
Erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Backend Status */}
|
||||
{backendError && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-700">
|
||||
@@ -434,6 +574,7 @@ export default function ModulesPage() {
|
||||
isActive={state.modules.some(m => m.id === module.id)}
|
||||
onActivate={() => handleActivateModule(module)}
|
||||
onDeactivate={() => handleDeactivateModule(module.id)}
|
||||
onConfigure={() => router.push(`/sdk/modules/${module.id}`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useRef } from 'react'
|
||||
import { useSDK, ScreeningResult, SecurityIssue, SBOMComponent } from '@/lib/sdk'
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSDK, ScreeningResult, SecurityIssue, SBOMComponent, BacklogItem } from '@/lib/sdk'
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENTS
|
||||
@@ -163,12 +164,54 @@ function SecurityIssueCard({ issue }: { issue: SecurityIssue }) {
|
||||
|
||||
export default function ScreeningPage() {
|
||||
const { state, dispatch } = useSDK()
|
||||
const router = useRouter()
|
||||
const [isScanning, setIsScanning] = useState(false)
|
||||
const [scanProgress, setScanProgress] = useState(0)
|
||||
const [scanStatus, setScanStatus] = useState('')
|
||||
const [scanError, setScanError] = useState<string | null>(null)
|
||||
const [scanHistory, setScanHistory] = useState<any[]>([])
|
||||
const [historyLoading, setHistoryLoading] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// 5.2: Load scan history
|
||||
useEffect(() => {
|
||||
const loadHistory = async () => {
|
||||
setHistoryLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/sdk/v1/screening?tenant_id=default')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setScanHistory(Array.isArray(data) ? data : data.items || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load scan history:', err)
|
||||
} finally {
|
||||
setHistoryLoading(false)
|
||||
}
|
||||
}
|
||||
loadHistory()
|
||||
}, [state.screening])
|
||||
|
||||
// 5.1: Add issues to security backlog
|
||||
const addToSecurityBacklog = () => {
|
||||
const issues = state.screening?.securityScan?.issues || []
|
||||
issues.forEach(issue => {
|
||||
const backlogItem: BacklogItem = {
|
||||
id: `backlog-${issue.id}`,
|
||||
title: issue.title,
|
||||
description: `${issue.description}\n\nBetroffene Komponente: ${issue.affectedComponent}\nEmpfehlung: ${issue.remediation}`,
|
||||
severity: issue.severity,
|
||||
securityIssueId: issue.id,
|
||||
status: 'OPEN',
|
||||
assignee: null,
|
||||
dueDate: null,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
dispatch({ type: 'ADD_BACKLOG_ITEM', payload: backlogItem })
|
||||
})
|
||||
router.push('/sdk/security-backlog')
|
||||
}
|
||||
|
||||
const startScan = async (file: File) => {
|
||||
setIsScanning(true)
|
||||
setScanProgress(0)
|
||||
@@ -380,12 +423,54 @@ export default function ScreeningPage() {
|
||||
>
|
||||
Neuen Scan starten
|
||||
</button>
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||
Zum Security Backlog hinzufügen
|
||||
<button
|
||||
onClick={addToSecurityBacklog}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Zum Security Backlog hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Scan-Verlauf */}
|
||||
{scanHistory.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 className="font-semibold text-gray-900">Scan-Verlauf</h3>
|
||||
<p className="text-sm text-gray-500">{scanHistory.length} fruehere Scans</p>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{scanHistory.map((scan: any, idx: number) => (
|
||||
<div key={scan.id || idx} className="px-6 py-4 flex items-center justify-between hover:bg-gray-50">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
Scan #{scan.id?.slice(0, 8) || idx + 1}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{scan.completed_at ? new Date(scan.completed_at).toLocaleString('de-DE') : 'Unbekannt'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500">
|
||||
{scan.total_issues || 0} Issues
|
||||
</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
(scan.critical_issues || 0) > 0
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{(scan.critical_issues || 0) > 0 ? `${scan.critical_issues} Kritisch` : 'Keine kritischen'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{historyLoading && (
|
||||
<div className="text-center py-4 text-sm text-gray-500">Scan-Verlauf wird geladen...</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,15 +15,8 @@ import { OperationsMatrixTab } from '@/components/sdk/source-policy/OperationsMa
|
||||
import { PIIRulesTab } from '@/components/sdk/source-policy/PIIRulesTab'
|
||||
import { AuditTab } from '@/components/sdk/source-policy/AuditTab'
|
||||
|
||||
// API base URL for backend-compliance
|
||||
const getApiBase = () => {
|
||||
if (typeof window === 'undefined') return 'http://localhost:8002/api'
|
||||
const hostname = window.location.hostname
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return 'http://localhost:8002/api'
|
||||
}
|
||||
return `https://${hostname}:8002/api`
|
||||
}
|
||||
// API base URL — now uses Next.js proxy routes
|
||||
const API_BASE = '/api/sdk/v1/source-policy'
|
||||
|
||||
interface PolicyStats {
|
||||
active_policies: number
|
||||
@@ -41,23 +34,15 @@ export default function SourcePolicyPage() {
|
||||
const [stats, setStats] = useState<PolicyStats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [apiBase, setApiBase] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const base = getApiBase()
|
||||
setApiBase(base)
|
||||
fetchStats()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (apiBase !== null) {
|
||||
fetchStats()
|
||||
}
|
||||
}, [apiBase])
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/policy-stats`)
|
||||
const res = await fetch(`${API_BASE}/policy-stats`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler beim Laden der Statistiken')
|
||||
@@ -182,21 +167,82 @@ export default function SourcePolicyPage() {
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{apiBase === null ? (
|
||||
<div className="text-center py-12 text-slate-500">Initialisiere...</div>
|
||||
) : (
|
||||
<>
|
||||
{activeTab === 'dashboard' && (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
{loading ? 'Lade Dashboard...' : 'Dashboard-Ansicht - Wechseln Sie zu einem Tab fuer Details.'}
|
||||
<>
|
||||
{activeTab === 'dashboard' && stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Quellen-Uebersicht</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Zugelassene Quellen</span>
|
||||
<span className="text-2xl font-bold text-green-600">{stats.allowed_sources}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Aktive Policies</span>
|
||||
<span className="text-2xl font-bold text-purple-600">{stats.active_policies}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 rounded-full"
|
||||
style={{ width: `${stats.allowed_sources > 0 ? Math.min((stats.active_policies / stats.allowed_sources) * 100, 100) : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'sources' && <SourcesTab apiBase={apiBase} onUpdate={fetchStats} />}
|
||||
{activeTab === 'operations' && <OperationsMatrixTab apiBase={apiBase} />}
|
||||
{activeTab === 'pii' && <PIIRulesTab apiBase={apiBase} onUpdate={fetchStats} />}
|
||||
{activeTab === 'audit' && <AuditTab apiBase={apiBase} />}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Datenschutz-Regeln</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">PII-Regeln aktiv</span>
|
||||
<span className="text-2xl font-bold text-blue-600">{stats.pii_rules}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Blockiert (heute)</span>
|
||||
<span className={`text-2xl font-bold ${stats.blocked_today > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{stats.blocked_today}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Blockiert (gesamt)</span>
|
||||
<span className="text-lg font-semibold text-gray-500">{stats.blocked_total}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Compliance-Status</h3>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex-1 text-center p-4 bg-green-50 rounded-lg">
|
||||
<div className="text-sm text-green-700 font-medium">Quellen konfiguriert</div>
|
||||
<div className="text-3xl font-bold text-green-600 mt-1">
|
||||
{stats.allowed_sources > 0 ? 'Ja' : 'Nein'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 text-center p-4 bg-blue-50 rounded-lg">
|
||||
<div className="text-sm text-blue-700 font-medium">PII-Schutz aktiv</div>
|
||||
<div className="text-3xl font-bold text-blue-600 mt-1">
|
||||
{stats.pii_rules > 0 ? 'Ja' : 'Nein'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 text-center p-4 bg-purple-50 rounded-lg">
|
||||
<div className="text-sm text-purple-700 font-medium">Policies definiert</div>
|
||||
<div className="text-3xl font-bold text-purple-600 mt-1">
|
||||
{stats.active_policies > 0 ? 'Ja' : 'Nein'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'dashboard' && !stats && loading && (
|
||||
<div className="text-center py-12 text-slate-500">Lade Dashboard...</div>
|
||||
)}
|
||||
{activeTab === 'sources' && <SourcesTab apiBase={API_BASE} onUpdate={fetchStats} />}
|
||||
{activeTab === 'operations' && <OperationsMatrixTab apiBase={API_BASE} />}
|
||||
{activeTab === 'pii' && <PIIRulesTab apiBase={API_BASE} onUpdate={fetchStats} />}
|
||||
{activeTab === 'audit' && <AuditTab apiBase={API_BASE} />}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,6 +140,12 @@ export default function AssessmentDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/sdk/use-cases/new?edit=${assessmentId}`}
|
||||
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Bearbeiten
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
|
||||
@@ -26,20 +26,31 @@ export default function UseCasesPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [filterFeasibility, setFilterFeasibility] = useState<string>('all')
|
||||
const [filterRisk, setFilterRisk] = useState<string>('all')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [page, setPage] = useState(0)
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
useEffect(() => {
|
||||
fetchAssessments()
|
||||
}, [])
|
||||
}, [page, searchQuery])
|
||||
|
||||
async function fetchAssessments() {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch('/api/sdk/v1/ucca/assessments')
|
||||
const params = new URLSearchParams({
|
||||
limit: String(PAGE_SIZE),
|
||||
offset: String(page * PAGE_SIZE),
|
||||
})
|
||||
if (searchQuery) params.set('search', searchQuery)
|
||||
|
||||
const response = await fetch(`/api/sdk/v1/ucca/assessments?${params}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Laden der Assessments')
|
||||
}
|
||||
const data = await response.json()
|
||||
setAssessments(data.assessments || [])
|
||||
setTotalCount(data.total || data.assessments?.length || 0)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
@@ -101,6 +112,20 @@ export default function UseCasesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Assessments durchsuchen..."
|
||||
value={searchQuery}
|
||||
onChange={e => { setSearchQuery(e.target.value); setPage(0) }}
|
||||
className="w-full px-4 py-2 pl-10 bg-white border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
<svg className="absolute left-3 top-2.5 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -185,6 +210,31 @@ export default function UseCasesPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{!loading && totalCount > PAGE_SIZE && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">
|
||||
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, totalCount)} von {totalCount}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 text-sm bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Zurueck
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
disabled={(page + 1) * PAGE_SIZE >= totalCount}
|
||||
className="px-3 py-1 text-sm bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!loading && filtered.length === 0 && !error && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||||
|
||||
@@ -79,3 +79,43 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy: PATCH /api/sdk/v1/company-profile → Backend PATCH /api/v1/company-profile
|
||||
* Partial updates for individual fields
|
||||
*/
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const tenantId = body.tenant_id || 'default'
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/company-profile?tenant_id=${encodeURIComponent(tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': tenantId,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
)
|
||||
|
||||
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 patch company profile:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
84
admin-compliance/app/api/sdk/v1/compliance-scope/route.ts
Normal file
84
admin-compliance/app/api/sdk/v1/compliance-scope/route.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
/**
|
||||
* Proxy: GET /api/sdk/v1/compliance-scope → Backend GET /api/v1/compliance-scope
|
||||
* Retrieves the persisted scope decision for a tenant.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tenantId = searchParams.get('tenant_id') || 'default'
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance-scope?tenant_id=${encodeURIComponent(tenantId)}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return NextResponse.json(null, { status: 404 })
|
||||
}
|
||||
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 fetch compliance scope:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy: POST /api/sdk/v1/compliance-scope → Backend POST /api/v1/compliance-scope
|
||||
* Persists the scope decision and answers.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const tenantId = body.tenant_id || 'default'
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/v1/compliance-scope?tenant_id=${encodeURIComponent(tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': tenantId,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
)
|
||||
|
||||
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 save compliance scope:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
41
admin-compliance/app/api/sdk/v1/import/[id]/route.ts
Normal file
41
admin-compliance/app/api/sdk/v1/import/[id]/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
/**
|
||||
* Proxy: DELETE /api/sdk/v1/import/:id → Backend DELETE /api/v1/import/:id
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/import/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: 'Backend error', details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Failed to delete import:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
45
admin-compliance/app/api/sdk/v1/modules/[moduleId]/route.ts
Normal file
45
admin-compliance/app/api/sdk/v1/modules/[moduleId]/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
/**
|
||||
* Proxy: GET /api/sdk/v1/modules/:moduleId → Backend GET /api/modules/:moduleId
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ moduleId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { moduleId } = await params
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/modules/${encodeURIComponent(moduleId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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 fetch module:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -54,3 +54,41 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy: POST /api/sdk/v1/modules → Backend POST /api/modules
|
||||
* Creates a new custom module.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/modules`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
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 create module:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryString = searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/v1/admin/blocked-content${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch blocked content:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryString = searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/v1/admin/compliance-report${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch compliance report:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/operations-matrix`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch operations matrix:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/operations/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to update operation:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/pii-rules/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to update PII rule:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/pii-rules/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to delete PII rule:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/pii-rules`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch PII rules:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/pii-rules`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to create PII rule:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryString = searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/v1/admin/policy-audit${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch policy audit:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/policy-stats`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch policy stats:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/sources/${encodeURIComponent(id)}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch source:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/sources/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to update source:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/sources/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to delete source:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const queryString = searchParams.toString()
|
||||
const url = `${BACKEND_URL}/api/v1/admin/sources${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sources:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/api/v1/admin/sources`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json(await response.json())
|
||||
} catch (error) {
|
||||
console.error('Failed to create source:', error)
|
||||
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,47 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy: PUT /api/sdk/v1/ucca/assessments/[id] → Go Backend PUT /sdk/v1/ucca/assessments/:id
|
||||
*/
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const response = await fetch(`${SDK_URL}/sdk/v1/ucca/assessments/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(request.headers.get('X-Tenant-ID') && {
|
||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return NextResponse.json(
|
||||
{ error: 'UCCA backend error', details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Failed to update UCCA assessment:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to UCCA backend' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy: DELETE /api/sdk/v1/ucca/assessments/[id] → Go Backend DELETE /sdk/v1/ucca/assessments/:id
|
||||
*/
|
||||
|
||||
@@ -5,9 +5,22 @@ import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_DESCRIPTIONS, DEPTH_LEVEL_COLORS, DOCUM
|
||||
|
||||
interface ScopeDecisionTabProps {
|
||||
decision: ScopeDecision | null
|
||||
answers?: unknown[]
|
||||
onBackToWizard?: () => void
|
||||
onGoToExport?: () => void
|
||||
canEvaluate?: boolean
|
||||
onEvaluate?: () => void
|
||||
isEvaluating?: boolean
|
||||
}
|
||||
|
||||
export function ScopeDecisionTab({ decision }: ScopeDecisionTabProps) {
|
||||
export function ScopeDecisionTab({
|
||||
decision,
|
||||
onBackToWizard,
|
||||
onGoToExport,
|
||||
canEvaluate,
|
||||
onEvaluate,
|
||||
isEvaluating,
|
||||
}: ScopeDecisionTabProps) {
|
||||
const [expandedTrigger, setExpandedTrigger] = useState<number | null>(null)
|
||||
const [showAuditTrail, setShowAuditTrail] = useState(false)
|
||||
|
||||
@@ -320,6 +333,35 @@ export function ScopeDecisionTab({ decision }: ScopeDecisionTabProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-3">
|
||||
{onBackToWizard && (
|
||||
<button
|
||||
onClick={onBackToWizard}
|
||||
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
Zurueck zum Wizard
|
||||
</button>
|
||||
)}
|
||||
{canEvaluate && onEvaluate && (
|
||||
<button
|
||||
onClick={onEvaluate}
|
||||
disabled={isEvaluating}
|
||||
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isEvaluating ? 'Bewertung laeuft...' : 'Neu bewerten'}
|
||||
</button>
|
||||
)}
|
||||
{onGoToExport && (
|
||||
<button
|
||||
onClick={onGoToExport}
|
||||
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Zum Export
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Audit Trail */}
|
||||
{decision.auditTrail && decision.auditTrail.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
|
||||
@@ -126,10 +126,63 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
})
|
||||
}, [generateMarkdownSummary])
|
||||
|
||||
/** Simple markdown-to-HTML converter for print view */
|
||||
const markdownToHtml = useCallback((md: string): string => {
|
||||
let html = md
|
||||
// Escape HTML entities
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
// Headers
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
// Bold
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
// Tables
|
||||
const lines = html.split('\n')
|
||||
let inTable = false
|
||||
const result: string[] = []
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||
if (line.includes('---')) continue // separator row
|
||||
const cells = line.split('|').filter(c => c.trim() !== '')
|
||||
if (!inTable) {
|
||||
result.push('<table><thead><tr>')
|
||||
cells.forEach(c => result.push(`<th>${c.trim()}</th>`))
|
||||
result.push('</tr></thead><tbody>')
|
||||
inTable = true
|
||||
} else {
|
||||
result.push('<tr>')
|
||||
cells.forEach(c => result.push(`<td>${c.trim()}</td>`))
|
||||
result.push('</tr>')
|
||||
}
|
||||
} else {
|
||||
if (inTable) {
|
||||
result.push('</tbody></table>')
|
||||
inTable = false
|
||||
}
|
||||
// List items
|
||||
if (line.trim().startsWith('- ')) {
|
||||
result.push(`<li>${line.trim().slice(2)}</li>`)
|
||||
} else if (/^\d+\.\s/.test(line.trim())) {
|
||||
result.push(`<li>${line.trim().replace(/^\d+\.\s/, '')}</li>`)
|
||||
} else if (line.trim() === '') {
|
||||
result.push('<br/>')
|
||||
} else {
|
||||
result.push(`<p>${line}</p>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inTable) result.push('</tbody></table>')
|
||||
return result.join('\n')
|
||||
}, [])
|
||||
|
||||
const handlePrintView = useCallback(() => {
|
||||
if (!decision) return
|
||||
|
||||
const markdown = generateMarkdownSummary()
|
||||
const renderedHtml = markdownToHtml(markdown)
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -152,6 +205,7 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
th { background-color: #f3f4f6; font-weight: 600; }
|
||||
ul { list-style-type: disc; padding-left: 20px; }
|
||||
li { margin: 8px 0; }
|
||||
p { margin: 4px 0; }
|
||||
@media print {
|
||||
body { margin: 20px; }
|
||||
h1, h2, h3 { page-break-after: avoid; }
|
||||
@@ -160,7 +214,7 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre style="white-space: pre-wrap; font-family: inherit;">${markdown}</pre>
|
||||
${renderedHtml}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
@@ -171,7 +225,7 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
printWindow.focus()
|
||||
setTimeout(() => printWindow.print(), 250)
|
||||
}
|
||||
}, [decision, generateMarkdownSummary])
|
||||
}, [decision, generateMarkdownSummary, markdownToHtml])
|
||||
|
||||
if (!decision) {
|
||||
return (
|
||||
|
||||
@@ -87,7 +87,7 @@ export function AuditTab({ apiBase }: AuditTabProps) {
|
||||
if (entityFilter) params.append('entity_type', entityFilter)
|
||||
params.append('limit', '100')
|
||||
|
||||
const res = await fetch(`${apiBase}/v1/admin/policy-audit?${params}`)
|
||||
const res = await fetch(`${apiBase}/policy-audit?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const data = await res.json()
|
||||
@@ -108,7 +108,7 @@ export function AuditTab({ apiBase }: AuditTabProps) {
|
||||
if (dateTo) params.append('to', dateTo)
|
||||
params.append('limit', '100')
|
||||
|
||||
const res = await fetch(`${apiBase}/v1/admin/blocked-content?${params}`)
|
||||
const res = await fetch(`${apiBase}/blocked-content?${params}`)
|
||||
if (!res.ok) {
|
||||
// Endpoint may not exist yet — show empty state
|
||||
setBlockedContent([])
|
||||
@@ -136,7 +136,7 @@ export function AuditTab({ apiBase }: AuditTabProps) {
|
||||
if (dateTo) params.append('to', dateTo)
|
||||
params.append('format', 'download')
|
||||
|
||||
const res = await fetch(`${apiBase}/v1/admin/compliance-report?${params}`)
|
||||
const res = await fetch(`${apiBase}/compliance-report?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Export')
|
||||
|
||||
const blob = await res.blob()
|
||||
|
||||
@@ -44,8 +44,8 @@ export function OperationsMatrixTab({ apiBase }: OperationsMatrixTabProps) {
|
||||
try {
|
||||
setLoading(true)
|
||||
const [sourcesRes, opsRes] = await Promise.all([
|
||||
fetch(`${apiBase}/v1/admin/sources`),
|
||||
fetch(`${apiBase}/v1/admin/operations-matrix`),
|
||||
fetch(`${apiBase}/sources`),
|
||||
fetch(`${apiBase}/operations-matrix`),
|
||||
])
|
||||
if (!sourcesRes.ok || !opsRes.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
@@ -95,7 +95,7 @@ export function OperationsMatrixTab({ apiBase }: OperationsMatrixTabProps) {
|
||||
setUpdating(updateId)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiBase}/v1/admin/operations/${permission.id}`, {
|
||||
const res = await fetch(`${apiBase}/operations/${permission.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ allowed: !permission.allowed }),
|
||||
|
||||
@@ -82,7 +82,7 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
|
||||
const fetchRules = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/pii-rules`)
|
||||
const res = await fetch(`${apiBase}/pii-rules`)
|
||||
if (!res.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const data = await res.json()
|
||||
@@ -97,7 +97,7 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
|
||||
const createRule = async () => {
|
||||
try {
|
||||
setSaving(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/pii-rules`, {
|
||||
const res = await fetch(`${apiBase}/pii-rules`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newRule),
|
||||
@@ -127,7 +127,7 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
|
||||
|
||||
try {
|
||||
setSaving(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/pii-rules/${editingRule.id}`, {
|
||||
const res = await fetch(`${apiBase}/pii-rules/${editingRule.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editingRule),
|
||||
@@ -149,7 +149,7 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
|
||||
if (!confirm('Regel wirklich loeschen? Diese Aktion wird im Audit-Log protokolliert.')) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiBase}/v1/admin/pii-rules/${id}`, {
|
||||
const res = await fetch(`${apiBase}/pii-rules/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
@@ -164,7 +164,7 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
|
||||
|
||||
const toggleRuleStatus = async (rule: PIIRule) => {
|
||||
try {
|
||||
const res = await fetch(`${apiBase}/v1/admin/pii-rules/${rule.id}`, {
|
||||
const res = await fetch(`${apiBase}/pii-rules/${rule.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ active: !rule.active }),
|
||||
|
||||
@@ -78,7 +78,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
if (licenseFilter) params.append('license', licenseFilter)
|
||||
if (statusFilter !== 'all') params.append('active_only', statusFilter === 'active' ? 'true' : 'false')
|
||||
|
||||
const res = await fetch(`${apiBase}/v1/admin/sources?${params}`)
|
||||
const res = await fetch(`${apiBase}/sources?${params}`)
|
||||
if (!res.ok) throw new Error('Fehler beim Laden')
|
||||
|
||||
const data = await res.json()
|
||||
@@ -93,7 +93,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
const createSource = async () => {
|
||||
try {
|
||||
setSaving(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/sources`, {
|
||||
const res = await fetch(`${apiBase}/sources`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newSource),
|
||||
@@ -124,7 +124,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
|
||||
try {
|
||||
setSaving(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/sources/${editingSource.id}`, {
|
||||
const res = await fetch(`${apiBase}/sources/${editingSource.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editingSource),
|
||||
@@ -146,7 +146,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
if (!confirm('Quelle wirklich loeschen? Diese Aktion wird im Audit-Log protokolliert.')) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiBase}/v1/admin/sources/${id}`, {
|
||||
const res = await fetch(`${apiBase}/sources/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
@@ -161,7 +161,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
|
||||
|
||||
const toggleSourceStatus = async (source: AllowedSource) => {
|
||||
try {
|
||||
const res = await fetch(`${apiBase}/v1/admin/sources/${source.id}`, {
|
||||
const res = await fetch(`${apiBase}/sources/${source.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ active: !source.active }),
|
||||
|
||||
147
admin-compliance/package-lock.json
generated
147
admin-compliance/package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^15.1.0",
|
||||
"pg": "^8.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"reactflow": "^11.11.4",
|
||||
@@ -4624,6 +4625,95 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.19.0.tgz",
|
||||
"integrity": "sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.11.0",
|
||||
"pg-pool": "^3.12.0",
|
||||
"pg-protocol": "^1.12.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
||||
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
|
||||
"integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.12.0.tgz",
|
||||
"integrity": "sha512-eIJ0DES8BLaziFHW7VgJEBPi5hg3Nyng5iKpYtj3wbcAUV9A1wLgWiY7ajf/f/oO1wfxt83phXPY8Emztg7ITg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.12.0.tgz",
|
||||
"integrity": "sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -4872,6 +4962,45 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.28.4",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz",
|
||||
@@ -5389,6 +5518,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
@@ -6145,6 +6283,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -408,6 +408,8 @@ async def list_blocked_content(
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
domain: Optional[str] = None,
|
||||
date_from: Optional[str] = Query(None, alias="from"),
|
||||
date_to: Optional[str] = Query(None, alias="to"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List blocked content entries."""
|
||||
@@ -416,6 +418,20 @@ async def list_blocked_content(
|
||||
if domain:
|
||||
query = query.filter(BlockedContentDB.domain == domain)
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
from_dt = datetime.fromisoformat(date_from)
|
||||
query = query.filter(BlockedContentDB.created_at >= from_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
to_dt = datetime.fromisoformat(date_to)
|
||||
query = query.filter(BlockedContentDB.created_at <= to_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
entries = query.order_by(BlockedContentDB.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
@@ -445,6 +461,8 @@ async def get_policy_audit(
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
entity_type: Optional[str] = None,
|
||||
date_from: Optional[str] = Query(None, alias="from"),
|
||||
date_to: Optional[str] = Query(None, alias="to"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get the audit trail for source policy changes."""
|
||||
@@ -452,6 +470,20 @@ async def get_policy_audit(
|
||||
if entity_type:
|
||||
query = query.filter(SourcePolicyAuditDB.entity_type == entity_type)
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
from_dt = datetime.fromisoformat(date_from)
|
||||
query = query.filter(SourcePolicyAuditDB.created_at >= from_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
to_dt = datetime.fromisoformat(date_to)
|
||||
query = query.filter(SourcePolicyAuditDB.created_at <= to_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
entries = query.order_by(SourcePolicyAuditDB.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
@@ -486,16 +518,13 @@ async def get_policy_stats(db: Session = Depends(get_db)):
|
||||
active_sources = db.query(AllowedSourceDB).filter(AllowedSourceDB.active == True).count()
|
||||
pii_rules = db.query(PIIRuleDB).filter(PIIRuleDB.active == True).count()
|
||||
|
||||
# Count audit entries from today
|
||||
# Count blocked content entries from today
|
||||
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
blocked_today = db.query(SourcePolicyAuditDB).filter(
|
||||
SourcePolicyAuditDB.action == "delete",
|
||||
SourcePolicyAuditDB.created_at >= today_start,
|
||||
blocked_today = db.query(BlockedContentDB).filter(
|
||||
BlockedContentDB.created_at >= today_start,
|
||||
).count()
|
||||
|
||||
blocked_total = db.query(SourcePolicyAuditDB).filter(
|
||||
SourcePolicyAuditDB.action == "delete",
|
||||
).count()
|
||||
blocked_total = db.query(BlockedContentDB).count()
|
||||
|
||||
return {
|
||||
"active_policies": active_sources,
|
||||
|
||||
Reference in New Issue
Block a user