feat: 6 Dokumentations-Module auf 100% — VVT Backend, Filter, PDF-Export

Phase 1 — VVT Backend (localStorage → API):
- migrations/006_vvt.sql: Neue Tabellen (vvt_organization, vvt_activities, vvt_audit_log)
- compliance/db/vvt_models.py: SQLAlchemy-Models für alle VVT-Tabellen
- compliance/api/vvt_routes.py: Vollständiger CRUD-Router (10 Endpoints)
- compliance/api/__init__.py: VVT-Router registriert
- compliance/api/schemas.py: VVT Pydantic-Schemas ergänzt
- app/(sdk)/sdk/vvt/page.tsx: API-Client + camelCase↔snake_case Mapping,
  localStorage durch persistente DB-Calls ersetzt (POST/PUT/DELETE/GET)
- tests/test_vvt_routes.py: 18 Tests (alle grün)

Phase 3 — Document Generator PDF-Export:
- document-generator/page.tsx: "Als PDF exportieren"-Button funktioniert jetzt
  via window.print() + Print-Window mit korrektem HTML
- Fallback-Banner wenn Template-Service (breakpilot-core) nicht erreichbar

Phase 4 — Source Policy erweiterte Filter:
- SourcesTab.tsx: source_type-Filter (Rechtlich / Leitlinien / Vorlagen / etc.)
- PIIRulesTab.tsx: category-Filter (E-Mail / Telefon / IBAN / etc.)
- source_policy_router.py: Backend-Endpoints unterstützen jetzt source_type
  und category als Query-Parameter
- requirements.txt: reportlab==4.2.5 ergänzt (fehlende Audit-PDF-Dependency)

Phase 2 — Training (Migration-Skripte):
- scripts/apply_training_migrations.sh: SSH-Skript für Mac Mini
- scripts/apply_vvt_migration.sh: Vollständiges Deploy-Skript für VVT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-02 17:14:58 +01:00
parent 7cc420bd9e
commit 34fc8dc654
14 changed files with 1332 additions and 65 deletions

View File

@@ -56,6 +56,9 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Category filter
const [categoryFilter, setCategoryFilter] = useState('')
// Test panel
const [testText, setTestText] = useState('')
const [testResult, setTestResult] = useState<PIITestResult | null>(null)
@@ -77,12 +80,14 @@ export function PIIRulesTab({ apiBase, onUpdate }: PIIRulesTabProps) {
useEffect(() => {
fetchRules()
}, [])
}, [categoryFilter])
const fetchRules = async () => {
try {
setLoading(true)
const res = await fetch(`${apiBase}/pii-rules`)
const params = new URLSearchParams()
if (categoryFilter) params.append('category', categoryFilter)
const res = await fetch(`${apiBase}/pii-rules?${params}`)
if (!res.ok) throw new Error('Fehler beim Laden')
const data = await res.json()
@@ -321,17 +326,29 @@ Meine IBAN ist DE89 3704 0044 0532 0130 00."
</div>
{/* Rules List Header */}
<div className="flex justify-between items-center mb-4">
<div className="flex flex-wrap justify-between items-center gap-3 mb-4">
<h3 className="font-semibold text-slate-900">PII-Erkennungsregeln</h3>
<button
onClick={() => setIsNewRule(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neue Regel
</button>
<div className="flex gap-3 items-center">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Alle Kategorien</option>
{CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>{c.label}</option>
))}
</select>
<button
onClick={() => setIsNewRule(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neue Regel
</button>
</div>
</div>
{/* Rules Table */}

View File

@@ -51,6 +51,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
const [searchTerm, setSearchTerm] = useState('')
const [licenseFilter, setLicenseFilter] = useState('')
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all')
const [sourceTypeFilter, setSourceTypeFilter] = useState('')
// Edit modal
const [editingSource, setEditingSource] = useState<AllowedSource | null>(null)
@@ -69,7 +70,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
useEffect(() => {
fetchSources()
}, [licenseFilter, statusFilter])
}, [licenseFilter, statusFilter, sourceTypeFilter])
const fetchSources = async () => {
try {
@@ -77,6 +78,7 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
const params = new URLSearchParams()
if (licenseFilter) params.append('license', licenseFilter)
if (statusFilter !== 'all') params.append('active_only', statusFilter === 'active' ? 'true' : 'false')
if (sourceTypeFilter) params.append('source_type', sourceTypeFilter)
const res = await fetch(`${apiBase}/sources?${params}`)
if (!res.ok) throw new Error('Fehler beim Laden')
@@ -230,6 +232,18 @@ export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) {
<option value="active">Aktiv</option>
<option value="inactive">Inaktiv</option>
</select>
<select
value={sourceTypeFilter}
onChange={(e) => setSourceTypeFilter(e.target.value)}
className="px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="">Alle Typen</option>
<option value="legal">Rechtlich</option>
<option value="guidance">Leitlinien</option>
<option value="template">Vorlagen</option>
<option value="technical">Technisch</option>
<option value="other">Sonstige</option>
</select>
<button
onClick={() => setIsNewSource(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"