fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
753
admin-v2/lib/sdk/export.ts
Normal file
753
admin-v2/lib/sdk/export.ts
Normal file
@@ -0,0 +1,753 @@
|
||||
/**
|
||||
* SDK Export Utilities
|
||||
* Handles PDF and ZIP export of SDK state and documents
|
||||
*/
|
||||
|
||||
import jsPDF from 'jspdf'
|
||||
import JSZip from 'jszip'
|
||||
import { SDKState, SDK_STEPS, getStepById } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ExportOptions {
|
||||
includeEvidence?: boolean
|
||||
includeDocuments?: boolean
|
||||
includeRawData?: boolean
|
||||
language?: 'de' | 'en'
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ExportOptions = {
|
||||
includeEvidence: true,
|
||||
includeDocuments: true,
|
||||
includeRawData: true,
|
||||
language: 'de',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LABELS (German)
|
||||
// =============================================================================
|
||||
|
||||
const LABELS_DE = {
|
||||
title: 'AI Compliance SDK - Export',
|
||||
subtitle: 'Compliance-Dokumentation',
|
||||
generatedAt: 'Generiert am',
|
||||
page: 'Seite',
|
||||
summary: 'Zusammenfassung',
|
||||
progress: 'Fortschritt',
|
||||
phase1: 'Phase 1: Automatisches Compliance Assessment',
|
||||
phase2: 'Phase 2: Dokumentengenerierung',
|
||||
useCases: 'Use Cases',
|
||||
risks: 'Risiken',
|
||||
controls: 'Controls',
|
||||
requirements: 'Anforderungen',
|
||||
modules: 'Compliance-Module',
|
||||
evidence: 'Nachweise',
|
||||
checkpoints: 'Checkpoints',
|
||||
noData: 'Keine Daten vorhanden',
|
||||
status: 'Status',
|
||||
completed: 'Abgeschlossen',
|
||||
pending: 'Ausstehend',
|
||||
inProgress: 'In Bearbeitung',
|
||||
severity: 'Schweregrad',
|
||||
mitigation: 'Mitigation',
|
||||
description: 'Beschreibung',
|
||||
category: 'Kategorie',
|
||||
implementation: 'Implementierung',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF EXPORT
|
||||
// =============================================================================
|
||||
|
||||
function formatDate(date: Date | string | undefined): string {
|
||||
if (!date) return '-'
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
return d.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void {
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
|
||||
// Header line
|
||||
doc.setDrawColor(147, 51, 234) // Purple
|
||||
doc.setLineWidth(0.5)
|
||||
doc.line(20, 15, pageWidth - 20, 15)
|
||||
|
||||
// Title
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(100)
|
||||
doc.text(title, 20, 12)
|
||||
|
||||
// Page number
|
||||
doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12)
|
||||
}
|
||||
|
||||
function addFooter(doc: jsPDF, state: SDKState): void {
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
const pageHeight = doc.internal.pageSize.getHeight()
|
||||
|
||||
// Footer line
|
||||
doc.setDrawColor(200)
|
||||
doc.setLineWidth(0.3)
|
||||
doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15)
|
||||
|
||||
// Footer text
|
||||
doc.setFontSize(8)
|
||||
doc.setTextColor(150)
|
||||
doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10)
|
||||
}
|
||||
|
||||
function addSectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||
doc.setFontSize(14)
|
||||
doc.setTextColor(147, 51, 234) // Purple
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(title, 20, y)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
return y + 10
|
||||
}
|
||||
|
||||
function addSubsectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(60)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(title, 25, y)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
return y + 7
|
||||
}
|
||||
|
||||
function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number {
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(60)
|
||||
const lines = doc.splitTextToSize(text, maxWidth)
|
||||
doc.text(lines, x, y)
|
||||
return y + lines.length * 5
|
||||
}
|
||||
|
||||
function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number {
|
||||
const pageHeight = doc.internal.pageSize.getHeight()
|
||||
if (y + requiredSpace > pageHeight - 25) {
|
||||
doc.addPage()
|
||||
return 30
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const doc = new jsPDF()
|
||||
|
||||
let y = 30
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
|
||||
// ==========================================================================
|
||||
// Title Page
|
||||
// ==========================================================================
|
||||
|
||||
// Logo/Title area
|
||||
doc.setFillColor(147, 51, 234)
|
||||
doc.rect(0, 0, pageWidth, 60, 'F')
|
||||
|
||||
doc.setFontSize(24)
|
||||
doc.setTextColor(255)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(LABELS_DE.title, 20, 35)
|
||||
|
||||
doc.setFontSize(14)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(LABELS_DE.subtitle, 20, 48)
|
||||
|
||||
// Reset for content
|
||||
y = 80
|
||||
|
||||
// Summary box
|
||||
doc.setDrawColor(200)
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD')
|
||||
|
||||
y += 15
|
||||
doc.setFontSize(12)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y)
|
||||
|
||||
y += 10
|
||||
doc.text(`Tenant ID: ${state.tenantId}`, 30, y)
|
||||
|
||||
y += 10
|
||||
doc.text(`Version: ${state.version}`, 30, y)
|
||||
|
||||
y += 10
|
||||
const completedSteps = state.completedSteps.length
|
||||
const totalSteps = SDK_STEPS.length
|
||||
doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y)
|
||||
|
||||
y += 30
|
||||
|
||||
// Table of Contents
|
||||
y = addSectionTitle(doc, 'Inhaltsverzeichnis', y)
|
||||
|
||||
const tocItems = [
|
||||
{ title: 'Zusammenfassung', page: 2 },
|
||||
{ title: 'Phase 1: Compliance Assessment', page: 3 },
|
||||
{ title: 'Phase 2: Dokumentengenerierung', page: 4 },
|
||||
{ title: 'Risiken & Controls', page: 5 },
|
||||
{ title: 'Checkpoints', page: 6 },
|
||||
]
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(80)
|
||||
tocItems.forEach((item, idx) => {
|
||||
doc.text(`${idx + 1}. ${item.title}`, 25, y)
|
||||
doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' })
|
||||
y += 7
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// Summary Page
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
|
||||
y = addSectionTitle(doc, LABELS_DE.summary, y)
|
||||
|
||||
// Progress overview
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F')
|
||||
|
||||
y += 15
|
||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
||||
const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
y += 8
|
||||
doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
|
||||
y += 25
|
||||
|
||||
// Key metrics
|
||||
y = addSubsectionTitle(doc, 'Kennzahlen', y)
|
||||
|
||||
const metrics = [
|
||||
{ label: 'Use Cases', value: state.useCases.length },
|
||||
{ label: 'Risiken identifiziert', value: state.risks.length },
|
||||
{ label: 'Controls definiert', value: state.controls.length },
|
||||
{ label: 'Anforderungen', value: state.requirements.length },
|
||||
{ label: 'Nachweise', value: state.evidence.length },
|
||||
]
|
||||
|
||||
metrics.forEach(metric => {
|
||||
doc.text(`${metric.label}: ${metric.value}`, 30, y)
|
||||
y += 7
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// Use Cases
|
||||
// ==========================================================================
|
||||
|
||||
y += 10
|
||||
y = checkPageBreak(doc, y)
|
||||
y = addSectionTitle(doc, LABELS_DE.useCases, y)
|
||||
|
||||
if (state.useCases.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.useCases.forEach((uc, idx) => {
|
||||
y = checkPageBreak(doc, y, 50)
|
||||
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F')
|
||||
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte`
|
||||
doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13)
|
||||
|
||||
if (uc.description) {
|
||||
y = addText(doc, uc.description, 25, y + 21, 160)
|
||||
}
|
||||
|
||||
y += 40
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Risks
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.risks, y)
|
||||
|
||||
if (state.risks.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
// Sort by severity
|
||||
const sortedRisks = [...state.risks].sort((a, b) => {
|
||||
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||
return (order[a.severity] || 4) - (order[b.severity] || 4)
|
||||
})
|
||||
|
||||
sortedRisks.forEach((risk, idx) => {
|
||||
y = checkPageBreak(doc, y, 45)
|
||||
|
||||
// Severity color
|
||||
const severityColors: Record<string, [number, number, number]> = {
|
||||
CRITICAL: [220, 38, 38],
|
||||
HIGH: [234, 88, 12],
|
||||
MEDIUM: [234, 179, 8],
|
||||
LOW: [34, 197, 94],
|
||||
}
|
||||
const color = severityColors[risk.severity] || [100, 100, 100]
|
||||
|
||||
doc.setFillColor(color[0], color[1], color[2])
|
||||
doc.rect(20, y - 3, 3, 30, 'F')
|
||||
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13)
|
||||
|
||||
if (risk.description) {
|
||||
y = addText(doc, risk.description, 28, y + 21, 155)
|
||||
}
|
||||
|
||||
if (risk.mitigation && risk.mitigation.length > 0) {
|
||||
y += 5
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(34, 197, 94)
|
||||
doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y)
|
||||
}
|
||||
|
||||
y += 15
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Controls
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.controls, y)
|
||||
|
||||
if (state.controls.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.controls.forEach((ctrl, idx) => {
|
||||
y = checkPageBreak(doc, y, 35)
|
||||
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F')
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13)
|
||||
|
||||
if (ctrl.description) {
|
||||
y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160)
|
||||
}
|
||||
|
||||
y += 35
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Checkpoints
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.checkpoints, y)
|
||||
|
||||
const checkpointIds = Object.keys(state.checkpoints)
|
||||
|
||||
if (checkpointIds.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
checkpointIds.forEach((cpId) => {
|
||||
const cp = state.checkpoints[cpId]
|
||||
y = checkPageBreak(doc, y, 25)
|
||||
|
||||
const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38]
|
||||
doc.setFillColor(statusColor[0], statusColor[1], statusColor[2])
|
||||
doc.circle(25, y + 2, 3, 'F')
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.text(cpId, 35, y + 5)
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12)
|
||||
|
||||
if (cp.errors && cp.errors.length > 0) {
|
||||
doc.setTextColor(220, 38, 38)
|
||||
doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19)
|
||||
y += 7
|
||||
}
|
||||
|
||||
y += 20
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Add page numbers
|
||||
// ==========================================================================
|
||||
|
||||
const pageCount = doc.getNumberOfPages()
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i)
|
||||
if (i > 1) {
|
||||
addHeader(doc, LABELS_DE.title, i, pageCount)
|
||||
}
|
||||
addFooter(doc, state)
|
||||
}
|
||||
|
||||
return doc.output('blob')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ZIP EXPORT
|
||||
// =============================================================================
|
||||
|
||||
export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const zip = new JSZip()
|
||||
|
||||
// Create folder structure
|
||||
const rootFolder = zip.folder('ai-compliance-sdk-export')
|
||||
if (!rootFolder) throw new Error('Failed to create ZIP folder')
|
||||
|
||||
const phase1Folder = rootFolder.folder('phase1-assessment')
|
||||
const phase2Folder = rootFolder.folder('phase2-documents')
|
||||
const dataFolder = rootFolder.folder('data')
|
||||
|
||||
// ==========================================================================
|
||||
// Main State JSON
|
||||
// ==========================================================================
|
||||
|
||||
if (opts.includeRawData && dataFolder) {
|
||||
dataFolder.file('state.json', JSON.stringify(state, null, 2))
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// README
|
||||
// ==========================================================================
|
||||
|
||||
const readmeContent = `# AI Compliance SDK Export
|
||||
|
||||
Generated: ${formatDate(new Date())}
|
||||
Tenant: ${state.tenantId}
|
||||
Version: ${state.version}
|
||||
|
||||
## Folder Structure
|
||||
|
||||
- **phase1-assessment/**: Compliance Assessment Ergebnisse
|
||||
- use-cases.json: Alle Use Cases
|
||||
- risks.json: Identifizierte Risiken
|
||||
- controls.json: Definierte Controls
|
||||
- requirements.json: Compliance-Anforderungen
|
||||
|
||||
- **phase2-documents/**: Generierte Dokumente
|
||||
- dsfa.json: Datenschutz-Folgenabschaetzung
|
||||
- toms.json: Technische und organisatorische Massnahmen
|
||||
- vvt.json: Verarbeitungsverzeichnis
|
||||
- documents.json: Rechtliche Dokumente
|
||||
|
||||
- **data/**: Rohdaten
|
||||
- state.json: Kompletter SDK State
|
||||
|
||||
## Progress
|
||||
|
||||
Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed
|
||||
Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed
|
||||
|
||||
## Key Metrics
|
||||
|
||||
- Use Cases: ${state.useCases.length}
|
||||
- Risks: ${state.risks.length}
|
||||
- Controls: ${state.controls.length}
|
||||
- Requirements: ${state.requirements.length}
|
||||
- Evidence: ${state.evidence.length}
|
||||
`
|
||||
|
||||
rootFolder.file('README.md', readmeContent)
|
||||
|
||||
// ==========================================================================
|
||||
// Phase 1 Files
|
||||
// ==========================================================================
|
||||
|
||||
if (phase1Folder) {
|
||||
// Use Cases
|
||||
phase1Folder.file('use-cases.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.useCases.length,
|
||||
useCases: state.useCases,
|
||||
}, null, 2))
|
||||
|
||||
// Risks
|
||||
phase1Folder.file('risks.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.risks.length,
|
||||
risks: state.risks,
|
||||
summary: {
|
||||
critical: state.risks.filter(r => r.severity === 'CRITICAL').length,
|
||||
high: state.risks.filter(r => r.severity === 'HIGH').length,
|
||||
medium: state.risks.filter(r => r.severity === 'MEDIUM').length,
|
||||
low: state.risks.filter(r => r.severity === 'LOW').length,
|
||||
},
|
||||
}, null, 2))
|
||||
|
||||
// Controls
|
||||
phase1Folder.file('controls.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.controls.length,
|
||||
controls: state.controls,
|
||||
}, null, 2))
|
||||
|
||||
// Requirements
|
||||
phase1Folder.file('requirements.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.requirements.length,
|
||||
requirements: state.requirements,
|
||||
}, null, 2))
|
||||
|
||||
// Modules
|
||||
phase1Folder.file('modules.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.modules.length,
|
||||
modules: state.modules,
|
||||
}, null, 2))
|
||||
|
||||
// Evidence
|
||||
if (opts.includeEvidence) {
|
||||
phase1Folder.file('evidence.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.evidence.length,
|
||||
evidence: state.evidence,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Checkpoints
|
||||
phase1Folder.file('checkpoints.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
checkpoints: state.checkpoints,
|
||||
}, null, 2))
|
||||
|
||||
// Screening
|
||||
if (state.screening) {
|
||||
phase1Folder.file('screening.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
screening: state.screening,
|
||||
}, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Phase 2 Files
|
||||
// ==========================================================================
|
||||
|
||||
if (phase2Folder) {
|
||||
// DSFA
|
||||
if (state.dsfa) {
|
||||
phase2Folder.file('dsfa.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
dsfa: state.dsfa,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// TOMs
|
||||
phase2Folder.file('toms.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.toms.length,
|
||||
toms: state.toms,
|
||||
}, null, 2))
|
||||
|
||||
// VVT (Processing Activities)
|
||||
phase2Folder.file('vvt.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.vvt.length,
|
||||
processingActivities: state.vvt,
|
||||
}, null, 2))
|
||||
|
||||
// Legal Documents
|
||||
if (opts.includeDocuments) {
|
||||
phase2Folder.file('documents.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.documents.length,
|
||||
documents: state.documents,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Cookie Banner Config
|
||||
if (state.cookieBanner) {
|
||||
phase2Folder.file('cookie-banner.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
config: state.cookieBanner,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Retention Policies
|
||||
phase2Folder.file('retention-policies.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.retentionPolicies.length,
|
||||
policies: state.retentionPolicies,
|
||||
}, null, 2))
|
||||
|
||||
// AI Act Classification
|
||||
if (state.aiActClassification) {
|
||||
phase2Folder.file('ai-act-classification.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
classification: state.aiActClassification,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Obligations
|
||||
phase2Folder.file('obligations.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.obligations.length,
|
||||
obligations: state.obligations,
|
||||
}, null, 2))
|
||||
|
||||
// Consent Records
|
||||
phase2Folder.file('consents.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.consents.length,
|
||||
consents: state.consents,
|
||||
}, null, 2))
|
||||
|
||||
// DSR Config
|
||||
if (state.dsrConfig) {
|
||||
phase2Folder.file('dsr-config.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
config: state.dsrConfig,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Escalation Workflows
|
||||
phase2Folder.file('escalation-workflows.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.escalationWorkflows.length,
|
||||
workflows: state.escalationWorkflows,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Security Data
|
||||
// ==========================================================================
|
||||
|
||||
if (dataFolder) {
|
||||
if (state.sbom) {
|
||||
dataFolder.file('sbom.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
sbom: state.sbom,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
if (state.securityIssues.length > 0) {
|
||||
dataFolder.file('security-issues.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.securityIssues.length,
|
||||
issues: state.securityIssues,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
if (state.securityBacklog.length > 0) {
|
||||
dataFolder.file('security-backlog.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.securityBacklog.length,
|
||||
backlog: state.securityBacklog,
|
||||
}, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Generate PDF and include in ZIP
|
||||
// ==========================================================================
|
||||
|
||||
try {
|
||||
const pdfBlob = await exportToPDF(state, options)
|
||||
const pdfArrayBuffer = await pdfBlob.arrayBuffer()
|
||||
rootFolder.file('compliance-report.pdf', pdfArrayBuffer)
|
||||
} catch (error) {
|
||||
console.error('Failed to generate PDF for ZIP:', error)
|
||||
// Continue without PDF
|
||||
}
|
||||
|
||||
// Generate ZIP
|
||||
return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT HELPER
|
||||
// =============================================================================
|
||||
|
||||
export async function downloadExport(
|
||||
state: SDKState,
|
||||
format: 'json' | 'pdf' | 'zip',
|
||||
options: ExportOptions = {}
|
||||
): Promise<void> {
|
||||
let blob: Blob
|
||||
let filename: string
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 10)
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
filename = `ai-compliance-sdk-${timestamp}.json`
|
||||
break
|
||||
|
||||
case 'pdf':
|
||||
blob = await exportToPDF(state, options)
|
||||
filename = `ai-compliance-sdk-${timestamp}.pdf`
|
||||
break
|
||||
|
||||
case 'zip':
|
||||
blob = await exportToZIP(state, options)
|
||||
filename = `ai-compliance-sdk-${timestamp}.zip`
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown export format: ${format}`)
|
||||
}
|
||||
|
||||
// Create download link
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
Reference in New Issue
Block a user