diff --git a/netx_api/biz_state/compare_service.py b/netx_api/biz_state/compare_service.py index 30908bf..7ed3e9f 100644 --- a/netx_api/biz_state/compare_service.py +++ b/netx_api/biz_state/compare_service.py @@ -205,7 +205,8 @@ def _filter_inline_diffs( for d in diffs: dk = str(d.get("kind") or "") if kind_n == "diff": - if dk == "unchanged": + # Fail = missing + mismatch; added is a special bucket + if dk not in ("removed", "changed"): continue elif kind_n != "all" and dk != kind_n: continue @@ -1328,10 +1329,16 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di before_count = int(summary.get("before_count") or 0) after_count = int(summary.get("after_count") or 0) total = added + removed + changed + unchanged + # Compare verdict: fail = missing + mismatch; success = match; added is special + fail_count = removed + changed + success_count = unchanged + judged = fail_count + success_count matched = changed + unchanged - diff_count = added + removed + changed - pass_rate = round((unchanged / matched) * 100, 1) if matched else (100.0 if total == 0 else 0.0) - diff_rate = round((diff_count / total) * 100, 1) if total else 0.0 + diff_count = fail_count + pass_rate = ( + round((success_count / judged) * 100, 1) if judged else (100.0 if total == 0 else 0.0) + ) + diff_rate = round((fail_count / judged) * 100, 1) if judged else 0.0 sheet_cards: list[dict[str, Any]] = [] for sh in sheets: @@ -1341,7 +1348,8 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di sc = int(ss.get("changed") or 0) su = int(ss.get("unchanged") or 0) st = sa + sr + sc + su - sm = sc + su + sf = sr + sc + sj = sf + su sheet_cards.append( { "metric_id": sh.get("metric_id") or "", @@ -1352,8 +1360,10 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di "unchanged": su, "before_count": int(ss.get("before_count") or 0), "after_count": int(ss.get("after_count") or 0), - "diff_count": sa + sr + sc, - "pass_rate": round((su / sm) * 100, 1) if sm else (100.0 if st == 0 else 0.0), + "fail_count": sf, + "success_count": su, + "diff_count": sf, + "pass_rate": round((su / sj) * 100, 1) if sj else (100.0 if st == 0 else 0.0), } ) @@ -1382,10 +1392,12 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di "sheet_count": int(summary.get("sheet_count") or len(sheets) or 0), "total_rows": total, "matched_rows": matched, + "fail_count": fail_count, + "success_count": success_count, "diff_count": diff_count, "pass_rate": pass_rate, "diff_rate": diff_rate, - "ok": diff_count == 0, + "ok": fail_count == 0, "sheet_cards": sheet_cards, "top_changed_fields": top_fields, } @@ -1476,7 +1488,7 @@ def list_run_diffs( BizCompareDiff.metric_id == mid, ) if kind_n == "diff": - q = q.filter(BizCompareDiff.kind.in_(("added", "removed", "changed"))) + q = q.filter(BizCompareDiff.kind.in_(("removed", "changed"))) elif kind_n != "all": q = q.filter(BizCompareDiff.kind == kind_n) if kw_n: diff --git a/tests/test_biz_state_compare.py b/tests/test_biz_state_compare.py index b2e91a6..df79cb9 100644 --- a/tests/test_biz_state_compare.py +++ b/tests/test_biz_state_compare.py @@ -303,7 +303,7 @@ class CompareDiffPagingTests(unittest.TestCase): }, ] only_diff = _filter_inline_diffs(diffs, kind="diff", kw="") - self.assertEqual({d["kind"] for d in only_diff}, {"added", "removed", "changed"}) + self.assertEqual({d["kind"] for d in only_diff}, {"removed", "changed"}) only_added = _filter_inline_diffs(diffs, kind="added", kw="") self.assertEqual(len(only_added), 1) hit = _filter_inline_diffs(diffs, kind="all", kw='"p":"4"') diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index d3e03e3..736163e 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -364,17 +364,20 @@ const en = { resultEmpty: "No matching diff rows", resultFilterPh: "Filter key / values…", kindAll: "All", - kindDiff: "Failures only", - kindAdded: "Extra only", + kindDiff: "Fail", + kindAdded: "Added", kindRemoved: "Missing only", kindChanged: "Mismatch only", - kindUnchanged: "Match only", - kindAddedShort: "Extra", + kindUnchanged: "Pass", + kindAddedShort: "Added", kindRemovedShort: "Missing", kindChangedShort: "Mismatch", - kindUnchangedShort: "Match", + kindUnchangedShort: "Pass", kindFail: "Fail", kindPass: "Pass", + kindSuccess: "Pass", + successCount: "Pass", + passRateShort: "Pass %", verdictPass: "Pass", verdictFail: "Fail", failCount: "Failed rows", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index f83f772..80f1aa4 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -364,17 +364,20 @@ const zh = { resultEmpty: "无匹配失败/结果行", resultFilterPh: "筛选身份 / 字段值…", kindAll: "全部", - kindDiff: "仅失败", - kindAdded: "仅多余", + kindDiff: "失败", + kindAdded: "新增", kindRemoved: "仅缺失", kindChanged: "仅不一致", - kindUnchanged: "仅一致", - kindAddedShort: "多余", + kindUnchanged: "成功", + kindAddedShort: "新增", kindRemovedShort: "缺失", kindChangedShort: "不一致", - kindUnchangedShort: "一致", + kindUnchangedShort: "成功", kindFail: "失败", - kindPass: "通过", + kindPass: "成功", + kindSuccess: "成功", + successCount: "成功", + passRateShort: "通过率", verdictPass: "通过", verdictFail: "不通过", failCount: "失败条目", diff --git a/web/src/index.css b/web/src/index.css index 7a6cbd3..3e50eda 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -10263,8 +10263,8 @@ html.login-page--paused .login-page__flare { .bs-cmp-nav__legend { display: grid; - grid-template-columns: repeat(4, 1.6em); - gap: 2px; + grid-template-columns: repeat(3, minmax(1.6em, auto)); + gap: 4px 6px; justify-items: end; margin-top: 6px; margin-left: auto; @@ -10273,6 +10273,40 @@ html.login-page--paused .login-page__flare { font-weight: 700; } +.bs-cmp-nav__stats { + display: inline-grid; + grid-template-columns: repeat(3, minmax(1.6em, auto)); + gap: 2px 6px; + justify-items: end; + align-items: center; + flex: 0 0 auto; + font-variant-numeric: tabular-nums; +} + +.bs-cmp-nav__num { + font-size: 11px; + font-weight: 700; + line-height: 1; + color: #475569; + opacity: 0.55; +} + +.bs-cmp-nav__num--fail.is-hot { + color: #f87171; + opacity: 1; +} + +.bs-cmp-nav__num--ok.is-hot { + color: #4ade80; + opacity: 1; +} + +.bs-cmp-nav__num--rate { + color: #93c5fd; + opacity: 0.95; + min-width: 2.6em; +} + .bs-cmp-nav__list { flex: 1 1 auto; min-height: 0; @@ -10336,44 +10370,6 @@ html.login-page--paused .login-page__flare { min-width: 0; } -.bs-cmp-nav__stats { - display: inline-grid; - grid-template-columns: repeat(4, 1.6em); - gap: 2px; - justify-items: end; - align-items: center; - flex: 0 0 auto; - font-variant-numeric: tabular-nums; -} - -.bs-cmp-nav__num { - font-size: 11px; - font-weight: 700; - line-height: 1; - color: #475569; - opacity: 0.55; -} - -.bs-cmp-nav__num--removed.is-hot { - color: #f87171; - opacity: 1; -} - -.bs-cmp-nav__num--added.is-hot { - color: #4ade80; - opacity: 1; -} - -.bs-cmp-nav__num--changed.is-hot { - color: #fbbf24; - opacity: 1; -} - -.bs-cmp-nav__num--unchanged { - color: #94a3b8; - opacity: 0.85; -} - .bs-cmp-main { display: flex; flex-direction: column; diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 38ee983..70830fe 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -795,17 +795,51 @@ export function BizComparePage() { return kind; }; - const verdictLabel = (kind: string) => { - if (kind === "unchanged") return t("bizCompare.kindPass"); - return t("bizCompare.kindFail"); + /** Table verdict: fail = missing+mismatch; success = match; added is special. */ + const rowVerdict = (kind: string): { label: string; tone: "fail" | "pass" | "added" } => { + if (kind === "added") return { label: t("bizCompare.kindAddedShort"), tone: "added" }; + if (kind === "unchanged") return { label: t("bizCompare.kindSuccess"), tone: "pass" }; + return { label: t("bizCompare.kindFail"), tone: "fail" }; }; const failFieldsLabel = (d: DiffRow) => { - if (d.kind === "added" || d.kind === "removed") return t("bizCompare.failWholeRow"); + if (d.kind === "added") return t("bizCompare.kindAddedShort"); + if (d.kind === "removed") return t("bizCompare.failWholeRow"); const names = failFieldNames(d); return names.length ? names.join(" · ") : t("bizCompare.failFieldsEmpty"); }; + const sheetFailOf = (c: { + fail_count?: number; + diff_count?: number; + removed?: number; + changed?: number; + }) => + Number( + c.fail_count ?? + c.diff_count ?? + Number(c.removed || 0) + Number(c.changed || 0), + ); + + const sheetSuccessOf = (c: { success_count?: number; unchanged?: number }) => + Number(c.success_count ?? c.unchanged ?? 0); + + const sheetPassRateOf = (c: { + pass_rate?: number; + fail_count?: number; + diff_count?: number; + removed?: number; + changed?: number; + success_count?: number; + unchanged?: number; + }) => { + if (c.pass_rate !== undefined && c.pass_rate !== null) return Number(c.pass_rate); + const fail = sheetFailOf(c); + const ok = sheetSuccessOf(c); + const judged = fail + ok; + return judged ? Math.round((ok / judged) * 1000) / 10 : 100; + }; + const summary = runDetail?.summary || {}; const resultPages = Math.max(1, Math.ceil(resultTotal / Math.max(1, resultPageSize))); const sheetCards = useMemo(() => { @@ -818,20 +852,25 @@ export function BizComparePage() { unchanged?: number; before_count?: number; after_count?: number; + fail_count?: number; + success_count?: number; diff_count?: number; pass_rate?: number; }>; - // Diffs first so ops can scan quickly when many sheets + // Failures first so ops can scan quickly when many sheets return [...raw].sort((a, b) => { - const da = Number(a.diff_count || 0); - const db = Number(b.diff_count || 0); + const da = sheetFailOf(a); + const db = sheetFailOf(b); if (da !== db) return db - da; return String(a.metric_id).localeCompare(String(b.metric_id)); }); }, [summary.sheet_cards]); - const sheetFailCount = sheetCards.filter((c) => Number(c.diff_count || 0) > 0).length; + const sheetFailCount = sheetCards.filter((c) => sheetFailOf(c) > 0).length; const activeSheetCard = sheetCards.find((c) => c.metric_id === resultSheetId) || sheetCards[0]; - + const activeFail = sheetFailOf(activeSheetCard || {}); + const activeSuccess = sheetSuccessOf(activeSheetCard || {}); + const activePassRate = sheetPassRateOf(activeSheetCard || {}); + const activeAdded = Number(activeSheetCard?.added || 0); useEffect(() => { const syncFs = () => { const el = boardRef.current; @@ -2247,51 +2286,55 @@ export function BizComparePage() {