= h($health['ci_version'] ?? '') ?>| Code | Status | Phase | @@ -52,13 +53,22 @@|
|---|---|---|---|
|
+
+
+
+
+ |
= h($b['build_code']) ?> | - = build_status_icon((string) ($b['status'] ?? '')) ?> - = h($b['status']) ?> + = build_status_icon($bStatus) ?> + = h($bStatus) ?> | = h($b['phase']) ?> | diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index a600cb1..8124419 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -1282,6 +1282,72 @@ button.report-tag--filter:hover { pointer-events: none; } +.build-col-controls { width: 4.5rem; } +.build-row-controls { vertical-align: middle; } +.build-transport-actions { + display: inline-flex; + align-items: center; + gap: 6px; +} +.build-transport-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + border-radius: 50%; + border: 1px solid var(--border); + background: var(--surface); + cursor: pointer; + box-shadow: + 0 0 0 1px rgba(61, 139, 253, .12), + 0 2px 8px rgba(0, 0, 0, .25), + 0 0 14px rgba(61, 139, 253, .1); + transition: box-shadow .18s ease, border-color .18s ease, transform .12s ease; +} +.build-transport-btn:hover:not(:disabled), +.build-transport-btn:focus-visible:not(:disabled) { + border-color: var(--accent); + box-shadow: + 0 0 0 1px rgba(61, 139, 253, .45), + 0 0 18px rgba(61, 139, 253, .35); + outline: none; +} +.build-transport-btn:active:not(:disabled) { transform: translateY(1px); } +.build-transport-btn:disabled { + opacity: .35; + cursor: not-allowed; + box-shadow: none; +} +.build-transport-play::before { + content: ''; + display: block; + width: 0; + height: 0; + margin-left: 3px; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 10px solid var(--accent); + filter: drop-shadow(0 0 4px rgba(61, 139, 253, .45)); +} +.build-transport-stop { + border-color: rgba(248, 113, 113, .55); + box-shadow: + 0 0 0 1px rgba(248, 113, 113, .25), + 0 2px 8px rgba(0, 0, 0, .25), + 0 0 12px rgba(248, 113, 113, .18); +} +.build-transport-stop::before { + content: ''; + display: block; + width: 9px; + height: 9px; + border-radius: 2px; + background: var(--danger); + box-shadow: 0 0 8px rgba(248, 113, 113, .55); +} + .tag-edit-btn { flex-shrink: 0; margin-left: 2px; diff --git a/examples/crash_reporter/backend/public/assets/js/graphs.js b/examples/crash_reporter/backend/public/assets/js/graphs.js index 3f03ea4..d2503a9 100644 --- a/examples/crash_reporter/backend/public/assets/js/graphs.js +++ b/examples/crash_reporter/backend/public/assets/js/graphs.js @@ -40,6 +40,48 @@ .replace(/"/g, '"'); } + function graphPermalink(scopeKey, suffix, days) { + const url = new URL(window.location.href); + url.searchParams.set('graph', scopeKey + '-' + suffix); + if (days) { + url.searchParams.set('days', String(days)); + } + return url.toString(); + } + + function syncGraphPermalink(scopeKey, suffix) { + const daysSel = el('graphs-days'); + const days = daysSel && daysSel.value ? daysSel.value : '14'; + const link = graphPermalink(scopeKey, suffix, days); + if (window.history && window.history.replaceState) { + window.history.replaceState(null, '', link); + } + const share = el('graph-detail-share'); + if (share) { + share.href = link; + } + } + + function clearGraphPermalink() { + const url = new URL(window.location.href); + url.searchParams.delete('graph'); + if (window.history && window.history.replaceState) { + window.history.replaceState(null, '', url.toString()); + } + } + + function openGraphFromPermalink() { + const params = new URLSearchParams(window.location.search); + const token = params.get('graph'); + if (!token) return; + const node = document.getElementById('graph-' + token); + if (!node) return; + const card = node.closest('.card.card--lift'); + if (card) { + openGraphDetail(card); + } + } + function parseCanvasId(id) { const m = String(id || '').match(/^graph-(user|slug|platform)-(.+)$/); if (!m) return null; @@ -430,6 +472,7 @@ const overlay = el('graph-detail-overlay'); if (overlay) overlay.hidden = true; detailState = null; + clearGraphPermalink(); const tip = el('graph-detail-tooltip'); if (tip) tip.hidden = true; const statsEl = el('graph-detail-stats'); @@ -449,6 +492,7 @@ const scope = scopeData(parsed.scopeKey); if (!scope) return; const def = parsed.def; + syncGraphPermalink(parsed.scopeKey, parsed.suffix); const fieldVal = scope[def.field]; const titleEl = el('graph-detail-title'); const canvasEl = el('graph-detail-canvas'); @@ -655,6 +699,21 @@ }); const closeBtn = el('graph-detail-close'); if (closeBtn) closeBtn.addEventListener('click', closeGraphDetail); + const copyBtn = el('graph-detail-copy-link'); + if (copyBtn) { + copyBtn.addEventListener('click', function () { + const share = el('graph-detail-share'); + const link = (share && share.href && share.href !== '#') ? share.href : window.location.href; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(link).then(function () { + copyBtn.textContent = 'Copied'; + setTimeout(function () { copyBtn.textContent = 'Copy link'; }, 1500); + }).catch(function () { window.prompt('Copy graph link:', link); }); + } else { + window.prompt('Copy graph link:', link); + } + }); + } } document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape') closeGraphDetail(); @@ -680,6 +739,11 @@ }); function load() { + const params = new URLSearchParams(window.location.search); + const daysParam = params.get('days'); + if (daysParam && daysSel) { + daysSel.value = daysParam; + } const days = Number(daysSel && daysSel.value ? daysSel.value : 14); status.textContent = 'Loading…'; const xhr = new XMLHttpRequest(); @@ -708,6 +772,7 @@ markGraphBricks(); const cols = colsSel && colsSel.value ? colsSel.value : '2'; status.textContent = 'Loaded · viewer=' + viewer + ' · window=' + days + 'd · columns=' + cols; + openGraphFromPermalink(); }; xhr.onerror = function () { status.textContent = 'Network error'; diff --git a/examples/crash_reporter/backend/src/GraphRepository.php b/examples/crash_reporter/backend/src/GraphRepository.php index 3c8fcc4..d383aa1 100644 --- a/examples/crash_reporter/backend/src/GraphRepository.php +++ b/examples/crash_reporter/backend/src/GraphRepository.php @@ -246,7 +246,7 @@ final class GraphRepository { 'viewer' => $viewer, 'links' => self::serviceLinks(), ]; - $payload['user'] = self::buildScope($days, $activeCompanyId, $scopeUserId); + $payload['user'] = self::buildScope($days, $activeCompanyId, null); if ($viewer === 'user') { return $payload; } diff --git a/examples/crash_reporter/backend/src/bootstrap.php b/examples/crash_reporter/backend/src/bootstrap.php index d42d154..2b9cbb1 100644 --- a/examples/crash_reporter/backend/src/bootstrap.php +++ b/examples/crash_reporter/backend/src/bootstrap.php @@ -94,6 +94,23 @@ function json_out(array $data, int $code = 200): void { exit; } +/** Public URL prefix for the active console view (graphs vs crashes). */ +function console_base_path(?string $view = null): string { + $view = $view ?? (string) ($_GET['view'] ?? ''); + if ($view === 'graphs') { + $graphs = trim((string) cfg('graphs_base_path', '')); + if ($graphs !== '') { + return rtrim($graphs, '/'); + } + $crashes = rtrim((string) cfg('base_path', ''), '/'); + if (str_ends_with($crashes, '/crashes')) { + return substr($crashes, 0, -strlen('/crashes')) . '/graphs'; + } + return '/app/androidcast_project/graphs'; + } + return rtrim((string) cfg('base_path', ''), '/'); +} + /** * Raw POST body for crash ingest. Supports plain JSON or Content-Encoding: gzip * (when nginx gunzip is off or the client talks to PHP directly). diff --git a/examples/crash_reporter/backend/views/layout.php b/examples/crash_reporter/backend/views/layout.php index 2348c80..7709b65 100644 --- a/examples/crash_reporter/backend/views/layout.php +++ b/examples/crash_reporter/backend/views/layout.php @@ -42,7 +42,7 @@ - @@ -357,6 +357,8 @@ + +