From cd2e69ad6bc6d561fe5e488227af037c416ac806 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 5 Jun 2026 22:11:38 +0200 Subject: [PATCH] sync BE --- examples/app_hub/assets/hub-globe.js | 87 ++++++++++++------- .../backend/public/api/build_actions.php | 4 + .../backend/public/api/builds.php | 2 + .../backend/public/assets/js/builder.js | 81 ++++++++++++++--- .../backend/src/BuildRepository.php | 1 + .../build_console/backend/src/BuildRunner.php | 75 ++++++++++++++-- examples/build_console/backend/views/home.php | 26 ++++-- .../backend/public/assets/css/app.css | 66 ++++++++++++++ .../backend/public/assets/js/graphs.js | 65 ++++++++++++++ .../backend/src/GraphRepository.php | 2 +- .../crash_reporter/backend/src/bootstrap.php | 17 ++++ .../crash_reporter/backend/views/layout.php | 4 +- 12 files changed, 368 insertions(+), 62 deletions(-) diff --git a/examples/app_hub/assets/hub-globe.js b/examples/app_hub/assets/hub-globe.js index bbc6a97..f4cfa3e 100644 --- a/examples/app_hub/assets/hub-globe.js +++ b/examples/app_hub/assets/hub-globe.js @@ -118,10 +118,7 @@ function bakeEquirectangular(compositeData, mercatorFlatImg) { rgba = sampleMercatorFlat(mercCtx, lon, lat); } } else { - rgba = sampleMercatorFlat(mercCtx, lon, lat); - if (rgba[3] < 16) { - rgba = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; - } + rgba = [OCEAN_FALLBACK.r, OCEAN_FALLBACK.g, OCEAN_FALLBACK.b, 255]; } const p = (j * EQ_WIDTH + i) * 4; @@ -139,30 +136,60 @@ function bakeEquirectangular(compositeData, mercatorFlatImg) { function drawEquirectSlice(ctx, eqCanvas, rotation, dx, dy, diam) { const w = eqCanvas.width; const h = eqCanvas.height; - const offsetPx = ((rotation / (2 * Math.PI)) % 1) * w; - const ox = ((offsetPx % w) + w) % w; + const ox = ((rotation / (2 * Math.PI)) * w % w + w) % w; + const destX = dx - diam * 0.5; + const destY = dy - diam * 0.5; ctx.save(); ctx.beginPath(); ctx.arc(dx, dy, diam * 0.5, 0, Math.PI * 2); ctx.clip(); - ctx.drawImage(eqCanvas, -ox, 0, w, h, dx - diam * 0.5, dy - diam * 0.5, diam, diam); - ctx.drawImage(eqCanvas, w - ox, 0, w, h, dx - diam * 0.5 + diam, dy - diam * 0.5, diam, diam); + + const slice1w = w - ox; + const destSlice1 = diam * (slice1w / w); + ctx.drawImage(eqCanvas, ox, 0, slice1w, h, destX, destY, destSlice1, diam); + if (ox > 0.5) { + ctx.drawImage(eqCanvas, 0, 0, ox, h, destX + destSlice1, destY, diam - destSlice1, diam); + } ctx.restore(); } +function rotateY(x, y, z, rotY) { + return { + x: x * Math.cos(rotY) + z * Math.sin(rotY), + y, + z: -x * Math.sin(rotY) + z * Math.cos(rotY), + }; +} + +function strokeParallel(ctx, cx, cy, r, latRad, rotY, strokeStyle, lineWidth) { + const pts = []; + for (let i = 0; i <= 64; i++) { + const lon = -Math.PI + (i / 64) * 2 * Math.PI; + const cosLat = Math.cos(latRad); + const p = rotateY(cosLat * Math.sin(lon), Math.sin(latRad), cosLat * Math.cos(lon), rotY); + if (p.z > 0.04) { + pts.push({ x: cx + p.x * r, y: cy - p.y * r }); + } + } + if (pts.length < 2) return; + ctx.beginPath(); + ctx.moveTo(pts[0].x, pts[0].y); + for (let k = 1; k < pts.length; k++) ctx.lineTo(pts[k].x, pts[k].y); + ctx.strokeStyle = strokeStyle; + ctx.lineWidth = lineWidth; + ctx.lineCap = 'round'; + ctx.stroke(); +} + function strokeMeridian(ctx, cx, cy, r, lonRad, rotY, strokeStyle, lineWidth) { const pts = []; for (let i = 0; i <= 64; i++) { const lat = -Math.PI / 2 + (i / 64) * Math.PI; const cosLat = Math.cos(lat); - let x = cosLat * Math.sin(lonRad); - let y = Math.sin(lat); - let z = cosLat * Math.cos(lonRad); - const x2 = x * Math.cos(rotY) + z * Math.sin(rotY); - const z2 = -x * Math.sin(rotY) + z * Math.cos(rotY); - if (z2 > 0.04) { - pts.push({ x: cx + x2 * r, y: cy - y * r }); + const p = rotateY(cosLat * Math.sin(lonRad), Math.sin(lat), cosLat * Math.cos(lonRad), rotY); + if (p.z > 0.04) { + pts.push({ x: cx + p.x * r, y: cy - p.y * r }); } } if (pts.length < 2) return; @@ -190,26 +217,16 @@ function drawGridOverlay(ctx, rect, rotY) { equatorGrad.addColorStop(0.5, 'rgba(219,231,246,0.88)'); equatorGrad.addColorStop(1, 'rgba(31,44,64,0.74)'); - ctx.beginPath(); - ctx.ellipse(cx, cy, r * 0.99, r * 0.034, 0, 0, 2 * Math.PI); - ctx.strokeStyle = equatorGrad; - ctx.lineWidth = Math.max(2, rect.scale * 0.007); - ctx.stroke(); - - const poleRy = r * 0.12; - ctx.beginPath(); - ctx.ellipse(cx, cy - r * 0.52, r * 0.82, poleRy, 0, 0, 2 * Math.PI); - ctx.strokeStyle = 'rgba(231,236,243,0.72)'; - ctx.lineWidth = Math.max(1.5, rect.scale * 0.0055); - ctx.stroke(); - ctx.beginPath(); - ctx.ellipse(cx, cy + r * 0.52, r * 0.82, poleRy, 0, 0, 2 * Math.PI); - ctx.stroke(); - const meridianStroke = 'rgba(238,245,255,0.92)'; const primeStroke = 'rgba(238,245,255,0.98)'; + const parallelStroke = 'rgba(231,236,243,0.72)'; const lw = Math.max(2.5, rect.scale * 0.008); const lwPrime = Math.max(3, rect.scale * 0.0095); + const lwThin = Math.max(1.5, rect.scale * 0.0055); + + strokeParallel(ctx, cx, cy, r, 0, rotY, equatorGrad, Math.max(2, rect.scale * 0.007)); + strokeParallel(ctx, cx, cy, r, Math.PI * 0.25, rotY, parallelStroke, lwThin); + strokeParallel(ctx, cx, cy, r, -Math.PI * 0.25, rotY, parallelStroke, lwThin); for (let lonDeg = -150; lonDeg <= 150; lonDeg += 30) { const lon = (lonDeg * Math.PI) / 180; @@ -330,7 +347,13 @@ export async function mountHubGlobe(stage, opts) { ctx.clearRect(0, 0, px, px); drawEquirectSlice(ctx, eqCanvas, rotation, cx, cy, r * 2); drawGridOverlay(ctx, drawRect, rotation); - drawRim(ctx, composite.canvas, cx, cy, r, rim, rect.scale * dpr); + ctx.save(); + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(10,14,20,0.55)'; + ctx.lineWidth = Math.max(2, rect.scale * dpr * 0.006); + ctx.stroke(); + ctx.restore(); }; const onPointerDown = (ev) => { diff --git a/examples/build_console/backend/public/api/build_actions.php b/examples/build_console/backend/public/api/build_actions.php index 3d8be86..077c834 100644 --- a/examples/build_console/backend/public/api/build_actions.php +++ b/examples/build_console/backend/public/api/build_actions.php @@ -31,6 +31,10 @@ try { $build = BuildRunner::rerunBuild($buildId, null, $userId); json_out(['ok' => true, 'build' => $build]); } + if ($action === 'force_rerun') { + $build = BuildRunner::forceRerun($buildId, $userId); + json_out(['ok' => true, 'build' => $build]); + } if ($action === 'rerun_as') { $override = is_array($body['params'] ?? null) ? $body['params'] : []; $build = BuildRunner::rerunBuild($buildId, $override, $userId); diff --git a/examples/build_console/backend/public/api/builds.php b/examples/build_console/backend/public/api/builds.php index 482f1a7..49a04cb 100644 --- a/examples/build_console/backend/public/api/builds.php +++ b/examples/build_console/backend/public/api/builds.php @@ -2,4 +2,6 @@ declare(strict_types=1); require_once __DIR__ . '/../../src/bootstrap.php'; Auth::check(); +BuildRunner::reconcileRunningBuilds(); +BuildRunner::reconcileQueuedBuilds(); json_out(['ok' => true, 'builds' => BuildRepository::listRecent(10)]); diff --git a/examples/build_console/backend/public/assets/js/builder.js b/examples/build_console/backend/public/assets/js/builder.js index d8c44bd..2747e30 100644 --- a/examples/build_console/backend/public/assets/js/builder.js +++ b/examples/build_console/backend/public/assets/js/builder.js @@ -39,27 +39,82 @@ } } + function updateBuildRow(row, build) { + if (!row || !build) return; + row.setAttribute('data-build-status', build.status || ''); + var cell = row.querySelector('[data-build-status-cell="' + build.id + '"]'); + updateBuildStatusCell(cell, build.status, build.phase); + var play = row.querySelector('.build-transport-play'); + var stop = row.querySelector('.build-transport-stop'); + var active = build.status === 'running' || build.status === 'queued'; + if (play) { + play.setAttribute('data-build-status', build.status || ''); + } + if (stop) { + stop.setAttribute('data-build-status', build.status || ''); + stop.disabled = !active; + } + } + function refreshRecentBuilds() { var tbody = document.getElementById('builds-recent-tbody'); if (!tbody) return; - fetch(base + '/api/builds.php', { credentials: 'same-origin' }) - .then(function (r) { return r.json(); }) - .then(function (j) { - if (!j.ok || !j.builds) return; - j.builds.forEach(function (b) { - var cell = tbody.querySelector('[data-build-status-cell="' + b.id + '"]'); - updateBuildStatusCell(cell, b.status, b.phase); - }); - }) - .catch(function () {}) - .then(function () { - setTimeout(refreshRecentBuilds, 4000); + var xhr = new XMLHttpRequest(); + xhr.open('GET', base + '/api/builds.php', true); + xhr.onload = function () { + var j = null; + try { j = JSON.parse(xhr.responseText); } catch (e) { j = null; } + if (!j || !j.ok || !j.builds) return; + j.builds.forEach(function (b) { + var row = tbody.querySelector('tr[data-build-id="' + b.id + '"]'); + updateBuildRow(row, b); }); + }; + xhr.send(); + setTimeout(refreshRecentBuilds, 1000); + } + + function bindBuildListTransport() { + var tbody = document.getElementById('builds-recent-tbody'); + if (!tbody) return; + tbody.addEventListener('click', function (ev) { + var play = ev.target.closest('.build-transport-play'); + var stop = ev.target.closest('.build-transport-stop'); + if (play) { + var pid = Number(play.getAttribute('data-build-id')); + var pst = play.getAttribute('data-build-status') || ''; + if (!pid) return; + var msg = pst === 'running' || pst === 'queued' + ? 'Force re-run? This stops the current run and starts a new build with the same parameters.' + : null; + if (msg && !confirm(msg)) return; + postBuildAction({ action: 'force_rerun', build_id: pid }).then(function (j) { + if (j.build && j.build.id) { + window.location.href = base + '/?view=build&id=' + j.build.id; + } + }).catch(function (err) { + alert(err.message || 'Re-run failed'); + }); + return; + } + if (stop) { + var sid = Number(stop.getAttribute('data-build-id')); + if (!sid || stop.disabled) return; + if (!confirm('Stop this build now?')) return; + postBuildAction({ action: 'stop', build_id: sid }).then(function () { + refreshRecentBuilds(); + }).catch(function (err) { + alert(err.message || 'Stop failed'); + }); + } + }); } if (document.body.getAttribute('data-view') === 'home' && document.getElementById('builds-recent-tbody')) { - setTimeout(refreshRecentBuilds, 4000); + bindBuildListTransport(); + refreshRecentBuilds(); } + var themeSel = document.getElementById('theme-select'); if (themeSel) { var cur = localStorage.getItem('crash_console_theme') || 'dark'; diff --git a/examples/build_console/backend/src/BuildRepository.php b/examples/build_console/backend/src/BuildRepository.php index c4c6868..e723eee 100644 --- a/examples/build_console/backend/src/BuildRepository.php +++ b/examples/build_console/backend/src/BuildRepository.php @@ -112,6 +112,7 @@ SQL); public static function healthSummary(): array { self::ensureSchema(); BuildRunner::reconcileRunningBuilds(); + BuildRunner::reconcileQueuedBuilds(); $pdo = Database::pdo(); $running = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'running'")->fetchColumn(); $failed = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'failed' AND created_at > (NOW() - INTERVAL 7 DAY)")->fetchColumn(); diff --git a/examples/build_console/backend/src/BuildRunner.php b/examples/build_console/backend/src/BuildRunner.php index a550e1e..e9ed74c 100644 --- a/examples/build_console/backend/src/BuildRunner.php +++ b/examples/build_console/backend/src/BuildRunner.php @@ -131,12 +131,6 @@ YAML; } $phase = !empty($params['ssh_debug']) ? 'ssh_debug' : 'docker'; - BuildRepository::update($id, [ - 'status' => 'running', - 'phase' => $phase, - 'started_at' => gmdate('Y-m-d H:i:s'), - ]); - $runner = (string) cfg('build.runner_script', '/workspace/scripts/docker-build-runner.sh'); $repo = (string) cfg('build.repo_root', '/workspace'); $gitRemote = trim((string) shell_exec( @@ -176,7 +170,16 @@ YAML; } $cmd = implode(' ', $env) . ' nohup ' . escapeshellarg($runner) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 & echo $!'; $pid = trim((string) shell_exec($cmd)); + if ($pid === '' || !ctype_digit($pid)) { + self::failBuild($id, 'Build runner failed to start', $logPath); + return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode, 'status' => 'failed']; + } file_put_contents($dir . '/runner.pid', $pid); + BuildRepository::update($id, [ + 'status' => 'running', + 'phase' => $phase, + 'started_at' => gmdate('Y-m-d H:i:s'), + ]); return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode]; } @@ -195,7 +198,25 @@ YAML; public static function stopBuild(int $id): bool { $build = BuildRepository::getById($id); - if (!$build || ($build['status'] ?? '') !== 'running') { + if (!$build) { + return false; + } + $status = (string) ($build['status'] ?? ''); + if ($status === 'queued') { + $dir = self::artifactDir($id); + $logPath = $build['log_path'] ?? ($dir . '/build.log'); + if ($logPath) { + file_put_contents($logPath, "\n[builder] Stopped while queued\n", FILE_APPEND); + } + BuildRepository::update($id, [ + 'status' => 'cancelled', + 'phase' => 'stopped', + 'error_message' => 'Stopped while queued', + 'finished_at' => gmdate('Y-m-d H:i:s'), + ]); + return true; + } + if ($status !== 'running') { return false; } $dir = self::artifactDir($id); @@ -240,6 +261,19 @@ YAML; return self::enqueue($params, $userId); } + /** Stop an in-flight build (if any) and enqueue a fresh run with the same params. */ + public static function forceRerun(int $id, ?int $userId = null): array { + $build = BuildRepository::getById($id); + if (!$build) { + throw new InvalidArgumentException('build_not_found'); + } + $status = (string) ($build['status'] ?? ''); + if (in_array($status, ['running', 'queued'], true)) { + self::stopBuild($id); + } + return self::rerunBuild($id, null, $userId); + } + /** CircleCI-style: resume from the step that failed (git → docker-build → docker-run). */ public static function detectFailedStep(int $buildId): string { $dir = self::artifactDir($buildId); @@ -364,6 +398,33 @@ YAML; } } + public static function reconcileQueuedBuilds(): void { + BuildRepository::ensureSchema(); + $stmt = Database::pdo()->query("SELECT id, log_path, created_at FROM builds WHERE status = 'queued'"); + if (!$stmt) { + return; + } + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $id = (int) ($row['id'] ?? 0); + if ($id <= 0) { + continue; + } + $logPath = (string) ($row['log_path'] ?? (self::artifactDir($id) . '/build.log')); + if (self::runnerAlive($id)) { + BuildRepository::update($id, [ + 'status' => 'running', + 'phase' => 'docker', + 'started_at' => gmdate('Y-m-d H:i:s'), + ]); + continue; + } + $created = strtotime((string) ($row['created_at'] ?? '')) ?: 0; + if ($created > 0 && $created < time() - 120) { + self::failBuild($id, 'Build stuck in queued (runner never started)', $logPath); + } + } + } + /** Last lines / markers from build.log for reconcile error_message (max 512 chars). */ public static function summarizeLogFailure(?string $logPath): string { if (!$logPath || !is_file($logPath)) { diff --git a/examples/build_console/backend/views/home.php b/examples/build_console/backend/views/home.php index 349d0f1..f6a6970 100644 --- a/examples/build_console/backend/views/home.php +++ b/examples/build_console/backend/views/home.php @@ -4,10 +4,10 @@

Ecosystem health

-
@@ -40,9 +40,10 @@

Recent builds (last 10)

- +
+ @@ -52,13 +53,22 @@ - - + + + 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 @@

+ +
Code Status Phase
+
+ + +
+
- - + +