refactor(admin): split lib document generators and data catalogs into domain barrels

obligations-document, tom-document, loeschfristen-document, compliance-scope-triggers,
sdk-flow/flow-data, processing-activities, loeschfristen-baseline-catalog,
catalog-registry, dsfa mitigation-library + risk-catalog, vvt-baseline-catalog,
vendor contract-review checklists + findings, demo-data, tom-compliance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-18 00:07:03 +02:00
parent b00fe6cb73
commit 91063f09b8
65 changed files with 9514 additions and 9544 deletions

View File

@@ -0,0 +1,620 @@
// =============================================================================
// Obligations Document — HTML Document Builder
// =============================================================================
import type { Obligation, ObligationComplianceCheckResult, ObligationComplianceIssueSeverity } from '../obligations-compliance'
import { OBLIGATION_SEVERITY_LABELS_DE, OBLIGATION_SEVERITY_COLORS } from '../obligations-compliance'
import type { ObligationDocumentOrgHeader, ObligationDocumentRevision } from './types-defaults'
import {
STATUS_LABELS_DE,
STATUS_BADGE_CLASSES,
PRIORITY_LABELS_DE,
PRIORITY_BADGE_CLASSES,
} from './types-defaults'
import { escHtml, formatDateDE, daysBetween } from './helpers'
import { getDocumentStyles } from './html-styles'
export function buildObligationDocumentHtml(
obligations: Obligation[],
orgHeader: ObligationDocumentOrgHeader,
complianceResult: ObligationComplianceCheckResult | null,
revisions: ObligationDocumentRevision[]
): string {
const today = new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
const orgName = orgHeader.organizationName || 'Organisation'
// Group obligations by source (regulation)
const bySource = new Map<string, Obligation[]>()
for (const o of obligations) {
const src = o.source || 'Sonstig'
if (!bySource.has(src)) bySource.set(src, [])
bySource.get(src)!.push(o)
}
// Build role map
const roleMap = new Map<string, Obligation[]>()
for (const o of obligations) {
const role = o.responsible || 'Nicht zugewiesen'
if (!roleMap.has(role)) roleMap.set(role, [])
roleMap.get(role)!.push(o)
}
const distinctSources = Array.from(bySource.keys()).sort()
let html = getDocumentStyles(`Pflichtenregister — ${escHtml(orgName)}`)
html += buildCoverPage(orgHeader, orgName, today)
html += buildTOC()
html += buildSection1(orgName)
html += buildSection2(orgName, orgHeader, bySource, obligations, distinctSources)
html += buildSection3()
html += buildSection4(bySource, obligations)
html += buildSection5(bySource)
html += buildSection6(bySource)
html += buildSection7(roleMap)
html += buildSection8(obligations, today)
html += buildSection9(obligations)
html += buildSection10(complianceResult)
html += buildSection11(revisions, orgHeader, today)
html += buildFooter(orgName, today, orgHeader.documentVersion)
return html
}
function buildCoverPage(
orgHeader: ObligationDocumentOrgHeader,
orgName: string,
today: string
): string {
return `
<div class="cover">
<h1>Pflichtenregister</h1>
<div class="subtitle">Regulatorische Pflichten — DSGVO, AI Act, NIS2 und weitere</div>
<div class="org-info">
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
${orgHeader.dpoName ? `<div><span class="label">DSB:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
${orgHeader.legalDepartment ? `<div><span class="label">Rechtsabteilung:</span> ${escHtml(orgHeader.legalDepartment)}</div>` : ''}
</div>
<div class="legal-ref">
Version ${escHtml(orgHeader.documentVersion)} | Stand: ${today}<br/>
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
</div>
</div>
`
}
function buildTOC(): string {
const sections = [
'Ziel und Zweck',
'Geltungsbereich',
'Methodik',
'Regulatorische Grundlagen',
'Pflichtenuebersicht',
'Detaillierte Pflichten',
'Verantwortlichkeiten',
'Fristen und Termine',
'Nachweisverzeichnis',
'Compliance-Status',
'Aenderungshistorie',
]
return `
<div class="toc">
<h2>Inhaltsverzeichnis</h2>
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
</div>
`
}
function buildSection1(orgName: string): string {
return `
<div class="section">
<div class="section-header">1. Ziel und Zweck</div>
<div class="section-body">
<p>Dieses Pflichtenregister dokumentiert alle regulatorischen Pflichten, denen
<strong>${escHtml(orgName)}</strong> unterliegt. Es dient der systematischen Erfassung,
Ueberwachung und Nachverfolgung aller Compliance-Anforderungen aus den anwendbaren
Regulierungen.</p>
<p style="margin-top: 8px;">Das Register erfuellt folgende Zwecke:</p>
<ul style="margin: 8px 0 8px 24px;">
<li>Vollstaendige Erfassung aller anwendbaren regulatorischen Pflichten</li>
<li>Zuordnung von Verantwortlichkeiten und Fristen</li>
<li>Nachverfolgung des Umsetzungsstatus</li>
<li>Dokumentation von Nachweisen fuer Audits</li>
<li>Identifikation von Compliance-Luecken und Handlungsbedarf</li>
</ul>
<table>
<tr><th>Rechtsrahmen</th><th>Relevanz</th></tr>
<tr><td><strong>DSGVO (EU) 2016/679</strong></td><td>Datenschutz-Grundverordnung — Kernregulierung fuer personenbezogene Daten</td></tr>
<tr><td><strong>AI Act (EU) 2024/1689</strong></td><td>KI-Verordnung — Anforderungen an KI-Systeme nach Risikoklasse</td></tr>
<tr><td><strong>NIS2 (EU) 2022/2555</strong></td><td>Netzwerk- und Informationssicherheit — Cybersicherheitspflichten</td></tr>
<tr><td><strong>BDSG</strong></td><td>Bundesdatenschutzgesetz — Nationale Ergaenzung zur DSGVO</td></tr>
</table>
</div>
</div>
`
}
function buildSection2(
orgName: string,
orgHeader: ObligationDocumentOrgHeader,
bySource: Map<string, Obligation[]>,
obligations: Obligation[],
distinctSources: string[]
): string {
let html = `
<div class="section">
<div class="section-header">2. Geltungsbereich</div>
<div class="section-body">
<p>Dieses Pflichtenregister gilt fuer alle Geschaeftsprozesse und IT-Systeme von
<strong>${escHtml(orgName)}</strong>${orgHeader.industry ? ` (Branche: ${escHtml(orgHeader.industry)})` : ''}.</p>
<p style="margin-top: 8px;">Anwendbare Regulierungen:</p>
<table>
<tr><th>Regulierung</th><th>Anzahl Pflichten</th><th>Status</th></tr>
`
for (const [source, obls] of bySource.entries()) {
const completed = obls.filter(o => o.status === 'completed').length
const pct = obls.length > 0 ? Math.round((completed / obls.length) * 100) : 0
html += ` <tr>
<td>${escHtml(source)}</td>
<td>${obls.length}</td>
<td>${completed}/${obls.length} abgeschlossen (${pct}%)</td>
</tr>
`
}
html += ` </table>
<p>Insgesamt umfasst dieses Register <strong>${obligations.length}</strong> Pflichten aus
<strong>${distinctSources.length}</strong> Regulierungen.</p>
</div>
</div>
`
return html
}
function buildSection3(): string {
return `
<div class="section">
<div class="section-header">3. Methodik</div>
<div class="section-body">
<p>Die Identifikation und Bewertung der Pflichten erfolgt in drei Schritten:</p>
<div class="principle"><strong>Pflicht-Identifikation:</strong> Systematische Analyse aller anwendbaren Regulierungen und Extraktion der einzelnen Pflichten mit Artikel-Referenz, Beschreibung und Zielgruppe.</div>
<div class="principle"><strong>Bewertung und Priorisierung:</strong> Jede Pflicht wird nach Prioritaet (kritisch, hoch, mittel, niedrig) und Dringlichkeit (Frist) bewertet. Die Bewertung basiert auf dem Risikopotenzial bei Nichterfuellung.</div>
<div class="principle"><strong>Ueberwachung und Nachverfolgung:</strong> Regelmaessige Pruefung des Umsetzungsstatus, Aktualisierung der Fristen und Dokumentation von Nachweisen.</div>
<p style="margin-top: 12px;">Die Pflichten werden ueber einen automatisierten Compliance-Check geprueft, der
11 Kriterien umfasst (siehe Abschnitt 10: Compliance-Status).</p>
</div>
</div>
`
}
function buildSection4(bySource: Map<string, Obligation[]>, obligations: Obligation[]): string {
const totalCritical = obligations.filter(o => o.priority === 'critical').length
const totalHigh = obligations.filter(o => o.priority === 'high').length
const totalMedium = obligations.filter(o => o.priority === 'medium').length
const totalLow = obligations.filter(o => o.priority === 'low').length
const totalCompleted = obligations.filter(o => o.status === 'completed').length
let html = `
<div class="section page-break">
<div class="section-header">4. Regulatorische Grundlagen</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt die regulatorischen Grundlagen mit Artikelzahl und Umsetzungsstatus:</p>
<table>
<tr>
<th>Regulierung</th>
<th>Pflichten</th>
<th>Kritisch</th>
<th>Hoch</th>
<th>Mittel</th>
<th>Niedrig</th>
<th>Abgeschlossen</th>
</tr>
`
for (const [source, obls] of bySource.entries()) {
const critical = obls.filter(o => o.priority === 'critical').length
const high = obls.filter(o => o.priority === 'high').length
const medium = obls.filter(o => o.priority === 'medium').length
const low = obls.filter(o => o.priority === 'low').length
const completed = obls.filter(o => o.status === 'completed').length
html += ` <tr>
<td><strong>${escHtml(source)}</strong></td>
<td>${obls.length}</td>
<td>${critical}</td>
<td>${high}</td>
<td>${medium}</td>
<td>${low}</td>
<td>${completed}</td>
</tr>
`
}
html += ` <tr style="font-weight: 700; background: #f5f3ff;">
<td>Gesamt</td>
<td>${obligations.length}</td>
<td>${totalCritical}</td>
<td>${totalHigh}</td>
<td>${totalMedium}</td>
<td>${totalLow}</td>
<td>${totalCompleted}</td>
</tr>
</table>
</div>
</div>
`
return html
}
function buildSection5(bySource: Map<string, Obligation[]>): string {
let html = `
<div class="section">
<div class="section-header">5. Pflichtenuebersicht</div>
<div class="section-body">
<p>Uebersicht aller Pflichten nach Regulierung und Status:</p>
<table>
<tr>
<th>Regulierung</th>
<th>Gesamt</th>
<th>Ausstehend</th>
<th>In Bearbeitung</th>
<th>Abgeschlossen</th>
<th>Ueberfaellig</th>
</tr>
`
for (const [source, obls] of bySource.entries()) {
const pending = obls.filter(o => o.status === 'pending').length
const inProgress = obls.filter(o => o.status === 'in-progress').length
const completed = obls.filter(o => o.status === 'completed').length
const overdue = obls.filter(o => o.status === 'overdue').length
html += ` <tr>
<td>${escHtml(source)}</td>
<td>${obls.length}</td>
<td>${pending}</td>
<td>${inProgress}</td>
<td>${completed}</td>
<td>${overdue > 0 ? `<span class="badge badge-critical">${overdue}</span>` : '0'}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
return html
}
function buildSection6(bySource: Map<string, Obligation[]>): string {
const priorityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 }
let html = `
<div class="section">
<div class="section-header">6. Detaillierte Pflichten</div>
<div class="section-body">
`
for (const [source, obls] of bySource.entries()) {
const sorted = [...obls].sort((a, b) => {
const pa = priorityOrder[a.priority] ?? 2
const pb = priorityOrder[b.priority] ?? 2
if (pa !== pb) return pa - pb
return a.title.localeCompare(b.title)
})
html += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(source)} <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${sorted.length} Pflichten)</span></h3>
`
for (const o of sorted) {
const statusLabel = STATUS_LABELS_DE[o.status] || o.status
const statusBadge = STATUS_BADGE_CLASSES[o.status] || 'badge-draft'
const priorityLabel = PRIORITY_LABELS_DE[o.priority] || o.priority
const priorityBadge = PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'
const deadlineStr = o.deadline ? formatDateDE(o.deadline) : '—'
const evidenceStr = o.evidence && o.evidence.length > 0
? o.evidence.map(e => escHtml(e)).join(', ')
: '<em style="color: #d97706;">Kein Nachweis</em>'
const systemsStr = o.linked_systems && o.linked_systems.length > 0
? o.linked_systems.map(s => escHtml(s)).join(', ')
: '—'
html += `
<div class="policy-detail">
<div class="policy-detail-header">
<span>${escHtml(o.title)}</span>
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
</div>
<div class="policy-detail-body">
<table>
<tr><th>Rechtsquelle</th><td>${escHtml(o.source)} ${escHtml(o.source_article || '')}</td></tr>
<tr><th>Beschreibung</th><td>${escHtml(o.description || '—')}</td></tr>
<tr><th>Prioritaet</th><td><span class="badge ${priorityBadge}">${escHtml(priorityLabel)}</span></td></tr>
<tr><th>Status</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
<tr><th>Verantwortlich</th><td>${escHtml(o.responsible || '—')}</td></tr>
<tr><th>Frist</th><td>${deadlineStr}</td></tr>
<tr><th>Nachweise</th><td>${evidenceStr}</td></tr>
<tr><th>Betroffene Systeme</th><td>${systemsStr}</td></tr>
${o.linked_vendor_ids && o.linked_vendor_ids.length > 0 ? `<tr><th>Auftragsverarbeiter</th><td>${o.linked_vendor_ids.map(id => escHtml(id)).join(', ')}</td></tr>` : ''}
${o.notes ? `<tr><th>Notizen</th><td>${escHtml(o.notes)}</td></tr>` : ''}
</table>
</div>
</div>
`
}
}
html += ` </div>
</div>
`
return html
}
function buildSection7(roleMap: Map<string, Obligation[]>): string {
let html = `
<div class="section page-break">
<div class="section-header">7. Verantwortlichkeiten</div>
<div class="section-body">
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Pflichten
die Umsetzungsverantwortung tragen:</p>
<table>
<tr><th>Verantwortlich</th><th>Pflichten</th><th>Anzahl</th><th>Davon offen</th></tr>
`
for (const [role, obls] of roleMap.entries()) {
const openCount = obls.filter(o => o.status !== 'completed').length
const titles = obls.slice(0, 5).map(o => escHtml(o.title))
const suffix = obls.length > 5 ? `, ... (+${obls.length - 5})` : ''
html += ` <tr>
<td>${escHtml(role)}</td>
<td>${titles.join('; ')}${suffix}</td>
<td>${obls.length}</td>
<td>${openCount}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
return html
}
function buildSection8(obligations: Obligation[], _today: string): string {
const now = new Date()
const withDeadline = obligations
.filter(o => o.deadline && o.status !== 'completed')
.sort((a, b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime())
const overdue = withDeadline.filter(o => new Date(o.deadline!) < now)
const upcoming = withDeadline.filter(o => new Date(o.deadline!) >= now)
let html = `
<div class="section">
<div class="section-header">8. Fristen und Termine</div>
<div class="section-body">
`
if (overdue.length > 0) {
html += ` <h4 style="color: #dc2626; margin-bottom: 8px;">Ueberfaellige Pflichten (${overdue.length})</h4>
<table>
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Tage ueberfaellig</th><th>Prioritaet</th></tr>
`
for (const o of overdue) {
const days = daysBetween(new Date(o.deadline!), now)
html += ` <tr>
<td>${escHtml(o.title)}</td>
<td>${escHtml(o.source)}</td>
<td>${formatDateDE(o.deadline)}</td>
<td><span class="badge badge-critical">${days} Tage</span></td>
<td><span class="badge ${PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'}">${escHtml(PRIORITY_LABELS_DE[o.priority] || o.priority)}</span></td>
</tr>
`
}
html += ` </table>
`
}
if (upcoming.length > 0) {
html += ` <h4 style="color: #5b21b6; margin: 16px 0 8px 0;">Anstehende Fristen (${upcoming.length})</h4>
<table>
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Verbleibend</th><th>Verantwortlich</th></tr>
`
for (const o of upcoming.slice(0, 20)) {
const days = daysBetween(now, new Date(o.deadline!))
html += ` <tr>
<td>${escHtml(o.title)}</td>
<td>${escHtml(o.source)}</td>
<td>${formatDateDE(o.deadline)}</td>
<td>${days} Tage</td>
<td>${escHtml(o.responsible || '—')}</td>
</tr>
`
}
if (upcoming.length > 20) {
html += ` <tr><td colspan="5" style="text-align: center; color: #64748b;">... und ${upcoming.length - 20} weitere</td></tr>
`
}
html += ` </table>
`
}
if (withDeadline.length === 0) {
html += ` <p><em>Keine offenen Pflichten mit Fristen vorhanden.</em></p>
`
}
html += ` </div>
</div>
`
return html
}
function buildSection9(obligations: Obligation[]): string {
const withEvidence = obligations.filter(o => o.evidence && o.evidence.length > 0)
const withoutEvidence = obligations.filter(o => !o.evidence || o.evidence.length === 0)
let html = `
<div class="section page-break">
<div class="section-header">9. Nachweisverzeichnis</div>
<div class="section-body">
<p>${withEvidence.length} von ${obligations.length} Pflichten haben Nachweise hinterlegt.</p>
`
if (withEvidence.length > 0) {
html += ` <table>
<tr><th>Pflicht</th><th>Regulierung</th><th>Nachweise</th><th>Status</th></tr>
`
for (const o of withEvidence) {
html += ` <tr>
<td>${escHtml(o.title)}</td>
<td>${escHtml(o.source)}</td>
<td>${o.evidence!.map(e => escHtml(e)).join(', ')}</td>
<td><span class="badge ${STATUS_BADGE_CLASSES[o.status] || 'badge-draft'}">${escHtml(STATUS_LABELS_DE[o.status] || o.status)}</span></td>
</tr>
`
}
html += ` </table>
`
}
if (withoutEvidence.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Pflichten ohne Nachweise (${withoutEvidence.length}):</strong></p>
<ul style="margin: 4px 0 8px 24px; font-size: 9pt; color: #d97706;">
`
for (const o of withoutEvidence.slice(0, 15)) {
html += ` <li>${escHtml(o.title)} (${escHtml(o.source)})</li>
`
}
if (withoutEvidence.length > 15) {
html += ` <li>... und ${withoutEvidence.length - 15} weitere</li>
`
}
html += ` </ul>
`
}
html += ` </div>
</div>
`
return html
}
function buildSection10(complianceResult: ObligationComplianceCheckResult | null): string {
let html = `
<div class="section">
<div class="section-header">10. Compliance-Status</div>
<div class="section-body">
`
if (complianceResult) {
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
: complianceResult.score >= 75 ? 'score-good'
: complianceResult.score >= 50 ? 'score-needs-work'
: 'score-poor'
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
: complianceResult.score >= 75 ? 'Gut'
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
: 'Mangelhaft'
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
<table style="margin-top: 12px;">
<tr><th>Kennzahl</th><th>Wert</th></tr>
<tr><td>Geprueft am</td><td>${formatDateDE(complianceResult.checkedAt)}</td></tr>
<tr><td>Befunde gesamt</td><td>${complianceResult.summary.total}</td></tr>
<tr><td>Kritisch</td><td>${complianceResult.summary.critical}</td></tr>
<tr><td>Hoch</td><td>${complianceResult.summary.high}</td></tr>
<tr><td>Mittel</td><td>${complianceResult.summary.medium}</td></tr>
<tr><td>Niedrig</td><td>${complianceResult.summary.low}</td></tr>
</table>
`
if (complianceResult.issues.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
<table>
<tr><th>Schweregrad</th><th>Befund</th><th>Betroffene Pflichten</th><th>Empfehlung</th></tr>
`
const severityOrder: ObligationComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for (const sev of severityOrder) {
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
for (const issue of issuesForSev) {
html += ` <tr>
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${OBLIGATION_SEVERITY_COLORS[sev]}">${OBLIGATION_SEVERITY_LABELS_DE[sev]}</span></td>
<td>${escHtml(issue.message)}</td>
<td>${issue.affectedObligations.length > 0 ? issue.affectedObligations.length + ' Pflicht(en)' : '—'}</td>
<td>${escHtml(issue.recommendation)}</td>
</tr>
`
}
}
html += ` </table>
`
} else {
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Pflichten sind konform.</em></p>
`
}
} else {
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
Pflichtenregister-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
`
}
html += ` </div>
</div>
`
return html
}
function buildSection11(
revisions: ObligationDocumentRevision[],
orgHeader: ObligationDocumentOrgHeader,
today: string
): string {
let html = `
<div class="section">
<div class="section-header">11. Aenderungshistorie</div>
<div class="section-body">
<table>
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
`
if (revisions.length > 0) {
for (const rev of revisions) {
html += ` <tr>
<td>${escHtml(rev.version)}</td>
<td>${formatDateDE(rev.date)}</td>
<td>${escHtml(rev.author)}</td>
<td>${escHtml(rev.changes)}</td>
</tr>
`
}
} else {
html += ` <tr>
<td>${escHtml(orgHeader.documentVersion)}</td>
<td>${today}</td>
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '—')}</td>
<td>Erstversion des Pflichtenregisters</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
return html
}
function buildFooter(orgName: string, today: string, version: string): string {
return `
<div class="page-footer">
<span>Pflichtenregister — ${escHtml(orgName)}</span>
<span>Stand: ${today} | Version ${escHtml(version)}</span>
</div>
</body>
</html>`
}