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(''); }