Each page.tsx exceeded the 500-LOC hard cap. Extracted components and hooks into colocated _components/ and _hooks/ directories; page.tsx is now a thin orchestrator. - controls/page.tsx: 944 → 180 LOC; extracted ControlCard, AddControlForm, LoadingSkeleton, TransitionErrorBanner, StatsCards, FilterBar, RAGPanel into _components/ and useControlsData, useRAGSuggestions into _hooks/; types into _types.ts - training/page.tsx: 780 → 288 LOC; extracted ContentTab (inline content generator tab) into _components/ContentTab.tsx - control-provenance/page.tsx: 739 → 122 LOC; extracted MarkdownRenderer, UsageBadge, PermBadge, LicenseMatrix, SourceRegistry into _components/; PROVENANCE_SECTIONS static data into _data/provenance-sections.ts - iace/[projectId]/verification/page.tsx: 673 → 196 LOC; extracted StatusBadge, VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable into _components/ Zero behavior changes; logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
197 lines
9.4 KiB
TypeScript
197 lines
9.4 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useParams } from 'next/navigation'
|
|
import { VerificationForm } from './_components/VerificationForm'
|
|
import { CompleteModal } from './_components/CompleteModal'
|
|
import { SuggestEvidenceModal } from './_components/SuggestEvidenceModal'
|
|
import { VerificationTable } from './_components/VerificationTable'
|
|
import type { VerificationFormData } from './_components/VerificationForm'
|
|
|
|
interface VerificationItem {
|
|
id: string
|
|
title: string
|
|
description: string
|
|
method: string
|
|
status: 'pending' | 'in_progress' | 'completed' | 'failed'
|
|
result: string | null
|
|
linked_hazard_id: string | null
|
|
linked_hazard_name: string | null
|
|
linked_mitigation_id: string | null
|
|
linked_mitigation_name: string | null
|
|
completed_at: string | null
|
|
completed_by: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export default function VerificationPage() {
|
|
const params = useParams()
|
|
const projectId = params.projectId as string
|
|
const [items, setItems] = useState<VerificationItem[]>([])
|
|
const [hazards, setHazards] = useState<{ id: string; name: string }[]>([])
|
|
const [mitigations, setMitigations] = useState<{ id: string; title: string }[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [showForm, setShowForm] = useState(false)
|
|
const [completingItem, setCompletingItem] = useState<VerificationItem | null>(null)
|
|
const [showSuggest, setShowSuggest] = useState(false)
|
|
|
|
useEffect(() => { fetchData() }, [projectId])
|
|
|
|
async function fetchData() {
|
|
try {
|
|
const [verRes, hazRes, mitRes] = await Promise.all([
|
|
fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`),
|
|
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards`),
|
|
fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`),
|
|
])
|
|
if (verRes.ok) { const json = await verRes.json(); setItems(json.verifications || json || []) }
|
|
if (hazRes.ok) { const json = await hazRes.json(); setHazards((json.hazards || json || []).map((h: { id: string; name: string }) => ({ id: h.id, name: h.name }))) }
|
|
if (mitRes.ok) { const json = await mitRes.json(); setMitigations((json.mitigations || json || []).map((m: { id: string; title: string }) => ({ id: m.id, title: m.title }))) }
|
|
} catch (err) { console.error('Failed to fetch data:', err) }
|
|
finally { setLoading(false) }
|
|
}
|
|
|
|
async function handleSubmit(data: VerificationFormData) {
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data),
|
|
})
|
|
if (res.ok) { setShowForm(false); await fetchData() }
|
|
} catch (err) { console.error('Failed to add verification:', err) }
|
|
}
|
|
|
|
async function handleAddSuggestedEvidence(title: string, description: string, method: string, mitigationId: string) {
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title, description, method, linked_mitigation_id: mitigationId }),
|
|
})
|
|
if (res.ok) await fetchData()
|
|
} catch (err) { console.error('Failed to add suggested evidence:', err) }
|
|
}
|
|
|
|
async function handleComplete(id: string, result: string, passed: boolean) {
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications/${id}/complete`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ result, passed }),
|
|
})
|
|
if (res.ok) { setCompletingItem(null); await fetchData() }
|
|
} catch (err) { console.error('Failed to complete verification:', err) }
|
|
}
|
|
|
|
async function handleDelete(id: string) {
|
|
if (!confirm('Verifikation wirklich loeschen?')) return
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications/${id}`, { method: 'DELETE' })
|
|
if (res.ok) await fetchData()
|
|
} catch (err) { console.error('Failed to delete verification:', err) }
|
|
}
|
|
|
|
const completed = items.filter((i) => i.status === 'completed').length
|
|
const failed = items.filter((i) => i.status === 'failed').length
|
|
const pending = items.filter((i) => i.status === 'pending' || i.status === 'in_progress').length
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Verifikationsplan</h1>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Nachweisfuehrung fuer alle Schutzmassnahmen und Sicherheitsanforderungen.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{mitigations.length > 0 && (
|
|
<button onClick={() => setShowSuggest(true)}
|
|
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors text-sm"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
</svg>
|
|
Nachweise vorschlagen
|
|
</button>
|
|
)}
|
|
<button onClick={() => setShowForm(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>
|
|
Verifikation hinzufuegen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{items.length > 0 && (
|
|
<div className="grid grid-cols-4 gap-3">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 text-center">
|
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">{items.length}</div>
|
|
<div className="text-xs text-gray-500">Gesamt</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-green-200 p-4 text-center">
|
|
<div className="text-2xl font-bold text-green-600">{completed}</div>
|
|
<div className="text-xs text-green-600">Abgeschlossen</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-red-200 p-4 text-center">
|
|
<div className="text-2xl font-bold text-red-600">{failed}</div>
|
|
<div className="text-xs text-red-600">Fehlgeschlagen</div>
|
|
</div>
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-yellow-200 p-4 text-center">
|
|
<div className="text-2xl font-bold text-yellow-600">{pending}</div>
|
|
<div className="text-xs text-yellow-600">Ausstehend</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showForm && (
|
|
<VerificationForm onSubmit={handleSubmit} onCancel={() => setShowForm(false)} hazards={hazards} mitigations={mitigations} />
|
|
)}
|
|
|
|
{completingItem && (
|
|
<CompleteModal item={completingItem} onSubmit={handleComplete} onClose={() => setCompletingItem(null)} />
|
|
)}
|
|
|
|
{showSuggest && (
|
|
<SuggestEvidenceModal mitigations={mitigations} projectId={projectId} onAddEvidence={handleAddSuggestedEvidence} onClose={() => setShowSuggest(false)} />
|
|
)}
|
|
|
|
{items.length > 0 ? (
|
|
<VerificationTable items={items} onComplete={setCompletingItem} onDelete={handleDelete} />
|
|
) : (
|
|
!showForm && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-12 text-center">
|
|
<div className="w-16 h-16 mx-auto bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mb-4">
|
|
<svg className="w-8 h-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<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>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Kein Verifikationsplan vorhanden</h3>
|
|
<p className="mt-2 text-gray-500 max-w-md mx-auto">
|
|
Definieren Sie Verifikationsschritte fuer Ihre Schutzmassnahmen.
|
|
Jede Massnahme sollte durch mindestens eine Verifikation abgedeckt sein.
|
|
</p>
|
|
<div className="mt-6 flex items-center justify-center gap-3">
|
|
{mitigations.length > 0 && (
|
|
<button onClick={() => setShowSuggest(true)} className="px-6 py-3 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors">
|
|
Nachweise vorschlagen
|
|
</button>
|
|
)}
|
|
<button onClick={() => setShowForm(true)} className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
|
Erste Verifikation anlegen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
)
|
|
}
|