diff --git a/netx_api/biz_state/compare_service.py b/netx_api/biz_state/compare_service.py index e273c9f..30908bf 100644 --- a/netx_api/biz_state/compare_service.py +++ b/netx_api/biz_state/compare_service.py @@ -39,49 +39,71 @@ def _utcnow() -> datetime: return utcnow_naive() -def _compare_side(db: Session, batch_id: str) -> dict[str, Any]: +def _compare_side( + db: Session, + batch_id: str, + *, + fallback_task_id: str = "", +) -> dict[str, Any]: """Human-readable before/after side for board header (who vs who).""" bid = str(batch_id or "").strip() + fb_tid = str(fallback_task_id or "").strip() + empty = { + "batch_id": bid, + "task_id": fb_tid, + "ne_name": "", + "ne_id": "", + "ne_ip": "", + "vendor": "", + "status": "", + "row_count": 0, + "started_at": None, + "label": "", + } if not bid: - return { - "batch_id": "", - "task_id": "", - "ne_name": "", - "ne_id": "", - "ne_ip": "", - "vendor": "", - "status": "", - "row_count": 0, - "started_at": None, - "label": "", - } + # Still resolve device from job task when batch not chosen yet + if fb_tid: + task = db.get(BizStateTask, fb_tid) + if task: + ne_name = str(task.ne_name or "").strip() + ne_ip = str(task.ne_ip or "").strip() + label = ne_name or ne_ip or fb_tid[:12] + return { + **empty, + "ne_name": ne_name, + "ne_id": str(task.ne_id or "").strip(), + "ne_ip": ne_ip, + "vendor": str(task.vendor or "").strip(), + "label": label, + } + return empty b = db.get(BizStateBatch, bid) if not b: + task = db.get(BizStateTask, fb_tid) if fb_tid else None + ne_name = str((task.ne_name if task else "") or "").strip() + ne_ip = str((task.ne_ip if task else "") or "").strip() + label = ne_name or ne_ip or bid[:12] return { - "batch_id": bid, - "task_id": "", - "ne_name": "", - "ne_id": "", - "ne_ip": "", - "vendor": "", + **empty, "status": "missing", - "row_count": 0, - "started_at": None, - "label": bid[:12], + "ne_name": ne_name, + "ne_id": str((task.ne_id if task else "") or "").strip(), + "ne_ip": ne_ip, + "vendor": str((task.vendor if task else "") or "").strip(), + "label": label, } task = db.get(BizStateTask, b.task_id) if b.task_id else None + if task is None and fb_tid: + task = db.get(BizStateTask, fb_tid) ne_name = str(b.ne_name or (task.ne_name if task else "") or "").strip() ne_id = str(b.ne_id or (task.ne_id if task else "") or "").strip() ne_ip = str((task.ne_ip if task else "") or "").strip() vendor = str(b.vendor or (task.vendor if task else "") or "").strip() - title = ne_name or ne_id or (b.task_id or bid)[:12] - if ne_ip and ne_ip not in (ne_name, ne_id, title): - label = f"{title} ({ne_ip})" - else: - label = title + title = ne_name or ne_ip or ne_id or "" + label = title if title else bid[:12] return { "batch_id": bid, - "task_id": str(b.task_id or ""), + "task_id": str(b.task_id or fb_tid or ""), "ne_name": ne_name, "ne_id": ne_id, "ne_ip": ne_ip, @@ -1394,8 +1416,12 @@ def get_run(db: Session, run_id: str) -> dict[str, Any]: stored = "rows" if _run_has_diff_rows(db, run_id) else "inline" job = db.get(BizCompareJob, r.job_id) if r.job_id else None mapping = db.get(BizPortMapping, r.mapping_id) if r.mapping_id else None - before_side = _compare_side(db, r.before_batch_id) - after_side = _compare_side(db, r.after_batch_id) + before_side = _compare_side( + db, r.before_batch_id, fallback_task_id=(job.before_task_id if job else "") + ) + after_side = _compare_side( + db, r.after_batch_id, fallback_task_id=(job.after_task_id if job else "") + ) return { "id": r.id, "job_id": r.job_id, @@ -1590,13 +1616,16 @@ def list_runs(db: Session, job_id: str, *, limit: int = 20) -> list[dict[str, An .limit(max(1, min(100, int(limit)))) .all() ) + job = db.get(BizCompareJob, job_id) + before_tid = str(job.before_task_id or "") if job else "" + after_tid = str(job.after_task_id or "") if job else "" return [ { "id": r.id, "before_batch_id": r.before_batch_id, "after_batch_id": r.after_batch_id, - "before": _compare_side(db, r.before_batch_id), - "after": _compare_side(db, r.after_batch_id), + "before": _compare_side(db, r.before_batch_id, fallback_task_id=before_tid), + "after": _compare_side(db, r.after_batch_id, fallback_task_id=after_tid), "status": r.status, "summary": { k: (r.summary_json or {}).get(k, 0) diff --git a/web/src/index.css b/web/src/index.css index cded587..f1c0602 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -10096,39 +10096,52 @@ html.login-page--paused .login-page__flare { .bs-cmp-board__toolbar { display: flex; - flex-wrap: wrap; - gap: 8px 12px; + flex-wrap: nowrap; + gap: 10px; align-items: center; justify-content: flex-start; flex: 0 0 auto; + min-height: 36px; +} + +.bs-cmp-board__run-select { + flex: 0 1 280px; + min-width: 160px; + max-width: 320px; + height: 32px; + margin: 0; } .bs-cmp-board__actions { margin-left: auto; flex: 0 0 auto; + align-items: center; } .bs-cmp-sides { display: inline-flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; gap: 6px 8px; min-width: 0; flex: 1 1 auto; - padding: 4px 8px; + height: 32px; + padding: 0 8px; border-radius: 8px; border: 1px solid var(--bs-cmp-border, rgba(148, 163, 184, 0.22)); background: rgba(15, 23, 42, 0.45); + overflow: hidden; } .bs-cmp-sides__side { display: inline-flex; - flex-wrap: wrap; - align-items: baseline; - gap: 4px 8px; + flex-wrap: nowrap; + align-items: center; + gap: 6px; min-width: 0; - padding: 2px 6px; + padding: 0 6px; border-radius: 6px; + height: 24px; } .bs-cmp-sides__side.is-before { @@ -10158,14 +10171,14 @@ html.login-page--paused .login-page__flare { } .bs-cmp-sides__name { - font-size: 13px; + font-size: 12px; font-weight: 700; color: #f1f5f9; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 14rem; - line-height: 1.3; + max-width: 10rem; + line-height: 1.2; } .bs-cmp-sides__meta { @@ -10175,19 +10188,22 @@ html.login-page--paused .login-page__flare { flex: 0 0 auto; } -.bs-cmp-sides__vs { +.bs-cmp-sides__arrow { flex: 0 0 auto; - padding: 2px 8px; - border-radius: 999px; - background: rgba(59, 130, 246, 0.18); - border: 1px solid rgba(96, 165, 250, 0.35); color: #93c5fd; - font-size: 11px; - font-weight: 800; - letter-spacing: 0.02em; + font-weight: 700; + opacity: 0.85; } -@media (max-width: 900px) { +.bs-cmp-sides__vs { + display: none; +} + +@media (max-width: 1100px) { + .bs-cmp-board__toolbar { + flex-wrap: wrap; + } + .bs-cmp-board__actions { margin-left: 0; } @@ -10195,10 +10211,9 @@ html.login-page--paused .login-page__flare { .bs-cmp-sides { flex: 1 1 100%; order: 3; - } - - .bs-cmp-board__actions { - order: 2; + height: auto; + min-height: 32px; + padding: 4px 8px; } } @@ -10370,6 +10385,16 @@ html.login-page--paused .login-page__flare { color: #94a3b8; } +.bs-cmp-nav__badge--pass.is-hot { + color: #bbf7d0; + background: rgba(34, 197, 94, 0.14); + border-color: rgba(74, 222, 128, 0.35); +} + +.bs-cmp-nav__badge--pass.is-hot b { + color: #86efac; +} + .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 17e74bd..4155356 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -213,13 +213,41 @@ function sideDeviceName(side?: { batch_id?: string; } | null): string { if (!side) return "—"; - return ( - String(side.ne_name || "").trim() || - String(side.ne_ip || "").trim() || - String(side.label || "").trim() || - String(side.batch_id || "").slice(0, 8) || - "—" - ); + const name = String(side.ne_name || "").trim(); + const ip = String(side.ne_ip || "").trim(); + const label = String(side.label || "").trim(); + // Prefer real device identity; ignore hex-ish fallbacks that look like batch ids + if (name) return name; + if (ip) return ip; + if (label && !/^[a-f0-9]{8,32}$/i.test(label)) return label; + return "—"; +} + +function sideCollectTime(side?: { started_at?: string | null } | null): string { + return fmtTime(side?.started_at); +} + +function enrichSideFromTask( + side: Record | null | undefined, + taskId: string, + tasks: TaskOpt[], +): { ne_name?: string; ne_ip?: string; label?: string; batch_id?: string; started_at?: string | null } { + const base = { ...(side || {}) } as { + ne_name?: string; + ne_ip?: string; + label?: string; + batch_id?: string; + started_at?: string | null; + }; + if (sideDeviceName(base) !== "—") return base; + const task = tasks.find((t) => t.id === taskId); + if (!task) return base; + return { + ...base, + ne_name: task.ne_name || base.ne_name, + ne_ip: task.ne_ip || base.ne_ip, + label: task.ne_name || task.ne_ip || base.label, + }; } function taskLabel(row: TaskOpt) { @@ -2074,8 +2102,9 @@ export function BizComparePage() { className={`bs-cmp-board${boardFs ? " is-fullscreen" : ""}`} >
- { const id = e.target.value; @@ -2084,44 +2113,70 @@ export function BizComparePage() { > {runs.map((r) => { - const bl = sideDeviceName((r as any).before); - const al = sideDeviceName((r as any).after); - const sides = - bl !== "—" || al !== "—" ? `${bl} → ${al} · ` : ""; + const before = enrichSideFromTask( + (r as any).before, + beforeTaskId, + tasks, + ); + const after = enrichSideFromTask( + (r as any).after, + afterTaskId || beforeTaskId, + tasks, + ); + const bl = sideDeviceName(before); + const al = sideDeviceName(after); return ( ); })} - + {runDetail ? (
-
- {t("bizCompare.sideBefore")} - - {sideDeviceName(runDetail.before)} - - - {fmtTime(runDetail.before?.started_at)} - -
- - {t("bizCompare.sidesVs")} - -
- {t("bizCompare.sideAfter")} - - {sideDeviceName(runDetail.after)} - - - {fmtTime(runDetail.after?.started_at)} - -
+ {(() => { + const before = enrichSideFromTask( + runDetail.before, + beforeTaskId, + tasks, + ); + const after = enrichSideFromTask( + runDetail.after, + afterTaskId || beforeTaskId, + tasks, + ); + return ( + <> +
+ {t("bizCompare.sideBefore")} + + {sideDeviceName(before)} + + + {sideCollectTime(before)} + +
+ + → + +
+ {t("bizCompare.sideAfter")} + + {sideDeviceName(after)} + + + {sideCollectTime(after)} + +
+ + ); + })()}
) : null}
@@ -2193,31 +2248,39 @@ export function BizComparePage() { > {c.metric_id} - - 0 ? " is-hot" : "" - }`} - > - {t("bizCompare.missCount")} {Number(c.removed || 0)} - - 0 ? " is-hot" : "" - }`} - > - {t("bizCompare.extraCount")} {Number(c.added || 0)} - - 0 ? " is-hot" : "" - }`} - > - {t("bizCompare.mismatchCount")} {Number(c.changed || 0)} - - - {t("bizCompare.matchCount")} {Number(c.unchanged || 0)} - + + {dirty > 0 ? ( + <> + {Number(c.removed || 0) > 0 ? ( + + {t("bizCompare.missCount")}{" "} + {Number(c.removed || 0)} + + ) : null} + {Number(c.added || 0) > 0 ? ( + + {t("bizCompare.extraCount")}{" "} + {Number(c.added || 0)} + + ) : null} + {Number(c.changed || 0) > 0 ? ( + + {t("bizCompare.mismatchCount")}{" "} + {Number(c.changed || 0)} + + ) : null} + + ) : ( + + {t("bizCompare.kindPass")} + {Number(c.unchanged || 0) > 0 ? ( + <> + {" "} + {Number(c.unchanged || 0)} + + ) : null} + + )} );