From 0386ee08a67dc8f095e05c672cd1140601250eed Mon Sep 17 00:00:00 2001 From: fs Date: Tue, 21 Apr 2026 22:15:43 +0200 Subject: [PATCH] feat(audit): CSV/Excel export for system-audit via core primitive Third consumer of the generic export primitive. Adds a GET /admin/system-audit/export endpoint, the reusable export dropdown to the list titlebar, and wires initListExport() into the page JS. - pages/audit/system-audit/export().php: guard + ABILITY_SYSTEM_AUDIT_VIEW, reuses the existing filter-schema.php via gridParseFiltersFromSchemaFile so exported rows match the grid under the same filters, caps the limit at 5000, and declares 11 ExportColumns (ID, created, event, status, channel, actor, actor email, target type/uuid, request id, error code) with translated enum labels for outcome/channel. - module.php: new admin/system-audit/export route. - index(default).phtml: app-list-export-dropdown partial placed above the existing purge action; exportUrl added to pageConfig via lurl('admin/system-audit/export'). - admin-system-audit-index.js: imports initListExport and invokes it whenever config.exportUrl is present. - i18n de/en: new "Actor email" key (required by TranslationKeysTest). Gates: PHPUnit 1880 OK, PHPStan 0 errors, module:sync ok. Co-Authored-By: Claude Opus 4.7 (1M context) --- modules/audit/i18n/default_de.json | 3 +- modules/audit/i18n/default_en.json | 3 +- modules/audit/module.php | 1 + .../pages/audit/system-audit/export().php | 57 +++++++++++++++++++ .../audit/system-audit/index(default).phtml | 2 + .../web/js/pages/admin-system-audit-index.js | 5 ++ 6 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 modules/audit/pages/audit/system-audit/export().php diff --git a/modules/audit/i18n/default_de.json b/modules/audit/i18n/default_de.json index 165c72b..778a710 100644 --- a/modules/audit/i18n/default_de.json +++ b/modules/audit/i18n/default_de.json @@ -68,5 +68,6 @@ "Lifecycle risks (7d)": "Lifecycle-Risiken (7d)", "Top lifecycle reason codes (7d)": "Top-Lifecycle-Fehlercodes (7d)", "Recent lifecycle risks (7d)": "Aktuelle Lifecycle-Risiken (7d)", - "Audit & logs": "Audit & Protokolle" + "Audit & logs": "Audit & Protokolle", + "Actor email": "Akteur-E-Mail" } diff --git a/modules/audit/i18n/default_en.json b/modules/audit/i18n/default_en.json index f6ce003..466b665 100644 --- a/modules/audit/i18n/default_en.json +++ b/modules/audit/i18n/default_en.json @@ -68,5 +68,6 @@ "Lifecycle risks (7d)": "Lifecycle risks (7d)", "Top lifecycle reason codes (7d)": "Top lifecycle reason codes (7d)", "Recent lifecycle risks (7d)": "Recent lifecycle risks (7d)", - "Audit & logs": "Audit & logs" + "Audit & logs": "Audit & logs", + "Actor email": "Actor email" } diff --git a/modules/audit/module.php b/modules/audit/module.php index 36a90c9..fd9c39a 100644 --- a/modules/audit/module.php +++ b/modules/audit/module.php @@ -24,6 +24,7 @@ return [ ['path' => 'admin/system-audit', 'target' => 'audit/system-audit/index'], ['path' => 'admin/system-audit/view', 'target' => 'audit/system-audit/view'], ['path' => 'admin/system-audit/data', 'target' => 'audit/system-audit/data'], + ['path' => 'admin/system-audit/export', 'target' => 'audit/system-audit/export'], ['path' => 'admin/system-audit/purge', 'target' => 'audit/system-audit/purge'], // Import audit pages ['path' => 'admin/import-audit', 'target' => 'audit/import-audit/index'], diff --git a/modules/audit/pages/audit/system-audit/export().php b/modules/audit/pages/audit/system-audit/export().php new file mode 100644 index 0000000..aeb4651 --- /dev/null +++ b/modules/audit/pages/audit/system-audit/export().php @@ -0,0 +1,57 @@ +query('limit'), 5000); +$filters['offset'] = 0; + +$result = app(SystemAuditService::class)->listPaged($filters); + +$resolveOutcomeLabel = static function (string $raw): string { + $outcome = SystemAuditOutcome::normalizeOr($raw, SystemAuditOutcome::Success); + return (string) t($outcome->labelToken()); +}; + +$resolveChannelLabel = static function (string $raw): string { + $channel = SystemAuditChannel::normalizeOr($raw, SystemAuditChannel::Web); + return strtoupper($channel->labelToken()); +}; + +$resolveActor = static function (array $row): string { + $name = trim((string) ($row['actor_user_display_name'] ?? '')); + if ($name !== '') { + return $name; + } + $email = trim((string) ($row['actor_user_email'] ?? '')); + return $email !== '' ? $email : ''; +}; + +$columns = [ + new ExportColumn(t('ID'), static fn (array $r): string => (string) ($r['id'] ?? '')), + new ExportColumn(t('Created'), static fn (array $r): string => (string) dt((string) ($r['created_at'] ?? ''), 'Y-m-d H:i:s')), + new ExportColumn(t('Event'), static fn (array $r): string => (string) ($r['event_type'] ?? '')), + new ExportColumn(t('Status'), static fn (array $r): string => $resolveOutcomeLabel((string) ($r['outcome'] ?? ''))), + new ExportColumn(t('Channel'), static fn (array $r): string => $resolveChannelLabel((string) ($r['channel'] ?? ''))), + new ExportColumn(t('Actor'), static fn (array $r): string => $resolveActor($r)), + new ExportColumn(t('Actor email'), static fn (array $r): string => (string) ($r['actor_user_email'] ?? '')), + new ExportColumn(t('Target type'), static fn (array $r): string => (string) ($r['target_type'] ?? '')), + new ExportColumn(t('Target UUID'), static fn (array $r): string => (string) ($r['target_uuid'] ?? '')), + new ExportColumn(t('Request ID'), static fn (array $r): string => (string) ($r['request_id'] ?? '')), + new ExportColumn(t('Error code'), static fn (array $r): string => (string) ($r['error_code'] ?? '')), +]; + +$filename = 'system-audit-' . date('Ymd-His') . '.csv'; + +exportSendCsv($filename, (array) ($result['rows'] ?? []), $columns, exportResolveFlavor($request)); diff --git a/modules/audit/pages/audit/system-audit/index(default).phtml b/modules/audit/pages/audit/system-audit/index(default).phtml index 064feb1..1d7d4b2 100644 --- a/modules/audit/pages/audit/system-audit/index(default).phtml +++ b/modules/audit/pages/audit/system-audit/index(default).phtml @@ -15,6 +15,7 @@ $searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null; $listTitle = t('System audit logs'); ob_start(); ?> + $clientFilterSchema, 'filterChipMeta' => $filterChipMeta, 'gridLang' => $gridLang, + 'exportUrl' => lurl('admin/system-audit/export'), 'labels' => [ 'created' => t('Created'), 'status' => t('Status'), diff --git a/modules/audit/web/js/pages/admin-system-audit-index.js b/modules/audit/web/js/pages/admin-system-audit-index.js index 13cf8a6..aea8f71 100644 --- a/modules/audit/web/js/pages/admin-system-audit-index.js +++ b/modules/audit/web/js/pages/admin-system-audit-index.js @@ -1,5 +1,6 @@ import { createListPageModule } from '/js/core/app-list-page-module.js'; import { withCurrentListReturn } from '/js/pages/app-list-utils.js'; +import { initListExport } from '/js/core/app-list-export.js'; createListPageModule({ configId: 'admin-system-audit-index', @@ -97,6 +98,10 @@ createListPageModule({ }, }) || {}; + if (gridConfig && config.exportUrl) { + initListExport({ gridConfig, exportUrl: config.exportUrl }); + } + return gridConfig; }, });