diff --git a/examples/crash_reporter/backend/config/config.example.php b/examples/crash_reporter/backend/config/config.example.php
index f51a251..57e49c8 100644
--- a/examples/crash_reporter/backend/config/config.example.php
+++ b/examples/crash_reporter/backend/config/config.example.php
@@ -11,7 +11,7 @@
* Digest: SHA256 fcd4a89ef4201736923d0e7e1ad88868bba7e39eadb9fcf0ffb9d1ba7efecd12
*/
return [
- 'app_name' => 'Android Cast Crashes',
+ 'app_name' => 'Android Cast Issues',
// Production: '/app/androidcast_project/crashes' — Local php -S: use '' (empty string)
'base_path' => '/app/androidcast_project/crashes',
'db' => [
diff --git a/examples/crash_reporter/backend/public/assets/i18n/en.json b/examples/crash_reporter/backend/public/assets/i18n/en.json
index d8bf387..b0b3fbe 100644
--- a/examples/crash_reporter/backend/public/assets/i18n/en.json
+++ b/examples/crash_reporter/backend/public/assets/i18n/en.json
@@ -1,7 +1,7 @@
{
- "app_name": "Android Cast Crashes",
+ "app_name": "Android Cast Issues",
"nav.home": "Home",
- "nav.reports": "Reports",
+ "nav.reports": "Issues",
"nav.tickets": "Tickets",
"nav.logout": "Logout",
"nav.toggle": "Navigation",
@@ -11,12 +11,12 @@
"lang.label": "Language",
"lang.en": "English",
"lang.ru": "Russian",
- "home.title": "Crash console",
+ "home.title": "Issues console",
"home.intro": "Anonymous Android Cast crash ingest is active. Use Reports to analyze grouped fingerprints.",
"home.upload_api": "Upload API:",
"home.schema": "Schema:",
"home.ticket_upload_api": "Ticket upload API:",
- "reports.title": "Crash reports",
+ "reports.title": "Issues",
"reports.by_time": "By time",
"reports.grouped": "Grouped",
"reports.per_page": "Per page",
diff --git a/examples/crash_reporter/backend/public/assets/i18n/ru.json b/examples/crash_reporter/backend/public/assets/i18n/ru.json
index bb382d0..78c3469 100644
--- a/examples/crash_reporter/backend/public/assets/i18n/ru.json
+++ b/examples/crash_reporter/backend/public/assets/i18n/ru.json
@@ -1,7 +1,7 @@
{
"app_name": "Сбои Android Cast",
"nav.home": "Главная",
- "nav.reports": "Отчёты",
+ "nav.reports": "Задачи (Issues)",
"nav.tickets": "Задачи",
"tickets.title": "Задачи",
"tickets.empty": "Задач пока нет.",
@@ -18,7 +18,7 @@
"home.intro": "Приём анонимных отчётов Android Cast активен. Откройте «Отчёты» для анализа сгруппированных отпечатков.",
"home.upload_api": "API загрузки:",
"home.schema": "Схема:",
- "reports.title": "Отчёты о сбоях",
+ "reports.title": "Issues",
"reports.by_time": "По времени",
"reports.grouped": "Группы",
"reports.per_page": "На странице",
diff --git a/examples/crash_reporter/backend/public/index.php b/examples/crash_reporter/backend/public/index.php
index 33e66c6..be0a128 100644
--- a/examples/crash_reporter/backend/public/index.php
+++ b/examples/crash_reporter/backend/public/index.php
@@ -209,7 +209,7 @@ $pageTitle = match ($view) {
'graphs' => 'Graphs',
'remote_access' => 'Remote access',
'rbac' => 'Access control',
- 'reports', 'report' => 'Crash reports',
+ 'reports', 'report' => 'Issues',
default => 'Console',
};
require __DIR__ . '/../views/layout.php';
diff --git a/examples/crash_reporter/backend/scripts/ingest_roadmap_tickets_20260604.php b/examples/crash_reporter/backend/scripts/ingest_roadmap_tickets_20260604.php
new file mode 100755
index 0000000..6c42ec5
--- /dev/null
+++ b/examples/crash_reporter/backend/scripts/ingest_roadmap_tickets_20260604.php
@@ -0,0 +1,111 @@
+#!/usr/bin/env php
+ */
+function roadmap_tags(string $status): array {
+ $statusMap = [
+ 'open' => ['id' => 'open', 'label' => 'open', 'bg' => '#22c55e'],
+ 'in-progress' => ['id' => 'in-progress', 'label' => 'in progress', 'bg' => '#3b82f6'],
+ 'closed' => ['id' => 'closed', 'label' => 'closed', 'bg' => '#6b7280'],
+ ];
+ return [
+ ['id' => 'ticket', 'label' => 'ticket', 'bg' => '#6366f1'],
+ ['id' => '20260604', 'label' => '20260604', 'bg' => '#0ea5e9'],
+ $statusMap[$status] ?? $statusMap['open'],
+ ];
+}
+
+/** title + body are the same description text */
+$tasks = [
+ ['key' => 'bug-ticket-detail-500', 'status' => 'closed', 'text' => 'ISSUE: Ticket detail page HTTP 500 (?view=ticket&id=N). Fix recursion in comment/attachment loaders when rendering detail.'],
+ ['key' => '8', 'status' => 'closed', 'text' => 'COMPLETED: Orchestration branch feature/20260527_orchestration — merged to next; work lives under orchestration/; close/delete branch.'],
+ ['key' => '1', 'status' => 'open', 'text' => 'BE/builder deployment phases — dedicated services on BE; WIP verifying (Docker, out/builds, runner_script).'],
+ ['key' => '2', 'status' => 'open', 'text' => 'OTA checks — part of builder/deploy path; verify artifacts and device OTA flow.'],
+ ['key' => '3', 'status' => 'open', 'text' => 'Opus/Speex validation — E2E, encryption, FEC, NACK, negotiations; user and developer controls.'],
+ ['key' => '4', 'status' => 'open', 'text' => 'RSSH/WG (WireGuard v1) — finish deploy, prod validations, minimal tests; RA E2E CLI + Android poll.'],
+ ['key' => '5', 'status' => 'open', 'text' => 'Data visualization / graphs — WIP; issues found on drill-down and layout.'],
+ ['key' => '5.1', 'status' => 'open', 'text' => 'ISSUE 5.1: Graph brick click must open full-browser detailed chart (axis, legend, hover per point like Java visualizer).'],
+ ['key' => '5.2', 'status' => 'open', 'text' => 'REQUIREMENT 5.2: All graph preview bricks clickable; Columns combo (1/2/4) after Window default 2; grid uses full width.'],
+ ['key' => '5.3', 'status' => 'closed', 'text' => 'ISSUE 5.3: Remove Direct DB metrics / graph_upload / graphs.php footnote from graphs page.'],
+ ['key' => '6', 'status' => 'open', 'text' => 'Landing page checks, fixes, improvements — hub, styling, integration with dashboards.'],
+ ['key' => '6.1', 'status' => 'open', 'text' => 'Landing 6.1: Hub dashboards for graphs, builder, remote access — partially done.'],
+ ['key' => '6.1.1', 'status' => 'closed', 'text' => 'ISSUE 6.1.1: Remote access / graphs quick links — shell nav icons and 8px spacing.'],
+ ['key' => '6.1.2', 'status' => 'closed', 'text' => 'ISSUE 6.1.2: Remove device poll / heartbeat dev footnote from Remote access page.'],
+ ['key' => '6.2', 'status' => 'open', 'text' => 'Landing 6.2: Globe/earth animations — choose base on next; branch cleanup (3d_earth vs 20260531_globe_animation); clock/footer styling.'],
+ ['key' => '6.3', 'status' => 'open', 'text' => 'Landing 6.3: Mobile device validations (mobile chrome) — LOW priority.'],
+ ['key' => '7', 'status' => 'open', 'text' => 'BE roles RBAC — admin/root, slug admins, privilege sets; finish after related pages stable.'],
+ ['key' => 'net-slowness', 'status' => 'open', 'text' => 'ISSUE: Sporadic FE/BE slowness (SSH, sshfs, web) — WAN/modem queueing suspected; investigate bufferbloat and paths.'],
+];
+
+$device = [
+ 'manufacturer' => 'roadmap',
+ 'model' => 'planning',
+ 'sdk_int' => 34,
+ 'release' => '14',
+];
+$app = ['package' => 'com.foxx.androidcast', 'version_name' => 'roadmap', 'version_code' => 1];
+$now = (int) round(microtime(true) * 1000);
+$ok = 0;
+$fail = 0;
+
+foreach ($tasks as $task) {
+ $text = trim($task['text']);
+ $key = $task['key'];
+ $status = $task['status'];
+ $payload = [
+ 'schema_version' => 1,
+ 'ingest_kind' => 'ticket',
+ 'ticket_id' => 'roadmap-20260604-' . preg_replace('/[^a-z0-9.-]+/i', '-', $key),
+ 'title' => mb_substr($text, 0, 256),
+ 'brief' => mb_substr($text, 0, 512),
+ 'body' => $text,
+ 'opened_at_epoch_ms' => $now,
+ 'rating' => $status === 'closed' ? 1 : 3,
+ 'owner' => $owner,
+ 'tags' => roadmap_tags($status),
+ 'device' => $device,
+ 'app' => $app,
+ ];
+ $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ if ($dryRun) {
+ echo "DRY {$key} [{$status}]\n";
+ $ok++;
+ continue;
+ }
+ $ctx = stream_context_create([
+ 'http' => [
+ 'method' => 'POST',
+ 'header' => "Content-Type: application/json\r\n",
+ 'content' => $json,
+ 'ignore_errors' => true,
+ ],
+ ]);
+ $resp = @file_get_contents($uploadUrl, false, $ctx);
+ $code = 0;
+ if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
+ $code = (int) $m[1];
+ }
+ if ($code === 200 || $code === 409) {
+ echo ($code === 409 ? 'DUP ' : 'OK ') . "{$key}\n";
+ $ok++;
+ } else {
+ echo "FAIL {$key} HTTP {$code} — {$resp}\n";
+ $fail++;
+ }
+}
+
+echo "Done: ok={$ok} fail={$fail}\n";
+exit($fail > 0 ? 1 : 0);
diff --git a/examples/crash_reporter/backend/src/TicketAttachmentRepository.php b/examples/crash_reporter/backend/src/TicketAttachmentRepository.php
index 2a4b71c..20a125b 100644
--- a/examples/crash_reporter/backend/src/TicketAttachmentRepository.php
+++ b/examples/crash_reporter/backend/src/TicketAttachmentRepository.php
@@ -22,9 +22,7 @@ final class TicketAttachmentRepository {
}
public static function listForTicket(int $ticketId): array {
- if (!TicketRepository::getById($ticketId)) {
- return [];
- }
+ // Do not call TicketRepository::getById here — avoids recursion from mapDetailRow.
$pdo = Database::pdo();
$stmt = $pdo->prepare(
'SELECT * FROM ticket_attachments WHERE ticket_id = ? ORDER BY created_at_ms ASC, id ASC'
diff --git a/examples/crash_reporter/backend/src/TicketCommentRepository.php b/examples/crash_reporter/backend/src/TicketCommentRepository.php
index f420c68..1f82d80 100644
--- a/examples/crash_reporter/backend/src/TicketCommentRepository.php
+++ b/examples/crash_reporter/backend/src/TicketCommentRepository.php
@@ -3,10 +3,7 @@ declare(strict_types=1);
final class TicketCommentRepository {
public static function listForTicket(int $ticketId): array {
- $ticket = TicketRepository::getById($ticketId);
- if (!$ticket) {
- return [];
- }
+ // Do not call TicketRepository::getById here — mapDetailRow already loads comments.
$pdo = Database::pdo();
$stmt = $pdo->prepare(
'SELECT id, ticket_id, author_username, body, created_at_ms, updated_at_ms
diff --git a/examples/crash_reporter/backend/views/layout.php b/examples/crash_reporter/backend/views/layout.php
index 12718cd..0c93728 100644
--- a/examples/crash_reporter/backend/views/layout.php
+++ b/examples/crash_reporter/backend/views/layout.php
@@ -202,6 +202,7 @@
+
diff --git a/examples/crash_reporter/backend/views/partials/console_quick_links.php b/examples/crash_reporter/backend/views/partials/console_quick_links.php
index 48702b7..6aa3f3b 100644
--- a/examples/crash_reporter/backend/views/partials/console_quick_links.php
+++ b/examples/crash_reporter/backend/views/partials/console_quick_links.php
@@ -9,7 +9,7 @@ $proj = '/app/androidcast_project';
$navClass = 'console-quick-links graphs-quick-links' . (isset($extra_class) ? ' ' . $extra_class : '');
$items = [
['Console home', $bp . '/?view=home', 'nav-icon--home'],
- ['Crash reports', $bp . '/?view=reports', 'nav-icon--reports'],
+ ['Issues', $bp . '/?view=reports', 'nav-icon--reports'],
['Tickets', $bp . '/?view=tickets', 'nav-icon--tickets'],
['Graphs', $proj . '/graphs/', 'nav-icon--graphs'],
['Builder', $proj . '/build/', 'nav-icon--builder'],
diff --git a/examples/crash_reporter/backend/views/ticket_detail.php b/examples/crash_reporter/backend/views/ticket_detail.php
index cee4bb4..c46c460 100644
--- a/examples/crash_reporter/backend/views/ticket_detail.php
+++ b/examples/crash_reporter/backend/views/ticket_detail.php
@@ -4,7 +4,7 @@ $p = is_array($ticket['payload'] ?? null) ? $ticket['payload'] : [];
$device = is_array($p['device'] ?? null) ? $p['device'] : [];
$app = is_array($p['app'] ?? null) ? $p['app'] : [];
$pageTitle = 'Ticket';
-$view = 'tickets';
+$view = 'ticket';
$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
$rawJson = safe_json_encode($p, $jsonFlags);
$bp = Auth::basePath();