From e234a7b53548eea052409203ade6efda827eb8c6 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Wed, 10 Jun 2026 09:15:19 +0200 Subject: [PATCH] be sync --- examples/app_hub/assets/i18n/hub-en.json | 3 + examples/app_hub/assets/i18n/hub-ru.json | 3 + examples/app_hub/hub-landing.css | 14 +++++ examples/app_hub/index.php | 47 +++++++++++++- .../backend/public/assets/css/app.css | 45 ++++++++++++++ .../backend/public/assets/i18n/en.json | 12 +++- .../backend/public/assets/i18n/ru.json | 14 ++++- .../backend/public/assets/js/app.js | 21 ++++++- .../backend/public/assets/js/ticket_create.js | 36 ++++++++++- .../backend/src/TicketTagCatalog.php | 61 +++++++++++++------ .../backend/src/TicketWorkflow.php | 2 +- .../crash_reporter/backend/views/layout.php | 8 +-- .../views/partials/console_home_landing.php | 46 ++++++++++++++ .../backend/views/ticket_detail.php | 2 +- 14 files changed, 280 insertions(+), 34 deletions(-) create mode 100644 examples/crash_reporter/backend/views/partials/console_home_landing.php diff --git a/examples/app_hub/assets/i18n/hub-en.json b/examples/app_hub/assets/i18n/hub-en.json index 895dddf..2464a9a 100644 --- a/examples/app_hub/assets/i18n/hub-en.json +++ b/examples/app_hub/assets/i18n/hub-en.json @@ -15,6 +15,9 @@ "nav.remote": "Remote access", "nav.builder": "Builder", "nav.toggle": "Navigation", + "nav.security": "Security", + "nav.logout": "Logout", + "nav.sign_in": "Sign in", "lang.label": "Language", "theme.label": "Theme", "rail.label": "Page navigation", diff --git a/examples/app_hub/assets/i18n/hub-ru.json b/examples/app_hub/assets/i18n/hub-ru.json index 209e1d5..215bad6 100644 --- a/examples/app_hub/assets/i18n/hub-ru.json +++ b/examples/app_hub/assets/i18n/hub-ru.json @@ -15,6 +15,9 @@ "nav.remote": "Удалённый доступ", "nav.builder": "Сборщик", "nav.toggle": "Навигация", + "nav.security": "Безопасность", + "nav.logout": "Выход", + "nav.sign_in": "Вход", "lang.label": "Язык", "theme.label": "Тема", "rail.label": "Навигация по страницам", diff --git a/examples/app_hub/hub-landing.css b/examples/app_hub/hub-landing.css index 52f80bf..c3dfb1f 100644 --- a/examples/app_hub/hub-landing.css +++ b/examples/app_hub/hub-landing.css @@ -158,12 +158,26 @@ /* (2) Left shell — console nav-pane */ .landing-left.nav-pane { position: relative; + display: flex; + flex-direction: column; flex: 0 0 var(--landing-left-w); width: var(--landing-left-w); height: 100%; + max-height: 100%; + overflow: hidden; z-index: 3; pointer-events: auto; border-right: 1px solid var(--border); + background: var(--surface); +} +.landing-left .nav-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; +} +.landing-left .nav-user { + margin-top: auto; + border-top: 1px solid var(--border); } .landing-left.nav-pane.open { width: 200px; diff --git a/examples/app_hub/index.php b/examples/app_hub/index.php index 74362d4..fe4e52b 100644 --- a/examples/app_hub/index.php +++ b/examples/app_hub/index.php @@ -4,7 +4,22 @@ if (!is_file($__platformDir . '/footer.php')) { $__platformDir = dirname(__DIR__) . '/platform'; } require_once $__platformDir . '/footer.php'; -unset($__platformDir); +$__sessionFile = $__platformDir . '/shared_session.php'; +$hubCrashesBase = '/app/androidcast_project/crashes'; +$hubUsername = ''; +if (is_file($__sessionFile) && function_exists('session_start')) { + require_once $__sessionFile; + platform_start_session('ac_crash_sess', '/app/androidcast_project'); + $hubUser = $_SESSION['user'] ?? null; + if (is_array($hubUser)) { + $hubUsername = trim((string) ($hubUser['username'] ?? '')); + } +} +unset($__platformDir, $__sessionFile); + +function hub_h(string $s): string { + return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +} ?> @@ -129,6 +144,36 @@ unset($__platformDir);
  • Remote access
  • Builder
  • +
    diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index d7cfcc9..e6a01cf 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -1576,6 +1576,51 @@ button.report-tag--filter:hover { .tag-modal-close:hover { color: var(--text); background: var(--row-hover); } .tag-editor--modal { margin: 0; border: none; border-radius: 0; } +.console-home-intro { + margin: 0 0 20px; + color: var(--muted); + max-width: 42rem; + line-height: 1.5; +} +.console-home-cards { + margin: 0 0 28px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} +.console-home-card { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + text-decoration: none; + color: var(--text); + cursor: pointer; + font: inherit; + text-align: left; + width: 100%; +} +.console-home-card--btn { + border: 1px solid var(--border); + background: var(--surface); +} +.console-home-card h2 { + margin: 0; + font-size: 1.05rem; + font-weight: 600; +} +.console-home-card p { + margin: 0; + font-size: 0.88rem; + line-height: 1.45; +} +.console-home-card .nav-icon { + opacity: 0.9; +} +.console-home-more { + margin: 8px 0 12px; + font-size: 0.95rem; + font-weight: 600; + color: var(--muted); +} .graphs-app .graphs-quick-links, .console-quick-links { display: flex; diff --git a/examples/crash_reporter/backend/public/assets/i18n/en.json b/examples/crash_reporter/backend/public/assets/i18n/en.json index 4286852..cc17e99 100644 --- a/examples/crash_reporter/backend/public/assets/i18n/en.json +++ b/examples/crash_reporter/backend/public/assets/i18n/en.json @@ -11,8 +11,15 @@ "lang.label": "Language", "lang.en": "English", "lang.ru": "Russian", - "home.title": "Issues console", - "home.intro": "Operator console for crash triage and tickets — not the public upload API. Open Issues to browse and group fingerprints, or use the shortcuts below.", + "home.title": "Console", + "home.intro": "Crash 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_new_issue_desc": "Log a new issue or task 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.more": "All services", "reports.title": "Issues", "reports.by_time": "By time", "reports.grouped": "Grouped", @@ -146,6 +153,7 @@ "nav.security": "Security", "footer.copyright": "© Anton Afanaasyeu, {year}", "tickets.title": "Tickets", + "issues.new": "New issue", "tickets.new": "New ticket", "ticket.create": "Create", "ticket.cancel": "Cancel", diff --git a/examples/crash_reporter/backend/public/assets/i18n/ru.json b/examples/crash_reporter/backend/public/assets/i18n/ru.json index 7aa9f0a..db53e57 100644 --- a/examples/crash_reporter/backend/public/assets/i18n/ru.json +++ b/examples/crash_reporter/backend/public/assets/i18n/ru.json @@ -4,7 +4,8 @@ "nav.reports": "Задачи (Issues)", "nav.tickets": "Задачи", "tickets.title": "Задачи", - "tickets.new": "Новая задача", + "issues.new": "Новая проблема", + "tickets.new": "Новый тикет", "ticket.create": "Создать", "ticket.cancel": "Отмена", "ticket.title": "Заголовок", @@ -20,8 +21,15 @@ "lang.label": "Язык", "lang.en": "English", "lang.ru": "Русский", - "home.title": "Консоль сбоев", - "home.intro": "Операторская консоль для триажа сбоев и тикетов — не публичный API загрузки. Откройте Issues для просмотра и группировки отпечатков или ссылки ниже.", + "home.title": "Консоль", + "home.intro": "Сбои, задачи и метрики сессий — выберите раздел ниже.", + "home.card_issues": "Проблемы", + "home.card_issues_desc": "Просмотр и разбор отчётов о сбоях, тегов и отпечатков.", + "home.card_new_issue_desc": "Создать новую проблему или задачу.", + "home.card_tickets_desc": "Задачи, QA и теги workflow.", + "home.card_graphs": "Графики", + "home.card_graphs_desc": "Сессии, сбои и активность устройств.", + "home.more": "Все сервисы", "reports.title": "Issues", "reports.by_time": "По времени", "reports.grouped": "Группы", diff --git a/examples/crash_reporter/backend/public/assets/js/app.js b/examples/crash_reporter/backend/public/assets/js/app.js index c4a3864..8d606f8 100644 --- a/examples/crash_reporter/backend/public/assets/js/app.js +++ b/examples/crash_reporter/backend/public/assets/js/app.js @@ -1284,6 +1284,20 @@ return basePath() + (entityType === 'ticket' ? '/api/ticket_tags.php' : '/api/report_tags.php'); } + const TICKET_META_IDS = ['ticket', 'issue']; + + function isTicketMetaId(id) { + return TICKET_META_IDS.indexOf(id) >= 0; + } + + function stripTicketMetaTags(tags) { + return (tags || []).filter((t) => !isTicketMetaId(t.id)); + } + + function tagsJson(tags) { + return JSON.stringify(tags || []); + } + function createTagEditor(cfg) { const entityType = cfg.entityType === 'ticket' ? 'ticket' : 'report'; const state = { @@ -1357,6 +1371,9 @@ state.workflow.find((x) => x.id === id) || state.presets.find((x) => x.id === id); if (!p || state.tags.some((t) => t.id === p.id)) return; + if (entityType === 'ticket' && isTicketMetaId(p.id)) { + state.tags = stripTicketMetaTags(state.tags); + } state.tags.push({ ...p }); renderAll(); }); @@ -1436,6 +1453,7 @@ if (els.saveBtn) { els.saveBtn.addEventListener('click', () => { setStatus(t('tag.saving'), true); + const sentTags = state.tags.map((t) => ({ ...t })); const xhr = new XMLHttpRequest(); xhr.open('POST', tagsApiUrl(state.entityType), true); xhr.setRequestHeader('Content-Type', 'application/json'); @@ -1453,7 +1471,8 @@ } state.tags = data.tags || []; renderAll(); - setStatus('Saved', true); + const adjusted = tagsJson(sentTags) !== tagsJson(state.tags); + setStatus(adjusted ? 'Saved (type/status tags normalized)' : 'Saved', true); if (cfg.onSaved) cfg.onSaved(state.tags, state.entityId); }; xhr.onerror = function () { diff --git a/examples/crash_reporter/backend/public/assets/js/ticket_create.js b/examples/crash_reporter/backend/public/assets/js/ticket_create.js index ac6077f..3a8a792 100644 --- a/examples/crash_reporter/backend/public/assets/js/ticket_create.js +++ b/examples/crash_reporter/backend/public/assets/js/ticket_create.js @@ -1,16 +1,34 @@ /** - * Global "New issue" dialog — one click from nav or any toolbar. + * Create ticket/issue dialog — Issues toolbar uses "issue" type tag; Tickets uses "ticket". */ (function () { + const META = { + ticket: { id: 'ticket', label: 'ticket', bg: '#6366f1' }, + issue: { id: 'issue', label: 'issue', bg: '#0d9488' }, + }; + function basePath() { return document.body.getAttribute('data-base-path') || ''; } - function openDialog() { + function t(key) { + if (window.CrashI18n && window.CrashI18n.t) { + return window.CrashI18n.t(key); + } + return key; + } + + function openDialog(kind) { const dialog = document.getElementById('ticket-create-dialog'); const form = document.getElementById('ticket-create-form'); const status = document.getElementById('ticket-create-status'); + const heading = document.getElementById('ticket-create-heading'); if (!dialog || !form) return; + const createKind = kind === 'issue' ? 'issue' : 'ticket'; + form.dataset.createKind = createKind; + if (heading) { + heading.textContent = t(createKind === 'issue' ? 'issues.new' : 'tickets.new'); + } if (status) status.textContent = ''; form.reset(); if (typeof dialog.showModal === 'function') { @@ -26,10 +44,16 @@ const status = document.getElementById('ticket-create-status'); if (!dialog || !form) return; + document.querySelectorAll('.js-new-issue-btn').forEach((btn) => { + btn.addEventListener('click', (ev) => { + ev.preventDefault(); + openDialog('issue'); + }); + }); document.querySelectorAll('.js-new-ticket-btn').forEach((btn) => { btn.addEventListener('click', (ev) => { ev.preventDefault(); - openDialog(); + openDialog('ticket'); }); }); @@ -47,10 +71,16 @@ const titleEl = document.getElementById('ticket-create-title'); const briefEl = document.getElementById('ticket-create-brief'); const bodyEl = document.getElementById('ticket-create-body'); + const kind = form.dataset.createKind === 'issue' ? 'issue' : 'ticket'; + const meta = META[kind] || META.ticket; const payload = { title: titleEl && titleEl.value ? titleEl.value.trim() : '', brief: briefEl && briefEl.value ? briefEl.value.trim() : '', body: bodyEl && bodyEl.value ? bodyEl.value.trim() : '', + tags: [ + meta, + { id: 'open', label: 'open', bg: '#22c55e' }, + ], }; if (!payload.title) return; if (status) status.textContent = 'Creating…'; diff --git a/examples/crash_reporter/backend/src/TicketTagCatalog.php b/examples/crash_reporter/backend/src/TicketTagCatalog.php index a71d5db..1c9758e 100644 --- a/examples/crash_reporter/backend/src/TicketTagCatalog.php +++ b/examples/crash_reporter/backend/src/TicketTagCatalog.php @@ -1,10 +1,19 @@ */ + public const META_IDS = ['ticket', 'issue']; + + /** @var array */ + private const META = [ + 'ticket' => ['label' => 'ticket', 'bg' => '#6366f1'], + 'issue' => ['label' => 'issue', 'bg' => '#0d9488'], + ]; + /** @var array */ private const STATUS = [ 'open' => ['label' => 'open', 'bg' => '#22c55e'], @@ -25,54 +34,72 @@ final class TicketTagCatalog { /** @return list */ public static function presets(): array { - $out = [ - ['id' => self::META_ID, 'label' => 'ticket', 'bg' => '#6366f1'], - ]; + $out = []; + foreach (self::META as $id => $def) { + $out[] = ['id' => $id, 'label' => $def['label'], 'bg' => $def['bg']]; + } foreach (self::STATUS as $id => $def) { $out[] = ['id' => $id, 'label' => $def['label'], 'bg' => $def['bg']]; } return $out; } + public static function isMetaId(string $id): bool { + return isset(self::META[$id]); + } + /** * @param list $tags * @return list */ public static function normalize(array $tags): array { $normalized = normalize_report_tags($tags); - $hasMeta = false; + $metaId = null; $hasStatus = false; + $rest = []; foreach ($normalized as $t) { - if ($t['id'] === self::META_ID) { - $hasMeta = true; + if (self::isMetaId($t['id'])) { + $metaId = $t['id']; + continue; } if (isset(self::STATUS[$t['id']])) { $hasStatus = true; } + $rest[] = $t; } - if (!$hasMeta) { - $normalized[] = ['id' => self::META_ID, 'label' => 'ticket', 'bg' => '#6366f1']; + if ($metaId === null) { + $metaId = self::META_ID; + } + $meta = self::META[$metaId]; + $out = [ + ['id' => $metaId, 'label' => $meta['label'], 'bg' => $meta['bg']], + ]; + foreach ($rest as $t) { + $out[] = $t; } if (!$hasStatus) { - $normalized[] = ['id' => 'open', 'label' => 'open', 'bg' => '#22c55e']; + $out[] = ['id' => 'open', 'label' => 'open', 'bg' => '#22c55e']; } - return $normalized; + return $out; } public static function validate(array $tags): ?string { - $normalized = self::normalize($tags); - $hasMeta = false; + $normalized = normalize_report_tags($tags); + $metaCount = 0; $hasStatus = false; foreach ($normalized as $t) { - if ($t['id'] === self::META_ID) { - $hasMeta = true; + if (self::isMetaId($t['id'])) { + $metaCount++; } if (isset(self::STATUS[$t['id']])) { $hasStatus = true; } } - if (!$hasMeta) { - return 'tags must include ticket meta tag'; + if ($metaCount === 0) { + return 'tags must include a type tag (ticket or issue)'; + } + if ($metaCount > 1) { + return 'only one type tag allowed: ticket or issue'; } if (!$hasStatus) { return 'tags must include a status tag (open, in-progress, closed, …)'; diff --git a/examples/crash_reporter/backend/src/TicketWorkflow.php b/examples/crash_reporter/backend/src/TicketWorkflow.php index 297e3d1..7954480 100644 --- a/examples/crash_reporter/backend/src/TicketWorkflow.php +++ b/examples/crash_reporter/backend/src/TicketWorkflow.php @@ -62,7 +62,7 @@ final class TicketWorkflow { $keep = []; $statusIds = array_merge(TicketTagCatalog::statusIds(), TicketWorkflow::all()); foreach (normalize_report_tags($tags) as $t) { - if ($t['id'] === TicketTagCatalog::META_ID) { + if (TicketTagCatalog::isMetaId($t['id'])) { $keep[] = $t; continue; } diff --git a/examples/crash_reporter/backend/views/layout.php b/examples/crash_reporter/backend/views/layout.php index ab9acd6..f675d98 100644 --- a/examples/crash_reporter/backend/views/layout.php +++ b/examples/crash_reporter/backend/views/layout.php @@ -163,9 +163,7 @@
    -

    Issues console

    -

    Operator console for crash triage and tickets — not the public upload API. Open Issues to browse and group fingerprints, or use the shortcuts below.

    - + @@ -229,7 +227,7 @@

    Issues

    - +