+55
-6
@@ -154,6 +154,8 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 align-items-center mb-2" id="bulkBar" style="display:none">
|
<div class="d-flex gap-2 align-items-center mb-2" id="bulkBar" style="display:none">
|
||||||
<small class="text-secondary"><span id="selectedCount">0</span> selected</small>
|
<small class="text-secondary"><span id="selectedCount">0</span> selected</small>
|
||||||
|
<button class="btn btn-outline-primary btn-sm py-0" onclick="selectAllVisible()"><i class="bi bi-check-all"></i> Select all</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm py-0" onclick="deselectAll()"><i class="bi bi-x"></i> Clear</button>
|
||||||
<button class="btn btn-outline-success btn-sm" onclick="bulkAction('acknowledged')"><i class="bi bi-check"></i> Acknowledge</button>
|
<button class="btn btn-outline-success btn-sm" onclick="bulkAction('acknowledged')"><i class="bi bi-check"></i> Acknowledge</button>
|
||||||
<button class="btn btn-outline-secondary btn-sm" onclick="bulkAction('resolved')"><i class="bi bi-check-all"></i> Resolve</button>
|
<button class="btn btn-outline-secondary btn-sm" onclick="bulkAction('resolved')"><i class="bi bi-check-all"></i> Resolve</button>
|
||||||
<button class="btn btn-outline-info btn-sm" onclick="exportAlerts('json')"><i class="bi bi-download"></i> Export JSON</button>
|
<button class="btn btn-outline-info btn-sm" onclick="exportAlerts('json')"><i class="bi bi-download"></i> Export JSON</button>
|
||||||
@@ -164,7 +166,7 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-sm mb-0" id="alertsTable">
|
<table class="table table-hover table-sm mb-0" id="alertsTable">
|
||||||
<thead class="table-dark"><tr>
|
<thead class="table-dark"><tr>
|
||||||
<th style="width:80px;cursor:pointer" onclick="sortAlerts('id')">ID <i class="bi bi-arrow-down-up" style="font-size:.7rem"></i></th>
|
<th style="width:80px"><input type="checkbox" class="form-check-input" id="selectAllCheckbox" onchange="toggleSelectAll()" title="Select all visible"> <span style="cursor:pointer" onclick="sortAlerts('id')">ID <i class="bi bi-arrow-down-up" style="font-size:.7rem"></i></span></th>
|
||||||
<th style="width:90px;cursor:pointer" onclick="sortAlerts('severity')">Severity <i class="bi bi-arrow-down-up" style="font-size:.7rem"></i></th>
|
<th style="width:90px;cursor:pointer" onclick="sortAlerts('severity')">Severity <i class="bi bi-arrow-down-up" style="font-size:.7rem"></i></th>
|
||||||
<th style="width:100px;cursor:pointer" onclick="sortAlerts('status')">Status <i class="bi bi-arrow-down-up" style="font-size:.7rem"></i></th>
|
<th style="width:100px;cursor:pointer" onclick="sortAlerts('status')">Status <i class="bi bi-arrow-down-up" style="font-size:.7rem"></i></th>
|
||||||
<th>Message</th>
|
<th>Message</th>
|
||||||
@@ -271,11 +273,11 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="row mb-0">
|
<dl class="row mb-0">
|
||||||
<dt class="col-sm-4">Health</dt><dd class="col-sm-8"><span id="sysHealth" class="badge bg-secondary">checking...</span></dd>
|
<dt class="col-sm-4">Health</dt><dd class="col-sm-8"><span id="sysHealth" class="badge bg-secondary">checking...</span></dd>
|
||||||
|
<dt class="col-sm-4">SQLite</dt><dd class="col-sm-8"><span id="sysSqlite" class="badge bg-secondary">checking...</span></dd>
|
||||||
|
<dt class="col-sm-4">ClickHouse</dt><dd class="col-sm-8"><span id="sysClickhouse" class="badge bg-secondary">checking...</span></dd>
|
||||||
<dt class="col-sm-4">DB Size</dt><dd class="col-sm-8"><span id="sysDbSize">—</span></dd>
|
<dt class="col-sm-4">DB Size</dt><dd class="col-sm-8"><span id="sysDbSize">—</span></dd>
|
||||||
<dt class="col-sm-4">Auth Server</dt><dd class="col-sm-8"><a href="https://auth.jakach.ch" target="_blank">auth.jakach.ch</a></dd>
|
<dt class="col-sm-4">Auth Server</dt><dd class="col-sm-8"><a href="https://auth.jakach.ch" target="_blank">auth.jakach.ch</a></dd>
|
||||||
<dt class="col-sm-4">Logged in as</dt><dd class="col-sm-8" id="settingsUser">—</dd>
|
<dt class="col-sm-4">Logged in as</dt><dd class="col-sm-8" id="settingsUser">—</dd>
|
||||||
<dt class="col-sm-4">DB Path</dt><dd class="col-sm-8"><code>/app/data/logging.db</code></dd>
|
|
||||||
<dt class="col-sm-4">Worker</dt><dd class="col-sm-8"><code>php bin/consume</code></dd>
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1031,6 +1033,10 @@ async function loadSettings() {
|
|||||||
const health = await api('/health');
|
const health = await api('/health');
|
||||||
document.getElementById('sysHealth').textContent = health.status === 'ok' ? 'Healthy' : 'Degraded';
|
document.getElementById('sysHealth').textContent = health.status === 'ok' ? 'Healthy' : 'Degraded';
|
||||||
document.getElementById('sysHealth').className = 'badge bg-' + (health.status === 'ok' ? 'success' : 'warning');
|
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) {
|
if (health.db_size) {
|
||||||
document.getElementById('sysDbSize').textContent = health.db_size;
|
document.getElementById('sysDbSize').textContent = health.db_size;
|
||||||
}
|
}
|
||||||
@@ -1296,8 +1302,41 @@ let selectedAlertIds = new Set();
|
|||||||
function toggleAlertSelection(id) {
|
function toggleAlertSelection(id) {
|
||||||
if (selectedAlertIds.has(id)) selectedAlertIds.delete(id);
|
if (selectedAlertIds.has(id)) selectedAlertIds.delete(id);
|
||||||
else selectedAlertIds.add(id);
|
else selectedAlertIds.add(id);
|
||||||
|
updateBulkBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBulkBar() {
|
||||||
document.getElementById('bulkBar').classList.toggle('d-none', selectedAlertIds.size === 0);
|
document.getElementById('bulkBar').classList.toggle('d-none', selectedAlertIds.size === 0);
|
||||||
document.getElementById('selectedCount').textContent = selectedAlertIds.size;
|
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) {
|
function renderAlerts(query) {
|
||||||
@@ -1345,7 +1384,7 @@ function renderAlerts(query) {
|
|||||||
}
|
}
|
||||||
const label = document.getElementById('searchInput').value.trim() ? 'search results' : 'alerts';
|
const label = document.getElementById('searchInput').value.trim() ? 'search results' : 'alerts';
|
||||||
document.getElementById('alertsCount').textContent = sorted.length + ' ' + label;
|
document.getElementById('alertsCount').textContent = sorted.length + ' ' + label;
|
||||||
document.getElementById('bulkBar').classList.toggle('d-none', selectedAlertIds.size === 0);
|
updateBulkBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkAction(status) {
|
async function bulkAction(status) {
|
||||||
@@ -1363,8 +1402,18 @@ async function exportAlerts(format) {
|
|||||||
const ids = Array.from(selectedAlertIds);
|
const ids = Array.from(selectedAlertIds);
|
||||||
if (!ids.length) { toast('Select alerts first', 'warning'); return; }
|
if (!ids.length) { toast('Select alerts first', 'warning'); return; }
|
||||||
try {
|
try {
|
||||||
const res = await api('/alerts/export', { method: 'POST', body: JSON.stringify({ ids, format }) });
|
const res = await api('/alerts/export', { method: 'POST', body: JSON.stringify({ ids, format: 'json' }) });
|
||||||
if (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 blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
|
|||||||
+4
-33
@@ -512,48 +512,19 @@ class Router
|
|||||||
return $this->repo->getLogContext($id, min($before, 50), min($after, 50));
|
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);
|
$body = json_decode(file_get_contents('php://input'), true);
|
||||||
$ids = $body['ids'] ?? [];
|
$ids = $body['ids'] ?? [];
|
||||||
$format = $body['format'] ?? 'json';
|
|
||||||
$alerts = $this->repo->exportAlerts($ids);
|
$alerts = $this->repo->exportAlerts($ids);
|
||||||
|
return array_map(fn($a) => $a->toArray(), $alerts);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function exportLogs(): never
|
private function exportLogs(): array
|
||||||
{
|
{
|
||||||
$body = json_decode(file_get_contents('php://input'), true);
|
$body = json_decode(file_get_contents('php://input'), true);
|
||||||
$ids = $body['ids'] ?? [];
|
$ids = $body['ids'] ?? [];
|
||||||
$format = $body['format'] ?? 'json';
|
return $this->repo->exportLogs($ids);
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function runRetention(): array
|
private function runRetention(): array
|
||||||
|
|||||||
Reference in New Issue
Block a user