diff --git a/examples/app_hub/assets/i18n/hub-en.json b/examples/app_hub/assets/i18n/hub-en.json index 2a68924..90271b9 100644 --- a/examples/app_hub/assets/i18n/hub-en.json +++ b/examples/app_hub/assets/i18n/hub-en.json @@ -17,11 +17,11 @@ "nav.remote": "Remote access", "nav.short_links": "Short links", "nav.short_links_desc": "Mint bearer tokens and shorten URLs for s.f0xx.org.", - "nav.console_desc": "Crash triage home and workspace cards.", + "nav.console_desc": "Issue triage home and workspace cards.", "nav.git_desc": "Browse repos, branches, and tags.", - "nav.issues_desc": "Browse and triage crash reports.", + "nav.issues_desc": "Browse and triage issues.", "nav.tickets_desc": "Roadmap tasks, QA items, and tags.", - "nav.graphs_desc": "Sessions, crashes, and device activity.", + "nav.graphs_desc": "Sessions, issues, and device activity.", "nav.remote_desc": "WireGuard sessions and device reachability.", "nav.builder_desc": "Docker CI for APK baking and OTA artifacts.", "nav.builder": "Builder", diff --git a/examples/app_hub/assets/i18n/hub-ru.json b/examples/app_hub/assets/i18n/hub-ru.json index 21492a1..965a125 100644 --- a/examples/app_hub/assets/i18n/hub-ru.json +++ b/examples/app_hub/assets/i18n/hub-ru.json @@ -19,9 +19,9 @@ "nav.short_links_desc": "Токены и сокращение URL для s.f0xx.org.", "nav.console_desc": "Главная консоли и карточки разделов.", "nav.git_desc": "Репозитории, ветки и теги.", - "nav.issues_desc": "Просмотр и разбор отчётов о сбоях.", + "nav.issues_desc": "Просмотр и разбор issues.", "nav.tickets_desc": "Задачи roadmap, QA и теги.", - "nav.graphs_desc": "Сессии, сбои и активность устройств.", + "nav.graphs_desc": "Сессии, issues и активность устройств.", "nav.remote_desc": "Сессии WireGuard и доступ к устройствам.", "nav.builder_desc": "Docker CI для сборки APK и OTA.", "nav.builder": "Сборщик", diff --git a/examples/app_hub/landing-pages.inc.php b/examples/app_hub/landing-pages.inc.php index 9a23e26..98e4051 100644 --- a/examples/app_hub/landing-pages.inc.php +++ b/examples/app_hub/landing-pages.inc.php @@ -17,7 +17,7 @@

Wi‑Fi and USB transport with adaptive bitrate and session metrics.

-

Crashes

+

Issues

Ingest, triage tickets, and correlate fingerprints across releases.

