Privacy Policy DSGVO with sheets for categories, export-, check- and first erase-functions

This commit is contained in:
2026-08-05 20:34:38 +00:00
parent c9e6438222
commit fc82a28290
19 changed files with 1917 additions and 5 deletions
+8
View File
@@ -82,12 +82,20 @@
<a href="/sync/meshcentral">{{ t('settings.meshcentral', 'MeshCentral') }}</a>
<a href="/settings/software">{{ t('software.settings.menu') }}</a>
<a href="/settings/backup">{{ t('settings.backup', 'Sicherung') }}</a>
<a href="/settings/privacy">{{ t('privacy.menu') }}</a>
<a href="/status-options">{{ t('app.status_values', 'Statuswerte') }}</a>
<a href="/settings/system">{{ t('settings.system_information', 'Systeminformationen') }}</a>
<a href="/settings/translations">{{ t('settings.translations', 'Übersetzungen') }}</a>
<a href="/users">{{ t('users.management', 'Benutzerverwaltung') }}</a>
</div>
</div>
{% else %}
<div class="side-nav-settings">
<a href="/settings/privacy" class="settings-trigger" title="{{ t('app.settings') }}"><span class="nav-icon"></span><span class="nav-label">{{ t('app.settings') }}</span><span class="nav-caret"></span></a>
<div class="settings-hover-menu" role="menu">
<a href="/settings/privacy">{{ t('privacy.menu') }}</a>
</div>
</div>
{% endif %}
</aside>
<main class="app-content">{% block content %}{% endblock %}</main>
+1 -1
View File
@@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<div class="toolbar"><h1>{{ t('dashboard.overview') }}</h1>{% if is_admin %}<a class="button" href="/assets/new"> {{ t('assets.create') }}</a>{% endif %}</div>
<div class="cards">{% for c in categories %}<a class="card" href="/assets?category_id={{c.id}}">{% if c.image_path %}<img src="{{c.image_path}}">{% else %}<div class="placeholder">{{c.name[:1]}}</div>{% endif %}<div><strong>{{c.name}}</strong><small>{{c.assets|length}} {{ t('common.assets') }}</small></div></a>{% endfor %}</div>
<div class="cards">{% for c in categories %}<a class="card" href="/assets?category_id={{c.id}}">{% if c.image_path %}<img src="{{c.image_path}}">{% else %}<div class="placeholder">{{c.name[:1]}}</div>{% endif %}<div><strong>{{c.name}}</strong><small>{{ category_asset_counts.get(c.id, 0) }} {{ t('common.assets') }}</small></div></a>{% endfor %}</div>
{% if dashboard_charts %}<div class="section-heading"><h2>{{ t('app.charts') }}</h2><a href="/charts">{{ t('charts.show_all') }}</a></div><div class="chart-grid dashboard-charts">{% for item in dashboard_charts %}{% include '_chart.html' %}{% endfor %}</div>{% endif %}
<h2>{{ t('dashboard.recently_changed') }}</h2>
<p class="table-hint">{{ t('dashboard.list_fields_hint') }}</p>
+100
View File
@@ -0,0 +1,100 @@
<!doctype html>
<html lang="{{ 'de' if language_code.startswith('de') else 'en' }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ 'Verzeichnis von Verarbeitungstätigkeiten / Löschkonzept' if language_code.startswith('de') else 'Record of Processing Activities / Retention Concept' }}</title>
<style>
:root { color-scheme: light; font-family: Arial, Helvetica, sans-serif; color: #172033; background: #fff; }
body { max-width: 1180px; margin: 0 auto; padding: 2rem; line-height: 1.45; }
h1 { margin-bottom: .35rem; }
h2 { margin-top: 2rem; border-bottom: 2px solid #20a5df; padding-bottom: .35rem; }
h3 { margin-top: 1.4rem; }
.meta { color: #526173; margin-bottom: 1.5rem; }
.notice { border: 1px solid #d9a400; background: #fff8d8; padding: .8rem 1rem; border-radius: .45rem; }
dl { display: grid; grid-template-columns: minmax(180px, 280px) 1fr; gap: .35rem 1rem; }
dt { font-weight: 700; }
dd { margin: 0; white-space: pre-wrap; }
table { width: 100%; border-collapse: collapse; margin: .8rem 0 1.5rem; font-size: .92rem; }
th, td { border: 1px solid #b7c2ce; padding: .5rem .6rem; text-align: left; vertical-align: top; white-space: pre-wrap; }
th { background: #d9eaf7; }
.category { break-inside: avoid; page-break-inside: avoid; }
.category table th { width: 24%; }
code { overflow-wrap: anywhere; }
@media print { body { max-width: none; padding: 0; } h2 { break-after: avoid; } a { color: inherit; text-decoration: none; } }
</style>
</head>
<body>
{% set de = language_code.startswith('de') %}
<h1>{{ 'Verzeichnis von Verarbeitungstätigkeiten / Löschkonzept' if de else 'Record of Processing Activities / Retention Concept' }}</h1>
<div class="meta">{{ 'Erzeugt' if de else 'Generated' }}: {{ generated_at }} · AssetManager {{ app_version }} · {{ 'Dokumentversion' if de else 'Document version' }}: {{ activity.document_version or '-' }}</div>
<div class="notice">{{ 'Dieser Bericht dokumentiert die konfigurierten Regeln. Automatische Löschungen sind noch nicht aktiv. Prüfläufe und manuell bestätigte Löschläufe sind für ausgewählte AssetManager-Kategorien verfügbar.' if de else 'This report documents the configured rules. Automatic deletion is not active. Dry runs and manually confirmed deletion runs are available for selected AssetManager categories.' }}</div>
<h2>{{ '1. Verarbeitungstätigkeit und Verantwortlichkeit' if de else '1. Processing activity and responsibility' }}</h2>
<dl>
<dt>{{ 'Bezeichnung' if de else 'Title' }}</dt><dd>{{ activity.title or '-' }}</dd>
<dt>{{ 'Verantwortlicher' if de else 'Controller' }}</dt><dd>{{ activity.controller or '-' }}</dd>
<dt>{{ 'Fachbereich' if de else 'Responsible department' }}</dt><dd>{{ activity.responsible_department or '-' }}</dd>
<dt>{{ 'Verantwortliche Personen' if de else 'Responsible people' }}</dt><dd>{{ activity.responsible_people or '-' }}</dd>
<dt>{{ 'Datenschutzkontakt' if de else 'Data protection contact' }}</dt><dd>{{ activity.data_protection_contact or '-' }}</dd>
</dl>
<h2>{{ '2. Zwecke' if de else '2. Purposes' }}</h2>
<ul>{% for item in activity.purposes %}<li>{{ item }}</li>{% else %}<li>-</li>{% endfor %}</ul>
<h2>{{ '3. Rechtsgrundlage und betroffene Personen' if de else '3. Legal basis and data subjects' }}</h2>
<dl><dt>{{ 'Rechtsgrundlage' if de else 'Legal basis' }}</dt><dd>{{ activity.legal_basis or '-' }}</dd><dt>{{ 'Betroffene Personengruppen' if de else 'Data subject groups' }}</dt><dd>{{ activity.data_subject_groups or '-' }}</dd></dl>
<h2>{{ '4. Empfänger und Drittlandübermittlung' if de else '4. Recipients and third-country transfers' }}</h2>
<dl>
<dt>{{ 'Interne Empfänger' if de else 'Internal recipients' }}</dt><dd>{{ activity.internal_recipients or '-' }}</dd>
<dt>{{ 'Externe Empfänger' if de else 'External recipients' }}</dt><dd>{{ activity.external_recipients or '-' }}</dd>
<dt>{{ 'Drittlandübermittlung' if de else 'Third-country transfer' }}</dt><dd>{{ ('Ja' if activity.third_country_transfer else 'Nein') if de else ('Yes' if activity.third_country_transfer else 'No') }}</dd>
<dt>{{ 'Details' if de else 'Details' }}</dt><dd>{{ activity.third_country_details or '-' }}</dd>
</dl>
<h2>{{ '5. Systeme und Sicherungen' if de else '5. Systems and backups' }}</h2>
<dl>
<dt>{{ 'Systeme' if de else 'Systems' }}</dt><dd>{{ activity.systems or '-' }}</dd>
<dt>{{ 'Backup-System' if de else 'Backup system' }}</dt><dd>{{ activity.backup_system or '-' }}</dd>
<dt>{{ 'Backup-Aufbewahrung' if de else 'Backup retention' }}</dt><dd>{{ activity.backup_retention_days if activity.backup_retention_days is not none else '-' }} {{ 'Tage' if de else 'days' }}</dd>
<dt>{{ 'Verfahren nach Wiederherstellung' if de else 'Restore procedure' }}</dt><dd>{{ activity.restore_deletion_procedure or '-' }}</dd>
</dl>
<h2>{{ '6. Datenkategorien und Löschfristen' if de else '6. Data categories and retention' }}</h2>
{% for category in settings.categories %}
<section class="category">
<h3>6.{{ loop.index }} {{ category.name }}</h3>
<table>
<tr><th>{{ 'System' if de else 'System' }}</th><td>{{ category.system or '-' }}</td></tr>
<tr><th>{{ 'Betroffene Personen' if de else 'Data subjects' }}</th><td>{{ category.data_subjects or '-' }}</td></tr>
<tr><th>{{ 'Personenbezogene Daten' if de else 'Personal data' }}</th><td>{{ category.personal_data or '-' }}</td></tr>
<tr><th>{{ 'Zweck' if de else 'Purpose' }}</th><td>{{ category.purpose or '-' }}</td></tr>
<tr><th>{{ 'Löschart' if de else 'Retention type' }}</th><td>{{ category.retention_type or '-' }}</td></tr>
<tr><th>{{ 'Löschfrist' if de else 'Retention period' }}</th><td>{{ category.retention_days if category.retention_days is not none else '-' }} {{ 'Tage' if de else 'days' }}</td></tr>
<tr><th>{{ 'Fristbeginn/Auslöser' if de else 'Trigger' }}</th><td>{{ category.retention_trigger or '-' }}</td></tr>
<tr><th>{{ 'Automatisch technisch möglich' if de else 'Automatic deletion technically possible' }}</th><td>{{ ('Ja' if category.automatic_possible else 'Nein') if de else ('Yes' if category.automatic_possible else 'No') }}</td></tr>
<tr><th>{{ 'Manuelles Verfahren' if de else 'Manual procedure' }}</th><td>{{ category.manual_procedure or '-' }}</td></tr>
<tr><th>{{ 'Behandlung in Sicherungen' if de else 'Backup handling' }}</th><td>{{ category.backup_handling or '-' }}</td></tr>
<tr><th>{{ 'Verantwortlich' if de else 'Responsible' }}</th><td>{{ category.responsible or '-' }}</td></tr>
<tr><th>{{ 'Hinweise' if de else 'Notes' }}</th><td>{{ category.notes or '-' }}</td></tr>
</table>
</section>
{% endfor %}
<h2>{{ '7. Technische und organisatorische Maßnahmen' if de else '7. Technical and organisational measures' }}</h2>
<ul>{% for item in activity.toms %}<li>{{ item }}</li>{% else %}<li>-</li>{% endfor %}</ul>
<h2>{{ '8. Dokumentenprüfung' if de else '8. Document review' }}</h2>
<dl>
<dt>{{ 'Prüfintervall' if de else 'Review interval' }}</dt><dd>{{ activity.review_interval_months or 12 }} {{ 'Monate' if de else 'months' }}</dd>
<dt>{{ 'Zuletzt geprüft' if de else 'Last reviewed' }}</dt><dd>{{ activity.last_reviewed_at or '-' }}</dd>
<dt>{{ 'Freigegeben durch' if de else 'Approved by' }}</dt><dd>{{ activity.approved_by or '-' }}</dd>
</dl>
<h2>{{ '9. Änderungsprotokoll' if de else '9. Policy audit' }}</h2>
<table><thead><tr><th>{{ 'Zeitstempel UTC' if de else 'Timestamp UTC' }}</th><th>{{ 'Aktion' if de else 'Action' }}</th><th>{{ 'Geändert von' if de else 'Changed by' }}</th><th>Details</th></tr></thead><tbody>
{% for row in privacy_audit %}<tr><td>{{ row.timestamp_utc }}</td><td>{{ row.action }}</td><td>{{ row.changed_by }}</td><td><code>{{ row.details }}</code></td></tr>{% else %}<tr><td colspan="4">-</td></tr>{% endfor %}
</tbody></table>
</body>
</html>
+374
View File
@@ -0,0 +1,374 @@
{% extends "base.html" %}
{% block content %}
<div class="toolbar privacy-toolbar">
<div>
<h1>{{ t('privacy.title') }}</h1>
<p class="muted">{{ t('privacy.intro') }}</p>
</div>
<div class="toolbar-actions">
<a class="button button-secondary" href="/settings/privacy/export.txt">{{ t('privacy.export_text') }}</a>
<a class="button button-secondary" href="/settings/privacy/export.html">{{ t('privacy.export_html') }}</a>
<a class="button" href="/settings/privacy/export.xlsx">{{ t('privacy.export_excel') }}</a>
</div>
</div>
<div class="notice warning privacy-phase-notice">
<strong>{{ t('privacy.phase2_title') }}</strong>
{{ t('privacy.phase2_help') }}
</div>
{% if not can_edit %}<div class="notice info"><strong>{{ t('privacy.read_only_title') }}</strong> {{ t('privacy.read_only_help') }}</div>{% endif %}
<form method="post" action="/settings/privacy" class="settings-form privacy-settings-form" data-privacy-readonly="{{ 'false' if can_edit else 'true' }}">
{% set activity = privacy_settings.processing_activity %}
<section class="settings-section">
<h2>{{ t('privacy.processing_activity') }}</h2>
<div class="form-grid two-columns">
<label>{{ t('privacy.activity_title') }}<input type="text" name="title" value="{{ activity.title }}" required></label>
<label>{{ t('privacy.document_version') }}<input type="text" name="document_version" value="{{ activity.document_version }}"></label>
<label>{{ t('privacy.controller') }}<input type="text" name="controller" value="{{ activity.controller }}"></label>
<label>{{ t('privacy.responsible_department') }}<input type="text" name="responsible_department" value="{{ activity.responsible_department }}"></label>
<label>{{ t('privacy.responsible_people') }}<input type="text" name="responsible_people" value="{{ activity.responsible_people }}"></label>
<label>{{ t('privacy.data_protection_contact') }}<input type="text" name="data_protection_contact" value="{{ activity.data_protection_contact }}"></label>
</div>
<label>{{ t('privacy.legal_basis') }}<textarea name="legal_basis" rows="3">{{ activity.legal_basis }}</textarea></label>
<label>{{ t('privacy.data_subject_groups') }}<textarea name="data_subject_groups" rows="3">{{ activity.data_subject_groups }}</textarea></label>
</section>
<section class="settings-section">
<div class="settings-section-heading">
<div><h2>{{ t('privacy.purposes') }}</h2><p class="muted">{{ t('privacy.purposes_help') }}</p></div>
<button type="button" class="button-secondary" data-add-list-row="purposes-list">{{ t('common.add', 'Add') }}</button>
</div>
<div id="purposes-list" class="repeatable-list">
{% for value in activity.purposes %}
<div class="repeatable-row"><input type="text" name="purposes" value="{{ value }}"><button type="button" class="button-danger button-small" data-remove-row>×</button></div>
{% endfor %}
</div>
</section>
<section class="settings-section">
<h2>{{ t('privacy.recipients_transfers') }}</h2>
<div class="form-grid two-columns">
<label>{{ t('privacy.internal_recipients') }}<textarea name="internal_recipients" rows="3">{{ activity.internal_recipients }}</textarea></label>
<label>{{ t('privacy.external_recipients') }}<textarea name="external_recipients" rows="3">{{ activity.external_recipients }}</textarea></label>
</div>
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="third_country_transfer" {% if activity.third_country_transfer %}checked{% endif %}> {{ t('privacy.third_country_transfer') }}</label>
<label>{{ t('privacy.third_country_details') }}<textarea name="third_country_details" rows="3">{{ activity.third_country_details }}</textarea></label>
</section>
<section class="settings-section">
<h2>{{ t('privacy.systems_backups') }}</h2>
<label>{{ t('privacy.systems') }}<textarea name="systems" rows="3">{{ activity.systems }}</textarea></label>
<div class="form-grid two-columns">
<label>{{ t('privacy.backup_system') }}<input type="text" name="backup_system" value="{{ activity.backup_system }}"></label>
<label>{{ t('privacy.backup_retention_days') }}<input type="number" min="0" max="36500" name="backup_retention_days" value="{{ activity.backup_retention_days if activity.backup_retention_days is not none else '' }}"></label>
</div>
<label>{{ t('privacy.restore_deletion_procedure') }}<textarea name="restore_deletion_procedure" rows="3">{{ activity.restore_deletion_procedure }}</textarea></label>
</section>
<section class="settings-section privacy-category-section">
<div class="settings-section-heading">
<div><h2>{{ t('privacy.data_categories') }}</h2><p class="muted">{{ t('privacy.data_categories_tabs_help') }}</p></div>
{% if can_edit %}<button type="button" class="button-secondary" id="add-privacy-category">{{ t('privacy.add_category') }}</button>{% endif %}
</div>
<div class="privacy-category-workbook" data-category-workbook>
<div class="privacy-category-tabs" role="tablist" aria-label="{{ t('privacy.data_categories') }}" data-category-tabs>
{% for category in privacy_settings.categories %}
<button type="button" class="privacy-category-tab{% if loop.first %} active{% endif %}" role="tab" aria-selected="{{ 'true' if loop.first else 'false' }}" aria-controls="privacy-category-panel-{{ loop.index0 }}" data-category-tab>
<span data-category-tab-title>{{ category.name }}</span>{% if category.is_system %}<span class="privacy-system-badge" title="{{ t('privacy.system_category_help') }}">🔒</span>{% endif %}
</button>
{% endfor %}
</div>
<div id="privacy-category-list" class="privacy-category-panels" data-category-panels>
{% for category in privacy_settings.categories %}
<section id="privacy-category-panel-{{ loop.index0 }}" class="privacy-category-panel{% if loop.first %} active{% endif %}" role="tabpanel" {% if not loop.first %}hidden{% endif %} data-category-panel>
<input type="hidden" name="category_key" value="{{ category.key }}">
<div class="form-grid two-columns">
<label>{{ t('privacy.category_name') }}<input type="text" name="category_name" value="{{ category.name }}" data-category-name></label>
<label>{{ t('privacy.system') }}<input type="text" name="category_system" value="{{ category.system }}"></label>
</div>
<label>{{ t('privacy.data_subjects') }}<textarea name="category_data_subjects" rows="2">{{ category.data_subjects }}</textarea></label>
<label>{{ t('privacy.personal_data') }}<textarea name="category_personal_data" rows="3">{{ category.personal_data }}</textarea></label>
<label>{{ t('privacy.purpose') }}<textarea name="category_purpose" rows="2">{{ category.purpose }}</textarea></label>
<div class="form-grid three-columns">
<label>{{ t('privacy.retention_type') }}
<select name="category_retention_type">
{% for value, key in [
('days','privacy.retention_days_type'),
('manual_parent_delete','privacy.retention_manual_parent'),
('overwrite_parent_delete','privacy.retention_overwrite_parent'),
('external_days','privacy.retention_external_days'),
('external_manual','privacy.retention_external_manual'),
('external_overwrite_parent_delete','privacy.retention_external_overwrite'),
('not_stored','privacy.retention_not_stored'),
('manual','privacy.retention_manual')
] %}<option value="{{ value }}" {% if category.retention_type == value %}selected{% endif %}>{{ t(key) }}</option>{% endfor %}
</select>
</label>
<label>{{ t('privacy.retention_days') }}<input type="number" min="0" max="36500" name="category_retention_days" value="{{ category.retention_days if category.retention_days is not none else '' }}"></label>
<label>{{ t('privacy.automatic_possible') }}
<select name="category_automatic_possible"><option value="0" {% if not category.automatic_possible %}selected{% endif %}>{{ t('common.no') }}</option><option value="1" {% if category.automatic_possible %}selected{% endif %}>{{ t('common.yes') }}</option></select>
</label>
</div>
<label>{{ t('privacy.retention_trigger') }}<textarea name="category_retention_trigger" rows="2">{{ category.retention_trigger }}</textarea></label>
<label>{{ t('privacy.manual_procedure') }}<textarea name="category_manual_procedure" rows="3">{{ category.manual_procedure }}</textarea></label>
<label>{{ t('privacy.backup_handling') }}<textarea name="category_backup_handling" rows="2">{{ category.backup_handling }}</textarea></label>
<div class="form-grid two-columns">
<label>{{ t('privacy.responsible') }}<input type="text" name="category_responsible" value="{{ category.responsible }}"></label>
<label>{{ t('privacy.notes') }}<textarea name="category_notes" rows="2">{{ category.notes }}</textarea></label>
</div>
<div class="privacy-retention-actions">
{% if category.is_system %}<span class="muted privacy-system-note">🔒 {{ t('privacy.system_category_help') }}</span>{% endif %}
{% if can_edit and category.key in implemented_retention_keys %}
<button type="button" class="button-secondary" data-retention-check-url="/settings/privacy/retention/{{ category.key }}/check">{{ t('privacy.run_check') }}</button>
<button type="button" class="button-danger" data-retention-delete-url="/settings/privacy/retention/{{ category.key }}/delete" data-category-name="{{ category.name }}">{{ t('privacy.run_manual_deletion') }}</button>
{% elif category.automatic_possible %}
<span class="muted">{{ t('privacy.handler_not_implemented') }}</span>
{% endif %}
{% if can_edit and not category.is_system %}<button type="button" class="button-danger" data-remove-category>{{ t('privacy.remove_category') }}</button>{% endif %}
</div>
{% set preview = preview_results.get(category.key) %}
{% if preview %}
<div class="notice info privacy-preview-result">
<strong>{{ t('privacy.check_result') }}</strong>
<span>{{ t('privacy.matched_records') }}: {{ preview.matched_records or 0 }}</span>
<span>{{ t('privacy.approx_size') }}: {{ ((preview.approx_bytes or 0) / 1024 / 1024)|round(2) }} MB</span>
<span>{{ t('privacy.cutoff') }}: {{ preview.cutoff_utc or '-' }}</span>
{% if not preview.supported %}<span>{{ preview.reason }}</span>{% endif %}
</div>
{% endif %}
</section>
{% endfor %}
</div>
</div>
</section>
<section class="settings-section">
<div class="settings-section-heading">
<div><h2>{{ t('privacy.toms') }}</h2><p class="muted">{{ t('privacy.toms_help') }}</p></div>
<button type="button" class="button-secondary" data-add-list-row="toms-list">{{ t('common.add', 'Add') }}</button>
</div>
<div id="toms-list" class="repeatable-list">
{% for value in activity.toms %}
<div class="repeatable-row"><textarea name="toms" rows="2">{{ value }}</textarea><button type="button" class="button-danger button-small" data-remove-row>×</button></div>
{% endfor %}
</div>
</section>
<section class="settings-section">
<h2>{{ t('privacy.review') }}</h2>
<div class="form-grid three-columns">
<label>{{ t('privacy.review_interval_months') }}<input type="number" min="1" max="120" name="review_interval_months" value="{{ activity.review_interval_months }}"></label>
<label>{{ t('privacy.last_reviewed_at') }}<input type="date" name="last_reviewed_at" value="{{ activity.last_reviewed_at }}"></label>
<label>{{ t('privacy.approved_by') }}<input type="text" name="approved_by" value="{{ activity.approved_by }}"></label>
</div>
</section>
{% if can_edit %}<div class="form-actions sticky-form-actions"><button type="submit" class="button-save">{{ t('common.save') }}</button></div>{% endif %}
</form>
<section class="settings-section">
<h2>{{ t('privacy.policy_audit') }}</h2>
<p class="muted">{{ t('privacy.policy_audit_help') }}</p>
<div class="table-wrap"><table class="data-table" data-table-tools="true"><thead><tr><th>{{ t('common.date') }}</th><th>{{ t('privacy.action') }}</th><th>{{ t('privacy.changed_by') }}</th><th>{{ t('privacy.details') }}</th></tr></thead><tbody>
{% for row in privacy_audit %}<tr><td data-local-datetime="{{ row.timestamp_utc }}">{{ row.timestamp_utc }}</td><td>{{ row.action }}</td><td>{{ row.changed_by }}</td><td><code>{{ row.details }}</code></td></tr>{% else %}<tr><td colspan="4" class="muted">{{ t('privacy.no_audit_entries') }}</td></tr>{% endfor %}
</tbody></table></div>
</section>
<section class="settings-section">
<h2>{{ t('privacy.deletion_audit') }}</h2>
<p class="muted">{{ t('privacy.deletion_audit_help') }}</p>
<div class="table-wrap"><table class="data-table" data-table-tools="true"><thead><tr><th>{{ t('common.date') }}</th><th>{{ t('privacy.run_id') }}</th><th>{{ t('privacy.action') }}</th><th>{{ t('privacy.category_name') }}</th><th>{{ t('privacy.changed_by') }}</th><th>{{ t('privacy.details') }}</th></tr></thead><tbody>
{% for row in deletion_audit %}<tr><td data-local-datetime="{{ row.timestamp_utc }}">{{ row.timestamp_utc }}</td><td><code>{{ row.run_id }}</code></td><td>{{ row.action }}</td><td>{{ row.category_key }}</td><td>{{ row.changed_by }}</td><td><code>{{ row.result }}</code></td></tr>{% else %}<tr><td colspan="6" class="muted">{{ t('privacy.no_deletion_entries') }}</td></tr>{% endfor %}
</tbody></table></div>
</section>
<template id="privacy-category-template">
<section class="privacy-category-panel" role="tabpanel" hidden data-category-panel>
<input type="hidden" name="category_key" value="">
<div class="form-grid two-columns">
<label>{{ t('privacy.category_name') }}<input type="text" name="category_name" value="" data-category-name></label>
<label>{{ t('privacy.system') }}<input type="text" name="category_system" value=""></label>
</div>
<label>{{ t('privacy.data_subjects') }}<textarea name="category_data_subjects" rows="2"></textarea></label>
<label>{{ t('privacy.personal_data') }}<textarea name="category_personal_data" rows="3"></textarea></label>
<label>{{ t('privacy.purpose') }}<textarea name="category_purpose" rows="2"></textarea></label>
<div class="form-grid three-columns">
<label>{{ t('privacy.retention_type') }}
<select name="category_retention_type">
<option value="days">{{ t('privacy.retention_days_type') }}</option>
<option value="manual_parent_delete">{{ t('privacy.retention_manual_parent') }}</option>
<option value="overwrite_parent_delete">{{ t('privacy.retention_overwrite_parent') }}</option>
<option value="external_days">{{ t('privacy.retention_external_days') }}</option>
<option value="external_manual">{{ t('privacy.retention_external_manual') }}</option>
<option value="external_overwrite_parent_delete">{{ t('privacy.retention_external_overwrite') }}</option>
<option value="not_stored">{{ t('privacy.retention_not_stored') }}</option>
<option value="manual" selected>{{ t('privacy.retention_manual') }}</option>
</select>
</label>
<label>{{ t('privacy.retention_days') }}<input type="number" min="0" max="36500" name="category_retention_days"></label>
<label>{{ t('privacy.automatic_possible') }}<select name="category_automatic_possible"><option value="0">{{ t('common.no') }}</option><option value="1">{{ t('common.yes') }}</option></select></label>
</div>
<label>{{ t('privacy.retention_trigger') }}<textarea name="category_retention_trigger" rows="2"></textarea></label>
<label>{{ t('privacy.manual_procedure') }}<textarea name="category_manual_procedure" rows="3"></textarea></label>
<label>{{ t('privacy.backup_handling') }}<textarea name="category_backup_handling" rows="2"></textarea></label>
<div class="form-grid two-columns"><label>{{ t('privacy.responsible') }}<input type="text" name="category_responsible"></label><label>{{ t('privacy.notes') }}<textarea name="category_notes" rows="2"></textarea></label></div>
<div class="form-actions"><button type="button" class="button-danger" data-remove-category>{{ t('privacy.remove_category') }}</button></div>
</section>
</template>
<script>
(() => {
const privacyForm = document.querySelector('.privacy-settings-form');
const readOnly = privacyForm?.dataset.privacyReadonly === 'true';
if (readOnly) {
privacyForm.querySelectorAll('input, textarea').forEach(control => {
if (control.type === 'hidden') return;
control.readOnly = true;
control.setAttribute('aria-readonly', 'true');
});
privacyForm.querySelectorAll('select').forEach(control => {
control.disabled = true;
});
privacyForm.querySelectorAll('button:not([data-category-tab])').forEach(control => {
control.disabled = true;
});
}
document.addEventListener('click', event => {
const checkButton = event.target.closest('[data-retention-check-url]');
const deleteButton = event.target.closest('[data-retention-delete-url]');
const url = checkButton?.dataset.retentionCheckUrl || deleteButton?.dataset.retentionDeleteUrl;
if (!url) return;
event.preventDefault();
let confirmation = '';
if (deleteButton) {
const categoryName = deleteButton.dataset.categoryName || '';
confirmation = window.prompt({{ t('privacy.delete_confirmation_prompt')|tojson }}.replace('{category}', categoryName), '');
if (confirmation === null) return;
}
const form = document.createElement('form');
form.method = 'post';
form.action = url;
if (deleteButton) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirmation';
input.value = confirmation;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
});
const workbook = document.querySelector('[data-category-workbook]');
const tabs = workbook?.querySelector('[data-category-tabs]');
const panels = workbook?.querySelector('[data-category-panels]');
const newCategoryLabel = {{ t('privacy.new_category')|tojson }};
let nextPanelId = document.querySelectorAll('[data-category-panel]').length;
const activate = panel => {
if (!panel || !tabs || !panels) return;
panels.querySelectorAll('[data-category-panel]').forEach(item => {
const active = item === panel;
item.classList.toggle('active', active);
item.hidden = !active;
});
tabs.querySelectorAll('[data-category-tab]').forEach(tab => {
const active = tab.getAttribute('aria-controls') === panel.id;
tab.classList.toggle('active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
tab.tabIndex = active ? 0 : -1;
});
tabs.querySelector(`[aria-controls="${CSS.escape(panel.id)}"]`)?.scrollIntoView({block: 'nearest', inline: 'nearest'});
};
const createTab = (panel, title) => {
if (!tabs) return null;
const tab = document.createElement('button');
tab.type = 'button';
tab.className = 'privacy-category-tab';
tab.setAttribute('role', 'tab');
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('aria-controls', panel.id);
tab.tabIndex = -1;
const label = document.createElement('span');
label.dataset.categoryTabTitle = '';
label.textContent = title || newCategoryLabel;
tab.appendChild(label);
tabs.appendChild(tab);
return tab;
};
const bindPanel = panel => {
if (!panel || panel.dataset.bound === 'true') return;
panel.dataset.bound = 'true';
const nameInput = panel.querySelector('[data-category-name]');
const tab = tabs?.querySelector(`[aria-controls="${CSS.escape(panel.id)}"]`);
nameInput?.addEventListener('input', () => {
const label = tab?.querySelector('[data-category-tab-title]');
if (label) label.textContent = nameInput.value.trim() || newCategoryLabel;
});
panel.querySelector('[data-remove-category]')?.addEventListener('click', () => {
const allPanels = [...panels.querySelectorAll('[data-category-panel]')];
const index = allPanels.indexOf(panel);
const fallback = allPanels[index + 1] || allPanels[index - 1] || null;
tab?.remove();
panel.remove();
if (fallback) activate(fallback);
});
};
tabs?.addEventListener('click', event => {
const tab = event.target.closest('[data-category-tab]');
if (!tab) return;
activate(document.getElementById(tab.getAttribute('aria-controls')));
});
tabs?.addEventListener('keydown', event => {
const tab = event.target.closest('[data-category-tab]');
if (!tab || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
const allTabs = [...tabs.querySelectorAll('[data-category-tab]')];
let index = allTabs.indexOf(tab);
if (event.key === 'ArrowLeft') index = (index - 1 + allTabs.length) % allTabs.length;
if (event.key === 'ArrowRight') index = (index + 1) % allTabs.length;
if (event.key === 'Home') index = 0;
if (event.key === 'End') index = allTabs.length - 1;
event.preventDefault();
allTabs[index]?.focus();
allTabs[index]?.click();
});
document.querySelectorAll('[data-category-panel]').forEach(bindPanel);
const initiallyActive = document.querySelector('[data-category-panel].active') || document.querySelector('[data-category-panel]');
if (initiallyActive) activate(initiallyActive);
const bindRemoveRows = root => root.querySelectorAll('[data-remove-row]').forEach(button => button.onclick = () => button.closest('.repeatable-row')?.remove());
bindRemoveRows(document);
document.querySelectorAll('[data-add-list-row]').forEach(button => button.addEventListener('click', () => {
const list = document.getElementById(button.dataset.addListRow);
if (!list) return;
const name = list.id === 'purposes-list' ? 'purposes' : 'toms';
const row = document.createElement('div');
row.className = 'repeatable-row';
row.innerHTML = name === 'purposes'
? `<input type="text" name="${name}"><button type="button" class="button-danger button-small" data-remove-row>×</button>`
: `<textarea name="${name}" rows="2"></textarea><button type="button" class="button-danger button-small" data-remove-row>×</button>`;
list.appendChild(row);
bindRemoveRows(row);
}));
document.getElementById('add-privacy-category')?.addEventListener('click', () => {
const template = document.getElementById('privacy-category-template');
if (!template || !panels || !tabs) return;
const fragment = template.content.cloneNode(true);
const panel = fragment.querySelector('[data-category-panel]');
panel.id = `privacy-category-panel-${nextPanelId++}`;
panels.appendChild(fragment);
const inserted = panels.lastElementChild;
createTab(inserted, newCategoryLabel);
bindPanel(inserted);
activate(inserted);
inserted.querySelector('[data-category-name]')?.focus();
});
})();
</script>
{% endblock %}