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:
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user