Files
storage-labos/前端UI八页面完成/LiveRun.dc.html
T
yuanshuai c91a64fddb
CI / Python 3.12 (push) Waiting to run
CI / Python 3.9 (push) Waiting to run
chore(repo): initialize team collaboration repository
2026-07-27 20:40:12 +08:00

451 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="stylesheet" href="_ds/industry-22c874fd-9888-4e99-8234-d60d39b3d719/styles.css">
<script src="_ds/industry-22c874fd-9888-4e99-8234-d60d39b3d719/_ds_bundle.js"></script>
<style>
body{margin:0;background:#f2f2f3;min-width:1280px}
*{scrollbar-width:thin;scrollbar-color:var(--color-accent-300) transparent}
a{color:#5980a6}a:hover{color:#416180}
@keyframes omIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes omScan{to{background-position:28px 0}}
@keyframes omDraw{to{stroke-dashoffset:0}}
@keyframes omGrow{from{width:0}}
@keyframes omPulse{0%,100%{opacity:1}50%{opacity:.3}}
@keyframes omBlink{0%,49%{opacity:1}50%,100%{opacity:.15}}
</style>
</helmet>
<dc-import name="ConsoleNav" active="live" hint-size="100%,92px"></dc-import>
<div data-screen-label="实时运行" style="max-width:1400px;margin:0 auto;padding:22px 24px 64px;font-family:var(--font-body);animation:omIn .5s cubic-bezier(.2,.7,.2,1) both">
<div style="display:flex;align-items:flex-end;gap:16px;margin-bottom:16px">
<div>
<div style="font-size:10px;letter-spacing:.18em;color:var(--color-accent-700);font-weight:500">03 · LIVE RUN</div>
<h2 style="margin:2px 0 0;font-size:30px">实时运行</h2>
<div class="text-muted" style="font-size:12.5px;margin-top:2px">{{ hostName }} · 只在必须时人工接管</div>
</div>
<span style="margin-left:auto"></span>
<button class="btn btn-secondary" onClick="{{ startDemo }}" disabled="{{ demoBusy }}" style="gap:7px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
演示:注入主机失联
</button>
<div class="seg" style="background:var(--color-bg)">
<label class="seg-opt"><input type="radio" name="lrv" checked="{{ vA }}" onChange="{{ setA }}"><span>A · 控制室三栏</span></label>
<label class="seg-opt"><input type="radio" name="lrv" checked="{{ vB }}" onChange="{{ setB }}"><span>B · 时间线泳道</span></label>
</div>
</div>
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;flex-direction:row;align-items:center;gap:20px;padding:12px 16px;margin-bottom:18px">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<div>
<div style="font-family:ui-monospace,Menlo,monospace;font-size:14px;font-weight:700">{{ runId }}</div>
<div style="font-size:11.5px;color:var(--color-neutral-600)">fw_ab_powerstate_regression v3 · 已审核模板</div>
</div>
<span style="width:1px;height:30px;background:var(--color-divider)"></span>
<div style="font-size:12px;line-height:1.5;color:var(--color-neutral-800)"><span class="text-muted">固件</span> {{ firmwareLabel }}<br><span class="text-muted">DUT</span> {{ dutLabel }}</div>
<span style="width:1px;height:30px;background:var(--color-divider)"></span>
<div style="font-size:12px;line-height:1.5;color:var(--color-neutral-800)"><span class="text-muted">开始</span> {{ startText }}<br><span class="text-muted">检查点</span> {{ checkpointText }}</div>
<span style="width:1px;height:30px;background:var(--color-divider)"></span>
<div style="flex:1;min-width:180px">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--color-neutral-700);margin-bottom:4px"><span>轮次 {{ cycle }} / {{ target }}</span><span>由控制平面实时投影</span></div>
<div style="height:6px;border:1px solid var(--color-divider);position:relative"><div style="{{ progStyle }}"></div></div>
</div>
<span style="{{ badgeStyle }}">{{ badgeText }}</span>
<button class="btn btn-secondary" onClick="{{ togglePause }}" disabled="{{ pauseDisabled }}" style="font-size:12.5px">{{ pauseLabel }}</button>
<button class="btn btn-secondary" disabled="{{ true }}" style="font-size:12.5px" title="真实 Agent 接入后开放">人工接管</button>
<button onClick="{{ estop }}" disabled="{{ stopDisabled }}" style="display:inline-flex;align-items:center;gap:6px;background:transparent;border:1px solid #b03d2e;color:#8a2e22;padding:7px 12px;font-family:var(--font-heading);font-weight:600;font-size:12.5px;letter-spacing:.05em;cursor:pointer" style-hover="background:#f7e9e6">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon></svg>
停止本工位
</button>
</div>
<sc-if value="{{ estopped }}" hint-placeholder-val="{{ false }}">
<div style="display:flex;align-items:center;gap:12px;background:#b03d2e;color:#f7f4f2;padding:10px 16px;margin-bottom:18px;font-size:13px">
<strong style="font-family:var(--font-heading);letter-spacing:.05em">{{ hostName }} 已安全停止</strong>
<span style="opacity:.85">任务已安全停止,现场与检查点已保留 · 已写入不可变审计事件</span>
<button onClick="{{ resumeEstop }}" style="margin-left:auto;background:transparent;border:1px solid rgba(247,244,242,.55);color:#f7f4f2;padding:4px 12px;font-family:var(--font-heading);font-size:12px;cursor:pointer" style-hover="background:rgba(247,244,242,.14)">新建演示任务</button>
</div>
</sc-if>
<sc-if value="{{ vA }}" hint-placeholder-val="{{ true }}">
<div data-screen-label="实时运行 · 变体A 控制室" style="display:grid;grid-template-columns:300px 1fr 300px;gap:18px;align-items:start">
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:14px;gap:4px">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<h6 style="margin:0 0 8px;color:var(--color-neutral-700)">步骤执行器 · WORKFLOW</h6>
<div style="display:flex;gap:9px;align-items:center;padding:6px 0;font-size:12.5px"><span style="width:16px;height:16px;border:1px solid var(--color-accent-400);color:var(--color-accent-700);display:inline-flex;align-items:center;justify-content:center;font-size:10px"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11.5px">lab.precheck</span><span style="margin-left:auto;font-size:10px;color:var(--color-neutral-500)">09:12</span></div>
<div style="display:flex;gap:9px;align-items:center;padding:6px 0;font-size:12.5px"><span style="width:16px;height:16px;border:1px solid var(--color-accent-400);color:var(--color-accent-700);display:inline-flex;align-items:center;justify-content:center;font-size:10px"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11.5px">vendor.flash_firmware B</span><span style="margin-left:auto;font-size:10px;color:var(--color-neutral-500)">09:14</span></div>
<div style="display:flex;gap:9px;align-items:center;padding:6px 0;font-size:12.5px"><span style="width:16px;height:16px;border:1px solid var(--color-accent-400);color:var(--color-accent-700);display:inline-flex;align-items:center;justify-content:center;font-size:10px"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11.5px">host.reboot · 版本确认</span><span style="margin-left:auto;font-size:10px;color:var(--color-neutral-500)">09:18</span></div>
<div style="border:1px solid var(--color-divider);padding:8px 10px;margin:6px 0 2px">
<div style="display:flex;align-items:center;gap:8px;font-size:11px;color:var(--color-neutral-700);margin-bottom:6px"><span style="font-family:ui-monospace,Menlo,monospace">loop · repeat {{ target }}</span><span style="margin-left:auto;font-family:ui-monospace,Menlo,monospace;color:var(--color-accent-700)">cycle {{ cycle }}</span></div>
<div style="display:flex;gap:9px;align-items:center;padding:5px 0;font-size:12px"><span style="width:14px;height:14px;border:1px solid var(--color-accent-400);color:var(--color-accent-700);display:inline-flex;align-items:center;justify-content:center;font-size:9px"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11px">nvme.collect_baseline</span></div>
<div style="{{ ioRowStyle }}"><span style="{{ ioDotStyle }}"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11px;font-weight:700">io.run_profile 4k_mixed</span><span style="margin-left:auto;font-size:10px">{{ ioNote }}</span></div>
<div style="display:flex;gap:9px;align-items:center;padding:5px 0;font-size:12px;color:var(--color-neutral-500)"><span style="width:14px;height:14px;border:1px dashed var(--color-divider)"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11px">host.hibernate</span></div>
<div style="display:flex;gap:9px;align-items:center;padding:5px 0;font-size:12px;color:var(--color-neutral-500)"><span style="width:14px;height:14px;border:1px dashed var(--color-divider)"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11px">host.wait_resume</span></div>
<div style="display:flex;gap:9px;align-items:center;padding:5px 0;font-size:12px;color:var(--color-neutral-500)"><span style="width:14px;height:14px;border:1px dashed var(--color-divider)"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11px">dut.verify_enumeration</span></div>
<div style="display:flex;gap:9px;align-items:center;padding:5px 0;font-size:12px;color:var(--color-neutral-500)"><span style="width:14px;height:14px;border:1px dashed var(--color-divider)"></span><span style="font-family:ui-monospace,Menlo,monospace;font-size:11px">data.verify_hash</span></div>
</div>
<div style="border:1px dashed #b03d2e;padding:8px 10px;margin-top:6px">
<div style="font-size:10px;letter-spacing:.1em;color:#8a2e22;margin-bottom:5px">ON_FAILURE</div>
<div style="font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:var(--color-neutral-700);line-height:1.8">evidence.capture_all<br>recovery.escalate<br>failure.classify</div>
</div>
<div style="display:flex;gap:14px;font-size:10.5px;color:var(--color-neutral-600);margin-top:8px;font-family:ui-monospace,Menlo,monospace"><span>PASS 66</span><span>FAIL 0</span><span>自动恢复 1</span><span>介入 0</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:16px">
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:0;gap:0;overflow:hidden">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<div style="display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--color-divider)">
<h6 style="margin:0;color:var(--color-neutral-700)">实时日志流 · EVENT LOG</h6>
<span style="margin-left:auto;display:inline-flex;gap:6px;font-size:10px;font-family:ui-monospace,Menlo,monospace">
<span style="padding:2px 8px;background:var(--color-accent);color:#f2f2f3">ALL</span>
<span style="padding:2px 8px;border:1px solid var(--color-divider);color:var(--color-neutral-600)">CMD</span>
<span style="padding:2px 8px;border:1px solid var(--color-divider);color:var(--color-neutral-600)">OOB</span>
<span style="padding:2px 8px;border:1px solid var(--color-divider);color:var(--color-neutral-600)">WARN</span>
</span>
</div>
<div ref="{{ logRef }}" style="height:330px;overflow-y:auto;padding:10px 14px;background:var(--color-neutral-100);font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.75">
<sc-for list="{{ logs }}" as="ln" hint-placeholder-count="8">
<div style="animation:omIn .3s ease both;display:flex;gap:10px"><span style="color:var(--color-neutral-500);white-space:nowrap">{{ ln.t }}</span><span style="{{ ln.laneStyle }}">{{ ln.lane }}</span><span style="{{ ln.textStyle }}">{{ ln.text }}</span></div>
</sc-for>
</div>
</div>
<div style="display:grid;grid-template-columns:1.15fr 1fr;gap:16px">
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:12px;gap:8px">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<div style="display:flex;align-items:center"><h6 style="margin:0;color:var(--color-neutral-700)">HDMI 采集画面</h6><span style="margin-left:auto;font-size:10px;font-family:ui-monospace,Menlo,monospace;color:{{ hdmiColor }}">{{ hdmiNote }}</span></div>
<div style="height:186px;border:1px solid var(--color-divider)">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="livehdmi" shape="rect" hint-size="100%,186px" placeholder="HDMI 采集帧 — 拖入主机画面截图"></x-import>
</div>
<div style="font-size:10.5px;color:var(--color-neutral-600);font-family:ui-monospace,Menlo,monospace">采集卡 CAP-01 · 1080p@5fps · OS 未启动/蓝屏时仍可留证</div>
</div>
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:12px;gap:10px">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<h6 style="margin:0;color:var(--color-neutral-700)">遥测 · TELEMETRY</h6>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:11px">
<div style="border:1px solid var(--color-divider);padding:8px 10px"><div class="text-muted" style="font-size:10px;letter-spacing:.08em">HOST 心跳</div><div style="font-family:ui-monospace,Menlo,monospace;font-size:15px;font-weight:700;color:{{ hbColor }};margin-top:2px;animation:{{ hbAnim }}">{{ hbText }}</div></div>
<div style="border:1px solid var(--color-divider);padding:8px 10px"><div class="text-muted" style="font-size:10px;letter-spacing:.08em">DUT 温度</div><div style="font-family:ui-monospace,Menlo,monospace;font-size:15px;font-weight:700;margin-top:2px">{{ temp }}°C</div></div>
<div style="border:1px solid var(--color-divider);padding:8px 10px"><div class="text-muted" style="font-size:10px;letter-spacing:.08em">设备枚举</div><div style="font-family:ui-monospace,Menlo,monospace;font-size:12px;font-weight:700;color:var(--color-accent-700);margin-top:4px">nvme0n1 · Gen4 x4</div></div>
<div style="border:1px solid var(--color-divider);padding:8px 10px"><div class="text-muted" style="font-size:10px;letter-spacing:.08em">供电 · OOB</div><div style="font-family:ui-monospace,Menlo,monospace;font-size:12px;font-weight:700;margin-top:4px">AC ON · ATX ON</div></div>
</div>
<div>
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--color-neutral-600);margin-bottom:3px"><span>温度 30min</span><span>阈值 55°C</span></div>
<svg width="100%" height="36" viewBox="0 0 240 36" preserveAspectRatio="none"><line x1="0" y1="8" x2="240" y2="8" stroke="rgba(29,31,32,.14)" stroke-dasharray="3 3"></line><polyline stroke-linejoin="round" stroke-linecap="round" points="0,26 20,25 40,27 60,24 80,22 100,23 120,20 140,21 160,19 180,22 200,20 220,21 240,20" fill="none" stroke="#5980a6" stroke-width="1.5"></polyline></svg>
</div>
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:16px">
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:14px;gap:10px;border-color:{{ ladderBorder }}">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<div style="display:flex;align-items:center"><h6 style="margin:0;color:var(--color-neutral-700)">分级恢复阶梯 · ESCALATION</h6><span style="margin-left:auto;font-size:10px;font-family:ui-monospace,Menlo,monospace;color:var(--color-neutral-600)">esc-default-v2</span></div>
<div style="display:flex;flex-direction:column">
<sc-for list="{{ ladder }}" as="lv" hint-placeholder-count="5">
<div style="{{ lv.rowStyle }}">
<span style="{{ lv.numStyle }}">{{ lv.num }}</span>
<div style="flex:1">
<div style="font-size:12.5px;font-weight:500">{{ lv.name }}</div>
<div style="font-size:10.5px;color:var(--color-neutral-600)">{{ lv.desc }}</div>
</div>
<span style="{{ lv.stateStyle }}">{{ lv.state }}</span>
</div>
</sc-for>
</div>
<div style="font-size:10.5px;color:var(--color-neutral-600);border-top:1px solid var(--color-divider);padding-top:8px">恢复前先取证 · 多次失败进入安全停止,禁止无限重启</div>
</div>
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:14px;gap:8px">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<h6 style="margin:0;color:var(--color-neutral-700)">本轮关键指标 · CYCLE {{ cycle }}</h6>
<div style="display:flex;flex-direction:column;font-family:ui-monospace,Menlo,monospace;font-size:11px">
<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid rgba(29,31,32,.08)"><span style="color:var(--color-neutral-600)">4k_mixed IOPS</span><span>218,430</span></div>
<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid rgba(29,31,32,.08)"><span style="color:var(--color-neutral-600)">P99 延迟</span><span>412 µs</span></div>
<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid rgba(29,31,32,.08)"><span style="color:var(--color-neutral-600)">唤醒耗时</span><span>11.2 s</span></div>
<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid rgba(29,31,32,.08)"><span style="color:var(--color-neutral-600)">SMART 温度峰值</span><span>46°C</span></div>
<div style="display:flex;justify-content:space-between;padding:5px 0"><span style="color:var(--color-neutral-600)">校验哈希</span><span style="color:var(--color-accent-700)">一致 ✓</span></div>
</div>
<a href="Evidence.dc.html" style="font-size:11.5px">打开本任务证据 →</a>
</div>
</div>
</div>
</sc-if>
<sc-if value="{{ vB }}" hint-placeholder-val="{{ false }}">
<div data-screen-label="实时运行 · 变体B 时间线泳道">
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:14px;gap:10px;margin-bottom:16px">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<div style="display:flex;align-items:center"><h6 style="margin:0;color:var(--color-neutral-700)">执行时间线 · 14:20 → NOW</h6><span style="margin-left:auto;font-size:10.5px;font-family:ui-monospace,Menlo,monospace;color:var(--color-neutral-600)">每格 2 分钟 · cycle 6667</span></div>
<div style="display:grid;grid-template-columns:64px 1fr;row-gap:6px;font-family:ui-monospace,Menlo,monospace;font-size:10px">
<div style="color:var(--color-neutral-600);padding-top:5px">TEST</div>
<div style="position:relative;height:26px;border:1px solid var(--color-divider);background:repeating-linear-gradient(90deg,transparent 0 calc(100%/9 - 1px),rgba(29,31,32,.07) calc(100%/9 - 1px) calc(100%/9))">
<div style="position:absolute;top:4px;bottom:4px;left:2%;width:24%;background:var(--color-accent-200);border:1px solid var(--color-accent-400);color:var(--color-accent-800);display:flex;align-items:center;padding:0 6px;overflow:hidden;white-space:nowrap">io 4k_mixed</div>
<div style="position:absolute;top:4px;bottom:4px;left:27%;width:9%;background:var(--color-accent-200);border:1px solid var(--color-accent-400);color:var(--color-accent-800);display:flex;align-items:center;padding:0 6px;overflow:hidden;white-space:nowrap">hib</div>
<div style="position:absolute;top:4px;bottom:4px;left:37%;width:7%;background:var(--color-accent-200);border:1px solid var(--color-accent-400);color:var(--color-accent-800);display:flex;align-items:center;padding:0 6px;overflow:hidden;white-space:nowrap">res</div>
<div style="position:absolute;top:4px;bottom:4px;left:45%;width:6%;background:var(--color-accent-200);border:1px solid var(--color-accent-400);color:var(--color-accent-800);display:flex;align-items:center;padding:0 4px;overflow:hidden;white-space:nowrap">枚举</div>
<div style="position:absolute;top:4px;bottom:4px;left:52%;width:10%;background:var(--color-accent-200);border:1px solid var(--color-accent-400);color:var(--color-accent-800);display:flex;align-items:center;padding:0 6px;overflow:hidden;white-space:nowrap">verify</div>
<div style="position:absolute;top:4px;bottom:4px;left:64%;width:26%;background-color:var(--color-accent);background-image:repeating-linear-gradient(90deg,rgba(242,242,243,.3) 0 6px,transparent 6px 14px);animation:omScan 1.1s linear infinite;border:1px solid var(--color-accent-700);color:#f2f2f3;display:flex;align-items:center;padding:0 6px;overflow:hidden;white-space:nowrap;animation:omPulse 2s infinite">cycle 67 · io 4k_mixed RUNNING</div>
</div>
<div style="color:var(--color-neutral-600);padding-top:5px">HOST</div>
<div style="position:relative;height:26px;border:1px solid var(--color-divider)">
<div style="position:absolute;top:4px;bottom:4px;left:2%;width:24%;border:1px solid var(--color-divider);color:var(--color-neutral-700);display:flex;align-items:center;padding:0 6px;white-space:nowrap;overflow:hidden">ONLINE · hb 1-3s</div>
<div style="position:absolute;top:4px;bottom:4px;left:27%;width:17%;background:repeating-linear-gradient(135deg,rgba(29,31,32,.14) 0 2px,transparent 2px 5px);border:1px solid var(--color-divider);color:var(--color-neutral-700);display:flex;align-items:center;padding:0 6px;white-space:nowrap;overflow:hidden">S4 休眠</div>
<div style="position:absolute;top:4px;bottom:4px;left:45%;width:45%;border:1px solid var(--color-divider);color:var(--color-neutral-700);display:flex;align-items:center;padding:0 6px;white-space:nowrap;overflow:hidden">ONLINE · resume 11.2s</div>
</div>
<div style="color:var(--color-neutral-600);padding-top:5px">DUT</div>
<div style="position:relative;height:26px;border:1px solid var(--color-divider)">
<div style="position:absolute;top:4px;bottom:4px;left:2%;width:42%;border:1px solid var(--color-accent-400);color:var(--color-accent-800);display:flex;align-items:center;padding:0 6px;white-space:nowrap;overflow:hidden">nvme0n1 Gen4 x4 · 42-46°C</div>
<div style="position:absolute;top:4px;bottom:4px;left:45%;width:3%;background:var(--color-accent-800)"></div>
<div style="position:absolute;top:4px;bottom:4px;left:49%;width:41%;border:1px solid var(--color-accent-400);color:var(--color-accent-800);display:flex;align-items:center;padding:0 6px;white-space:nowrap;overflow:hidden">重新枚举 1.8s · SMART 正常</div>
</div>
<div style="color:var(--color-neutral-600);padding-top:5px">OOB</div>
<div style="position:relative;height:26px;border:1px solid var(--color-divider)">
<div style="position:absolute;top:4px;bottom:4px;left:2%;width:88%;border:1px dashed var(--color-divider);color:var(--color-neutral-500);display:flex;align-items:center;padding:0 6px;white-space:nowrap;overflow:hidden">待命 · 心跳监测中 · 无供电动作</div>
</div>
</div>
<div style="position:relative;height:0"><div style="position:absolute;right:9.5%;top:-118px;width:1px;height:118px;background:#b03d2e;animation:omBlink 1.2s infinite"></div></div>
<div style="display:flex;gap:14px;font-size:10px;color:var(--color-neutral-600)"><span><span style="display:inline-block;width:10px;height:8px;background:var(--color-accent-200);border:1px solid var(--color-accent-400);vertical-align:-1px"></span> 测试步骤</span><span><span style="display:inline-block;width:10px;height:8px;background:repeating-linear-gradient(135deg,rgba(29,31,32,.14) 0 2px,transparent 2px 5px);border:1px solid var(--color-divider);vertical-align:-1px"></span> 电源状态</span><span><span style="display:inline-block;width:10px;height:8px;background:var(--color-accent-800);vertical-align:-1px"></span> 链路事件</span><span style="color:#8a2e22">│ 播放头 NOW</span></div>
</div>
<div style="display:grid;grid-template-columns:1fr 300px;gap:16px;align-items:start">
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:0;gap:0">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<div style="padding:10px 14px;border-bottom:1px solid var(--color-divider)"><h6 style="margin:0;color:var(--color-neutral-700)">日志尾部 · TAIL</h6></div>
<div ref="{{ logRefB }}" style="height:260px;overflow-y:auto;padding:10px 14px;background:var(--color-neutral-100);font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.75">
<sc-for list="{{ logs }}" as="ln" hint-placeholder-count="8">
<div style="animation:omIn .3s ease both;display:flex;gap:10px"><span style="color:var(--color-neutral-500);white-space:nowrap">{{ ln.t }}</span><span style="{{ ln.laneStyle }}">{{ ln.lane }}</span><span style="{{ ln.textStyle }}">{{ ln.text }}</span></div>
</sc-for>
</div>
</div>
<div class="card blueprint" style-hover="border-color:var(--color-accent-400);box-shadow:var(--shadow-sm)" style="transition:border-color .15s ease,box-shadow .15s ease;padding:14px;gap:10px;border-color:{{ ladderBorder }}">
<i class="corner tl"></i><i class="corner tr"></i><i class="corner bl"></i><i class="corner br"></i>
<h6 style="margin:0;color:var(--color-neutral-700)">恢复阶梯</h6>
<div style="display:flex;flex-direction:column">
<sc-for list="{{ ladder }}" as="lv" hint-placeholder-count="5">
<div style="{{ lv.rowStyle }}">
<span style="{{ lv.numStyle }}">{{ lv.num }}</span>
<div style="flex:1"><div style="font-size:12px;font-weight:500">{{ lv.name }}</div></div>
<span style="{{ lv.stateStyle }}">{{ lv.state }}</span>
</div>
</sc-for>
</div>
</div>
</div>
</div>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1440,&quot;height&quot;:950},&quot;defaultVariant&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;a&quot;,&quot;b&quot;],&quot;default&quot;:&quot;a&quot;,&quot;tsType&quot;:&quot;string&quot;,&quot;section&quot;:&quot;布局&quot;}}">
class Component extends DCLogic {
state = {
variant: null, phase: 'running', runState: 'IDLE', estopped: false, demoBusy: false,
runId: '', hostName: 'WS-05', dutLabel: 'NV-E1T92', firmwareLabel: '候选固件',
startText: '尚未开始', checkpointText: '尚无', cycle: 0, target: 8, progress: 0, hbAge: 2,
logs: [
{ t: '14:20:44', lane: 'SYS', tone: 'mut', text: 'cycle 66 完成 · 检查点已写入 (state_hash 7c1f…9d02)' },
{ t: '14:21:52', lane: 'TEST', tone: 'ok', text: 'cycle 67 开始 · nvme.collect_baseline PASS' },
{ t: '14:22:10', lane: 'CMD', tone: 'mut', text: 'fio --profile 4k_mixed --runtime 300 --dut /dev/nvme0n1' },
{ t: '14:25:03', lane: 'TEST', tone: 'ok', text: 'hibernate → resume 成功 · 唤醒 11.2s · 唤醒源 RTC' },
{ t: '14:25:11', lane: 'DUT', tone: 'ok', text: 'verify_enumeration PASS · nvme0n1 Gen4 x4 · 1.8s' },
{ t: '14:28:51', lane: 'TEST', tone: 'ok', text: 'data.verify_hash PASS · 128 files · sha256 一致' },
{ t: '14:29:02', lane: 'CMD', tone: 'mut', text: 'io.run_profile 4k_mixed 启动 · duration 300s · QD32' },
{ t: '14:30:15', lane: 'HB', tone: 'mut', text: 'Agent 心跳正常 · CPU 34% · MEM 41% · 磁盘余量 812G' }
]
};
_log(lane, tone, text) {
const t = new Date().toLocaleTimeString('zh-CN', { hour12: false });
this.setState(s => ({ logs: [...s.logs, { t, lane, tone, text }] }));
}
_scroll() {
for (const r of [this._ref, this._refB]) if (r && r.current) r.current.scrollTop = r.current.scrollHeight;
}
componentDidMount() {
this._ref = React.createRef(); this._refB = React.createRef();
this.forceUpdate();
this._syncRun();
this._poll = setInterval(() => this._syncRun(), 650);
this._hb = setInterval(() => {
if (this.state.phase === 'running' && !this.state.estopped) this.setState({ hbAge: (this.state.hbAge % 4) + 1 });
}, 2000);
}
componentWillUnmount() { clearInterval(this._hb); clearInterval(this._poll); }
componentDidUpdate() { this._scroll(); }
_lane(event) {
if (event.kind.includes('RECOVERY')) return 'RCVR';
if (event.kind.includes('HEARTBEAT')) return 'HB';
if (event.source === 'oob') return 'OOB';
if (event.kind.includes('EVIDENCE')) return 'EVID';
if (event.kind.includes('LOOP')) return 'TEST';
if (event.kind.includes('DUT')) return 'DUT';
return 'SYS';
}
_tone(event) {
if (event.severity === 'CRITICAL' || event.severity === 'ERROR') return 'bad';
if (event.severity === 'WARNING') return 'warn';
if (event.kind.includes('SUCCEEDED') || event.kind.includes('COMPLETED') || event.kind === 'SAFETY_APPROVED') return 'ok';
return 'mut';
}
_phase(run, events) {
if (run.state !== 'RECOVERING') return run.state === 'COMPLETED' ? 'completed' : 'running';
const latest = [...events].reverse().find(e => e.kind === 'RECOVERY_STARTED');
const level = latest && latest.payload ? latest.payload.level : '';
if (level.startsWith('L3')) return 'l3';
if (level.startsWith('L2')) return 'l2';
if (level.startsWith('L1')) return 'l1';
return 'lost';
}
async _syncRun() {
const runId = this.state.runId || localStorage.getItem('flashops.activeRunId');
if (!runId) return;
try {
const [runResponse, eventResponse] = await Promise.all([
fetch('/api/v1/runs/' + encodeURIComponent(runId)),
fetch('/api/v1/runs/' + encodeURIComponent(runId) + '/events')
]);
if (!runResponse.ok || !eventResponse.ok) return;
const run = await runResponse.json();
const events = await eventResponse.json();
const logs = events.map(event => ({
t: new Date(event.ts).toLocaleTimeString('zh-CN', { hour12: false }),
lane: this._lane(event),
tone: this._tone(event),
text: '#' + event.seq + ' · ' + event.message
}));
this.setState({
runId: run.id,
hostName: (run.environment && run.environment.host) || 'WS-05',
dutLabel: run.environment ? ((run.environment.dut_serial || '') + ' · ' + (run.environment.dut_model || '')).trim() : '未绑定',
firmwareLabel: run.firmware_b_version ? ('FW_' + run.firmware_b_version + ' (B) 候选') : '候选固件',
startText: run.started_at ? new Date(run.started_at).toLocaleTimeString('zh-CN', { hour12: false }) : '等待调度',
checkpointText: run.checkpoint && run.checkpoint.loop_index ? ('cycle ' + run.checkpoint.loop_index) : '尚无',
runState: run.state,
cycle: run.loop_done,
target: run.loop_target,
progress: run.progress,
phase: this._phase(run, events),
estopped: run.state === 'ABORTED',
demoBusy: !['COMPLETED', 'ABORTED', 'FROZEN', 'REJECTED'].includes(run.state),
logs
});
} catch (_) {}
}
async startDemo() {
if (this.state.demoBusy || this.state.estopped) return;
this.setState({ demoBusy: true, estopped: false, logs: [] });
try {
const response = await fetch('/api/v1/runs/demo', { method: 'POST' });
if (!response.ok) throw new Error('创建演示任务失败');
const run = await response.json();
localStorage.setItem('flashops.activeRunId', run.id);
this.setState({ runId: run.id, cycle: 0, target: run.loop_target, progress: 0 });
this._syncRun();
} catch (error) {
this.setState({ demoBusy: false });
this._log('SYS', 'bad', error.message || '控制平面不可用');
}
}
async emergencyStop() {
if (!this.state.runId || this.state.estopped) return;
try {
await fetch('/api/v1/runs/' + encodeURIComponent(this.state.runId) + '/emergency-stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actor: 'console-user', reason: '控制台紧急停止' })
});
this._syncRun();
} catch (_) {}
}
async togglePause() {
if (!this.state.runId || !['RUNNING', 'PAUSED'].includes(this.state.runState)) return;
const action = this.state.runState === 'PAUSED' ? 'resume' : 'pause';
try {
await fetch('/api/v1/runs/' + encodeURIComponent(this.state.runId) + '/' + action, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actor: 'console-user', reason: action === 'pause' ? '控制台暂停' : '控制台继续' })
});
this._syncRun();
} catch (_) {}
}
renderVals() {
const s = this.state;
const mono = { fontFamily: 'ui-monospace,Menlo,monospace' };
const laneColors = { HB: '#8a2e22', OOB: 'var(--color-accent-800)', RCVR: '#8a2e22', TEST: 'var(--color-accent-700)', CMD: 'var(--color-neutral-600)', SYS: 'var(--color-neutral-600)', EVID: 'var(--color-neutral-600)', DUT: 'var(--color-accent-800)' };
const toneColors = { ok: 'var(--color-accent-800)', bad: '#8a2e22', warn: '#8a2e22', mut: 'var(--color-neutral-700)', dut: 'var(--color-accent-800)' };
const logs = s.logs.map(l => ({
...l,
laneStyle: { color: laneColors[l.lane] || 'var(--color-neutral-600)', whiteSpace: 'nowrap', minWidth: '38px' },
textStyle: { color: toneColors[l.tone] || 'var(--color-text)' }
}));
const recovering = ['lost', 'l1', 'l2', 'l3', 'online'].includes(s.phase);
const phaseIdx = { running: -1, lost: 0, l1: 0, l2: 1, l3: 2, online: 2 }[s.phase];
const results = {
running: [], lost: ['active'], l1: ['active'], l2: ['fail', 'active'], l3: ['fail', 'fail', 'active'], online: ['fail', 'fail', 'ok']
}[s.phase] || [];
const defs = [
{ num: 'L1', name: 'Agent 软恢复', desc: '终止异常进程 · 清理资源' },
{ num: 'L2', name: 'OS 通道重启', desc: 'SSH / WinRM 远程软重启' },
{ num: 'L3', name: '带外 Reset', desc: '光耦短按主板 Reset' },
{ num: 'L4', name: 'AC 电源循环', desc: '整机断电 · 安全间隔后上电' },
{ num: 'L5', name: '安全停止', desc: '保留现场 · 禁止无限重启' }
];
const ladder = defs.map((d, i) => {
const r = results[i];
let state = '待命', stateStyle = { ...mono, fontSize: '10px', color: 'var(--color-neutral-500)' };
let rowStyle = { display: 'flex', gap: '10px', alignItems: 'center', padding: '8px 6px', borderBottom: i < 4 ? '1px solid rgba(29,31,32,.08)' : 'none', transition: 'background .25s ease' };
let numStyle = { ...mono, width: '26px', height: '22px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '10.5px', border: '1px dashed var(--color-divider)', color: 'var(--color-neutral-500)', transition: 'background .25s ease, border-color .25s ease, color .25s ease' };
if (r === 'active') { state = '执行中'; stateStyle = { ...mono, fontSize: '10px', color: 'var(--color-accent-700)', animation: 'omPulse 1.2s infinite' }; numStyle = { ...numStyle, border: 'none', background: 'var(--color-accent-800)', color: '#f2f2f3' }; rowStyle = { ...rowStyle, background: 'var(--color-accent-100)' }; }
if (r === 'fail') { state = '失败'; stateStyle = { ...mono, fontSize: '10px', color: '#8a2e22' }; numStyle = { ...numStyle, border: '1px solid #b03d2e', color: '#8a2e22' }; }
if (r === 'ok') { state = '成功'; stateStyle = { ...mono, fontSize: '10px', color: 'var(--color-accent-700)' }; numStyle = { ...numStyle, border: 'none', background: 'var(--color-accent)', color: '#f2f2f3' }; }
return { ...d, state, stateStyle, rowStyle, numStyle };
});
let badgeText = 'RUNNING', badgeStyle = { display: 'inline-flex', alignItems: 'center', gap: '6px', background: 'var(--color-accent)', color: '#f2f2f3', fontSize: '11px', letterSpacing: '.06em', padding: '5px 12px', fontFamily: 'var(--font-heading)', fontWeight: 600, transition: 'background .25s ease, color .25s ease' };
if (recovering) { badgeText = 'RECOVERING'; badgeStyle = { ...badgeStyle, background: 'var(--color-accent-200)', color: 'var(--color-accent-800)', animation: 'omPulse 1.2s infinite' }; }
if (s.runState === 'QUEUED' || s.runState === 'PREFLIGHT') { badgeText = s.runState; badgeStyle = { ...badgeStyle, background: 'var(--color-neutral-600)' }; }
if (s.runState === 'COMPLETED') { badgeText = 'COMPLETED · PASS'; badgeStyle = { ...badgeStyle, background: 'var(--color-accent-700)' }; }
if (s.runState === 'REJECTED' || s.runState === 'FROZEN') { badgeText = s.runState; badgeStyle = { ...badgeStyle, background: '#b03d2e' }; }
if (s.estopped) { badgeText = 'EMERGENCY_STOP'; badgeStyle = { ...badgeStyle, background: '#b03d2e' }; }
const hbLost = recovering && s.phase !== 'online';
const vv = s.variant ?? (this.props.defaultVariant ?? 'a');
return {
vA: vv === 'a', vB: vv === 'b',
setA: () => this.setState({ variant: 'a' }), setB: () => this.setState({ variant: 'b' }),
startDemo: () => this.startDemo(), demoBusy: s.demoBusy,
togglePause: () => this.togglePause(),
pauseLabel: s.runState === 'PAUSED' ? '继续' : '暂停',
pauseDisabled: !['RUNNING', 'PAUSED'].includes(s.runState),
stopDisabled: !['QUEUED', 'PREFLIGHT', 'RUNNING', 'RECOVERING', 'PAUSED'].includes(s.runState),
estop: () => this.emergencyStop(),
resumeEstop: () => { this.setState({ estopped: false, demoBusy: false, runId: '' }, () => this.startDemo()); },
estopped: s.estopped,
runId: s.runId || '尚未创建真实任务',
hostName: s.hostName,
dutLabel: s.dutLabel,
firmwareLabel: s.firmwareLabel,
startText: s.startText,
checkpointText: s.checkpointText,
cycle: s.cycle,
target: s.target,
progStyle: { position: 'absolute', inset: 0, width: s.progress + '%', transition: 'width .4s ease', backgroundColor: s.estopped ? '#b03d2e' : 'var(--color-accent)', backgroundImage: s.estopped ? 'none' : 'repeating-linear-gradient(90deg,rgba(242,242,243,.3) 0 6px,transparent 6px 14px)', animation: s.estopped ? 'none' : 'omScan 1.1s linear infinite' },
badgeText, badgeStyle,
logs, logRef: this._ref, logRefB: this._refB, ladder,
ladderBorder: recovering ? 'var(--color-accent-600)' : 'var(--color-divider)',
hbText: s.estopped ? '已冻结' : (hbLost ? '丢失 ' + (s.phase === 'lost' ? '34s' : '2m+') : s.hbAge + 's 前'),
hbColor: hbLost ? '#8a2e22' : 'var(--color-accent-700)',
hbAnim: hbLost ? 'omPulse 1.2s infinite' : 'none',
temp: '43.' + ((s.hbAge * 3) % 9),
hdmiNote: hbLost ? '死机帧已冻结留证' : 'LIVE · 5fps',
hdmiColor: hbLost ? '#8a2e22' : 'var(--color-accent-700)',
ioRowStyle: { display: 'flex', gap: '9px', alignItems: 'center', padding: '5px 0', fontSize: '12px', color: recovering ? 'var(--color-neutral-500)' : 'var(--color-text)' },
ioDotStyle: { width: '14px', height: '14px', background: recovering ? 'transparent' : 'var(--color-accent)', border: recovering ? '1px dashed var(--color-divider)' : 'none', animation: recovering ? 'none' : 'omPulse 1.6s infinite' },
ioNote: recovering ? '冻结,等待恢复' : '178s / 300s'
};
}
}
</script>
</body>
</html>