diff --git a/examples/crash_reporter/backend/public/api/remote_access.php b/examples/crash_reporter/backend/public/api/remote_access.php index 3248029..d9b4863 100644 --- a/examples/crash_reporter/backend/public/api/remote_access.php +++ b/examples/crash_reporter/backend/public/api/remote_access.php @@ -31,7 +31,10 @@ if ($method === 'GET') { $inactive = array_values(array_filter($sessions, static fn($s) => !in_array($s['status'] ?? '', ['pending', 'active'], true))); json_out([ 'ok' => true, - 'devices' => RemoteAccessRepository::listDevices(), + 'devices' => RemoteAccessRepository::listDevicesForDashboard( + rtrim(Auth::basePath(), '/'), + '/app/androidcast_project' + ), 'active_sessions' => $active, 'inactive_sessions' => array_slice($inactive, 0, 50), 'recent_events' => RemoteAccessRepository::listEvents(30), diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index cf44f88..c22bc3f 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -547,6 +547,13 @@ a:hover { text-decoration: underline; } padding: 24px 28px; box-sizing: border-box; -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; +} +.main-pane::-webkit-scrollbar { + display: none; + width: 0; + height: 0; } body { --footer-h: 40px; } .bottom-bar { @@ -2167,3 +2174,36 @@ button.report-tag--filter:hover { .graph-breakdown-row a { color: var(--accent, #3d8bfd); text-decoration: none; } .graph-breakdown-row a:hover { text-decoration: underline; } .graphs-footnote { margin-top: 20px; font-size: 0.88rem; } + +.tag-pill { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.78rem; + font-weight: 600; + background: rgba(61, 139, 253, .18); + color: var(--accent); +} +.tag-pill--warn { + background: rgba(245, 158, 11, .18); + color: #fbbf24; +} +[data-theme="light"] .tag-pill--warn { + color: #b45309; +} +.ra-device-row--needs-wl { + background: rgba(245, 158, 11, .06); +} +.ra-device-actions { + white-space: nowrap; +} +.ra-detail-links { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} +.ra-whitelist-hint { + max-width: 72ch; + line-height: 1.5; +} diff --git a/examples/crash_reporter/backend/public/assets/i18n/en.json b/examples/crash_reporter/backend/public/assets/i18n/en.json index 2685063..204b525 100644 --- a/examples/crash_reporter/backend/public/assets/i18n/en.json +++ b/examples/crash_reporter/backend/public/assets/i18n/en.json @@ -6,10 +6,10 @@ "nav.logout": "Logout", "nav.toggle": "Navigation", "nav.home_desc": "Console home and workspace cards.", - "nav.reports_desc": "Browse and triage crash reports.", + "nav.reports_desc": "Browse and triage issues.", "nav.tickets_desc": "Roadmap tasks, QA items, and tags.", "nav.graphs": "Graphs", - "nav.graphs_desc": "Sessions, crashes, and device activity.", + "nav.graphs_desc": "Sessions, issues, and device activity.", "nav.remote": "Remote access", "nav.remote_desc": "WireGuard sessions and device reachability.", "nav.access": "Access", @@ -23,14 +23,14 @@ "lang.en": "English", "lang.ru": "Russian", "home.title": "Console", - "home.intro": "Crash triage, tickets, and session metrics — pick a workspace below.", + "home.intro": "Issue triage, tickets, and session metrics — pick a workspace below.", "home.card_issues": "Issues", - "home.card_issues_desc": "Browse and triage crash reports, tags, and fingerprints.", + "home.card_issues_desc": "Browse and triage issues, tags, and fingerprints.", "home.card_new_issue_desc": "Log a new issue or task in one step.", "home.card_new_ticket_desc": "Create a roadmap or QA ticket in one step.", "home.card_tickets_desc": "Roadmap tasks, QA items, and workflow tags.", "home.card_graphs": "Graphs", - "home.card_graphs_desc": "Sessions, crashes, and device activity over time.", + "home.card_graphs_desc": "Sessions, issues, and device activity over time.", "home.card_short_links": "Short links", "home.card_short_links_desc": "Mint bearer tokens and shorten URLs for s.f0xx.org.", "home.more": "All services", @@ -109,7 +109,7 @@ "tag.filter_and": "All tags (AND)", "tag.filter_or": "Any tag (OR)", "detail.back": "Back to list", - "detail.title": "Crash report", + "detail.title": "Issue report", "detail.report_meta": "Report {id} · {type}", "detail.tags": "Tags", "detail.device": "Device", @@ -125,13 +125,13 @@ "detail.filter_version": "Show reports for this app version", "detail.filter_build": "Show reports from this build", "detail.java_exception": "Java exception", - "detail.native_crash": "Native crash", - "detail.find_similar": "Find similar reports", + "detail.native_crash": "Native issue", + "detail.find_similar": "Find similar issues", "detail.thread": "Thread:", "detail.signal": "Signal:", - "detail.session_context": "Session context (crash)", + "detail.session_context": "Session context (issue)", "detail.raw_json": "Raw JSON", - "login.sign_in_hint": "Sign in to view anonymous crash reports", + "login.sign_in_hint": "Sign in to view anonymous issue reports", "login.username": "Username", "login.password": "Password", "login.submit": "Sign in", diff --git a/examples/crash_reporter/backend/public/assets/i18n/ru.json b/examples/crash_reporter/backend/public/assets/i18n/ru.json index eda8815..b72d81c 100644 --- a/examples/crash_reporter/backend/public/assets/i18n/ru.json +++ b/examples/crash_reporter/backend/public/assets/i18n/ru.json @@ -16,10 +16,10 @@ "nav.logout": "Выход", "nav.toggle": "Навигация", "nav.home_desc": "Главная консоли и карточки разделов.", - "nav.reports_desc": "Просмотр и разбор отчётов о сбоях.", + "nav.reports_desc": "Просмотр и разбор issues.", "nav.tickets_desc": "Задачи roadmap, QA и теги.", "nav.graphs": "Графики", - "nav.graphs_desc": "Сессии, сбои и активность устройств.", + "nav.graphs_desc": "Сессии, issues и активность устройств.", "nav.remote": "Удалённый доступ", "nav.remote_desc": "Сессии WireGuard и доступ к устройствам.", "nav.access": "Доступ", @@ -35,12 +35,12 @@ "home.title": "Консоль", "home.intro": "Сбои, задачи и метрики сессий — выберите раздел ниже.", "home.card_issues": "Проблемы", - "home.card_issues_desc": "Просмотр и разбор отчётов о сбоях, тегов и отпечатков.", + "home.card_issues_desc": "Просмотр и разбор issues, тегов и отпечатков.", "home.card_new_issue_desc": "Создать новую проблему или задачу.", "home.card_new_ticket_desc": "Создать тикет roadmap или QA за один шаг.", "home.card_tickets_desc": "Задачи, QA и теги workflow.", "home.card_graphs": "Графики", - "home.card_graphs_desc": "Сессии, сбои и активность устройств.", + "home.card_graphs_desc": "Сессии, issues и активность устройств.", "home.card_short_links": "Короткие ссылки", "home.card_short_links_desc": "Токены и сокращение URL для s.f0xx.org.", "home.more": "Все сервисы", @@ -118,7 +118,7 @@ "tag.filter_and": "Все метки (И)", "tag.filter_or": "Любая метка (ИЛИ)", "detail.back": "К списку", - "detail.title": "Отчёт о сбое", + "detail.title": "Issue-отчёт", "detail.report_meta": "Отчёт {id} · {type}", "detail.tags": "Метки", "detail.device": "Устройство", @@ -134,13 +134,13 @@ "detail.filter_version": "Показать отчёты для этой версии приложения", "detail.filter_build": "Показать отчёты этой сборки", "detail.java_exception": "Исключение Java", - "detail.native_crash": "Нативный сбой", + "detail.native_crash": "Нативный issue", "detail.find_similar": "Найти похожие отчёты", "detail.thread": "Поток:", "detail.signal": "Сигнал:", - "detail.session_context": "Контекст сессии (сбой)", + "detail.session_context": "Контекст сессии (issue)", "detail.raw_json": "Исходный JSON", - "login.sign_in_hint": "Войдите для просмотра анонимных отчётов о сбоях", + "login.sign_in_hint": "Войдите для просмотра анонимных issue-отчётов", "login.username": "Имя пользователя", "login.password": "Пароль", "login.submit": "Войти", diff --git a/examples/crash_reporter/backend/public/assets/js/graphs.js b/examples/crash_reporter/backend/public/assets/js/graphs.js index 1e92f40..6c4b902 100644 --- a/examples/crash_reporter/backend/public/assets/js/graphs.js +++ b/examples/crash_reporter/backend/public/assets/js/graphs.js @@ -9,7 +9,7 @@ recv: { type: 'line', field: 'recv_sessions_per_day', title: 'Receive sessions / day', color: COLORS[4] }, bitrate: { type: 'line', field: 'avg_bitrate_kbps_per_day', title: 'Outbound kbps / day', color: COLORS[0] }, 'recv-kbps': { type: 'line', field: 'avg_recv_kbps_per_day', title: 'Inbound kbps / day', color: COLORS[1] }, - crashes: { type: 'line', field: 'crashes_per_day', title: 'Crashes / day', color: '#f87171' }, + crashes: { type: 'line', field: 'crashes_per_day', title: 'Issues / day', color: '#f87171' }, tickets: { type: 'line', field: 'tickets_per_day', title: 'Tickets / day', color: '#c084fc' }, 'install-pie': { type: 'pie', field: 'install_sources', title: 'Install sources' }, 'ntp-pie': { type: 'pie', field: 'ntp_sources', title: 'NTP sources' }, @@ -17,8 +17,8 @@ versions: { type: 'breakdown', field: 'app_versions', title: 'App versions' }, success: { type: 'kpi', field: 'success_ratio_pct', title: 'Success ratio', suffix: '%' }, 'ntp-avg': { type: 'kpi', field: 'ntp_correction_avg_s', title: 'NTP correction', suffix: ' s avg correction' }, - fingerprints: { type: 'toplist', field: 'top_crash_fingerprints', title: 'Top crash fingerprints' }, - 'crash-versions': { type: 'pie', field: 'crashes_by_app_version', title: 'Crashes by app version' }, + fingerprints: { type: 'toplist', field: 'top_crash_fingerprints', title: 'Top issue fingerprints' }, + 'crash-versions': { type: 'pie', field: 'crashes_by_app_version', title: 'Issues by app version' }, }; let graphScopes = { user: null, slug: null, platform: null }; diff --git a/examples/crash_reporter/backend/public/assets/js/remote_access.js b/examples/crash_reporter/backend/public/assets/js/remote_access.js index 45c4a77..84d0ac7 100644 --- a/examples/crash_reporter/backend/public/assets/js/remote_access.js +++ b/examples/crash_reporter/backend/public/assets/js/remote_access.js @@ -39,34 +39,166 @@ return d.innerHTML; } + function statusBadge(d) { + const label = d.status_label || ''; + if (label === 'needs_whitelist') { + return 'Needs whitelist'; + } + if (label === 'whitelisted') { + return 'Whitelisted'; + } + if (label === 'stale') { + return 'Stale'; + } + if (label === 'not_opted_in') { + return 'Not opted in'; + } + return 'Polling'; + } + + function deviceTitle(d) { + const name = (d.device_display || '').trim(); + if (name) { + return name; + } + const id = String(d.device_id || ''); + if (id.length > 14) { + return id.slice(0, 8) + '…' + id.slice(-4); + } + return id; + } + + function detailLines(d) { + const lines = []; + lines.push('Device ID: ' + (d.device_id || '—')); + if (d.device_display) { + lines.push('Name: ' + d.device_display); + } + if (d.manufacturer || d.model) { + lines.push('Hardware: ' + [d.manufacturer, d.model].filter(Boolean).join(' ')); + } + if (d.os_release || d.sdk_int) { + lines.push( + 'Android: ' + + [d.os_release || '', d.sdk_int ? 'SDK ' + d.sdk_int : ''].filter(Boolean).join(' · ') + ); + } + lines.push('App version: ' + (d.app_version || '—')); + lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none')); + lines.push('Last seen: ' + (d.last_seen_at || '—')); + if (d.notes) { + lines.push('Notes: ' + d.notes); + } + if (d.issue_count != null) { + lines.push('Linked issues: ' + d.issue_count); + } + if (d.graph_session_count != null) { + lines.push('Graph sessions: ' + d.graph_session_count); + } + return lines; + } + + function detailLinks(d) { + const links = d.links || []; + if (!links.length) { + return ''; + } + let html = ''; + return html; + } + + function bindRaTree(root) { + root.querySelectorAll('.report-tree-toggle').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const id = btn.getAttribute('aria-controls'); + const briefRow = id ? document.getElementById(id) : null; + const row = btn.closest('.report-row'); + if (!briefRow || !row) return; + const open = briefRow.hidden; + briefRow.hidden = !open; + btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + row.classList.toggle('report-row--open', open); + }); + }); + } + function renderDevices(devices) { const tbody = document.getElementById('ra-devices-tbody'); if (!tbody) return; - tbody.innerHTML = ''; if (!devices || !devices.length) { - tbody.innerHTML = 'No devices have polled yet.'; + tbody.innerHTML = + 'No devices have polled yet. Enable remote access on a device (dev settings) and wait for the next poll.'; return; } - (devices || []).forEach((d) => { - const tr = document.createElement('tr'); + let html = ''; + devices.forEach((d, i) => { + const rowKey = 'ra-' + i; + const briefId = 'ra-brief-' + rowKey; const wl = Number(d.whitelisted) === 1; const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh'); - const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard opt-in poll"'; + const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard/RSSH opt-in poll"'; const wlDisabled = canAdmin() ? '' : ' disabled'; - tr.innerHTML = - '' + esc(d.device_id) + '' + - '' + esc(d.opt_in_mode || 'none') + '' + - '' + (wl ? 'yes' : 'no') + '' + - '' + esc(d.last_seen_at || '—') + '' + - '' + esc(d.app_version || '—') + '' + - '' + - ' ' + - ' ' + - '' + + const rowClass = + 'report-row' + (d.needs_whitelist ? ' ra-device-row--needs-wl' : ''); + html += ''; + html += + ''; + html += + '' + + esc(deviceTitle(d)) + + ''; + html += '' + statusBadge(d) + ''; + html += '' + esc(d.opt_in_mode || 'none') + ''; + html += '' + esc(d.last_seen_at || '—') + ''; + html += '' + esc(d.app_version || '—') + ''; + html += + '' + + ' ' + + ' ' + + '' + ''; - tbody.appendChild(tr); + html += ''; + + html += + '
'; + detailLines(d).forEach((line) => { + html += '

' + esc(line) + '

'; + }); + html += detailLinks(d); + html += '
'; }); + tbody.innerHTML = html; + bindRaTree(tbody); } function renderSessions(active, inactive) { @@ -149,6 +281,9 @@ document.addEventListener('click', async (ev) => { const t = ev.target; if (!(t instanceof HTMLElement)) return; + if (t.closest('.report-tree-toggle, .ra-detail-links a')) { + return; + } const copyId = t.getAttribute('data-copy-id'); if (copyId) { try { diff --git a/examples/crash_reporter/backend/src/RemoteAccessRepository.php b/examples/crash_reporter/backend/src/RemoteAccessRepository.php index 224265e..239397e 100644 --- a/examples/crash_reporter/backend/src/RemoteAccessRepository.php +++ b/examples/crash_reporter/backend/src/RemoteAccessRepository.php @@ -739,6 +739,176 @@ final class RemoteAccessRepository { return Database::isMysql() ? date('Y-m-d H:i:s') : gmdate('Y-m-d H:i:s'); } + /** @return list> */ + public static function listDevicesForDashboard(string $issuesBase, string $projectBase): array { + $devices = self::listDevices(); + $out = []; + foreach ($devices as $row) { + $out[] = self::enrichDeviceRow($row, $issuesBase, $projectBase); + } + return $out; + } + + /** @param array $row */ + private static function enrichDeviceRow(array $row, string $issuesBase, string $projectBase): array { + $deviceId = (string) ($row['device_id'] ?? ''); + $companyId = (int) ($row['company_id'] ?? 1); + $ctx = self::lookupDeviceContext($deviceId, $companyId); + $row['needs_whitelist'] = self::deviceNeedsWhitelist($row); + $row['status_label'] = self::deviceStatusLabel($row); + $row['device_display'] = $ctx['device_display'] ?? ''; + $row['manufacturer'] = $ctx['manufacturer'] ?? ''; + $row['model'] = $ctx['model'] ?? ''; + $row['os_release'] = $ctx['os_release'] ?? ''; + $row['sdk_int'] = $ctx['sdk_int'] ?? null; + $row['issue_count'] = $ctx['issue_count'] ?? 0; + $row['graph_session_count'] = $ctx['graph_session_count'] ?? 0; + $row['latest_issue_id'] = $ctx['latest_issue_id'] ?? null; + $row['links'] = self::deviceLinks($deviceId, $issuesBase, $projectBase, $ctx); + if (($row['app_version'] ?? '') === '' && ($ctx['app_version'] ?? '') !== '') { + $row['app_version'] = $ctx['app_version']; + } + return $row; + } + + /** @param array $row */ + private static function deviceNeedsWhitelist(array $row): bool { + if ((int) ($row['whitelisted'] ?? 0) === 1) { + return false; + } + $mode = (string) ($row['opt_in_mode'] ?? 'none'); + if ($mode === self::OPT_NONE) { + return false; + } + return self::isRecentTimestamp($row['last_seen_at'] ?? null, 7 * 86400); + } + + /** @param array $row */ + private static function deviceStatusLabel(array $row): string { + if ((int) ($row['whitelisted'] ?? 0) === 1) { + return 'whitelisted'; + } + if (self::deviceNeedsWhitelist($row)) { + return 'needs_whitelist'; + } + $mode = (string) ($row['opt_in_mode'] ?? 'none'); + if ($mode === self::OPT_NONE) { + return 'not_opted_in'; + } + if (!self::isRecentTimestamp($row['last_seen_at'] ?? null, 7 * 86400)) { + return 'stale'; + } + return 'polling'; + } + + private static function isRecentTimestamp(?string $ts, int $maxAgeSec): bool { + if ($ts === null || trim($ts) === '') { + return false; + } + $t = strtotime($ts); + if ($t === false) { + return false; + } + return (time() - $t) <= $maxAgeSec; + } + + /** @return array */ + private static function lookupDeviceContext(string $deviceId, int $companyId): array { + if ($deviceId === '') { + return []; + } + $pdo = Database::pdo(); + $like = '%' . $deviceId . '%'; + $issueCount = 0; + $latestIssueId = null; + $deviceDisplay = ''; + $manufacturer = ''; + $model = ''; + $osRelease = ''; + $sdkInt = null; + $appVersion = ''; + + if (Database::tableExists($pdo, 'reports')) { + $cntSt = $pdo->prepare( + 'SELECT COUNT(*) FROM reports r WHERE r.company_id = ? AND r.payload_json LIKE ?' + ); + $cntSt->execute([$companyId, $like]); + $issueCount = (int) $cntSt->fetchColumn(); + + $latestSt = $pdo->prepare( + 'SELECT r.id, r.device_model, r.app_version, r.payload_json + FROM reports r + WHERE r.company_id = ? AND r.payload_json LIKE ? + ORDER BY r.received_at_ms DESC LIMIT 1' + ); + $latestSt->execute([$companyId, $like]); + $latest = $latestSt->fetch(PDO::FETCH_ASSOC); + if (is_array($latest)) { + $latestIssueId = (int) ($latest['id'] ?? 0) ?: null; + $appVersion = (string) ($latest['app_version'] ?? ''); + $payload = json_decode((string) ($latest['payload_json'] ?? '{}'), true); + if (is_array($payload)) { + $device = is_array($payload['device'] ?? null) ? $payload['device'] : []; + $manufacturer = trim((string) ($device['manufacturer'] ?? '')); + $model = trim((string) ($device['model'] ?? $latest['device_model'] ?? '')); + $osRelease = trim((string) ($device['release'] ?? '')); + $sdkInt = isset($device['sdk_int']) ? (int) $device['sdk_int'] : null; + $deviceDisplay = trim($manufacturer . ' ' . $model); + if ($appVersion === '' && is_array($payload['app'] ?? null)) { + $appVersion = trim((string) ($payload['app']['version_name'] ?? '')); + } + } + } + } + + $graphCount = 0; + if (Database::tableExists($pdo, 'graph_sessions')) { + $gSt = $pdo->prepare('SELECT COUNT(*) FROM graph_sessions WHERE device_id = ?'); + $gSt->execute([$deviceId]); + $graphCount = (int) $gSt->fetchColumn(); + } + + return [ + 'issue_count' => $issueCount, + 'latest_issue_id' => $latestIssueId, + 'graph_session_count' => $graphCount, + 'device_display' => $deviceDisplay, + 'manufacturer' => $manufacturer, + 'model' => $model, + 'os_release' => $osRelease, + 'sdk_int' => $sdkInt, + 'app_version' => $appVersion, + ]; + } + + /** @param array $ctx @return list */ + private static function deviceLinks(string $deviceId, string $issuesBase, string $projectBase, array $ctx): array { + if ($deviceId === '') { + return []; + } + $links = []; + $issueCount = (int) ($ctx['issue_count'] ?? 0); + $issuesLabel = $issueCount > 0 ? 'Issues (' . $issueCount . ')' : 'Issues'; + $links[] = [ + 'label' => $issuesLabel, + 'href' => $issuesBase . '/?view=reports&q=' . rawurlencode($deviceId), + ]; + if (!empty($ctx['latest_issue_id'])) { + $links[] = [ + 'label' => 'Latest issue #' . (int) $ctx['latest_issue_id'], + 'href' => $issuesBase . '/?view=report&id=' . (int) $ctx['latest_issue_id'], + ]; + } + $graphCount = (int) ($ctx['graph_session_count'] ?? 0); + if ($graphCount > 0) { + $links[] = [ + 'label' => 'Graph sessions (' . $graphCount . ')', + 'href' => rtrim($projectBase, '/') . '/graphs/', + ]; + } + return $links; + } + private static function uuid4(): string { $b = random_bytes(16); $b[6] = chr((ord($b[6]) & 0x0f) | 0x40); diff --git a/examples/crash_reporter/backend/views/layout.php b/examples/crash_reporter/backend/views/layout.php index 70bb251..c978018 100644 --- a/examples/crash_reporter/backend/views/layout.php +++ b/examples/crash_reporter/backend/views/layout.php @@ -86,7 +86,7 @@ Reports - Browse and triage crash reports. + Browse and triage issues. @@ -111,7 +111,7 @@ Graphs - Sessions, crashes, and device activity. + Sessions, issues, and device activity. @@ -370,7 +370,7 @@

Success ratio

Outbound kbps / day

Inbound kbps / day

-

Crashes / day

+

Issues / day

@@ -383,7 +383,7 @@

NTP sources

Transport mix

App versions

-

Crashes / day

+

Issues / day

Tickets / day

@@ -392,12 +392,12 @@

Platform reliability

Sessions / day (all slugs)

-

Crashes / day

+

Issues / day

Tickets / day

Install sources

NTP correction

-

Top crash fingerprints

-

Crashes by app version

+

Top issue fingerprints

+

Issues by app version

@@ -438,14 +438,20 @@

Loading…

-

Whitelisted devices

+

Devices

+

+ Devices appear after they poll remote access (dev settings → WireGuard or RSSH). Sort is by last seen — top rows are actively polling. + Whitelist rows marked Needs whitelist (opted in, not yet allowed). + Stale rows have not polled in 7+ days. +

- +
- + + + - diff --git a/examples/crash_reporter/backend/views/login.php b/examples/crash_reporter/backend/views/login.php index 9c07d58..39c1f7b 100644 --- a/examples/crash_reporter/backend/views/login.php +++ b/examples/crash_reporter/backend/views/login.php @@ -42,7 +42,7 @@ $bp = Auth::basePath();

-

Sign in to view anonymous crash reports

+

Sign in to view anonymous issue reports

diff --git a/examples/crash_reporter/backend/views/partials/console_home_landing.php b/examples/crash_reporter/backend/views/partials/console_home_landing.php index ecb2d68..b8d660a 100644 --- a/examples/crash_reporter/backend/views/partials/console_home_landing.php +++ b/examples/crash_reporter/backend/views/partials/console_home_landing.php @@ -16,13 +16,13 @@ $proj = '/app/androidcast_project'; -

Crash triage, tickets, and session metrics — pick a workspace below.

+

Issue triage, tickets, and session metrics — pick a workspace below.

@@ -145,7 +145,7 @@ if (!empty($device['abis']) && is_array($device['abis'])) {
-

Native crash

+

Native issue

Find similar reports

Signal:

@@ -162,7 +162,7 @@ if (!empty($device['abis']) && is_array($device['abis'])) { - Session context (crash) + Session context (issue)
Device IDDeviceStatus Opt-inWhitelisted Last seen App Actions