async function api(path, opts) {
const r = await fetch(path, opts);
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(j.error || r.statusText);
return j;
}
function qs(name) {
return new URLSearchParams(location.search).get(name) || '';
}
/** 状态徽章:text 为中文标签,code 为原始值(用于配色) */
function badge(code, text) {
const display = text != null && text !== '' ? text : (code || '-');
const s = (code || display || '').toLowerCase();
let cls = 'info';
if (s === 'success' || s === 'done') cls = 'ok';
else if (s === 'failed' || s === 'fail' || s === 'error') cls = 'err';
else if (s === 'skipped' || s === 'warning' || s === 'waiting' || s === 'pending') cls = 'warn';
else if (s === 'running' || s === 'pulling') cls = 'info';
return `${esc(display)}`;
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
function fmtTime(nt) {
if (!nt || !nt.Valid || !nt.Time) return '-';
try { return new Date(nt.Time).toLocaleString(); } catch { return nt.Time; }
}
function fmtTimeAny(t) {
if (!t) return '-';
if (typeof t === 'string') {
try { return new Date(t).toLocaleString(); } catch { return t; }
}
return fmtTime(t);
}
async function loadMeta() {
try {
const m = await api('/api/meta');
const el = document.getElementById('meta-footer');
if (el) {
el.innerHTML = `日志目录: ${esc(m.logDir)} | MySQL: ${m.mysqlOk ? '已连接' : '' + esc(m.mysqlError || '未连接') + ''}`;
}
return m;
} catch (e) {
return null;
}
}
function fillStepSelect(sel, options, includeAll) {
if (!sel || !options) return;
sel.innerHTML = options.map(o =>
``
).join('');
}