1
0
mirror of git://f0xx.org/ac/ac-be-builder synced 2026-07-29 04:18:20 +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

355
public/assets/js/builder.js Normal file
View File

@@ -0,0 +1,355 @@
(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 '<span class="build-status-icon build-status-icon--running" role="img" aria-label="In progress">'
+ '<span class="build-status-spinner"></span></span>';
}
if (s === 'passed') {
return '<span class="build-status-icon build-status-icon--passed" role="img" aria-label="Succeeded"></span>';
}
return '<span class="build-status-icon build-status-icon--failed" role="img" aria-label="Failed or cancelled">'
+ '<span class="build-status-fail-mark" aria-hidden="true">!</span></span>';
}
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 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;
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')) {
bindBuildListTransport();
refreshRecentBuilds();
}
var themeSel = document.getElementById('theme-select');
if (themeSel) {
var cur = localStorage.getItem('crash_console_theme') || 'dark';
themeSel.value = cur;
themeSel.addEventListener('change', function () {
document.documentElement.setAttribute('data-theme', themeSel.value);
localStorage.setItem('crash_console_theme', themeSel.value);
});
}
var form = document.getElementById('build-trigger-form');
if (form) {
form.addEventListener('submit', function (ev) {
ev.preventDefault();
var status = document.getElementById('build-trigger-status');
var fd = new FormData(form);
var payload = {
git_ref: fd.get('git_ref') || 'next',
gradle_task: fd.get('gradle_task') || 'assembleDebug',
ota_channel: fd.get('ota_channel') || 'staging',
run_tests: !!form.querySelector('[name=run_tests]').checked,
run_native: !!form.querySelector('[name=run_native]').checked,
run_apk: !!form.querySelector('[name=run_apk]').checked,
auto_ota: !!form.querySelector('[name=auto_ota]').checked,
auto_deploy: !!form.querySelector('[name=auto_deploy]').checked
};
status.textContent = 'Starting…';
fetch(base + '/api/build_trigger.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(payload)
}).then(function (r) {
return r.text().then(function (text) {
var j = null;
try {
j = JSON.parse(text);
} catch (e) {
throw new Error(r.status + ': ' + (text.slice(0, 120) || r.statusText));
}
if (!r.ok) {
throw new Error((j && j.error) || ('HTTP ' + r.status));
}
return j;
});
}).then(function (j) {
if (j.ok && j.build && j.build.id) {
window.location.href = base + '/?view=build&id=' + j.build.id;
} else {
status.textContent = j.error || 'Failed';
}
}).catch(function (err) {
status.textContent = err && err.message ? err.message : 'Network error';
});
});
}
function postBuildAction(payload) {
return fetch(base + '/api/build_actions.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(payload)
}).then(function (r) {
return r.text().then(function (text) {
var j = null;
try { j = JSON.parse(text); } catch (e) {
throw new Error(r.status + ': ' + (text.slice(0, 120) || r.statusText));
}
if (!r.ok || !j.ok) {
throw new Error((j && j.error) || ('HTTP ' + r.status));
}
return j;
});
});
}
var rerunBtn = document.getElementById('build-rerun-btn');
var rerunFailedBtn = document.getElementById('build-rerun-failed-btn');
var rerunSshBtn = document.getElementById('build-rerun-ssh-btn');
var stopBtn = document.getElementById('build-stop-btn');
var rerunAsToggle = document.getElementById('build-rerun-as-toggle');
var rerunAsForm = document.getElementById('build-rerun-as-form');
var actionStatus = document.getElementById('build-action-status');
function goToBuild(j) {
if (j.build && j.build.id) {
window.location.href = base + '/?view=build&id=' + j.build.id;
}
}
function bindRerunAction(btn, action, confirmMsg) {
if (!btn) return;
btn.addEventListener('click', function () {
var id = Number(btn.getAttribute('data-build-id'));
if (!id) return;
if (confirmMsg && !confirm(confirmMsg)) return;
if (actionStatus) actionStatus.textContent = 'Starting…';
postBuildAction({ action: action, build_id: id }).then(goToBuild).catch(function (err) {
if (actionStatus) actionStatus.textContent = err.message || 'Failed';
});
});
}
if (rerunAsToggle && rerunAsForm) {
rerunAsToggle.addEventListener('click', function () {
rerunAsForm.hidden = !rerunAsForm.hidden;
});
}
bindRerunAction(rerunBtn, 'rerun', null);
bindRerunAction(rerunFailedBtn, 'rerun_from_failed', null);
bindRerunAction(
rerunSshBtn,
'rerun_with_ssh',
'Start an SSH debug session on the builder? You will need shell access to the builder host.'
);
if (stopBtn) {
stopBtn.addEventListener('click', function () {
var id = Number(stopBtn.getAttribute('data-build-id'));
if (!id || !confirm('Stop this build?')) return;
if (actionStatus) actionStatus.textContent = 'Stopping…';
postBuildAction({ action: 'stop', build_id: id }).then(function () {
window.location.reload();
}).catch(function (err) {
if (actionStatus) actionStatus.textContent = err.message || 'Failed';
});
});
}
if (rerunAsForm) {
rerunAsForm.addEventListener('submit', function (ev) {
ev.preventDefault();
var id = Number(rerunBtn && rerunBtn.getAttribute('data-build-id'));
if (!id) return;
var fd = new FormData(rerunAsForm);
var params = {
git_ref: fd.get('git_ref') || 'next',
gradle_task: fd.get('gradle_task') || 'assembleDebug',
ota_channel: fd.get('ota_channel') || 'staging',
run_tests: !!rerunAsForm.querySelector('[name=run_tests]').checked,
run_native: !!rerunAsForm.querySelector('[name=run_native]').checked,
run_apk: !!rerunAsForm.querySelector('[name=run_apk]').checked,
auto_ota: !!rerunAsForm.querySelector('[name=auto_ota]').checked,
auto_deploy: !!rerunAsForm.querySelector('[name=auto_deploy]').checked
};
if (actionStatus) actionStatus.textContent = 'Starting…';
postBuildAction({ action: 'rerun_as', build_id: id, params: params }).then(function (j) {
if (j.build && j.build.id) {
window.location.href = base + '/?view=build&id=' + j.build.id;
}
}).catch(function (err) {
if (actionStatus) actionStatus.textContent = err.message || 'Failed';
});
});
}
var buildId = document.body.getAttribute('data-build-id');
var buildStatus = document.body.getAttribute('data-build-status') || '';
var logEl = document.getElementById('build-log-tail');
if (buildId && logEl) {
var offset = 0;
var full = '';
var showFull = false;
var DISPLAY_TAIL = 50000;
function renderLog() {
logEl.textContent = showFull ? full : full.slice(-DISPLAY_TAIL);
logEl.scrollTop = logEl.scrollHeight;
}
function poll() {
var url = base + '/api/build_log.php?id=' + encodeURIComponent(buildId) + '&offset=' + offset;
fetch(url)
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.ok) return;
if (j.log && j.log.text) {
full += j.log.text;
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 ? (' · <span class="error-text" id="build-error-message">'
+ String(j.build.error_message).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+ '</span>') : '';
textEl.innerHTML = 'Status: <strong>' + (j.build.status || '') + '</strong> · 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) {
setTimeout(poll, running ? 1000 : 250);
} else if (j.build && j.build.phase === 'ssh_debug' && !document.getElementById('build-ssh-panel')) {
window.location.reload();
}
}).catch(function () { setTimeout(poll, 2000); });
}
function loadFullLog() {
return fetch(base + '/api/build_log.php?id=' + encodeURIComponent(buildId) + '&full=1')
.then(function (r) { return r.json(); })
.then(function (j) {
if (j.ok && j.log && j.log.text) {
full = j.log.text;
offset = j.log.offset || 0;
renderLog();
}
});
}
if (buildStatus && buildStatus !== 'running' && buildStatus !== 'queued') {
loadFullLog().catch(function () { poll(); });
} else {
poll();
}
var expand = document.getElementById('build-log-expand');
if (expand) {
expand.addEventListener('click', function () {
showFull = true;
if (full === '') {
loadFullLog().then(function () { renderLog(); });
} else {
renderLog();
}
expand.textContent = 'Showing full log';
expand.disabled = true;
});
}
}
})();