2025-10-10 14:01:19 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
|
|
|
|
use Core\Model;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ErrorLog Model
|
|
|
|
|
*
|
|
|
|
|
* Manages error log database operations for tracking and debugging
|
|
|
|
|
*/
|
|
|
|
|
class ErrorLog extends Model
|
|
|
|
|
{
|
|
|
|
|
protected static string $table = 'error_logs';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Log an error to database
|
2026-01-08 14:19:09 +02:00
|
|
|
* If the same error exists (same type + file + line + message), increment occurrence count
|
|
|
|
|
* Otherwise, create a new record with unique error_id
|
2025-10-10 14:01:19 +03:00
|
|
|
*/
|
|
|
|
|
public function logError(array $errorData): ?int
|
|
|
|
|
{
|
2026-01-08 14:19:09 +02:00
|
|
|
// Check if this error already exists (same type, file, line, and message)
|
2025-10-10 14:01:19 +03:00
|
|
|
$existing = $this->findBySimilar(
|
|
|
|
|
$errorData['error_type'],
|
|
|
|
|
$errorData['error_file'],
|
2026-01-08 14:19:09 +02:00
|
|
|
$errorData['error_line'],
|
|
|
|
|
$errorData['error_message']
|
2025-10-10 14:01:19 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($existing) {
|
2026-01-08 14:19:09 +02:00
|
|
|
// Update existing error: increment occurrence and update timestamp
|
|
|
|
|
// Keep the original error_id (don't use the new one from errorData)
|
2025-10-10 14:01:19 +03:00
|
|
|
$this->incrementOccurrence($existing['id']);
|
|
|
|
|
return $existing['id'];
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 14:19:09 +02:00
|
|
|
// Create new error log with the unique error_id
|
2025-10-10 14:01:19 +03:00
|
|
|
return $this->create($errorData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-08 14:19:09 +02:00
|
|
|
* Find similar error (same type, file, line, and message)
|
2025-10-10 14:01:19 +03:00
|
|
|
*/
|
2026-01-08 14:19:09 +02:00
|
|
|
private function findBySimilar(string $type, string $file, int $line, string $message): ?array
|
2025-10-10 14:01:19 +03:00
|
|
|
{
|
|
|
|
|
$sql = "SELECT * FROM error_logs
|
|
|
|
|
WHERE error_type = ?
|
|
|
|
|
AND error_file = ?
|
|
|
|
|
AND error_line = ?
|
2026-01-08 14:19:09 +02:00
|
|
|
AND error_message = ?
|
2025-10-10 14:01:19 +03:00
|
|
|
AND is_resolved = FALSE
|
|
|
|
|
LIMIT 1";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($sql);
|
2026-01-08 14:19:09 +02:00
|
|
|
$stmt->execute([$type, $file, $line, $message]);
|
2025-10-10 14:01:19 +03:00
|
|
|
$result = $stmt->fetch();
|
|
|
|
|
|
|
|
|
|
return $result ?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-08 14:19:09 +02:00
|
|
|
* Increment occurrence counter and update last occurrence timestamp
|
2025-10-10 14:01:19 +03:00
|
|
|
*/
|
|
|
|
|
private function incrementOccurrence(int $id): void
|
|
|
|
|
{
|
|
|
|
|
$sql = "UPDATE error_logs
|
|
|
|
|
SET occurrences = occurrences + 1,
|
|
|
|
|
last_occurred_at = NOW()
|
|
|
|
|
WHERE id = ?";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($sql);
|
|
|
|
|
$stmt->execute([$id]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 14:19:09 +02:00
|
|
|
|
2025-10-10 14:01:19 +03:00
|
|
|
/**
|
|
|
|
|
* Find error by error_id (unique reference)
|
|
|
|
|
*/
|
|
|
|
|
public function findByErrorId(string $errorId): ?array
|
|
|
|
|
{
|
|
|
|
|
$sql = "SELECT el.*, u.username, u.full_name, r.username as resolved_by_name
|
|
|
|
|
FROM error_logs el
|
|
|
|
|
LEFT JOIN users u ON el.user_id = u.id
|
|
|
|
|
LEFT JOIN users r ON el.resolved_by = r.id
|
|
|
|
|
WHERE el.error_id = ?";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($sql);
|
|
|
|
|
$stmt->execute([$errorId]);
|
|
|
|
|
$result = $stmt->fetch();
|
|
|
|
|
|
|
|
|
|
return $result ?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get recent errors with pagination
|
|
|
|
|
*/
|
|
|
|
|
public function getRecent(int $limit = 50, int $offset = 0, array $filters = []): array
|
|
|
|
|
{
|
|
|
|
|
$where = ['1=1'];
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
// Filter by resolution status
|
|
|
|
|
if (isset($filters['resolved'])) {
|
|
|
|
|
$where[] = 'el.is_resolved = ?';
|
|
|
|
|
$params[] = $filters['resolved'] ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter by error type
|
|
|
|
|
if (!empty($filters['type'])) {
|
|
|
|
|
$where[] = 'el.error_type LIKE ?';
|
|
|
|
|
$params[] = '%' . $filters['type'] . '%';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter by user
|
|
|
|
|
if (!empty($filters['user_id'])) {
|
|
|
|
|
$where[] = 'el.user_id = ?';
|
|
|
|
|
$params[] = $filters['user_id'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$whereClause = implode(' AND ', $where);
|
|
|
|
|
|
|
|
|
|
$sql = "SELECT el.*, u.username, u.full_name
|
|
|
|
|
FROM error_logs el
|
|
|
|
|
LEFT JOIN users u ON el.user_id = u.id
|
|
|
|
|
WHERE {$whereClause}
|
|
|
|
|
ORDER BY el.last_occurred_at DESC
|
|
|
|
|
LIMIT ? OFFSET ?";
|
|
|
|
|
|
|
|
|
|
$params[] = $limit;
|
|
|
|
|
$params[] = $offset;
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($sql);
|
|
|
|
|
$stmt->execute($params);
|
|
|
|
|
return $stmt->fetchAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Count total errors
|
|
|
|
|
*/
|
|
|
|
|
public function count(array $filters = []): int
|
|
|
|
|
{
|
|
|
|
|
$where = ['1=1'];
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
if (isset($filters['resolved'])) {
|
|
|
|
|
$where[] = 'is_resolved = ?';
|
|
|
|
|
$params[] = $filters['resolved'] ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($filters['type'])) {
|
|
|
|
|
$where[] = 'error_type LIKE ?';
|
|
|
|
|
$params[] = '%' . $filters['type'] . '%';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($filters['user_id'])) {
|
|
|
|
|
$where[] = 'user_id = ?';
|
|
|
|
|
$params[] = $filters['user_id'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$whereClause = implode(' AND ', $where);
|
|
|
|
|
|
|
|
|
|
$sql = "SELECT COUNT(*) as count FROM error_logs WHERE {$whereClause}";
|
|
|
|
|
$stmt = $this->db->prepare($sql);
|
|
|
|
|
$stmt->execute($params);
|
|
|
|
|
return (int)$stmt->fetch()['count'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get error statistics
|
|
|
|
|
*/
|
|
|
|
|
public function getStats(): array
|
|
|
|
|
{
|
|
|
|
|
$sql = "SELECT
|
|
|
|
|
COUNT(*) as total_errors,
|
|
|
|
|
SUM(occurrences) as total_occurrences,
|
|
|
|
|
COUNT(CASE WHEN is_resolved = FALSE THEN 1 END) as unresolved,
|
|
|
|
|
COUNT(CASE WHEN is_resolved = TRUE THEN 1 END) as resolved,
|
|
|
|
|
COUNT(CASE WHEN occurred_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN 1 END) as last_24h,
|
|
|
|
|
COUNT(CASE WHEN occurred_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as last_7d
|
|
|
|
|
FROM error_logs";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->query($sql);
|
|
|
|
|
return $stmt->fetch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark error as resolved
|
|
|
|
|
*/
|
|
|
|
|
public function resolve(int $id, int $resolvedBy, ?string $notes = null): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->update($id, [
|
|
|
|
|
'is_resolved' => true,
|
|
|
|
|
'resolved_at' => date('Y-m-d H:i:s'),
|
|
|
|
|
'resolved_by' => $resolvedBy,
|
|
|
|
|
'notes' => $notes
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete old resolved errors
|
|
|
|
|
*/
|
|
|
|
|
public function deleteOldResolved(int $daysOld = 30): int
|
|
|
|
|
{
|
|
|
|
|
$sql = "DELETE FROM error_logs
|
|
|
|
|
WHERE is_resolved = TRUE
|
|
|
|
|
AND resolved_at < DATE_SUB(NOW(), INTERVAL ? DAY)";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($sql);
|
|
|
|
|
$stmt->execute([$daysOld]);
|
|
|
|
|
return $stmt->rowCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get most frequent errors
|
|
|
|
|
*/
|
|
|
|
|
public function getMostFrequent(int $limit = 10): array
|
|
|
|
|
{
|
|
|
|
|
$sql = "SELECT el.*, u.username, u.full_name
|
|
|
|
|
FROM error_logs el
|
|
|
|
|
LEFT JOIN users u ON el.user_id = u.id
|
|
|
|
|
WHERE el.is_resolved = FALSE
|
|
|
|
|
ORDER BY el.occurrences DESC, el.last_occurred_at DESC
|
|
|
|
|
LIMIT ?";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($sql);
|
|
|
|
|
$stmt->execute([$limit]);
|
|
|
|
|
return $stmt->fetchAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get paginated errors with filters for admin panel
|
|
|
|
|
*/
|
|
|
|
|
public function getPaginatedErrors(array $filters, int $perPage, int $offset): array
|
|
|
|
|
{
|
|
|
|
|
$where = [];
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
if ($filters['resolved'] !== '') {
|
|
|
|
|
$where[] = 'is_resolved = ?';
|
|
|
|
|
$params[] = (int)$filters['resolved'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($filters['type'])) {
|
|
|
|
|
$where[] = 'error_type LIKE ?';
|
|
|
|
|
$params[] = '%' . $filters['type'] . '%';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
Improve security, validation, and isolation checks
Add multiple security and validation improvements across the app:
- Prevent session fixation: regenerate session ID on login and after successful 2FA; tighten session cookie params (Secure, HttpOnly, SameSite=Lax).
- Harden installer: add CSRF checks for install/update flows and use PDO::quote when injecting admin credentials into SQL migration to avoid injection; add csrf_field() to installer templates.
- Template hardening: add safe_url and safe_mailto Twig filters, escape tag names for JS, and add rel="noopener noreferrer" to external links to mitigate XSS/opener risks.
- Domain controller: validate referrer to avoid open redirects, enforce user isolation mode when finding/deleting/updating domains and when assigning notification groups (ensures users only affect their own resources).
- Notification groups: verify channel belongs to group before deleting or toggling to prevent unauthorized access.
- ErrorLog: whitelist allowed sort columns to avoid arbitrary column injection in ORDER BY.
- Routes: move the debug whois route to protected/admin area.
These changes collectively reduce attack surface (XSS, open redirect, session fixation, SQL injection) and enforce proper resource isolation and input validation.
2026-03-11 00:03:54 +02:00
|
|
|
|
|
|
|
|
$allowedSort = ['error_id', 'error_type', 'error_message', 'is_resolved', 'occurred_at', 'last_occurred_at', 'occurrences'];
|
|
|
|
|
$sortColumn = in_array($filters['sort'], $allowedSort, true) ? $filters['sort'] : 'last_occurred_at';
|
2025-10-10 14:01:19 +03:00
|
|
|
$sortOrder = strtoupper($filters['order']) === 'DESC' ? 'DESC' : 'ASC';
|
|
|
|
|
|
|
|
|
|
$query = "
|
|
|
|
|
SELECT
|
|
|
|
|
error_id,
|
|
|
|
|
error_type,
|
|
|
|
|
error_message,
|
|
|
|
|
error_file,
|
|
|
|
|
error_line,
|
|
|
|
|
is_resolved,
|
2026-01-08 14:19:09 +02:00
|
|
|
occurred_at,
|
|
|
|
|
last_occurred_at,
|
|
|
|
|
occurrences
|
2025-10-10 14:01:19 +03:00
|
|
|
FROM error_logs
|
|
|
|
|
$whereClause
|
|
|
|
|
ORDER BY $sortColumn $sortOrder
|
|
|
|
|
LIMIT ? OFFSET ?
|
|
|
|
|
";
|
|
|
|
|
|
|
|
|
|
$stmt = $this->db->prepare($query);
|
|
|
|
|
$stmt->execute([...$params, $perPage, $offset]);
|
|
|
|
|
return $stmt->fetchAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Count total unique errors with filters
|
|
|
|
|
*/
|
|
|
|
|
public function countUniqueErrors(array $filters): int
|
|
|
|
|
{
|
|
|
|
|
$where = [];
|
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
|
|
if ($filters['resolved'] !== '') {
|
|
|
|
|
$where[] = 'is_resolved = ?';
|
|
|
|
|
$params[] = (int)$filters['resolved'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($filters['type'])) {
|
|
|
|
|
$where[] = 'error_type LIKE ?';
|
|
|
|
|
$params[] = '%' . $filters['type'] . '%';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
|
|
|
|
|
2026-01-08 14:19:09 +02:00
|
|
|
$query = "SELECT COUNT(*) as total FROM error_logs $whereClause";
|
2025-10-10 14:01:19 +03:00
|
|
|
$stmt = $this->db->prepare($query);
|
|
|
|
|
$stmt->execute($params);
|
|
|
|
|
return (int)$stmt->fetch()['total'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all occurrences of a specific error
|
2026-01-08 14:19:09 +02:00
|
|
|
* Since we deduplicate errors, this returns a single record (or empty if not found)
|
|
|
|
|
* The occurrences count shows how many times this error happened
|
2025-10-10 14:01:19 +03:00
|
|
|
*/
|
|
|
|
|
public function getOccurrencesByErrorId(string $errorId): array
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare("
|
|
|
|
|
SELECT * FROM error_logs
|
|
|
|
|
WHERE error_id = ?
|
2026-01-08 14:19:09 +02:00
|
|
|
LIMIT 1
|
2025-10-10 14:01:19 +03:00
|
|
|
");
|
|
|
|
|
$stmt->execute([$errorId]);
|
2026-01-08 14:19:09 +02:00
|
|
|
$result = $stmt->fetch();
|
|
|
|
|
return $result ? [$result] : [];
|
2025-10-10 14:01:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get admin statistics
|
|
|
|
|
*/
|
|
|
|
|
public function getAdminStats(): array
|
|
|
|
|
{
|
2026-01-08 14:19:09 +02:00
|
|
|
// Total unique errors (one record per unique error signature)
|
|
|
|
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs");
|
2025-10-10 14:01:19 +03:00
|
|
|
$totalErrors = $stmt->fetch()['total'];
|
|
|
|
|
|
|
|
|
|
// Unresolved errors
|
2026-01-08 14:19:09 +02:00
|
|
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs WHERE is_resolved = 0");
|
2025-10-10 14:01:19 +03:00
|
|
|
$unresolved = $stmt->fetch()['total'];
|
|
|
|
|
|
2026-01-08 14:19:09 +02:00
|
|
|
// Errors in last 24h (errors that occurred or were last seen in last 24h)
|
|
|
|
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs WHERE last_occurred_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)");
|
2025-10-10 14:01:19 +03:00
|
|
|
$last24h = $stmt->fetch()['total'];
|
|
|
|
|
|
|
|
|
|
// Total occurrences
|
|
|
|
|
$stmt = $this->db->query("SELECT COUNT(*) as total FROM error_logs");
|
|
|
|
|
$totalOccurrences = $stmt->fetch()['total'];
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'total_errors' => $totalErrors,
|
|
|
|
|
'unresolved' => $unresolved,
|
|
|
|
|
'last_24h' => $last24h,
|
|
|
|
|
'total_occurrences' => $totalOccurrences
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark all occurrences of an error as resolved
|
2026-01-08 14:19:09 +02:00
|
|
|
* Resolves all errors with the same type, file, line, and message as the given error_id
|
2025-10-10 14:01:19 +03:00
|
|
|
*/
|
|
|
|
|
public function markErrorResolved(string $errorId, int $userId, ?string $notes): bool
|
|
|
|
|
{
|
2026-01-08 14:19:09 +02:00
|
|
|
// First get the error details to find all similar errors
|
|
|
|
|
$error = $this->findByErrorId($errorId);
|
|
|
|
|
if (!$error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark all errors with the same signature as resolved
|
2025-10-10 14:01:19 +03:00
|
|
|
$stmt = $this->db->prepare("
|
|
|
|
|
UPDATE error_logs
|
|
|
|
|
SET is_resolved = 1,
|
|
|
|
|
resolved_at = NOW(),
|
|
|
|
|
resolved_by = ?,
|
2025-10-11 20:27:46 +03:00
|
|
|
notes = ?
|
2026-01-08 14:19:09 +02:00
|
|
|
WHERE error_type = ?
|
|
|
|
|
AND error_file = ?
|
|
|
|
|
AND error_line = ?
|
|
|
|
|
AND error_message = ?
|
2025-10-10 14:01:19 +03:00
|
|
|
");
|
|
|
|
|
|
2026-01-08 14:19:09 +02:00
|
|
|
return $stmt->execute([
|
|
|
|
|
$userId,
|
|
|
|
|
$notes,
|
|
|
|
|
$error['error_type'],
|
|
|
|
|
$error['error_file'],
|
|
|
|
|
$error['error_line'],
|
|
|
|
|
$error['error_message']
|
|
|
|
|
]);
|
2025-10-10 14:01:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark all occurrences of an error as unresolved
|
2026-01-08 14:19:09 +02:00
|
|
|
* Unresolves all errors with the same type, file, line, and message as the given error_id
|
2025-10-10 14:01:19 +03:00
|
|
|
*/
|
|
|
|
|
public function markErrorUnresolved(string $errorId): bool
|
|
|
|
|
{
|
2026-01-08 14:19:09 +02:00
|
|
|
// First get the error details to find all similar errors
|
|
|
|
|
$error = $this->findByErrorId($errorId);
|
|
|
|
|
if (!$error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark all errors with the same signature as unresolved
|
2025-10-10 14:01:19 +03:00
|
|
|
$stmt = $this->db->prepare("
|
|
|
|
|
UPDATE error_logs
|
|
|
|
|
SET is_resolved = 0,
|
|
|
|
|
resolved_at = NULL,
|
|
|
|
|
resolved_by = NULL,
|
2025-10-11 20:27:46 +03:00
|
|
|
notes = NULL
|
2026-01-08 14:19:09 +02:00
|
|
|
WHERE error_type = ?
|
|
|
|
|
AND error_file = ?
|
|
|
|
|
AND error_line = ?
|
|
|
|
|
AND error_message = ?
|
2025-10-10 14:01:19 +03:00
|
|
|
");
|
|
|
|
|
|
2026-01-08 14:19:09 +02:00
|
|
|
return $stmt->execute([
|
|
|
|
|
$error['error_type'],
|
|
|
|
|
$error['error_file'],
|
|
|
|
|
$error['error_line'],
|
|
|
|
|
$error['error_message']
|
|
|
|
|
]);
|
2025-10-10 14:01:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete all occurrences of an error
|
2026-01-08 14:19:09 +02:00
|
|
|
* Deletes all errors with the same type, file, line, and message as the given error_id
|
2025-10-10 14:01:19 +03:00
|
|
|
*/
|
|
|
|
|
public function deleteByErrorId(string $errorId): bool
|
|
|
|
|
{
|
2026-01-08 14:19:09 +02:00
|
|
|
// First get the error details to find all similar errors
|
|
|
|
|
$error = $this->findByErrorId($errorId);
|
|
|
|
|
if (!$error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete all errors with the same signature
|
|
|
|
|
$stmt = $this->db->prepare("
|
|
|
|
|
DELETE FROM error_logs
|
|
|
|
|
WHERE error_type = ?
|
|
|
|
|
AND error_file = ?
|
|
|
|
|
AND error_line = ?
|
|
|
|
|
AND error_message = ?
|
|
|
|
|
");
|
|
|
|
|
|
|
|
|
|
return $stmt->execute([
|
|
|
|
|
$error['error_type'],
|
|
|
|
|
$error['error_file'],
|
|
|
|
|
$error['error_line'],
|
|
|
|
|
$error['error_message']
|
|
|
|
|
]);
|
2025-10-10 14:01:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear old resolved errors
|
|
|
|
|
*/
|
|
|
|
|
public function clearOldResolved(int $daysOld): int
|
|
|
|
|
{
|
|
|
|
|
$stmt = $this->db->prepare("
|
|
|
|
|
DELETE FROM error_logs
|
|
|
|
|
WHERE is_resolved = 1
|
|
|
|
|
AND resolved_at < DATE_SUB(NOW(), INTERVAL ? DAY)
|
|
|
|
|
");
|
|
|
|
|
$stmt->execute([$daysOld]);
|
|
|
|
|
return $stmt->rowCount();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|