diff --git a/public/index.html b/public/index.html
index 2d48c9f..4e8c756 100644
--- a/public/index.html
+++ b/public/index.html
@@ -154,6 +154,8 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
0 selected
+
+
@@ -164,7 +166,7 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
- | ID |
+ ID |
Severity |
Status |
Message |
@@ -271,11 +273,11 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
- Health
- checking...
+ - SQLite
- checking...
+ - ClickHouse
- checking...
- DB Size
- —
- Auth Server
- auth.jakach.ch
- Logged in as
- —
- - DB Path
/app/data/logging.db
- - Worker
php bin/consume
@@ -1031,6 +1033,10 @@ async function loadSettings() {
const health = await api('/health');
document.getElementById('sysHealth').textContent = health.status === 'ok' ? 'Healthy' : 'Degraded';
document.getElementById('sysHealth').className = 'badge bg-' + (health.status === 'ok' ? 'success' : 'warning');
+ document.getElementById('sysSqlite').textContent = health.sqlite === 'connected' ? 'Connected' : 'Error';
+ document.getElementById('sysSqlite').className = 'badge bg-' + (health.sqlite === 'connected' ? 'success' : 'danger');
+ document.getElementById('sysClickhouse').textContent = health.clickhouse === 'connected' ? 'Connected' : 'Error';
+ document.getElementById('sysClickhouse').className = 'badge bg-' + (health.clickhouse === 'connected' ? 'success' : 'danger');
if (health.db_size) {
document.getElementById('sysDbSize').textContent = health.db_size;
}
@@ -1296,8 +1302,41 @@ let selectedAlertIds = new Set();
function toggleAlertSelection(id) {
if (selectedAlertIds.has(id)) selectedAlertIds.delete(id);
else selectedAlertIds.add(id);
+ updateBulkBar();
+}
+
+function updateBulkBar() {
document.getElementById('bulkBar').classList.toggle('d-none', selectedAlertIds.size === 0);
document.getElementById('selectedCount').textContent = selectedAlertIds.size;
+ const cb = document.getElementById('selectAllCheckbox');
+ if (cb) cb.checked = selectedAlertIds.size > 0 && selectedAlertIds.size === state.alerts.length;
+}
+
+function toggleSelectAll() {
+ const cb = document.getElementById('selectAllCheckbox');
+ if (cb.checked) {
+ state.alerts.forEach(a => selectedAlertIds.add(a.id));
+ } else {
+ selectedAlertIds.clear();
+ }
+ updateBulkBar();
+ renderAlerts();
+}
+
+function selectAllVisible() {
+ state.alerts.forEach(a => selectedAlertIds.add(a.id));
+ const cb = document.getElementById('selectAllCheckbox');
+ if (cb) cb.checked = true;
+ updateBulkBar();
+ renderAlerts();
+}
+
+function deselectAll() {
+ selectedAlertIds.clear();
+ const cb = document.getElementById('selectAllCheckbox');
+ if (cb) cb.checked = false;
+ updateBulkBar();
+ renderAlerts();
}
function renderAlerts(query) {
@@ -1345,7 +1384,7 @@ function renderAlerts(query) {
}
const label = document.getElementById('searchInput').value.trim() ? 'search results' : 'alerts';
document.getElementById('alertsCount').textContent = sorted.length + ' ' + label;
- document.getElementById('bulkBar').classList.toggle('d-none', selectedAlertIds.size === 0);
+ updateBulkBar();
}
async function bulkAction(status) {
@@ -1363,8 +1402,18 @@ async function exportAlerts(format) {
const ids = Array.from(selectedAlertIds);
if (!ids.length) { toast('Select alerts first', 'warning'); return; }
try {
- const res = await api('/alerts/export', { method: 'POST', body: JSON.stringify({ ids, format }) });
- if (format === 'json') {
+ const res = await api('/alerts/export', { method: 'POST', body: JSON.stringify({ ids, format: 'json' }) });
+ const alerts = res.data || [];
+ if (format === 'csv') {
+ const csv = ['ID,Rule,Severity,Status,Message,Source,Created', ...alerts.map(a =>
+ [a.id, '"' + (a.rule_name || '').replace(/"/g, '""') + '"', a.severity, a.status,
+ '"' + (a.message || '').replace(/"/g, '""') + '"', a.source_name || '', a.created_at].join(','))].join('\n');
+ const blob = new Blob([csv], { type: 'text/csv' });
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = 'alerts.csv';
+ a.click();
+ } else {
const blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
diff --git a/src/Api/Router.php b/src/Api/Router.php
index 6258a81..e5f952a 100644
--- a/src/Api/Router.php
+++ b/src/Api/Router.php
@@ -512,48 +512,19 @@ class Router
return $this->repo->getLogContext($id, min($before, 50), min($after, 50));
}
- private function exportAlerts(): never
+ private function exportAlerts(): array
{
$body = json_decode(file_get_contents('php://input'), true);
$ids = $body['ids'] ?? [];
- $format = $body['format'] ?? 'json';
$alerts = $this->repo->exportAlerts($ids);
-
- if ($format === 'csv') {
- header('Content-Type: text/csv');
- header('Content-Disposition: attachment; filename="alerts.csv"');
- $out = fopen('php://output', 'w');
- fputcsv($out, ['ID', 'Rule', 'Severity', 'Status', 'Message', 'Source', 'Created']);
- foreach ($alerts as $a) {
- fputcsv($out, [$a->id, $a->ruleName, $a->severity->value, $a->status->value, $a->message, $a->sourceName ?? '', $a->createdAt->format('c')]);
- }
- fclose($out);
- } else {
- echo json_encode(['data' => array_map(fn($a) => $a->toArray(), $alerts)], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
- }
- exit;
+ return array_map(fn($a) => $a->toArray(), $alerts);
}
- private function exportLogs(): never
+ private function exportLogs(): array
{
$body = json_decode(file_get_contents('php://input'), true);
$ids = $body['ids'] ?? [];
- $format = $body['format'] ?? 'json';
- $logs = $this->repo->exportLogs($ids);
-
- if ($format === 'csv') {
- header('Content-Type: text/csv');
- header('Content-Disposition: attachment; filename="logs.csv"');
- $out = fopen('php://output', 'w');
- fputcsv($out, ['ID', 'Line', 'Source', 'Level', 'Created']);
- foreach ($logs as $l) {
- fputcsv($out, [$l['id'], $l['line'], $l['source_name'] ?? '', $l['level'] ?? '', $l['created_at']]);
- }
- fclose($out);
- } else {
- echo json_encode(['data' => $logs], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
- }
- exit;
+ return $this->repo->exportLogs($ids);
}
private function runRetention(): array