rack:主板 / 底板功率与功率曲线;底板曲线加电压电流、开机重按事件

- 每路主板功率(V×I):总览槽位、单板卡片、单路曲线新增「功率」一张
- 底板:单板页新增功率仪表(底板输入 / 五路主板合计 / 损耗),底板曲线加
  功率图(输入 vs 主板合计);board_history 每点多回一列 rails_mw
- 底板曲线改为纵向 N 张(温度 / 风扇 / 输入电压 / 输入电流 / 功率),断档断线
- LOG_EV_BOOT_CHECK(19)事件命名,只有「没开起来」标红

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-28 08:39:47 +08:00
共同撰写人 Claude Opus 5.5
父节点 1f6862ac38
当前提交 830b4cdff8
共修改 14 个文件,包含 1288 行新增和 1122 行删除
+13 -4
查看文件
@@ -35,6 +35,7 @@
<div class="bl" :title="phone(b, i).label">{{ phone(b, i).label || ' ' }}</div>
<template v-if="rail(b, i)?.pr">
<div class="bv readout">{{ mA(rail(b, i)?.ua) }}<small>mA</small></div>
<div class="bw readout">{{ W(rail(b, i)?.mv, rail(b, i)?.ua) }}<small>W</small></div>
<Spark :board="b.id" :rail="i" :color="loud(b, i) ? LEVEL_COLOR[health(b, i).level] : 'var(--ink-2)'" />
</template>
<div class="bs" :style="{ color: loud(b, i) ? LEVEL_COLOR[health(b, i).level] : 'var(--muted)' }">{{ health(b, i).text }}</div>
@@ -44,6 +45,8 @@
<footer class="pf" v-if="b.status">
<div><span class="k">输入</span><span class="readout">{{ V(b.status.in?.mv) }}</span> V
<span class="readout">{{ A(b.status.in?.ua) }}</span> A</div>
<div><span class="k">底板功率</span><span class="readout">{{ W(b.status.in?.mv, b.status.in?.ua) }}</span> W
<span class="k sub">主板</span><span class="readout">{{ railsW(b) }}</span> W</div>
<div><span class="k">温度</span>
<span v-for="(t, k) in b.status.temp || []" :key="k" class="readout t">{{ t == null ? '—' : C(t) }}</span> °C</div>
<div><span class="k">风扇</span><span class="readout">{{ (b.status.fan?.duty ?? 0) / 10 }}</span> %
@@ -62,7 +65,7 @@
<time class="muted readout">{{ fmtTime(f.ts) }}</time>
<span class="who">{{ boardName(f.board) }}</span>
<template v-if="f.kind === 'event'">
<span :class="{ bad: SEVERE.has(f.ev[3]) }">{{ f.ev[2] ? `#${f.ev[2]} ` : '' }}{{ evName(f.ev[3]) }}</span>
<span :class="{ bad: isSevere(f.ev) }">{{ f.ev[2] ? `#${f.ev[2]} ` : '' }}{{ evName(f.ev[3]) }}</span>
</template>
<template v-else-if="f.kind === 'audit'">
<span>{{ f.row.user }} 对 #{{ f.row.rail ?? '' }} 执行 {{ f.row.request.cmd }}:{{ rspText(f.row.response) }}</span>
@@ -82,16 +85,19 @@ import { Refresh } from '@element-plus/icons-vue'
import Spark from '../components/Spark.vue'
import { state } from '../store'
import { rspText } from '../api'
import { mA, V, C, ago, fmtTime, railHealth, LEVEL_COLOR, RECOVERY_ZH, REASON_ZH } from '../util'
import { mA, V, W, C, ago, fmtTime, railsMw, railHealth, LEVEL_COLOR, RECOVERY_ZH, REASON_ZH } from '../util'
// 与 backend/prrack/protocol.py 的 EVENT_TYPES / EVENT_SEVERE 同一张表(数字来自固件 evlog.h)
const EV = ['', '板子上电', '事件丢失', '过压关路', '欠压关路', '反灌断 VBUS', '顶高断 VBUS', 'eFuse 故障',
'PG 变化', 'I²C 失联', '过温降载', '风扇停转', '重启序列', '命令被拒', 'mux 失联全断', '刀片不在位', 'PD 限功率',
'上电等不到 PG', '固件升级']
'上电等不到 PG', '固件升级', '开机重按']
const SEVERE = new Set([3, 4, 5, 6, 7, 9, 11, 14, 17])
// 开机重按(19)只有 arg bit7「最终没开起来」才算严重
const isSevere = ev => SEVERE.has(ev[3]) || (ev[3] === 19 && (ev[4] & 0x80) !== 0)
const evName = t => EV[t] || `事件 ${t}`
const A = ua => (ua == null ? '—' : (ua / 1e6).toFixed(2))
const railsW = b => { const s = railsMw(b.status?.rails); return s == null ? '—' : (s / 1000).toFixed(2) }
const boards = computed(() => Object.values(state.boards).sort((a, b) =>
(a.location + a.name + a.id).localeCompare(b.location + b.name + b.id)))
const rail = (b, i) => b.status?.rails?.[i - 1]
@@ -152,12 +158,15 @@ const tally = computed(() => {
.bl { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bv { font-size: 24px; line-height: 1.1; color: var(--ink); margin-top: 2px; }
.bv small { font-size: 11px; color: var(--muted); margin-left: 2px; font-family: var(--ui); }
.bw { font-size: 13px; color: var(--ink-2); line-height: 1.2; }
.bw small { font-size: 10px; color: var(--muted); margin-left: 2px; font-family: var(--ui); }
.bs { font-size: 12px; font-weight: 600; margin-top: auto; }
.bay.off .bv, .bay.lost .bv { color: var(--muted); }
.bay.off .bv, .bay.lost .bv, .bay.off .bw, .bay.lost .bw { color: var(--muted); }
.pf { display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 13px; color: var(--ink-2);
border-top: 1px solid var(--line-soft); padding-top: 10px; }
.pf .k { color: var(--muted); margin-right: 6px; }
.pf .k.sub { margin-left: 6px; }
.pf .readout { font-size: 15px; color: var(--ink); }
.pf .t + .t::before { content: '/'; margin: 0 4px; color: var(--muted); }