1
0
mirror of git://f0xx.org/ac/ac-be-builder synced 2026-07-29 02:58:34 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-23 12:29:38 +02:00
commit 5e3e5939a8
17 changed files with 1745 additions and 0 deletions

124
views/build_detail.php Normal file
View File

@@ -0,0 +1,124 @@
<?php declare(strict_types=1);
$artifacts = json_decode($build['artifacts_json'] ?? '{}', true) ?: [];
$params = json_decode($build['params_json'] ?? '{}', true) ?: [];
$status = (string) ($build['status'] ?? '');
$phase = (string) ($build['phase'] ?? '');
$isRunning = $status === 'running';
$canRerunFromFailed = in_array($status, ['failed', 'cancelled'], true);
$failedStep = $canRerunFromFailed ? BuildRunner::detectFailedStep((int) ($build['id'] ?? 0)) : '';
$isSshDebug = $phase === 'ssh_debug' || ($isRunning && !empty($params['ssh_debug']));
$sshContainer = BuildRunner::debugContainerName((int) ($build['id'] ?? 0));
$builderHost = (string) ($build['builder_id'] ?? gethostname() ?: 'builder');
$pipelineYaml = trim((string) ($build['pipeline_yaml'] ?? ''));
if ($pipelineYaml === '') {
$pipelineYaml = '(none — will be generated on next trigger from build.config.yml or params)';
}
$stepLabels = [
'git' => 'Git checkout / shallow clone',
'docker-build' => 'Docker image build',
'docker-run' => 'Gradle pipeline (docker run)',
];
?>
<h1>Build <?= h($build['build_code'] ?? '') ?></h1>
<p class="muted build-status-line" id="build-status-line">
<span id="build-status-icon"><?= build_status_icon($status) ?></span>
<span id="build-status-text">Status: <strong><?= h($status) ?></strong> · Phase: <?= h($phase) ?>
<?php if (!empty($build['error_message'])): ?>
· <span class="error-text" id="build-error-message"><?= h($build['error_message']) ?></span>
<?php endif; ?>
</span>
</p>
<?php
$healthDocker = ($health['docker'] ?? '') === 'ok';
$dockerFail = str_contains((string) ($build['error_message'] ?? ''), 'Docker daemon not available');
if ($status === 'failed' && $dockerFail && $healthDocker):
?>
<p class="muted graphs-footnote">Ecosystem health reports <strong>Docker: ok</strong> now. The log below is from this failed run — use <strong>Re-run</strong> (do not only refresh).</p>
<?php endif; ?>
<?php if ($canRerunFromFailed && $failedStep !== ''): ?>
<p class="muted graphs-footnote">Failed at step: <strong><?= h($stepLabels[$failedStep] ?? $failedStep) ?></strong> — <em>Re-run from failed</em> skips earlier steps when artifacts allow (like CircleCI).</p>
<?php endif; ?>
<div class="build-actions card card--lift" style="margin-bottom:12px">
<h2>Actions</h2>
<div class="toolbar-actions" style="flex-wrap:wrap;gap:8px">
<a class="btn" href="<?= h(Auth::basePath()) ?>/">← Back</a>
<button type="button" class="btn" id="build-rerun-btn" data-build-id="<?= (int) $build['id'] ?>">Re-run</button>
<?php if ($canRerunFromFailed): ?>
<button type="button" class="btn" id="build-rerun-failed-btn" data-build-id="<?= (int) $build['id'] ?>" title="Resume from <?= h($failedStep) ?>">Re-run from failed</button>
<button type="button" class="btn" id="build-rerun-ssh-btn" data-build-id="<?= (int) $build['id'] ?>" title="Resume from failed step, then open an interactive container (CircleCI-style SSH debug)">Re-run with SSH</button>
<?php endif; ?>
<button type="button" class="btn" id="build-rerun-as-toggle">Re-run as…</button>
<?php if ($isRunning): ?>
<button type="button" class="btn btn-danger" id="build-stop-btn" data-build-id="<?= (int) $build['id'] ?>"><?= $isSshDebug ? 'End SSH session' : 'Stop' ?></button>
<?php endif; ?>
<span id="build-action-status" class="muted"></span>
</div>
<?php if ($isSshDebug): ?>
<div class="build-ssh-panel" id="build-ssh-panel">
<strong>SSH debug session</strong>
<p class="muted graphs-footnote" style="margin:6px 0 0">Shell into the CI container on the builder host (same idea as CircleCI “Rerun with SSH”). Stop ends the session.</p>
<pre id="build-ssh-command">ssh &lt;user&gt;@<?= h($builderHost) ?>
docker exec -it <?= h($sshContainer) ?> bash</pre>
</div>
<?php endif; ?>
<form id="build-rerun-as-form" class="build-form" hidden style="margin-top:12px">
<div class="build-form-grid">
<label>Git ref<input name="git_ref" value="<?= h($params['git_ref'] ?? $build['git_ref'] ?? 'next') ?>"></label>
<label>Gradle task<input name="gradle_task" value="<?= h($params['gradle_task'] ?? 'assembleDebug') ?>"></label>
<label>OTA channel<select name="ota_channel">
<?php foreach (['staging', 'dev', 'nightly', 'stable'] as $ch): ?>
<option value="<?= h($ch) ?>" <?= (($params['ota_channel'] ?? $build['ota_channel'] ?? 'staging') === $ch) ? 'selected' : '' ?>><?= h($ch) ?></option>
<?php endforeach; ?>
</select></label>
</div>
<div class="build-form-checks">
<label><input type="checkbox" name="run_tests" <?= !empty($params['run_tests']) ? 'checked' : '' ?>> Unit tests</label>
<label><input type="checkbox" name="run_native" <?= !isset($params['run_native']) || !empty($params['run_native']) ? 'checked' : '' ?>> Native codecs</label>
<label><input type="checkbox" name="run_apk" <?= !isset($params['run_apk']) || !empty($params['run_apk']) ? 'checked' : '' ?>> APK output</label>
<label><input type="checkbox" name="auto_ota" <?= !empty($params['auto_ota']) ? 'checked' : '' ?>> Create OTA artifacts</label>
<label><input type="checkbox" name="auto_deploy" <?= !empty($params['auto_deploy']) ? 'checked' : '' ?>> Publish OTA to mount</label>
</div>
<button type="submit" class="btn btn-primary">Start parametrized re-run</button>
</form>
</div>
<div class="detail-grid card card--lift">
<dl>
<dt>Builder</dt><dd><?= h($build['builder_id'] ?? '') ?></dd>
<dt>CI image</dt><dd><code><?= h($build['dockerfile_version'] ?? '') ?></code></dd>
<dt>Target OS</dt><dd><?= h($build['target_os'] ?? 'android') ?> (min API <?= (int) ($build['min_api_level'] ?? 24) ?>)</dd>
<dt>Git ref</dt><dd><code><?= h($build['git_ref'] ?? '—') ?></code></dd>
<dt>Git SHA</dt><dd><code><?= h($build['git_sha'] ?? '—') ?></code></dd>
<dt>OTA channel</dt><dd><?= h($build['ota_channel'] ?? '—') ?></dd>
<dt>Gradle task</dt><dd><code><?= h($params['gradle_task'] ?? '—') ?></code></dd>
</dl>
</div>
<?php if ($artifacts): ?>
<section class="card card--lift">
<h2>Artifacts</h2>
<ul>
<?php foreach ($artifacts as $k => $v): ?>
<li><strong><?= h($k) ?>:</strong> <code><?= h((string) $v) ?></code></li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
<section class="card card--lift">
<h2>Pipeline (YAML)</h2>
<p class="muted graphs-footnote">Snapshot stored with this build. New triggers use <code>build.config.yml</code> from the repo when present, otherwise a generated default.</p>
<pre class="log-preview"><?= h($pipelineYaml) ?></pre>
</section>
<section class="card card--lift">
<h2>Build log</h2>
<p class="muted graphs-footnote">Live tail shows the last ~50&nbsp;KB while running. Failed builds load the full log automatically. Per-step files (when present): <code>docker-build.log</code>, <code>docker-run.log</code> under this builds artifact dir.</p>
<pre id="build-log-tail" class="log-preview log-preview--live" aria-live="polite"></pre>
<div class="build-log-actions">
<button type="button" class="btn" id="build-log-expand">Show full log</button>
<a class="btn" id="build-log-download" href="<?= h(Auth::basePath()) ?>/api/build_log.php?id=<?= (int) $build['id'] ?>&amp;download=1">Download log</a>
</div>
</section>

