Fix vpnv6 ECMP under-parse and persist full CLI line counts.

RR vpnv6 wrap lines with next-hop+metrics were collapsing ECMP legs; store raw_line_count/declared_total before DB truncate so collect stats stay accurate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-23 14:53:19 +08:00
parent 86fcb98218
commit ac53af4a5d
16 changed files with 531 additions and 35 deletions

View file

@ -285,7 +285,8 @@ const en = {
batchId: "Batch ID",
copyBatchId: "Copy batch ID",
batchMessageEmpty: "No message or error",
batchCmdSummary: "{{ok}} ok / {{fail}} failed / {{other}} other · {{total}} commands",
batchCmdSummary:
"{{ok}} ok / {{fail}} failed / {{skipped}} skipped / {{other}} other · {{total}} counted (batch {{batchCmds}} cmds)",
sheetCommands: "Commands",
sheetLldp: "LLDP neighbors",
sheetVrfRoute: "VRF route summary",
@ -305,6 +306,7 @@ const en = {
rawLogTitle: "Raw collect log",
rawLogEmpty: "No raw output",
rawLogStats: "{{lines}} lines collected · {{rows}} rows stored",
rawLogDeclared: "declared {{declared}}",
openWorkbook: "Open workbook",
filterColumn: "Column",
filterAllCols: "All columns",
@ -322,6 +324,7 @@ const en = {
colTime: "Time",
colRows: "DB rows",
colRawLines: "CLI lines",
colDeclared: "Declared",
colActions: "Actions",
colCommand: "Command",
colMessage: "Message",

View file

@ -285,7 +285,8 @@ const zh = {
batchId: "批次 ID",
copyBatchId: "复制批次 ID",
batchMessageEmpty: "无额外说明或报错",
batchCmdSummary: "命令 {{ok}} 成功 / {{fail}} 失败 / {{other}} 其他 · 共 {{total}} 条",
batchCmdSummary:
"命令 {{ok}} 成功 / {{fail}} 失败 / {{skipped}} 跳过 / {{other}} 其他 · 统计 {{total}} 条(批次计数 {{batchCmds}})",
sheetCommands: "命令",
sheetLldp: "LLDP 邻居",
sheetVrfRoute: "VRF 路由摘要",
@ -305,6 +306,7 @@ const zh = {
rawLogTitle: "原始采集日志",
rawLogEmpty: "无原始输出",
rawLogStats: "采集 {{lines}} 行 · 入库 {{rows}} 条",
rawLogDeclared: "声明 {{declared}}",
openWorkbook: "打开工作簿",
filterColumn: "筛选列",
filterAllCols: "全部列",
@ -322,6 +324,7 @@ const zh = {
colTime: "时间",
colRows: "入库条数",
colRawLines: "采集行数",
colDeclared: "设备声明",
colActions: "操作",
colCommand: "命令",
colMessage: "说明",

View file

@ -10181,6 +10181,20 @@ html.login-page--paused .login-page__flare {
.bs-collect-detail-modal .bs-sheet-table {
max-height: min(48vh, 480px);
overflow: auto;
}
.bs-collect-detail-modal .bs-collect-detail-table table {
border-collapse: separate;
border-spacing: 0;
}
.bs-collect-detail-modal .bs-collect-detail-table thead th {
position: sticky;
top: 0;
z-index: 3;
background: rgba(15, 23, 42, 0.97);
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.25);
}
.bs-rawlog-pre {

View file

@ -111,6 +111,7 @@ type SheetCmd = {
parse_status?: string;
row_count?: number;
raw_line_count?: number;
declared_total?: number;
message?: string;
has_raw?: boolean;
profile_id?: string;
@ -162,6 +163,20 @@ function intervalUnitMax(unit: "days" | "hours" | "seconds") {
return 604800; // up to 7 days in seconds
}
function formatCmdCollectStats(
t: (key: string, vars?: Record<string, unknown>) => string,
lines: number,
rows: number,
declared?: number,
) {
const base = t("bizState.rawLogStats", { lines, rows });
const d = Number(declared || 0);
if (d > 0) {
return `${base} · ${t("bizState.rawLogDeclared", { declared: d })}`;
}
return base;
}
function convertIntervalValue(
value: number,
from: "days" | "hours" | "seconds",
@ -311,6 +326,7 @@ export function BizStatePage() {
const [rawLogCommandId, setRawLogCommandId] = useState("");
const [rawLogLines, setRawLogLines] = useState(0);
const [rawLogRows, setRawLogRows] = useState(0);
const [rawLogDeclared, setRawLogDeclared] = useState(0);
const [rawLogMessage, setRawLogMessage] = useState("");
/** Collect status / errors detail (lighter than workbook). */
const [collectDetail, setCollectDetail] = useState<any>(null);
@ -550,17 +566,33 @@ export function BizStatePage() {
}, [activeSheet, batchDetail, debouncedSheetKw, sheetColumn, sheetTotal]);
const collectDetailCmdSummary = useMemo(() => {
const stats = collectDetail?.command_stats as
| { ok?: number; failed?: number; skipped?: number; other?: number; total?: number; aux_failed?: number }
| undefined;
if (stats && typeof stats.total === "number") {
return {
ok: Number(stats.ok || 0),
fail: Number(stats.failed || 0),
skipped: Number(stats.skipped || 0),
other: Number(stats.other || 0),
total: Number(stats.total || 0),
auxFailed: Number(stats.aux_failed || 0),
};
}
const cmds = (collectDetail?.commands || []) as SheetCmd[];
let ok = 0;
let fail = 0;
let skipped = 0;
let other = 0;
for (const c of cmds) {
const st = String(c.parse_status || "").toLowerCase();
if (st === "ok" || st === "success" || st === "aux" || st === "aux_ok" || st === "aux_cached") ok += 1;
else if (st === "failed" || st === "error" || st === "fail") fail += 1;
else if (st === "failed" || st === "error" || st === "fail" || st === "aux_failed" || st.endsWith("_failed"))
fail += 1;
else if (st.startsWith("skipped")) skipped += 1;
else other += 1;
}
return { ok, fail, other, total: cmds.length };
return { ok, fail, skipped, other, total: cmds.length, auxFailed: 0 };
}, [collectDetail]);
const loadSheetPage = useCallback(
@ -1192,6 +1224,7 @@ export function BizStatePage() {
setRawLogCommandId(commandId);
setRawLogLines(0);
setRawLogRows(0);
setRawLogDeclared(0);
setRawLogMessage("");
try {
const d = await bizStateGetBatchCommand(bid, commandId);
@ -1199,13 +1232,15 @@ export function BizStatePage() {
setRawLogText(String(d.raw_text || ""));
const lines = Number(d.raw_line_count ?? 0);
const rows = Number(d.row_count ?? 0);
const declared = Number(d.declared_total ?? 0);
setRawLogLines(lines);
setRawLogRows(rows);
setRawLogDeclared(declared);
setRawLogMessage(String(d.message || ""));
const bits = [
d.parse_status,
d.metric_id,
t("bizState.rawLogStats", { lines, rows }),
formatCmdCollectStats(t, lines, rows, declared),
d.collected_at ? fmtTime(d.collected_at) : "",
].filter(Boolean);
setRawLogMeta(bits.join(" · "));
@ -2336,10 +2371,12 @@ export function BizStatePage() {
{t("bizState.exportRawLog")}
</Button>
<span className="muted bs-cmd-stat">
{t("bizState.rawLogStats", {
lines: Number(c.raw_line_count ?? 0),
rows: Number(c.row_count ?? 0),
})}
{formatCmdCollectStats(
t,
Number(c.raw_line_count ?? 0),
Number(c.row_count ?? 0),
Number(c.declared_total ?? 0),
)}
</span>
{c.parse_status ? (
<NmStatusChip color={jobChipColor(String(c.parse_status))}>
@ -2383,10 +2420,12 @@ export function BizStatePage() {
{t("bizState.exportRawLog")}
</Button>
<span className="muted bs-cmd-stat">
{t("bizState.rawLogStats", {
lines: Number(row.raw_line_count ?? 0),
rows: Number(row.row_count ?? 0),
})}
{formatCmdCollectStats(
t,
Number(row.raw_line_count ?? 0),
Number(row.row_count ?? 0),
Number(row.declared_total ?? 0),
)}
</span>
{cellText(row.parse_status) ? (
<NmStatusChip color={jobChipColor(cellText(row.parse_status))}>
@ -2573,7 +2612,7 @@ export function BizStatePage() {
{rawLogCmd ? <code className="bs-sheet-cmd-code bs-sheet-cmd-code--block">{rawLogCmd}</code> : null}
<div className="bs-rawlog-stats">
<NmStatusChip color="accent">
{t("bizState.rawLogStats", { lines: rawLogLines, rows: rawLogRows })}
{formatCmdCollectStats(t, rawLogLines, rawLogRows, rawLogDeclared)}
</NmStatusChip>
{rawLogMeta ? <span className="muted">{rawLogMeta}</span> : null}
</div>
@ -2655,11 +2694,13 @@ export function BizStatePage() {
{t("bizState.batchCmdSummary", {
ok: collectDetailCmdSummary.ok,
fail: collectDetailCmdSummary.fail,
skipped: collectDetailCmdSummary.skipped,
other: collectDetailCmdSummary.other,
total: collectDetailCmdSummary.total,
batchCmds: Number(collectDetail.command_count ?? collectDetailCmdSummary.total),
})}
</p>
<div className="pt-list-table-wrap bs-sheet-table">
<div className="pt-list-table-wrap bs-sheet-table bs-collect-detail-table">
<table className="data-table pt-list-table">
<thead>
<tr>
@ -2667,6 +2708,7 @@ export function BizStatePage() {
<th>{t("bizState.colStatus")}</th>
<th>{t("bizState.colRawLines")}</th>
<th>{t("bizState.colRows")}</th>
<th>{t("bizState.colDeclared")}</th>
<th>{t("bizState.colMessage")}</th>
<th>{t("bizState.colActions")}</th>
</tr>
@ -2684,6 +2726,9 @@ export function BizStatePage() {
</td>
<td className="pt-list-num">{Number(c.raw_line_count ?? 0)}</td>
<td className="pt-list-num">{Number(c.row_count ?? 0)}</td>
<td className="pt-list-num">
{Number(c.declared_total ?? 0) > 0 ? Number(c.declared_total) : "—"}
</td>
<td className="bs-msg-cell" title={c.message || ""}>
{c.message?.trim() ? c.message : "—"}
</td>

View file

@ -1915,6 +1915,7 @@ export const bizStateGetBatchCommand = (batchId: string, commandId: string) =>
parse_status: string;
row_count: number;
raw_line_count?: number;
declared_total?: number;
message: string;
raw_text: string;
collected_at?: string | null;