feat(monitor): 失败提示做醒目——顶部红色横幅(阶段名 + 返回码 + 终端里最后一行真实报错,如 OutOfMemory 那条)+ 右上角胶囊变红「失败 · 停在「预训练」返回码 1」+ 步骤条那一格红字加粗;并补三层"永不空白"保险:正文脚本前装看门狗(3 秒没跑完就显示"卡片空白不是训练出问题")、window.onerror 显示出错文件与行号、渲染调用 try/catch 让单个面板出错不拖垮整页;同时修掉我上一轮写坏的 JS 字符串(Python 转义把 \n 写成真实换行 → 整个脚本不执行 → 页面全空,就是用户看到的"失败时什么都不显示")

This commit is contained in:
cbb
2026-10-18 19:43:24 +08:00
parent 3c4bc6c8ab
commit 7fe1e5e0b3
+43 -1
View File
@@ -56,6 +56,14 @@
.step.done .ic { background: #34d39922; border-color: var(--ok); color: var(--ok); }
.step.active .ic { background: #5aa9ff22; border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px #5aa9ff1f; }
.step.fail .ic { background: #f8717122; border-color: var(--bad); color: var(--bad); }
.step.fail .ds { color: var(--bad); font-weight: 650; font-size: 12px; }
.alert { border-radius: 12px; padding: 13px 16px; margin: 2px 0 0; border: 1px solid; }
.alert.bad { background: linear-gradient(180deg,#7f1d1d66,#450a0a55); border-color: #f8717199; box-shadow: 0 0 0 1px #f8717122, 0 8px 26px #00000066; }
.alert.warn { background: #78350f55; border-color: #fbbf2488; }
.alert .alerth { font-size: 17px; font-weight: 700; color: #ffe3e3; letter-spacing: .2px; }
.alert.warn .alerth { color: #ffeccd; }
.alert .alerterr { font: 500 12.5px/1.5 var(--mono); color: #ffb4b4; margin-top: 7px; word-break: break-all; }
.alert .alerttip { font-size: 12.5px; color: #f0d9d9cc; margin-top: 7px; }
.step .nm { font-size: 12.5px; color: var(--text); white-space: nowrap; } .step .ds { font-size: 11px; color: var(--dim2); white-space: nowrap; }
.step .ds.big { font-size: 14px; font-weight: 650; color: var(--accent); }
.step.pending .nm { color: var(--dim); }
@@ -171,6 +179,7 @@
</header>
<main>
<div id="alert"></div>
<section class="card"><h2><span>流水线</span><span class="r" id="stepnote"></span></h2><div class="steps" id="steps"></div></section>
<section class="hero">
@@ -211,6 +220,27 @@
<div class="acts"><button id="fCancel">取消</button><button id="fGo" class="primary">▶ 开始</button></div>
</div></div>
<script>
/* 兜底:正文脚本一旦有语法错误或抛异常,整页会空白(2026-10-18 真踩过:一行字符串写断 → 页面什么都不显示)。
这段在正文之前跑,负责"坏也坏得看得见":看门狗 + 全局错误显示。 */
window.__boot = { ok: false };
function __pageFail(msg) {
const m = document.querySelector('main') || document.body;
let d = document.getElementById('pagefail');
if (!d) {
d = document.createElement('div'); d.id = 'pagefail';
d.style.cssText = 'margin:10px 24px 0;padding:14px 16px;border-radius:12px;background:#7f1d1dcc;border:1px solid #f87171;' +
'color:#ffe3e3;font:600 15px/1.6 system-ui;position:sticky;top:8px;z-index:99;box-shadow:0 8px 26px #00000088';
m.prepend(d);
}
d.textContent = msg;
}
window.addEventListener('error', e => __pageFail('页面脚本出错:' + (e.message || '未知错误') +
'(' + (e.filename || '').split('/').pop() + ':' + (e.lineno || '?') + ')— 面板可能不完整,但训练本身没受影响;按 F12 看 Console'));
setTimeout(() => { if (!window.__boot.ok) __pageFail('页面脚本没有启动(多半是 ml/monitor.html 有语法错误):卡片空白不是训练出问题。' +
'F12 → Console 看第一条报错,或把该文件回滚到上一个提交。'); }, 3000);
</script>
<script>
'use strict';
/* =====================================================================
@@ -488,8 +518,13 @@ async function poll() {
if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').closest('.hero').insertAdjacentHTML('beforebegin', `<div class="demobar" id="demobar">这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。</div>`); }
else if (busy) setPill('live', `运行中 · ${P && P.current ? SM[P.current]?.n || P.current : '启动中'}`);
else if (cur && Date.now() / 1000 - lastMtime < 120) setPill('live', `运行中 · ${ago(lastMtime)}更新`);
else if (P && P.plan && P.plan.some(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0)) {
const k = P.plan.find(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0), rc = P.stages[k].rc, nm = SM[k]?.n || k;
setPill(rc === -15 || rc === 143 ? 'stale' : 'dead', `${rc === -15 || rc === 143 ? '已停止' : '失败'} · 停在「${nm}」返回码 ${rc}`);
}
else setPill('', '空闲');
renderStatus(lastStatus); renderPipe(lastStatus); renderJob();
try { renderStatus(lastStatus); renderPipe(lastStatus); renderJob(); }
catch (e) { __pageFail('渲染面板出错:' + e.message + ' —— 其余面板可能不更新;训练本身不受影响'); }
await pollConsole();
} catch (err) {
setPill('dead', '连不上控制台(双击 ml/console.bat 或 python -m ml.monitor)'); console.error(err);
@@ -636,6 +671,12 @@ function renderPipe(s) {
: allDone ? `<div class="now">状态<b class="ok">全部完成</b>共 ${plan.length} 步</div>`
: failed ? `<div class="now">状态<b class="bad">停在「${SM[failed]?.n || failed}」</b>${(P.stages[failed].info || {}).timed_out || P.stages[failed].rc === 124 ? '超过时间上限,判定卡死' : P.stages[failed].rc === -15 || P.stages[failed].rc === 143 ? '已停止' : '失败,看终端输出'}</div>`
: `<div class="now">状态<b>未在运行</b>已完成 ${doneN} / ${plan.length} 步</div>`;
const esc2 = x => String(x).replace(/&/g, '&amp;').replace(/</g, '&lt;');
const fr = failed && P.stages[failed], stopped = !!fr && (fr.rc === -15 || fr.rc === 143);
const timedOut = !!fr && ((fr.info || {}).timed_out || fr.rc === 124);
const why = !fr ? '' : stopped ? '已手动停止 —— 点「继续训练」从停下的地方接着跑' : timedOut ? '超过时间上限,判定卡死' : `失败(返回码 ${fr.rc})`;
const lastErr = (() => { const t = $('term'); if (!t) return ''; const ls = (t.innerText || '').split(/\r?\n/).filter(x => /Error|error:|Traceback|不通过|失败|超过时间上限/.test(x)); return ls.length ? ls[ls.length - 1].slice(0, 300) : ''; })();
$('alert').innerHTML = failed ? `<div class="alert ${stopped ? 'warn' : 'bad'}"><div class="alerth">✕ 停在「${SM[failed]?.n || failed}」 · ${why}</div>${lastErr ? `<div class="alerterr">${esc2(lastErr)}</div>` : ''}<div class="alerttip">完整报错见下方「终端输出」;修好后点「继续训练」,已完成的步骤会自动跳过${stopped ? '、从停下的那一步接着跑' : ''}</div></div>` : '';
$('pipe').innerHTML = `<h2><span>全流程</span><span class="r">${doneN} / ${plan.length} 步完成</span></h2>
<div class="overall">${head}<div class="bigpct">${(100 * frac).toFixed(1)}<small>%</small></div></div>
<div class="bar ${cur ? '' : 'idle'}"><div class="fill" style="width:${100 * frac}%"></div></div>
@@ -828,6 +869,7 @@ function demoApi() {
fn.scenario = setup;
return fn;
}
window.__boot.ok = true; // 正文脚本跑到了最后一行,看门狗不用再报"脚本没启动"
</script>
</body>
</html>