+31
-10
@@ -322,10 +322,19 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header"><h5 class="modal-title">Alert Detail</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
|
<div class="modal-header"><h5 class="modal-title">Alert Detail</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
|
||||||
<div class="modal-body" id="detailBody"></div>
|
<div class="modal-body" id="detailBody"></div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer d-flex justify-content-between">
|
||||||
<button class="btn btn-success btn-sm" id="ackBtn"><i class="bi bi-check-circle"></i> Acknowledge</button>
|
<div>
|
||||||
|
<select class="form-select form-select-sm" id="statusSelect">
|
||||||
|
<option value="open">Open</option>
|
||||||
|
<option value="acknowledged">Acknowledge</option>
|
||||||
|
<option value="resolved">Resolved</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary btn-sm" id="updateStatusBtn"><i class="bi bi-check2"></i> Update</button>
|
||||||
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div></div></div>
|
</div></div></div>
|
||||||
|
|
||||||
<!-- Source Modal -->
|
<!-- Source Modal -->
|
||||||
@@ -629,7 +638,10 @@ async function loadAlerts() {
|
|||||||
<td class="log-line">${esc(a.message)}</td>
|
<td class="log-line">${esc(a.message)}</td>
|
||||||
<td>${esc(a.source_name || '—')}</td>
|
<td>${esc(a.source_name || '—')}</td>
|
||||||
<td class="text-secondary" style="white-space:nowrap">${new Date(a.created_at).toLocaleString()}</td>
|
<td class="text-secondary" style="white-space:nowrap">${new Date(a.created_at).toLocaleString()}</td>
|
||||||
<td>${a.status === 'open' ? `<button class="btn btn-outline-success btn-sm py-0" onclick="event.stopPropagation();ackAlert(${a.id})"><i class="bi bi-check"></i></button>` : ''}</td>
|
<td style="white-space:nowrap">
|
||||||
|
${a.status === 'open' ? `<button class="btn btn-outline-success btn-sm py-0 me-1" onclick="event.stopPropagation();quickAction(${a.id},'acknowledged')" title="Acknowledge"><i class="bi bi-check"></i></button>` : ''}
|
||||||
|
${a.status !== 'resolved' ? `<button class="btn btn-outline-secondary btn-sm py-0" onclick="event.stopPropagation();quickAction(${a.id},'resolved')" title="Resolve"><i class="bi bi-check-all"></i></button>` : ''}
|
||||||
|
</td>
|
||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
}
|
}
|
||||||
const label = query ? 'search results' : 'alerts';
|
const label = query ? 'search results' : 'alerts';
|
||||||
@@ -655,21 +667,30 @@ function showAlert(id) {
|
|||||||
<dt class="col-sm-3">Message</dt><dd class="col-sm-9">${esc(a.message)}</dd>
|
<dt class="col-sm-3">Message</dt><dd class="col-sm-9">${esc(a.message)}</dd>
|
||||||
<dt class="col-sm-3">Raw Line</dt><dd class="col-sm-9"><pre class="raw-line">${esc(a.raw_line)}</pre></dd>
|
<dt class="col-sm-3">Raw Line</dt><dd class="col-sm-9"><pre class="raw-line">${esc(a.raw_line)}</pre></dd>
|
||||||
</dl>`;
|
</dl>`;
|
||||||
document.getElementById('ackBtn').style.display = a.status === 'open' ? '' : 'none';
|
document.getElementById('statusSelect').value = a.status;
|
||||||
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
new bootstrap.Modal(document.getElementById('detailModal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('ackBtn').addEventListener('click', async () => {
|
document.getElementById('updateStatusBtn').addEventListener('click', async () => {
|
||||||
if (currentAlertId) await ackAlert(currentAlertId);
|
const newStatus = document.getElementById('statusSelect').value;
|
||||||
|
if (currentAlertId) await updateAlertStatus(currentAlertId, newStatus);
|
||||||
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
bootstrap.Modal.getInstance(document.getElementById('detailModal')).hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function ackAlert(id) {
|
async function updateAlertStatus(id, status) {
|
||||||
try {
|
try {
|
||||||
await api(`/alerts/${id}/ack`, { method: 'POST' });
|
await api(`/alerts/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) });
|
||||||
toast('Alert #' + id + ' acknowledged');
|
toast('Alert #' + id + ' ' + status);
|
||||||
loadPage(document.querySelector('.sidebar .nav-link.active')?.dataset.page || 'dashboard');
|
loadPage(document.querySelector('.sidebar .nav-link.active')?.dataset.page || 'dashboard');
|
||||||
} catch (e) { toast('Failed to acknowledge', 'danger'); }
|
} catch (e) { toast('Failed to update status', 'danger'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickAction(id, status) {
|
||||||
|
try {
|
||||||
|
await api(`/alerts/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) });
|
||||||
|
toast('Alert #' + id + ' ' + status);
|
||||||
|
loadPage(document.querySelector('.sidebar .nav-link.active')?.dataset.page || 'dashboard');
|
||||||
|
} catch (e) { toast('Failed', 'danger'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('filterSeverity').addEventListener('change', loadAlerts);
|
document.getElementById('filterSeverity').addEventListener('change', loadAlerts);
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ class Router
|
|||||||
$path === '/alerts/search' && $method === 'GET' => $this->searchAlerts(),
|
$path === '/alerts/search' && $method === 'GET' => $this->searchAlerts(),
|
||||||
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST'
|
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST'
|
||||||
=> $this->ackAlert((int) $m[1]),
|
=> $this->ackAlert((int) $m[1]),
|
||||||
|
preg_match('#^/alerts/(\d+)/status$#', $path, $m) && $method === 'POST'
|
||||||
|
=> $this->updateAlertStatus((int) $m[1]),
|
||||||
preg_match('#^/alerts/counts$#', $path) && $method === 'GET'
|
preg_match('#^/alerts/counts$#', $path) && $method === 'GET'
|
||||||
=> $this->repo->getAlertCounts(),
|
=> $this->repo->getAlertCounts(),
|
||||||
|
|
||||||
@@ -198,6 +200,18 @@ class Router
|
|||||||
return ['status' => 'acknowledged', 'id' => $id];
|
return ['status' => 'acknowledged', 'id' => $id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function updateAlertStatus(int $id): array
|
||||||
|
{
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$status = AlertStatus::tryFrom($body['status'] ?? '');
|
||||||
|
if (!$status) {
|
||||||
|
http_response_code(400);
|
||||||
|
return ['error' => 'Invalid status. Use: open, acknowledged, resolved'];
|
||||||
|
}
|
||||||
|
$this->repo->updateAlertStatus($id, $status);
|
||||||
|
return ['status' => $status->value, 'id' => $id];
|
||||||
|
}
|
||||||
|
|
||||||
private function searchAlerts(): array
|
private function searchAlerts(): array
|
||||||
{
|
{
|
||||||
$query = $_GET['q'] ?? '';
|
$query = $_GET['q'] ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user