mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:58:14 +03:00
sync BE
This commit is contained in:
@@ -118,11 +118,8 @@ function bakeEquirectangular(compositeData, mercatorFlatImg) {
|
|||||||
rgba = sampleMercatorFlat(mercCtx, lon, lat);
|
rgba = sampleMercatorFlat(mercCtx, lon, lat);
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
const p = (j * EQ_WIDTH + i) * 4;
|
||||||
out.data[p] = rgba[0];
|
out.data[p] = rgba[0];
|
||||||
@@ -139,30 +136,60 @@ function bakeEquirectangular(compositeData, mercatorFlatImg) {
|
|||||||
function drawEquirectSlice(ctx, eqCanvas, rotation, dx, dy, diam) {
|
function drawEquirectSlice(ctx, eqCanvas, rotation, dx, dy, diam) {
|
||||||
const w = eqCanvas.width;
|
const w = eqCanvas.width;
|
||||||
const h = eqCanvas.height;
|
const h = eqCanvas.height;
|
||||||
const offsetPx = ((rotation / (2 * Math.PI)) % 1) * w;
|
const ox = ((rotation / (2 * Math.PI)) * w % w + w) % w;
|
||||||
const ox = ((offsetPx % w) + w) % w;
|
const destX = dx - diam * 0.5;
|
||||||
|
const destY = dy - diam * 0.5;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(dx, dy, diam * 0.5, 0, Math.PI * 2);
|
ctx.arc(dx, dy, diam * 0.5, 0, Math.PI * 2);
|
||||||
ctx.clip();
|
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();
|
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) {
|
function strokeMeridian(ctx, cx, cy, r, lonRad, rotY, strokeStyle, lineWidth) {
|
||||||
const pts = [];
|
const pts = [];
|
||||||
for (let i = 0; i <= 64; i++) {
|
for (let i = 0; i <= 64; i++) {
|
||||||
const lat = -Math.PI / 2 + (i / 64) * Math.PI;
|
const lat = -Math.PI / 2 + (i / 64) * Math.PI;
|
||||||
const cosLat = Math.cos(lat);
|
const cosLat = Math.cos(lat);
|
||||||
let x = cosLat * Math.sin(lonRad);
|
const p = rotateY(cosLat * Math.sin(lonRad), Math.sin(lat), cosLat * Math.cos(lonRad), rotY);
|
||||||
let y = Math.sin(lat);
|
if (p.z > 0.04) {
|
||||||
let z = cosLat * Math.cos(lonRad);
|
pts.push({ x: cx + p.x * r, y: cy - p.y * r });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pts.length < 2) return;
|
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(0.5, 'rgba(219,231,246,0.88)');
|
||||||
equatorGrad.addColorStop(1, 'rgba(31,44,64,0.74)');
|
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 meridianStroke = 'rgba(238,245,255,0.92)';
|
||||||
const primeStroke = 'rgba(238,245,255,0.98)';
|
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 lw = Math.max(2.5, rect.scale * 0.008);
|
||||||
const lwPrime = Math.max(3, rect.scale * 0.0095);
|
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) {
|
for (let lonDeg = -150; lonDeg <= 150; lonDeg += 30) {
|
||||||
const lon = (lonDeg * Math.PI) / 180;
|
const lon = (lonDeg * Math.PI) / 180;
|
||||||
@@ -330,7 +347,13 @@ export async function mountHubGlobe(stage, opts) {
|
|||||||
ctx.clearRect(0, 0, px, px);
|
ctx.clearRect(0, 0, px, px);
|
||||||
drawEquirectSlice(ctx, eqCanvas, rotation, cx, cy, r * 2);
|
drawEquirectSlice(ctx, eqCanvas, rotation, cx, cy, r * 2);
|
||||||
drawGridOverlay(ctx, drawRect, rotation);
|
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) => {
|
const onPointerDown = (ev) => {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ try {
|
|||||||
$build = BuildRunner::rerunBuild($buildId, null, $userId);
|
$build = BuildRunner::rerunBuild($buildId, null, $userId);
|
||||||
json_out(['ok' => true, 'build' => $build]);
|
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') {
|
if ($action === 'rerun_as') {
|
||||||
$override = is_array($body['params'] ?? null) ? $body['params'] : [];
|
$override = is_array($body['params'] ?? null) ? $body['params'] : [];
|
||||||
$build = BuildRunner::rerunBuild($buildId, $override, $userId);
|
$build = BuildRunner::rerunBuild($buildId, $override, $userId);
|
||||||
|
|||||||
@@ -2,4 +2,6 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
require_once __DIR__ . '/../../src/bootstrap.php';
|
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||||
Auth::check();
|
Auth::check();
|
||||||
|
BuildRunner::reconcileRunningBuilds();
|
||||||
|
BuildRunner::reconcileQueuedBuilds();
|
||||||
json_out(['ok' => true, 'builds' => BuildRepository::listRecent(10)]);
|
json_out(['ok' => true, 'builds' => BuildRepository::listRecent(10)]);
|
||||||
|
|||||||
@@ -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() {
|
function refreshRecentBuilds() {
|
||||||
var tbody = document.getElementById('builds-recent-tbody');
|
var tbody = document.getElementById('builds-recent-tbody');
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
fetch(base + '/api/builds.php', { credentials: 'same-origin' })
|
var xhr = new XMLHttpRequest();
|
||||||
.then(function (r) { return r.json(); })
|
xhr.open('GET', base + '/api/builds.php', true);
|
||||||
.then(function (j) {
|
xhr.onload = function () {
|
||||||
if (!j.ok || !j.builds) return;
|
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) {
|
j.builds.forEach(function (b) {
|
||||||
var cell = tbody.querySelector('[data-build-status-cell="' + b.id + '"]');
|
var row = tbody.querySelector('tr[data-build-id="' + b.id + '"]');
|
||||||
updateBuildStatusCell(cell, b.status, b.phase);
|
updateBuildRow(row, b);
|
||||||
});
|
});
|
||||||
})
|
};
|
||||||
.catch(function () {})
|
xhr.send();
|
||||||
.then(function () {
|
setTimeout(refreshRecentBuilds, 1000);
|
||||||
setTimeout(refreshRecentBuilds, 4000);
|
}
|
||||||
|
|
||||||
|
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')) {
|
if (document.body.getAttribute('data-view') === 'home' && document.getElementById('builds-recent-tbody')) {
|
||||||
setTimeout(refreshRecentBuilds, 4000);
|
bindBuildListTransport();
|
||||||
|
refreshRecentBuilds();
|
||||||
}
|
}
|
||||||
|
|
||||||
var themeSel = document.getElementById('theme-select');
|
var themeSel = document.getElementById('theme-select');
|
||||||
if (themeSel) {
|
if (themeSel) {
|
||||||
var cur = localStorage.getItem('crash_console_theme') || 'dark';
|
var cur = localStorage.getItem('crash_console_theme') || 'dark';
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ SQL);
|
|||||||
public static function healthSummary(): array {
|
public static function healthSummary(): array {
|
||||||
self::ensureSchema();
|
self::ensureSchema();
|
||||||
BuildRunner::reconcileRunningBuilds();
|
BuildRunner::reconcileRunningBuilds();
|
||||||
|
BuildRunner::reconcileQueuedBuilds();
|
||||||
$pdo = Database::pdo();
|
$pdo = Database::pdo();
|
||||||
$running = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'running'")->fetchColumn();
|
$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();
|
$failed = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'failed' AND created_at > (NOW() - INTERVAL 7 DAY)")->fetchColumn();
|
||||||
|
|||||||
@@ -131,12 +131,6 @@ YAML;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$phase = !empty($params['ssh_debug']) ? 'ssh_debug' : 'docker';
|
$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');
|
$runner = (string) cfg('build.runner_script', '/workspace/scripts/docker-build-runner.sh');
|
||||||
$repo = (string) cfg('build.repo_root', '/workspace');
|
$repo = (string) cfg('build.repo_root', '/workspace');
|
||||||
$gitRemote = trim((string) shell_exec(
|
$gitRemote = trim((string) shell_exec(
|
||||||
@@ -176,7 +170,16 @@ YAML;
|
|||||||
}
|
}
|
||||||
$cmd = implode(' ', $env) . ' nohup ' . escapeshellarg($runner) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
|
$cmd = implode(' ', $env) . ' nohup ' . escapeshellarg($runner) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
|
||||||
$pid = trim((string) shell_exec($cmd));
|
$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);
|
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];
|
return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode];
|
||||||
}
|
}
|
||||||
@@ -195,7 +198,25 @@ YAML;
|
|||||||
|
|
||||||
public static function stopBuild(int $id): bool {
|
public static function stopBuild(int $id): bool {
|
||||||
$build = BuildRepository::getById($id);
|
$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;
|
return false;
|
||||||
}
|
}
|
||||||
$dir = self::artifactDir($id);
|
$dir = self::artifactDir($id);
|
||||||
@@ -240,6 +261,19 @@ YAML;
|
|||||||
return self::enqueue($params, $userId);
|
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). */
|
/** CircleCI-style: resume from the step that failed (git → docker-build → docker-run). */
|
||||||
public static function detectFailedStep(int $buildId): string {
|
public static function detectFailedStep(int $buildId): string {
|
||||||
$dir = self::artifactDir($buildId);
|
$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). */
|
/** Last lines / markers from build.log for reconcile error_message (max 512 chars). */
|
||||||
public static function summarizeLogFailure(?string $logPath): string {
|
public static function summarizeLogFailure(?string $logPath): string {
|
||||||
if (!$logPath || !is_file($logPath)) {
|
if (!$logPath || !is_file($logPath)) {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
<section class="card card--lift" style="margin-bottom:16px">
|
<section class="card card--lift" style="margin-bottom:16px">
|
||||||
<h2>Ecosystem health</h2>
|
<h2>Ecosystem health</h2>
|
||||||
<ul>
|
<ul id="build-health-list">
|
||||||
<li>Docker: <strong><?= h($health['docker'] ?? 'unknown') ?></strong></li>
|
<li>Docker: <strong id="build-health-docker"><?= h($health['docker'] ?? 'unknown') ?></strong></li>
|
||||||
<li>CI image version: <code><?= h($health['ci_version'] ?? '') ?></code></li>
|
<li>CI image version: <code><?= h($health['ci_version'] ?? '') ?></code></li>
|
||||||
<li>Running builds: <?= (int) ($health['running'] ?? 0) ?></li>
|
<li>Running builds: <span id="build-health-running"><?= (int) ($health['running'] ?? 0) ?></span></li>
|
||||||
<li>Passed (7d): <?= (int) ($health['passed_7d'] ?? 0) ?> · Failed (7d): <?= (int) ($health['failed_7d'] ?? 0) ?></li>
|
<li>Passed (7d): <?= (int) ($health['passed_7d'] ?? 0) ?> · Failed (7d): <?= (int) ($health['failed_7d'] ?? 0) ?></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
@@ -40,9 +40,10 @@
|
|||||||
<section class="card card--lift">
|
<section class="card card--lift">
|
||||||
<h2>Recent builds (last 10)</h2>
|
<h2>Recent builds (last 10)</h2>
|
||||||
<div class="reports-table-wrap">
|
<div class="reports-table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table" id="builds-recent-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="build-col-controls" aria-label="Job controls"></th>
|
||||||
<th>Code</th>
|
<th>Code</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Phase</th>
|
<th>Phase</th>
|
||||||
@@ -52,13 +53,22 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="builds-recent-tbody">
|
<tbody id="builds-recent-tbody">
|
||||||
<?php foreach ($builds as $b): ?>
|
<?php foreach ($builds as $b):
|
||||||
<tr data-build-id="<?= (int) $b['id'] ?>">
|
$bStatus = (string) ($b['status'] ?? '');
|
||||||
|
$isActive = in_array($bStatus, ['running', 'queued'], true);
|
||||||
|
?>
|
||||||
|
<tr data-build-id="<?= (int) $b['id'] ?>" data-build-status="<?= h($bStatus) ?>">
|
||||||
|
<td class="build-row-controls">
|
||||||
|
<div class="build-transport-actions">
|
||||||
|
<button type="button" class="build-transport-btn build-transport-play" data-build-id="<?= (int) $b['id'] ?>" data-build-status="<?= h($bStatus) ?>" title="Re-run job" aria-label="Re-run build <?= h($b['build_code']) ?>"></button>
|
||||||
|
<button type="button" class="build-transport-btn build-transport-stop" data-build-id="<?= (int) $b['id'] ?>" data-build-status="<?= h($bStatus) ?>" title="Stop job" aria-label="Stop build <?= h($b['build_code']) ?>"<?= $isActive ? '' : ' disabled' ?>></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td><a href="<?= h(Auth::basePath()) ?>/?view=build&id=<?= (int) $b['id'] ?>"><?= h($b['build_code']) ?></a></td>
|
<td><a href="<?= h(Auth::basePath()) ?>/?view=build&id=<?= (int) $b['id'] ?>"><?= h($b['build_code']) ?></a></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="build-status-cell-inner" data-build-status-cell="<?= (int) $b['id'] ?>">
|
<span class="build-status-cell-inner" data-build-status-cell="<?= (int) $b['id'] ?>">
|
||||||
<?= build_status_icon((string) ($b['status'] ?? '')) ?>
|
<?= build_status_icon($bStatus) ?>
|
||||||
<span class="build-status-label"><?= h($b['status']) ?></span>
|
<span class="build-status-label"><?= h($bStatus) ?></span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="build-phase-cell" data-build-phase-cell="<?= (int) $b['id'] ?>"><?= h($b['phase']) ?></td>
|
<td class="build-phase-cell" data-build-phase-cell="<?= (int) $b['id'] ?>"><?= h($b['phase']) ?></td>
|
||||||
|
|||||||
@@ -1282,6 +1282,72 @@ button.report-tag--filter:hover {
|
|||||||
pointer-events: none;
|
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 {
|
.tag-edit-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|||||||
@@ -40,6 +40,48 @@
|
|||||||
.replace(/"/g, '"');
|
.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) {
|
function parseCanvasId(id) {
|
||||||
const m = String(id || '').match(/^graph-(user|slug|platform)-(.+)$/);
|
const m = String(id || '').match(/^graph-(user|slug|platform)-(.+)$/);
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
@@ -430,6 +472,7 @@
|
|||||||
const overlay = el('graph-detail-overlay');
|
const overlay = el('graph-detail-overlay');
|
||||||
if (overlay) overlay.hidden = true;
|
if (overlay) overlay.hidden = true;
|
||||||
detailState = null;
|
detailState = null;
|
||||||
|
clearGraphPermalink();
|
||||||
const tip = el('graph-detail-tooltip');
|
const tip = el('graph-detail-tooltip');
|
||||||
if (tip) tip.hidden = true;
|
if (tip) tip.hidden = true;
|
||||||
const statsEl = el('graph-detail-stats');
|
const statsEl = el('graph-detail-stats');
|
||||||
@@ -449,6 +492,7 @@
|
|||||||
const scope = scopeData(parsed.scopeKey);
|
const scope = scopeData(parsed.scopeKey);
|
||||||
if (!scope) return;
|
if (!scope) return;
|
||||||
const def = parsed.def;
|
const def = parsed.def;
|
||||||
|
syncGraphPermalink(parsed.scopeKey, parsed.suffix);
|
||||||
const fieldVal = scope[def.field];
|
const fieldVal = scope[def.field];
|
||||||
const titleEl = el('graph-detail-title');
|
const titleEl = el('graph-detail-title');
|
||||||
const canvasEl = el('graph-detail-canvas');
|
const canvasEl = el('graph-detail-canvas');
|
||||||
@@ -655,6 +699,21 @@
|
|||||||
});
|
});
|
||||||
const closeBtn = el('graph-detail-close');
|
const closeBtn = el('graph-detail-close');
|
||||||
if (closeBtn) closeBtn.addEventListener('click', closeGraphDetail);
|
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) => {
|
document.addEventListener('keydown', (ev) => {
|
||||||
if (ev.key === 'Escape') closeGraphDetail();
|
if (ev.key === 'Escape') closeGraphDetail();
|
||||||
@@ -680,6 +739,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function load() {
|
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);
|
const days = Number(daysSel && daysSel.value ? daysSel.value : 14);
|
||||||
status.textContent = 'Loading…';
|
status.textContent = 'Loading…';
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
@@ -708,6 +772,7 @@
|
|||||||
markGraphBricks();
|
markGraphBricks();
|
||||||
const cols = colsSel && colsSel.value ? colsSel.value : '2';
|
const cols = colsSel && colsSel.value ? colsSel.value : '2';
|
||||||
status.textContent = 'Loaded · viewer=' + viewer + ' · window=' + days + 'd · columns=' + cols;
|
status.textContent = 'Loaded · viewer=' + viewer + ' · window=' + days + 'd · columns=' + cols;
|
||||||
|
openGraphFromPermalink();
|
||||||
};
|
};
|
||||||
xhr.onerror = function () {
|
xhr.onerror = function () {
|
||||||
status.textContent = 'Network error';
|
status.textContent = 'Network error';
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ final class GraphRepository {
|
|||||||
'viewer' => $viewer,
|
'viewer' => $viewer,
|
||||||
'links' => self::serviceLinks(),
|
'links' => self::serviceLinks(),
|
||||||
];
|
];
|
||||||
$payload['user'] = self::buildScope($days, $activeCompanyId, $scopeUserId);
|
$payload['user'] = self::buildScope($days, $activeCompanyId, null);
|
||||||
if ($viewer === 'user') {
|
if ($viewer === 'user') {
|
||||||
return $payload;
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,23 @@ function json_out(array $data, int $code = 200): void {
|
|||||||
exit;
|
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
|
* 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).
|
* (when nginx gunzip is off or the client talks to PHP directly).
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php AnalyticsHead::render('crashes'); ?>
|
<?php AnalyticsHead::render('crashes'); ?>
|
||||||
</head>
|
</head>
|
||||||
<body data-base-path="<?= h(Auth::basePath()) ?>"
|
<body data-base-path="<?= h(console_base_path($view ?? null)) ?>"
|
||||||
data-view="<?= h($view ?? 'home') ?>"
|
data-view="<?= h($view ?? 'home') ?>"
|
||||||
data-can-tag-edit="<?= Auth::canEditTags() ? '1' : '0' ?>"
|
data-can-tag-edit="<?= Auth::canEditTags() ? '1' : '0' ?>"
|
||||||
<?= (($view ?? '') === 'report' && !empty($report['id'])) ? ' data-report-id="' . (int) $report['id'] . '"' : '' ?>
|
<?= (($view ?? '') === 'report' && !empty($report['id'])) ? ' data-report-id="' . (int) $report['id'] . '"' : '' ?>
|
||||||
@@ -357,6 +357,8 @@
|
|||||||
<p id="graph-detail-stats" class="graph-detail-stats muted" aria-live="polite"></p>
|
<p id="graph-detail-stats" class="graph-detail-stats muted" aria-live="polite"></p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn graph-detail-close" id="graph-detail-close" aria-label="Close full view">Close</button>
|
<button type="button" class="btn graph-detail-close" id="graph-detail-close" aria-label="Close full view">Close</button>
|
||||||
|
<button type="button" class="btn" id="graph-detail-copy-link">Copy link</button>
|
||||||
|
<a class="btn" id="graph-detail-share" href="#" hidden aria-hidden="true"></a>
|
||||||
</header>
|
</header>
|
||||||
<div class="graph-detail-canvas-wrap">
|
<div class="graph-detail-canvas-wrap">
|
||||||
<canvas id="graph-detail-canvas" hidden></canvas>
|
<canvas id="graph-detail-canvas" hidden></canvas>
|
||||||
|
|||||||
Reference in New Issue
Block a user