83
views/home.php Normal file
View File

@@ -0,0 +1,83 @@
<?php declare(strict_types=1); ?>
<h1>Android Cast Builder</h1>
<p class="muted">Docker CI for APK baking, OTA staging, and pipeline history.</p>
<section class="card card--lift" style="margin-bottom:16px">
<h2>Ecosystem health</h2>
<ul id="build-health-list">
<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>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>
</ul>
</section>
<section class="card card--lift" id="build-trigger-panel">
<h2>Run pipeline</h2>
<form id="build-trigger-form" class="build-form">
<div class="build-form-grid">
<label>Git ref (branch / tag / commit)<input name="git_ref" placeholder="next" value="next"></label>
<label>Gradle task<input name="gradle_task" value="assembleDebug"></label>
<label>OTA channel<select name="ota_channel">
<option value="staging" selected>staging</option>
<option value="dev">dev</option>
<option value="nightly">nightly</option>
<option value="stable">stable / prod</option>
</select></label>
</div>
<div class="build-form-checks">
<label><input type="checkbox" name="run_tests" checked> Unit tests</label>
<label><input type="checkbox" name="run_native" checked> Native codecs</label>
<label><input type="checkbox" name="run_apk" checked> APK output</label>
<label><input type="checkbox" name="auto_ota"> Create OTA artifacts</label>
<label><input type="checkbox" name="auto_deploy"> Publish OTA to mount</label>
</div>
<button type="submit" class="btn btn-primary">Start build</button>
<span id="build-trigger-status" class="muted"></span>
</form>
</section>
<section class="card card--lift">
<h2>Recent builds (last 10)</h2>
<div class="reports-table-wrap">
<table class="data-table" id="builds-recent-table">
<thead>
<tr>
<th class="build-col-controls" aria-label="Job controls"></th>
<th>Code</th>
<th>Status</th>
<th>Phase</th>
<th>Branch/ref</th>
<th>Channel</th>
<th>Created</th>
</tr>
</thead>
<tbody id="builds-recent-tbody">
<?php foreach ($builds as $b):
$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>
<span class="build-status-cell-inner" data-build-status-cell="<?= (int) $b['id'] ?>">
<?= build_status_icon($bStatus) ?>
<span class="build-status-label"><?= h($bStatus) ?></span>
</span>
</td>
<td class="build-phase-cell" data-build-phase-cell="<?= (int) $b['id'] ?>"><?= h($b['phase']) ?></td>
<td><?= h($b['git_ref'] ?? $b['branch'] ?? '—') ?></td>
<td><?= h($b['ota_channel'] ?? '—') ?></td>
<td><?= h($b['created_at'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>

59
views/layout.php Normal file
View File

@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
$ab = assets_base();
$links = cfg('links', []);
?>
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($pageTitle ?? 'Builder') ?> — <?= h(cfg('app_name')) ?></title>
<script>
(function () {
var t = localStorage.getItem('crash_console_theme');
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
var l = localStorage.getItem('crash_console_lang');
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
})();
</script>
<link rel="stylesheet" href="<?= h($ab) ?>/assets/css/app.css">
<script src="<?= h($ab) ?>/assets/js/i18n.js" defer></script>
<script src="<?= h(Auth::basePath()) ?>/assets/js/builder.js" defer></script>
</head>
<body data-base-path="<?= h(Auth::basePath()) ?>"
data-view="<?= h($view ?? 'home') ?>"
<?= (($view ?? '') === 'build' && !empty($build['id'])) ? ' data-build-id="' . (int) $build['id'] . '"' : '' ?>
<?= (($view ?? '') === 'build' && !empty($build['status'])) ? ' data-build-status="' . h((string) $build['status']) . '"' : '' ?>>
<div class="shell">
<nav class="nav-pane" id="nav-pane" aria-label="Builder navigation">
<button type="button" class="nav-handle" id="nav-handle" aria-label="Toggle navigation">
<span class="nav-icon nav-icon--menu" aria-hidden="true"></span>
</button>
<ul class="nav-list">
<li><a href="<?= h(Auth::basePath()) ?>/" class="nav-link <?= ($view ?? '') === 'home' ? 'active' : '' ?>"><span class="nav-icon nav-icon--home"></span><span class="nav-label">Builder</span></a></li>
<li><a href="<?= h($links['crashes'] ?? '') ?>?view=reports" class="nav-link"><span class="nav-icon nav-icon--reports"></span><span class="nav-label">Crashes</span></a></li>
<li><a href="<?= h($links['tickets'] ?? '') ?>" class="nav-link"><span class="nav-icon nav-icon--tickets"></span><span class="nav-label">Tickets</span></a></li>
<li><a href="<?= h($links['git'] ?? '') ?>" class="nav-link"><span class="nav-icon nav-icon--git"></span><span class="nav-label">Git</span></a></li>
<li><a href="<?= h($links['hub'] ?? '') ?>" class="nav-link"><span class="nav-icon nav-icon--home"></span><span class="nav-label">Hub</span></a></li>
</ul>
<div class="nav-locale">
<label class="toolbar-select"><span>Theme</span>
<select id="theme-select" aria-label="UI theme"><option value="dark">Dark</option><option value="light">Light</option></select>
</label>
</div>
<div class="nav-user">
<span class="nav-user-name"><?= h(Auth::user()['username'] ?? '') ?></span>
<a href="<?= h(Auth::basePath()) ?>/logout" class="nav-link nav-link--logout">Logout</a>
</div>
</nav>
<main class="main-pane">
<?php if (($view ?? '') === 'build' && !empty($build)): ?>
<?php require __DIR__ . '/build_detail.php'; ?>
<?php else: ?>
<?php require __DIR__ . '/home.php'; ?>
<?php endif; ?>
</main>
</div>
<?php platform_render_footer(); ?>
</body>
</html>

32
views/login.php Normal file
View File

@@ -0,0 +1,32 @@
<?php declare(strict_types=1);
$bp = Auth::basePath();
$ab = assets_base();
?>
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login — <?= h(cfg('app_name')) ?></title>
<script>
(function () {
var t = localStorage.getItem('crash_console_theme');
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
})();
</script>
<link rel="stylesheet" href="<?= h($ab) ?>/assets/css/app.css">
</head>
<body class="login-page" data-base-path="<?= h($ab) ?>">
<form class="login-card" method="post" action="<?= h($bp) ?>/login">
<h1><?= h(cfg('app_name')) ?></h1>
<p class="muted">Sign in (same credentials as Crashes / Tickets)</p>
<?php if (!empty($loginError)): ?>
<div class="alert"><?= h($loginError) ?></div>
<?php endif; ?>
<label><span>Username</span><input name="username" autocomplete="username" required></label>
<label><span>Password</span><input name="password" type="password" autocomplete="current-password" required></label>
<button type="submit">Sign in</button>
</form>
<?php platform_render_footer(); ?>
</body>
</html>