@@ -790,7 +795,7 @@ async function loadRules() {
${severityBadge(r.severity)} |
${r.rate_limit_seconds ? r.rate_limit_seconds + 's' : '—'} |
${r.active ? 'Active' : 'Inactive'} |
-
|
+
|
`).join('');
}
} catch (e) { console.error('rules error', e); }
@@ -805,18 +810,51 @@ async function deleteRule(id) {
} catch (e) { toast('Delete failed', 'danger'); }
}
+function editRule(id) {
+ const r = state.rules.find(x => x.id === id);
+ if (!r) return;
+ document.getElementById('ruleModalTitle').textContent = 'Edit Rule';
+ document.getElementById('ruleFormSubmit').textContent = 'Update Rule';
+ document.getElementById('ruleFormId').value = r.id;
+ document.getElementById('ruleFormName').value = r.name;
+ document.getElementById('ruleFormPattern').value = r.pattern;
+ document.getElementById('ruleFormSeverity').value = r.severity;
+ document.getElementById('ruleFormRateLimit').value = r.rate_limit_seconds || '';
+ document.getElementById('ruleFormActive').checked = r.active;
+ new bootstrap.Modal(document.getElementById('ruleModal')).show();
+}
+
+function resetRuleForm() {
+ document.getElementById('ruleModalTitle').textContent = 'Add Alert Rule';
+ document.getElementById('ruleFormSubmit').textContent = 'Add Rule';
+ document.getElementById('ruleFormId').value = '';
+ document.getElementById('ruleForm').reset();
+ document.getElementById('ruleFormActive').checked = true;
+}
+
+document.getElementById('ruleModal').addEventListener('hidden.bs.modal', resetRuleForm);
+
document.getElementById('ruleForm').addEventListener('submit', async e => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
if (data.rate_limit_seconds) data.rate_limit_seconds = parseInt(data.rate_limit_seconds);
else data.rate_limit_seconds = null;
+ data.active = !!data.active;
+
+ const id = data.id;
+ delete data.id;
+
try {
- await api('/rules', { method: 'POST', body: JSON.stringify(data) });
- toast('Rule added');
+ if (id) {
+ await api('/rules/' + id, { method: 'PUT', body: JSON.stringify(data) });
+ toast('Rule updated');
+ } else {
+ await api('/rules', { method: 'POST', body: JSON.stringify(data) });
+ toast('Rule added');
+ }
bootstrap.Modal.getInstance(document.getElementById('ruleModal')).hide();
- e.target.reset();
loadRules();
- } catch (err) { toast('Failed to add rule', 'danger'); }
+ } catch (err) { toast('Failed to save rule', 'danger'); }
});
// --- SETTINGS ---
diff --git a/src/Api/Router.php b/src/Api/Router.php
index 05cbdb8..ccb73c3 100644
--- a/src/Api/Router.php
+++ b/src/Api/Router.php
@@ -65,6 +65,8 @@ class Router
$path === '/rules' && $method === 'POST' => $this->createRule(),
preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'DELETE'
=> $this->deleteEntity('rule', (int) $m[1]),
+ preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'PUT'
+ => $this->updateRule((int) $m[1]),
$path === '/alerts' && $method === 'GET' => $this->getAlerts(),
$path === '/alerts/search' && $method === 'GET' => $this->searchAlerts(),
@@ -185,6 +187,19 @@ class Router
return ['status' => 'deleted', 'id' => $id];
}
+ private function updateRule(int $id): mixed
+ {
+ $body = json_decode(file_get_contents('php://input'), true);
+ return $this->repo->updateRule(
+ id: $id,
+ name: $body['name'],
+ pattern: $body['pattern'],
+ severity: $body['severity'] ?? 'warning',
+ rateLimitSeconds: $body['rate_limit_seconds'] ?? null,
+ active: $body['active'] ?? true,
+ );
+ }
+
private function getAlerts(): mixed
{
$limit = (int) ($_GET['limit'] ?? 100);
diff --git a/src/Storage/Repository.php b/src/Storage/Repository.php
index 68a613c..0cbb14b 100644
--- a/src/Storage/Repository.php
+++ b/src/Storage/Repository.php
@@ -82,6 +82,15 @@ class Repository
$this->db->pdo()->prepare("DELETE FROM rules WHERE id = ?")->execute([$id]);
}
+ public function updateRule(int $id, string $name, string $pattern, string $severity, ?int $rateLimitSeconds = null, bool $active = true): Rule
+ {
+ $stmt = $this->db->pdo()->prepare(
+ "UPDATE rules SET name = ?, pattern = ?, severity = ?, rate_limit_seconds = ?, active = ? WHERE id = ?"
+ );
+ $stmt->execute([$name, $pattern, $severity, $rateLimitSeconds, (int) $active, $id]);
+ return $this->getRule($id);
+ }
+
// --- Alerts ---
public function createAlert(int $ruleId, string $ruleName, string $severity, string $message, string $rawLine, ?int $sourceId = null, ?string $sourceName = null): Alert