All 5 files reduced below 500 LOC (hard cap) by extracting sub-components: - training/page.tsx: 780→278 LOC — imports existing _components/, adds BlocksSection - control-provenance/page.tsx: 739→145 LOC — extracts provenance-data.ts, ProvenanceHelpers, LicenseMatrix, SourceRegistry - iace/[projectId]/verification/page.tsx: 673→164 LOC — extracts VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable - training/learner/page.tsx: 560→216 LOC — extracts AssignmentsList, ContentView, QuizView, CertificatesView - ControlDetail.tsx: 878→311 LOC — adds ControlSourceCitation, ControlTraceability, ControlRegulatorySection, ControlSimilarControls, ControlReviewActions siblings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { FileText, Shield } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { PROVENANCE_SECTIONS } from './_components/provenance-data'
|
|
import { MarkdownRenderer } from './_components/ProvenanceHelpers'
|
|
import { LicenseMatrix } from './_components/LicenseMatrix'
|
|
import { SourceRegistry } from './_components/SourceRegistry'
|
|
|
|
interface LicenseInfo {
|
|
license_id: string
|
|
name: string
|
|
terms_url: string | null
|
|
commercial_use: string
|
|
ai_training_restriction: string | null
|
|
tdm_allowed_under_44b: string | null
|
|
deletion_required: boolean
|
|
notes: string | null
|
|
}
|
|
|
|
interface SourceInfo {
|
|
source_id: string
|
|
title: string
|
|
publisher: string
|
|
url: string | null
|
|
version_label: string | null
|
|
language: string
|
|
license_id: string
|
|
license_name: string
|
|
commercial_use: string
|
|
allowed_analysis: boolean
|
|
allowed_store_excerpt: boolean
|
|
allowed_ship_embeddings: boolean
|
|
allowed_ship_in_product: boolean
|
|
vault_retention_days: number
|
|
vault_access_tier: string
|
|
}
|
|
|
|
export default function ControlProvenancePage() {
|
|
const [licenses, setLicenses] = useState<LicenseInfo[]>([])
|
|
const [sources, setSources] = useState<SourceInfo[]>([])
|
|
const [activeSection, setActiveSection] = useState('methodology')
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
async function load() {
|
|
try {
|
|
const [licRes, srcRes] = await Promise.all([
|
|
fetch('/api/sdk/v1/canonical?endpoint=licenses'),
|
|
fetch('/api/sdk/v1/canonical?endpoint=sources'),
|
|
])
|
|
if (licRes.ok) setLicenses(await licRes.json())
|
|
if (srcRes.ok) setSources(await srcRes.json())
|
|
} catch {
|
|
// silently continue — static content still shown
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [])
|
|
|
|
const currentSection = PROVENANCE_SECTIONS.find(s => s.id === activeSection)
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* Header */}
|
|
<div className="border-b border-gray-200 bg-white px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="w-6 h-6 text-green-600" />
|
|
<div>
|
|
<h1 className="text-lg font-semibold text-gray-900">Control Provenance Wiki</h1>
|
|
<p className="text-xs text-gray-500">
|
|
Dokumentation der unabhaengigen Herkunft aller Security Controls — rechtssicherer Nachweis
|
|
</p>
|
|
</div>
|
|
<Link href="/sdk/control-library" className="ml-auto flex items-center gap-1 text-sm text-purple-600 hover:text-purple-800">
|
|
<Shield className="w-4 h-4" />
|
|
Zur Control Library
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Left: Navigation */}
|
|
<div className="w-72 border-r border-gray-200 bg-gray-50 overflow-y-auto flex-shrink-0">
|
|
<div className="p-3 space-y-1">
|
|
<p className="text-xs font-semibold text-gray-400 uppercase px-3 mb-2">Dokumentation</p>
|
|
{PROVENANCE_SECTIONS.map(section => (
|
|
<button
|
|
key={section.id}
|
|
onClick={() => setActiveSection(section.id)}
|
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
|
|
activeSection === section.id
|
|
? 'bg-green-100 text-green-900 font-medium'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{section.title}
|
|
</button>
|
|
))}
|
|
|
|
<div className="border-t border-gray-200 mt-3 pt-3">
|
|
<p className="text-xs font-semibold text-gray-400 uppercase px-3 mb-2">Live-Daten</p>
|
|
{['license-matrix', 'source-registry'].map(id => (
|
|
<button
|
|
key={id}
|
|
onClick={() => setActiveSection(id)}
|
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
|
|
activeSection === id
|
|
? 'bg-green-100 text-green-900 font-medium'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{id === 'license-matrix' ? 'Lizenz-Matrix' : 'Quellenregister'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Content */}
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="max-w-3xl mx-auto">
|
|
{currentSection && (
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">{currentSection.title}</h2>
|
|
<div className="prose prose-sm max-w-none">
|
|
<MarkdownRenderer content={currentSection.content} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
{activeSection === 'license-matrix' && (
|
|
<LicenseMatrix licenses={licenses} loading={loading} />
|
|
)}
|
|
{activeSection === 'source-registry' && (
|
|
<SourceRegistry sources={sources} loading={loading} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|