From a656f40922e9422291945a40a9df919a1e599540 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 5 Jun 2026 17:00:26 +0200 Subject: [PATCH] be builders --- .../backend/public/assets/js/builder.js | 75 ++++++++++++++++++ .../build_console/backend/src/bootstrap.php | 32 ++++++++ .../backend/views/build_detail.php | 7 +- examples/build_console/backend/views/home.php | 13 +++- .../backend/public/assets/css/app.css | 76 +++++++++++++++++++ 5 files changed, 197 insertions(+), 6 deletions(-) diff --git a/examples/build_console/backend/public/assets/js/builder.js b/examples/build_console/backend/public/assets/js/builder.js index 621bc44..d8c44bd 100644 --- a/examples/build_console/backend/public/assets/js/builder.js +++ b/examples/build_console/backend/public/assets/js/builder.js @@ -1,6 +1,65 @@ (function () { 'use strict'; var base = document.body.getAttribute('data-base-path') || ''; + + function buildStatusIconHtml(status) { + var s = String(status || '').toLowerCase(); + if (s === 'running' || s === 'queued') { + return '' + + ''; + } + if (s === 'passed') { + return ''; + } + return '' + + ''; + } + + function updateBuildStatusCell(cell, status, phase) { + if (!cell) return; + var icon = cell.querySelector('.build-status-icon'); + var label = cell.querySelector('.build-status-label'); + var html = buildStatusIconHtml(status); + if (icon) { + icon.outerHTML = html; + } else { + cell.insertAdjacentHTML('afterbegin', html); + } + if (label) { + label.textContent = status || ''; + } + if (phase !== undefined) { + var row = cell.closest('tr'); + if (row) { + var phaseCell = row.querySelector('.build-phase-cell'); + if (phaseCell) { + phaseCell.textContent = phase || ''; + } + } + } + } + + 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); + }); + } + + if (document.body.getAttribute('data-view') === 'home' && document.getElementById('builds-recent-tbody')) { + setTimeout(refreshRecentBuilds, 4000); + } var themeSel = document.getElementById('theme-select'); if (themeSel) { var cur = localStorage.getItem('crash_console_theme') || 'dark'; @@ -180,6 +239,22 @@ offset = j.log.offset || offset; renderLog(); } + if (j.build) { + var iconEl = document.getElementById('build-status-icon'); + var textEl = document.getElementById('build-status-text'); + if (iconEl) { + iconEl.innerHTML = buildStatusIconHtml(j.build.status); + } + if (textEl) { + var errEl = document.getElementById('build-error-message'); + var err = j.build.error_message ? (' · ' + + String(j.build.error_message).replace(/&/g, '&').replace(//g, '>') + + '') : ''; + textEl.innerHTML = 'Status: ' + (j.build.status || '') + ' · Phase: ' + + (j.build.phase || '') + err; + } + document.body.setAttribute('data-build-status', j.build.status || ''); + } var running = j.build && j.build.status === 'running'; var eof = j.log && j.log.eof; if (running || !eof) { diff --git a/examples/build_console/backend/src/bootstrap.php b/examples/build_console/backend/src/bootstrap.php index 241ff3e..a8ccc56 100644 --- a/examples/build_console/backend/src/bootstrap.php +++ b/examples/build_console/backend/src/bootstrap.php @@ -54,3 +54,35 @@ function json_out(array $data, int $code = 200): void { function assets_base(): string { return rtrim((string) cfg('links.crashes', '/app/androidcast_project/crashes/'), '/'); } + +/** @return 'running'|'passed'|'failed' */ +function build_status_kind(string $status): string { + $s = strtolower(trim($status)); + if (in_array($s, ['running', 'queued'], true)) { + return 'running'; + } + if ($s === 'passed') { + return 'passed'; + } + return 'failed'; +} + +/** Inline status glyph: spinner (in progress), green check, or red alert triangle. */ +function build_status_icon(string $status): string { + $kind = build_status_kind($status); + $labels = [ + 'running' => 'In progress', + 'passed' => 'Succeeded', + 'failed' => 'Failed or cancelled', + ]; + $label = h($labels[$kind] ?? 'Unknown'); + if ($kind === 'running') { + return '' + . ''; + } + if ($kind === 'passed') { + return ''; + } + return '' + . ''; +} diff --git a/examples/build_console/backend/views/build_detail.php b/examples/build_console/backend/views/build_detail.php index 4686989..bb40e4a 100644 --- a/examples/build_console/backend/views/build_detail.php +++ b/examples/build_console/backend/views/build_detail.php @@ -20,10 +20,13 @@ $stepLabels = [ ]; ?>

Build

-

Status: · Phase: +

+ + Status: · Phase: - · + · +

Created - + - + - - + + + + + + + diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index 8db7cfc..a600cb1 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -1206,6 +1206,82 @@ button.report-tag--filter:hover { font-size: 13px; } +/* Builder status glyphs (list + detail) */ +.build-status-line, +.build-status-cell-inner { + display: inline-flex; + align-items: center; + gap: 8px; + vertical-align: middle; +} +.build-status-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} +.build-status-spinner { + display: block; + width: 15px; + height: 15px; + border: 2px solid rgba(61, 139, 253, .22); + border-top-color: var(--accent); + border-radius: 50%; + animation: build-status-spin .85s linear infinite; + box-shadow: 0 0 10px rgba(61, 139, 253, .35); +} +[data-theme="light"] .build-status-spinner { + border-color: rgba(37, 99, 235, .18); + border-top-color: var(--accent); + box-shadow: 0 0 8px rgba(37, 99, 235, .28); +} +@keyframes build-status-spin { + to { transform: rotate(360deg); } +} +.build-status-icon--passed { + width: 17px; + height: 17px; + border-radius: 50%; + background: linear-gradient(145deg, #34d399, #16a34a); + box-shadow: + 0 0 0 1px rgba(34, 197, 94, .45), + 0 0 12px rgba(34, 197, 94, .4); + position: relative; +} +.build-status-icon--passed::after { + content: ''; + position: absolute; + left: 5px; + top: 3px; + width: 4px; + height: 7px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} +.build-status-icon--failed { + width: 18px; + height: 16px; + position: relative; + clip-path: polygon(50% 0%, 6% 92%, 94% 92%); + background: linear-gradient(180deg, #fb7185, var(--danger)); + box-shadow: 0 0 12px rgba(248, 113, 113, .45); +} +.build-status-fail-mark { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding-top: 5px; + font-size: 11px; + font-weight: 800; + line-height: 1; + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, .25); + pointer-events: none; +} + .tag-edit-btn { flex-shrink: 0; margin-left: 2px;