+187
-6
@@ -13,6 +13,7 @@ class Router
|
||||
private AuthMiddleware $auth;
|
||||
private Engine $engine;
|
||||
private TelegramNotifier $telegram;
|
||||
private ?string $currentUser = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -23,6 +24,11 @@ class Router
|
||||
$this->telegram = new TelegramNotifier($this->repo);
|
||||
}
|
||||
|
||||
private function user(): ?string
|
||||
{
|
||||
return $this->currentUser;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
@@ -49,10 +55,11 @@ class Router
|
||||
echo json_encode(['error' => 'Unauthorized', 'login_url' => $this->loginUrl()]);
|
||||
return;
|
||||
}
|
||||
$this->currentUser = $user['username'] ?? null;
|
||||
}
|
||||
|
||||
$result = match (true) {
|
||||
$path === '/health' && $method === 'GET' => ['status' => 'ok'],
|
||||
$path === '/health' && $method === 'GET' => $this->health(),
|
||||
|
||||
$path === '/auth/csrf' && $method === 'GET' => $this->csrfToken(),
|
||||
|
||||
@@ -70,6 +77,7 @@ class Router
|
||||
|
||||
$path === '/rules' && $method === 'GET' => $this->repo->getRules(),
|
||||
$path === '/rules' && $method === 'POST' => $this->requireCsrf(fn() => $this->createRule()),
|
||||
$path === '/rules/test' && $method === 'POST' => $this->requireCsrf(fn() => $this->testRule()),
|
||||
preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'DELETE'
|
||||
=> $this->requireCsrf(fn() => $this->deleteEntity('rule', (int) $m[1])),
|
||||
preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'PUT'
|
||||
@@ -77,14 +85,28 @@ class Router
|
||||
|
||||
$path === '/alerts' && $method === 'GET' => $this->getAlerts(),
|
||||
$path === '/alerts/search' && $method === 'GET' => $this->searchAlerts(),
|
||||
$path === '/alerts/bulk' && $method === 'POST' => $this->requireCsrf(fn() => $this->bulkAlertAction()),
|
||||
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST'
|
||||
=> $this->requireCsrf(fn() => $this->ackAlert((int) $m[1])),
|
||||
preg_match('#^/alerts/(\d+)/status$#', $path, $m) && $method === 'POST'
|
||||
=> $this->requireCsrf(fn() => $this->updateAlertStatus((int) $m[1])),
|
||||
preg_match('#^/alerts/counts$#', $path) && $method === 'GET'
|
||||
=> $this->repo->getAlertCounts(),
|
||||
preg_match('#^/alerts/export$#', $path) && $method === 'POST'
|
||||
=> $this->requireCsrf(fn() => $this->exportAlerts()),
|
||||
|
||||
preg_match('#^/logs/context/(\d+)$#', $path, $m) && $method === 'GET'
|
||||
=> $this->logContext((int) $m[1]),
|
||||
$path === '/logs/search' && $method === 'GET' => $this->searchLogs(),
|
||||
preg_match('#^/logs/export$#', $path) && $method === 'POST'
|
||||
=> $this->requireCsrf(fn() => $this->exportLogs()),
|
||||
|
||||
$path === '/system/retention' && $method === 'POST'
|
||||
=> $this->requireCsrf(fn() => $this->runRetention()),
|
||||
$path === '/system/retention' && $method === 'GET'
|
||||
=> $this->getRetentionConfig(),
|
||||
$path === '/system/audit-log' && $method === 'GET'
|
||||
=> $this->getAuditLog(),
|
||||
|
||||
$path === '/config/allowed_tokens' && $method === 'GET'
|
||||
=> ['tokens' => $this->repo->getAllowedUserTokens()],
|
||||
@@ -140,6 +162,31 @@ class Router
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
private function health(): array
|
||||
{
|
||||
$dbOk = true;
|
||||
$dbSize = 'unknown';
|
||||
try {
|
||||
$this->repo->getAlerts(1);
|
||||
$dbPath = '/app/data/logging.db';
|
||||
if (file_exists($dbPath)) {
|
||||
$bytes = filesize($dbPath);
|
||||
$dbSize = $bytes > 1073741824 ? round($bytes / 1073741824, 1) . ' GB'
|
||||
: ($bytes > 1048576 ? round($bytes / 1048576, 1) . ' MB'
|
||||
: round($bytes / 1024, 1) . ' KB');
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$dbOk = false;
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $dbOk ? 'ok' : 'degraded',
|
||||
'database' => $dbOk ? 'connected' : 'error',
|
||||
'db_size' => $dbSize,
|
||||
'time' => date('c'),
|
||||
];
|
||||
}
|
||||
|
||||
private function loginUrl(): string
|
||||
{
|
||||
$redirect = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/oauth.php';
|
||||
@@ -177,32 +224,42 @@ class Router
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$type = LogSourceType::from($body['type'] ?? '');
|
||||
return $this->repo->createSource(
|
||||
$result = $this->repo->createSource(
|
||||
name: $body['name'],
|
||||
type: $type,
|
||||
address: $body['address'],
|
||||
labels: $body['labels'] ?? [],
|
||||
);
|
||||
$this->repo->logAudit('create', 'source', $result->id, 'Source "' . ($body['name'] ?? '') . '" created', $this->user());
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function createRule(): mixed
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
return $this->repo->createRule(
|
||||
$result = $this->repo->createRule(
|
||||
name: $body['name'],
|
||||
pattern: $body['pattern'],
|
||||
severity: $body['severity'] ?? 'warning',
|
||||
rateLimitSeconds: $body['rate_limit_seconds'] ?? null,
|
||||
);
|
||||
$this->repo->logAudit('create', 'rule', $result->id, 'Rule "' . ($body['name'] ?? '') . '" created', $this->user());
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function deleteEntity(string $type, int $id): array
|
||||
{
|
||||
$name = '';
|
||||
if ($type === 'source') {
|
||||
$s = $this->repo->getSource($id);
|
||||
$name = $s ? $s->name : '';
|
||||
$this->repo->deleteSource($id);
|
||||
} else {
|
||||
$r = $this->repo->getRule($id);
|
||||
$name = $r ? $r->name : '';
|
||||
$this->repo->deleteRule($id);
|
||||
}
|
||||
$this->repo->logAudit('delete', $type, $id, ucfirst($type) . ' "' . $name . '" deleted', $this->user());
|
||||
return ['status' => 'deleted', 'id' => $id];
|
||||
}
|
||||
|
||||
@@ -223,7 +280,7 @@ class Router
|
||||
private function updateRule(int $id): mixed
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
return $this->repo->updateRule(
|
||||
$result = $this->repo->updateRule(
|
||||
id: $id,
|
||||
name: $body['name'],
|
||||
pattern: $body['pattern'],
|
||||
@@ -231,6 +288,8 @@ class Router
|
||||
rateLimitSeconds: $body['rate_limit_seconds'] ?? null,
|
||||
active: $body['active'] ?? true,
|
||||
);
|
||||
$this->repo->logAudit('update', 'rule', $id, 'Rule "' . ($body['name'] ?? '') . '" updated', $this->user());
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getAlerts(): mixed
|
||||
@@ -239,12 +298,13 @@ class Router
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$status = $_GET['status'] ?? null;
|
||||
$severity = $_GET['severity'] ?? null;
|
||||
return $this->repo->getAlerts($limit, $offset, $status, $severity);
|
||||
return $this->repo->getAlerts($limit, $offset, $status, $severity, $_GET['since'] ?? null, $_GET['until'] ?? null);
|
||||
}
|
||||
|
||||
private function ackAlert(int $id): array
|
||||
{
|
||||
$this->repo->updateAlertStatus($id, AlertStatus::Acknowledged);
|
||||
$this->repo->logAudit('acknowledge', 'alert', $id, null, $this->user());
|
||||
return ['status' => 'acknowledged', 'id' => $id];
|
||||
}
|
||||
|
||||
@@ -257,6 +317,7 @@ class Router
|
||||
return ['error' => 'Invalid status. Use: open, acknowledged, resolved'];
|
||||
}
|
||||
$this->repo->updateAlertStatus($id, $status);
|
||||
$this->repo->logAudit('status_change', 'alert', $id, 'Status set to ' . $status->value, $this->user());
|
||||
return ['status' => $status->value, 'id' => $id];
|
||||
}
|
||||
|
||||
@@ -278,7 +339,7 @@ class Router
|
||||
}
|
||||
$limit = (int) ($_GET['limit'] ?? 200);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
return ['data' => $this->repo->searchLogEntries($query, $limit, $offset)];
|
||||
return ['data' => $this->repo->searchLogEntries($query, $limit, $offset, $_GET['since'] ?? null, $_GET['until'] ?? null)];
|
||||
}
|
||||
|
||||
private function updateAllowedTokens(): array
|
||||
@@ -286,6 +347,7 @@ class Router
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$tokens = $body['tokens'] ?? [];
|
||||
$this->repo->setAllowedUserTokens($tokens);
|
||||
$this->repo->logAudit('update', 'config', null, 'Allowed tokens updated', $this->user());
|
||||
return ['status' => 'saved', 'tokens' => $this->repo->getAllowedUserTokens()];
|
||||
}
|
||||
|
||||
@@ -327,6 +389,7 @@ class Router
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$this->repo->setConfig('telegram_bot_token', $body['bot_token'] ?? '');
|
||||
$this->repo->setConfig('telegram_chat_id', $body['chat_id'] ?? '');
|
||||
$this->repo->logAudit('update', 'config', null, 'Telegram config updated', $this->user());
|
||||
return $this->getTelegramConfig();
|
||||
}
|
||||
|
||||
@@ -352,6 +415,7 @@ class Router
|
||||
private function deleteFalsePositive(int $id): array
|
||||
{
|
||||
$this->repo->deleteFalsePositive($id);
|
||||
$this->repo->logAudit('delete', 'false_positive', $id, null, $this->user());
|
||||
return ['status' => 'deleted', 'id' => $id];
|
||||
}
|
||||
|
||||
@@ -369,4 +433,121 @@ class Router
|
||||
}
|
||||
return $fn();
|
||||
}
|
||||
|
||||
// --- New Handlers ---
|
||||
|
||||
private function testRule(): array
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$line = $body['line'] ?? '';
|
||||
if (empty($line)) {
|
||||
http_response_code(400);
|
||||
return ['error' => 'Missing "line" field'];
|
||||
}
|
||||
|
||||
$rules = $this->repo->getActiveRules();
|
||||
$matches = [];
|
||||
foreach ($rules as $rule) {
|
||||
$delimiter = $rule->pattern[0] ?? '/';
|
||||
if (preg_match($rule->pattern, $line)) {
|
||||
$matches[] = [
|
||||
'rule_id' => $rule->id,
|
||||
'rule_name' => $rule->name,
|
||||
'severity' => $rule->severity->value,
|
||||
'is_false_positive' => $this->repo->isFalsePositive($line),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['data' => $matches, 'line' => substr($line, 0, 200)];
|
||||
}
|
||||
|
||||
private function bulkAlertAction(): array
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$ids = $body['ids'] ?? [];
|
||||
$status = AlertStatus::tryFrom($body['status'] ?? '');
|
||||
if (empty($ids) || !$status) {
|
||||
http_response_code(400);
|
||||
return ['error' => 'Missing "ids" array or valid "status"'];
|
||||
}
|
||||
$count = $this->repo->bulkUpdateAlertStatus($ids, $status);
|
||||
$this->repo->logAudit('bulk_status', 'alert', null, count($ids) . ' alerts set to ' . $status->value, $this->user());
|
||||
return ['updated' => $count];
|
||||
}
|
||||
|
||||
private function logContext(int $id): array
|
||||
{
|
||||
$before = (int) ($_GET['before'] ?? 5);
|
||||
$after = (int) ($_GET['after'] ?? 5);
|
||||
return $this->repo->getLogContext($id, min($before, 50), min($after, 50));
|
||||
}
|
||||
|
||||
private function exportAlerts(): never
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
private function exportLogs(): never
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
private function runRetention(): array
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$logDays = (int) ($body['log_days'] ?? 30);
|
||||
$alertDays = (int) ($body['alert_days'] ?? 90);
|
||||
$result = $this->repo->purgeOldData($logDays, $alertDays);
|
||||
$this->repo->logAudit('retention_purge', 'system', null, json_encode($result), $this->user());
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getRetentionConfig(): array
|
||||
{
|
||||
return [
|
||||
'log_days' => (int) ($this->repo->getConfig('retention_log_days', '30')),
|
||||
'alert_days' => (int) ($this->repo->getConfig('retention_alert_days', '90')),
|
||||
];
|
||||
}
|
||||
|
||||
private function getAuditLog(): array
|
||||
{
|
||||
$limit = (int) ($_GET['limit'] ?? 50);
|
||||
return ['data' => $this->repo->getAuditLog($limit)];
|
||||
}
|
||||
}
|
||||
@@ -181,5 +181,17 @@ $this->pdo->exec("
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
");
|
||||
|
||||
$this->pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
action TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id INTEGER,
|
||||
details TEXT,
|
||||
username TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
");
|
||||
}
|
||||
}
|
||||
+136
-13
@@ -121,7 +121,7 @@ class Repository
|
||||
return $row ? Alert::fromRow($row) : null;
|
||||
}
|
||||
|
||||
public function getAlerts(int $limit = 100, int $offset = 0, ?string $status = null, ?string $severity = null): array
|
||||
public function getAlerts(int $limit = 100, int $offset = 0, ?string $status = null, ?string $severity = null, ?string $since = null, ?string $until = null): array
|
||||
{
|
||||
$where = [];
|
||||
$params = [];
|
||||
@@ -134,6 +134,14 @@ class Repository
|
||||
$where[] = 'severity = ?';
|
||||
$params[] = $severity;
|
||||
}
|
||||
if ($since) {
|
||||
$where[] = 'created_at >= ?';
|
||||
$params[] = $since;
|
||||
}
|
||||
if ($until) {
|
||||
$where[] = 'created_at <= ?';
|
||||
$params[] = $until;
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM alerts";
|
||||
if ($where) {
|
||||
@@ -187,29 +195,52 @@ class Repository
|
||||
$stmt->execute([$line, $sourceId, $sourceName, $level]);
|
||||
}
|
||||
|
||||
public function searchLogEntries(string $query, int $limit = 200, int $offset = 0): array
|
||||
public function searchLogEntries(string $query, int $limit = 200, int $offset = 0, ?string $since = null, ?string $until = null): array
|
||||
{
|
||||
$query = trim($query);
|
||||
if ($query === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($since) {
|
||||
$where[] = 'e.created_at >= ?';
|
||||
$params[] = $since;
|
||||
}
|
||||
if ($until) {
|
||||
$where[] = 'e.created_at <= ?';
|
||||
$params[] = $until;
|
||||
}
|
||||
|
||||
if ($query === '*') {
|
||||
$stmt = $this->db->pdo()->prepare(
|
||||
"SELECT * FROM log_entries ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||||
);
|
||||
$stmt->execute([$limit, $offset]);
|
||||
$sql = "SELECT * FROM log_entries e";
|
||||
if ($where) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
$sql .= " ORDER BY e.created_at DESC LIMIT ? OFFSET ?";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
$stmt = $this->db->pdo()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
$like = $this->toLikePattern($query);
|
||||
$stmt = $this->db->pdo()->prepare(
|
||||
"SELECT e.* FROM log_entries e
|
||||
WHERE e.line LIKE ?
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT ? OFFSET ?"
|
||||
);
|
||||
$stmt->execute([$like, $limit, $offset]);
|
||||
$where[] = 'e.line LIKE ?';
|
||||
$params[] = $like;
|
||||
$sql = "SELECT e.* FROM log_entries e";
|
||||
|
||||
if ($where) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
$sql .= " ORDER BY e.created_at DESC LIMIT ? OFFSET ?";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$stmt = $this->db->pdo()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
@@ -338,4 +369,96 @@ class Repository
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Audit Log ---
|
||||
|
||||
public function logAudit(string $action, string $entityType, ?int $entityId = null, ?string $details = null, ?string $username = null): void
|
||||
{
|
||||
$stmt = $this->db->pdo()->prepare(
|
||||
"INSERT INTO audit_log (action, entity_type, entity_id, details, username) VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$action, $entityType, $entityId, $details, $username]);
|
||||
}
|
||||
|
||||
public function getAuditLog(int $limit = 50): array
|
||||
{
|
||||
return $this->db->pdo()->prepare(
|
||||
"SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?"
|
||||
)->execute([$limit])->fetchAll();
|
||||
}
|
||||
|
||||
// --- Retention ---
|
||||
|
||||
public function purgeOldData(int $logDays = 30, int $alertDays = 90): array
|
||||
{
|
||||
$deletedLogs = $this->db->pdo()->prepare(
|
||||
"DELETE FROM log_entries WHERE created_at < datetime('now', ?)"
|
||||
)->execute(['-' . $logDays . ' days'])->rowCount();
|
||||
|
||||
$deletedAlerts = $this->db->pdo()->prepare(
|
||||
"DELETE FROM alerts WHERE status = 'resolved' AND created_at < datetime('now', ?)"
|
||||
)->execute(['-' . $alertDays . ' days'])->rowCount();
|
||||
|
||||
$this->db->pdo()->exec("DELETE FROM rate_limiter WHERE window_start < " . (time() - 86400));
|
||||
|
||||
return ['log_entries_deleted' => $deletedLogs, 'alerts_deleted' => $deletedAlerts];
|
||||
}
|
||||
|
||||
// --- Log Context ---
|
||||
|
||||
public function getLogContext(int $id, int $before = 5, int $after = 5): array
|
||||
{
|
||||
$beforeStmt = $this->db->pdo()->prepare(
|
||||
"SELECT * FROM log_entries WHERE id < ? ORDER BY id DESC LIMIT ?"
|
||||
);
|
||||
$beforeStmt->execute([$id, $before]);
|
||||
$beforeRows = array_reverse($beforeStmt->fetchAll());
|
||||
|
||||
$current = $this->db->pdo()->prepare("SELECT * FROM log_entries WHERE id = ?");
|
||||
$current->execute([$id]);
|
||||
$currentRow = $current->fetch();
|
||||
|
||||
$afterStmt = $this->db->pdo()->prepare(
|
||||
"SELECT * FROM log_entries WHERE id > ? ORDER BY id ASC LIMIT ?"
|
||||
);
|
||||
$afterStmt->execute([$id, $after]);
|
||||
$afterRows = $afterStmt->fetchAll();
|
||||
|
||||
return [
|
||||
'before' => $beforeRows,
|
||||
'current' => $currentRow,
|
||||
'after' => $afterRows,
|
||||
];
|
||||
}
|
||||
|
||||
// --- Bulk Operations ---
|
||||
|
||||
public function bulkUpdateAlertStatus(array $ids, AlertStatus $status): int
|
||||
{
|
||||
if (empty($ids)) return 0;
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmt = $this->db->pdo()->prepare(
|
||||
"UPDATE alerts SET status = ? WHERE id IN ($placeholders)"
|
||||
);
|
||||
$stmt->execute(array_merge([$status->value], $ids));
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
public function exportAlerts(array $ids): array
|
||||
{
|
||||
if (empty($ids)) return [];
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmt = $this->db->pdo()->prepare("SELECT * FROM alerts WHERE id IN ($placeholders) ORDER BY id");
|
||||
$stmt->execute($ids);
|
||||
return array_map(fn(array $r) => Alert::fromRow($r), $stmt->fetchAll());
|
||||
}
|
||||
|
||||
public function exportLogs(array $ids): array
|
||||
{
|
||||
if (empty($ids)) return [];
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$stmt = $this->db->pdo()->prepare("SELECT * FROM log_entries WHERE id IN ($placeholders) ORDER BY id");
|
||||
$stmt->execute($ids);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class Orchestrator
|
||||
private TelegramNotifier $telegram;
|
||||
private array $sourceMap = [];
|
||||
private bool $running = true;
|
||||
private int $lastCleanup = 0;
|
||||
|
||||
public function __construct(Repository $repo, Engine $engine)
|
||||
{
|
||||
@@ -47,8 +48,20 @@ class Orchestrator
|
||||
pcntl_signal_dispatch();
|
||||
$this->fileWatcher->tick();
|
||||
$this->socketListener->tick();
|
||||
|
||||
if (time() - $this->lastCleanup > 3600) {
|
||||
$this->lastCleanup = time();
|
||||
try {
|
||||
$result = $this->repo->purgeOldData();
|
||||
fprintf(STDERR, "Retention cleanup: %d log entries, %d alerts purged\n",
|
||||
$result['log_entries_deleted'], $result['alerts_deleted']);
|
||||
} catch (\Throwable $e) {
|
||||
fprintf(STDERR, "Retention cleanup error: %s\n", $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(STDERR, "Worker shutting down gracefully...\n");
|
||||
$this->stop();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user