feat: Vorbereitung-Module auf 100% — Compliance-Scope Backend, DELETE-Endpoints, Proxy-Fixes, blocked-content Tab
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 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s

Paket A — Kritische Blocker:
- compliance_scope_routes.py: GET + POST UPSERT für sdk_states JSONB-Feld
- compliance/api/__init__.py: compliance_scope_router registriert
- import/route.ts: POST-Proxy für multipart/form-data Upload
- screening/route.ts: POST-Proxy für Dependency-File Upload

Paket B — Backend + UI:
- company_profile_routes.py: DELETE-Endpoint (DSGVO Art. 17)
- company-profile/route.ts: DELETE-Proxy
- company-profile/page.tsx: Profil-löschen-Button mit Bestätigungs-Dialog
- source-policy/pii-rules/[id]/route.ts: GET ergänzt
- source-policy/operations/[id]/route.ts: GET + DELETE ergänzt

Paket C — Tests + UI:
- test_compliance_scope_routes.py: 27 Tests (neu)
- test_import_routes.py: +36 Tests → 60 gesamt
- test_screening_routes.py: +28 Tests → 80+ gesamt
- source-policy/page.tsx: "Blockierte Inhalte" Tab mit Tabelle + Remove

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-04 17:43:29 +01:00
parent 832c177688
commit dc0d38ea40
13 changed files with 1436 additions and 11 deletions

View File

@@ -26,7 +26,16 @@ interface PolicyStats {
blocked_total: number
}
type TabId = 'dashboard' | 'sources' | 'operations' | 'pii' | 'audit'
type TabId = 'dashboard' | 'sources' | 'operations' | 'pii' | 'audit' | 'blocked'
interface BlockedContent {
id: string
content_type: string
pattern: string
reason: string
blocked_at: string
source?: string
}
export default function SourcePolicyPage() {
const { state } = useSDK()
@@ -34,11 +43,43 @@ export default function SourcePolicyPage() {
const [stats, setStats] = useState<PolicyStats | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [blockedContent, setBlockedContent] = useState<BlockedContent[]>([])
const [blockedLoading, setBlockedLoading] = useState(false)
useEffect(() => {
fetchStats()
}, [])
useEffect(() => {
if (activeTab === 'blocked') {
fetchBlockedContent()
}
}, [activeTab])
const fetchBlockedContent = async () => {
setBlockedLoading(true)
try {
const res = await fetch(`${API_BASE}/blocked-content`)
if (res.ok) {
const data = await res.json()
setBlockedContent(Array.isArray(data) ? data : (data.items || []))
}
} catch {
// silently ignore — empty state shown
} finally {
setBlockedLoading(false)
}
}
const handleRemoveBlocked = async (id: string) => {
try {
await fetch(`${API_BASE}/blocked-content/${id}`, { method: 'DELETE' })
setBlockedContent(prev => prev.filter(item => item.id !== id))
} catch {
// ignore
}
}
const fetchStats = async () => {
try {
setLoading(true)
@@ -110,6 +151,15 @@ export default function SourcePolicyPage() {
</svg>
),
},
{
id: 'blocked',
name: 'Blockierte Inhalte',
icon: (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
),
},
]
return (
@@ -242,6 +292,72 @@ export default function SourcePolicyPage() {
{activeTab === 'operations' && <OperationsMatrixTab apiBase={API_BASE} />}
{activeTab === 'pii' && <PIIRulesTab apiBase={API_BASE} onUpdate={fetchStats} />}
{activeTab === 'audit' && <AuditTab apiBase={API_BASE} />}
{activeTab === 'blocked' && (
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Blockierte Inhalte</h3>
<button
onClick={fetchBlockedContent}
className="text-sm text-purple-600 hover:text-purple-700"
>
Aktualisieren
</button>
</div>
{blockedLoading ? (
<div className="p-8 text-center text-gray-500">Lade blockierte Inhalte...</div>
) : blockedContent.length === 0 ? (
<div className="p-8 text-center">
<div className="w-12 h-12 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="text-gray-500 text-sm">Keine blockierten Inhalte vorhanden.</p>
</div>
) : (
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="text-left px-4 py-3 text-gray-600 font-medium">Typ</th>
<th className="text-left px-4 py-3 text-gray-600 font-medium">Muster / Pattern</th>
<th className="text-left px-4 py-3 text-gray-600 font-medium">Grund</th>
<th className="text-left px-4 py-3 text-gray-600 font-medium">Blockiert am</th>
<th className="text-left px-4 py-3 text-gray-600 font-medium">Quelle</th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{blockedContent.map(item => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<span className="px-2 py-0.5 bg-red-100 text-red-700 rounded text-xs font-medium">
{item.content_type}
</span>
</td>
<td className="px-4 py-3 font-mono text-xs text-gray-700 max-w-xs truncate">
{item.pattern}
</td>
<td className="px-4 py-3 text-gray-600">{item.reason}</td>
<td className="px-4 py-3 text-gray-500">
{new Date(item.blocked_at).toLocaleDateString('de-DE')}
</td>
<td className="px-4 py-3 text-gray-500">{item.source || '—'}</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => handleRemoveBlocked(item.id)}
className="text-red-500 hover:text-red-700 text-xs px-2 py-1 rounded hover:bg-red-50"
>
Entfernen
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</>
</div>
)