chore(repo): initialize team collaboration repository
CI / Python 3.12 (push) Waiting to run
CI / Python 3.9 (push) Waiting to run

This commit is contained in:
2026-07-27 20:40:12 +08:00
commit c91a64fddb
109 changed files with 21121 additions and 0 deletions
+450
View File
@@ -0,0 +1,450 @@
<!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>