From 25e11d0c5bd4b0e2e3908ceac41e69cf7d023fbc Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 5 Jun 2026 12:57:00 +0200 Subject: [PATCH] BE sync, BLOCKER fix, confirmation needed --- .../backend/public/assets/js/app.js | 2 + .../migrations/006_graph_sessions.sqlite.sql | 6 + .../migrations/008_graph_sessions_indexes.sql | 5 + .../backend/src/GraphRepository.php | 130 ++++++++++-------- 4 files changed, 82 insertions(+), 61 deletions(-) create mode 100644 examples/crash_reporter/backend/sql/migrations/006_graph_sessions.sqlite.sql create mode 100644 examples/crash_reporter/backend/sql/migrations/008_graph_sessions_indexes.sql diff --git a/examples/crash_reporter/backend/public/assets/js/app.js b/examples/crash_reporter/backend/public/assets/js/app.js index d68a603..34d3bc4 100644 --- a/examples/crash_reporter/backend/public/assets/js/app.js +++ b/examples/crash_reporter/backend/public/assets/js/app.js @@ -1879,6 +1879,8 @@ }; xhr.send(JSON.stringify(body)); }); + } + function initTagFilterBar(listContext) { const chipsEl = document.getElementById('tag-filter-chips'); const modeWrap = document.getElementById('tag-mode-wrap'); diff --git a/examples/crash_reporter/backend/sql/migrations/006_graph_sessions.sqlite.sql b/examples/crash_reporter/backend/sql/migrations/006_graph_sessions.sqlite.sql new file mode 100644 index 0000000..005a86c --- /dev/null +++ b/examples/crash_reporter/backend/sql/migrations/006_graph_sessions.sqlite.sql @@ -0,0 +1,6 @@ +-- SQLite graph_sessions indexes (table may already exist via GraphRepository::ensureSchema). + +CREATE INDEX IF NOT EXISTS idx_graph_company_time ON graph_sessions (company_id, started_at_ms); +CREATE INDEX IF NOT EXISTS idx_graph_user_time ON graph_sessions (user_id, started_at_ms); +CREATE INDEX IF NOT EXISTS idx_graph_started ON graph_sessions (started_at_ms); +CREATE INDEX IF NOT EXISTS idx_graph_device ON graph_sessions (device_id); diff --git a/examples/crash_reporter/backend/sql/migrations/008_graph_sessions_indexes.sql b/examples/crash_reporter/backend/sql/migrations/008_graph_sessions_indexes.sql new file mode 100644 index 0000000..dfaadfc --- /dev/null +++ b/examples/crash_reporter/backend/sql/migrations/008_graph_sessions_indexes.sql @@ -0,0 +1,5 @@ +-- Extra graph_sessions indexes for graphs dashboard (user scope + platform-wide time range). +-- Idempotent on MariaDB 10.5+: duplicate index names are ignored manually when re-run. + +CREATE INDEX IF NOT EXISTS idx_graph_user_time ON graph_sessions (user_id, started_at_ms); +CREATE INDEX IF NOT EXISTS idx_graph_started ON graph_sessions (started_at_ms); diff --git a/examples/crash_reporter/backend/src/GraphRepository.php b/examples/crash_reporter/backend/src/GraphRepository.php index 58586f4..3c8fcc4 100644 --- a/examples/crash_reporter/backend/src/GraphRepository.php +++ b/examples/crash_reporter/backend/src/GraphRepository.php @@ -6,9 +6,11 @@ final class GraphRepository { $pdo = Database::pdo(); if (!Database::tableExists($pdo, 'graph_sessions')) { self::createTable($pdo); + self::ensureIndexes($pdo); return; } self::ensureColumns($pdo); + self::ensureIndexes($pdo); } private static function createTable(PDO $pdo): void { @@ -71,6 +73,30 @@ final class GraphRepository { ); } + private static function ensureIndexes(PDO $pdo): void { + $defs = Database::isMysql() + ? [ + 'idx_graph_user_time' => 'CREATE INDEX idx_graph_user_time ON graph_sessions (user_id, started_at_ms)', + 'idx_graph_started' => 'CREATE INDEX idx_graph_started ON graph_sessions (started_at_ms)', + ] + : [ + 'idx_graph_company_time' => 'CREATE INDEX IF NOT EXISTS idx_graph_company_time ON graph_sessions (company_id, started_at_ms)', + 'idx_graph_user_time' => 'CREATE INDEX IF NOT EXISTS idx_graph_user_time ON graph_sessions (user_id, started_at_ms)', + 'idx_graph_started' => 'CREATE INDEX IF NOT EXISTS idx_graph_started ON graph_sessions (started_at_ms)', + 'idx_graph_device' => 'CREATE INDEX IF NOT EXISTS idx_graph_device ON graph_sessions (device_id)', + ]; + foreach ($defs as $sql) { + try { + $pdo->exec($sql); + } catch (PDOException $e) { + $msg = strtolower($e->getMessage()); + if (!str_contains($msg, 'duplicate') && !str_contains($msg, 'already exists')) { + throw $e; + } + } + } + } + private static function ensureColumns(PDO $pdo): void { $cols = [ 'device_id' => Database::isMysql() ? 'VARCHAR(128) NULL' : 'TEXT NULL', @@ -299,55 +325,27 @@ final class GraphRepository { $params[] = $direction; } self::appendScope($where, $params, $companyId, $userId); - $sql = 'SELECT started_at_ms FROM graph_sessions WHERE ' . implode(' AND ', $where); - return self::countByDay($sql, $params, 'started_at_ms'); + return self::countByDay('graph_sessions', 'started_at_ms', $where, $params); } private static function uniqueDevicesPerDay(int $startMs, ?int $companyId, ?int $userId): array { $where = ['started_at_ms >= ?', "device_id IS NOT NULL", "device_id <> ''"]; $params = [$startMs]; self::appendScope($where, $params, $companyId, $userId); - $sql = 'SELECT started_at_ms, device_id FROM graph_sessions WHERE ' . implode(' AND ', $where); + $day = self::msDayExpr('started_at_ms'); + $sql = 'SELECT ' . $day . ' AS d, COUNT(DISTINCT device_id) AS c FROM graph_sessions WHERE ' + . implode(' AND ', $where) . ' GROUP BY d ORDER BY d'; $stmt = Database::pdo()->prepare($sql); $stmt->execute($params); $bucket = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $day = gmdate('Y-m-d', intdiv((int) ($row['started_at_ms'] ?? 0), 1000)); - $dev = (string) ($row['device_id'] ?? ''); - if ($dev === '') { - continue; - } - if (!isset($bucket[$day])) { - $bucket[$day] = []; - } - $bucket[$day][$dev] = true; + $bucket[(string) ($row['d'] ?? '')] = (int) ($row['c'] ?? 0); } - $out = []; - foreach ($bucket as $day => $set) { - $out[$day] = count($set); - } - return self::assocToSeries($out); + return self::assocToSeries($bucket); } private static function avgDurationPerDay(int $startMs, ?int $companyId, ?int $userId): array { - $where = ['started_at_ms >= ?']; - $params = [$startMs]; - self::appendScope($where, $params, $companyId, $userId); - $sql = 'SELECT started_at_ms, duration_s FROM graph_sessions WHERE ' . implode(' AND ', $where); - $stmt = Database::pdo()->prepare($sql); - $stmt->execute($params); - $sum = []; - $cnt = []; - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $day = gmdate('Y-m-d', intdiv((int) ($row['started_at_ms'] ?? 0), 1000)); - $sum[$day] = ($sum[$day] ?? 0) + (int) ($row['duration_s'] ?? 0); - $cnt[$day] = ($cnt[$day] ?? 0) + 1; - } - $avg = []; - foreach ($sum as $day => $total) { - $avg[$day] = $cnt[$day] > 0 ? round($total / $cnt[$day], 1) : 0.0; - } - return self::assocToSeries($avg); + return self::avgNumericPerDay($startMs, $companyId, $userId, 'duration_s'); } private static function successRatio(int $startMs, ?int $companyId, ?int $userId): float { @@ -378,25 +376,7 @@ final class GraphRepository { if (!in_array($column, $allowed, true)) { return []; } - $where = ['started_at_ms >= ?']; - $params = [$startMs]; - self::appendScope($where, $params, $companyId, $userId); - $sql = 'SELECT started_at_ms, ' . $column . ' AS v FROM graph_sessions WHERE ' - . implode(' AND ', $where); - $stmt = Database::pdo()->prepare($sql); - $stmt->execute($params); - $sum = []; - $cnt = []; - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $day = gmdate('Y-m-d', intdiv((int) ($row['started_at_ms'] ?? 0), 1000)); - $sum[$day] = ($sum[$day] ?? 0.0) + (float) ($row['v'] ?? 0); - $cnt[$day] = ($cnt[$day] ?? 0) + 1; - } - $avg = []; - foreach ($sum as $day => $total) { - $avg[$day] = $cnt[$day] > 0 ? round($total / $cnt[$day], 1) : 0.0; - } - return self::assocToSeries($avg); + return self::avgNumericPerDay($startMs, $companyId, $userId, $column); } private static function crashesPerDay(int $startMs, ?int $companyId): array { @@ -409,8 +389,7 @@ final class GraphRepository { $where[] = 'company_id = ?'; $params[] = $companyId; } - $sql = 'SELECT received_at_ms FROM reports WHERE ' . implode(' AND ', $where); - return self::countByDay($sql, $params, 'received_at_ms'); + return self::countByDay('reports', 'received_at_ms', $where, $params); } private static function ticketsPerDay(int $startMs, ?int $companyId): array { @@ -420,8 +399,7 @@ final class GraphRepository { $where[] = 'company_id = ?'; $params[] = $companyId; } - $sql = 'SELECT opened_at_ms FROM tickets WHERE ' . implode(' AND ', $where); - return self::countByDay($sql, $params, 'opened_at_ms'); + return self::countByDay('tickets', 'opened_at_ms', $where, $params); } private static function topCrashFingerprints(int $startMs, int $limit): array { @@ -502,17 +480,47 @@ final class GraphRepository { return $out; } - private static function countByDay(string $sql, array $params, string $msColumn): array { + private static function msDayExpr(string $msColumn): string { + if (Database::isMysql()) { + return 'DATE(FROM_UNIXTIME(FLOOR(' . $msColumn . ' / 1000)))'; + } + return "strftime('%Y-%m-%d', " . $msColumn . " / 1000, 'unixepoch')"; + } + + private static function countByDay(string $table, string $msColumn, array $where, array $params): array { + $day = self::msDayExpr($msColumn); + $sql = 'SELECT ' . $day . ' AS d, COUNT(*) AS c FROM ' . $table . ' WHERE ' + . implode(' AND ', $where) . ' GROUP BY d ORDER BY d'; $stmt = Database::pdo()->prepare($sql); $stmt->execute($params); $bucket = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $day = gmdate('Y-m-d', intdiv((int) ($row[$msColumn] ?? 0), 1000)); - $bucket[$day] = ($bucket[$day] ?? 0) + 1; + $bucket[(string) ($row['d'] ?? '')] = (int) ($row['c'] ?? 0); } return self::assocToSeries($bucket); } + private static function avgNumericPerDay( + int $startMs, + ?int $companyId, + ?int $userId, + string $column + ): array { + $where = ['started_at_ms >= ?']; + $params = [$startMs]; + self::appendScope($where, $params, $companyId, $userId); + $day = self::msDayExpr('started_at_ms'); + $sql = 'SELECT ' . $day . ' AS d, AVG(' . $column . ') AS v FROM graph_sessions WHERE ' + . implode(' AND ', $where) . ' GROUP BY d ORDER BY d'; + $stmt = Database::pdo()->prepare($sql); + $stmt->execute($params); + $avg = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $avg[(string) ($row['d'] ?? '')] = round((float) ($row['v'] ?? 0), 1); + } + return self::assocToSeries($avg); + } + private static function appendScope(array &$where, array &$params, ?int $companyId, ?int $userId): void { if ($companyId !== null && $companyId > 0) { $where[] = 'company_id = ?';