diff --git a/netx_api/biz_state/service.py b/netx_api/biz_state/service.py index faa0ac5..84d5aee 100644 --- a/netx_api/biz_state/service.py +++ b/netx_api/biz_state/service.py @@ -406,6 +406,8 @@ def list_batches(db: Session, task_id: str, *, limit: int = 50) -> list[dict[str "command_count": b.command_count, "row_count": b.row_count, "message": b.message, + "ne_name": b.ne_name or "", + "ne_id": b.ne_id or "", "started_at": b.started_at.isoformat() + "Z" if b.started_at else None, "ended_at": b.ended_at.isoformat() + "Z" if b.ended_at else None, } diff --git a/web/src/index.css b/web/src/index.css index f1c0602..7a6cbd3 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -10130,7 +10130,7 @@ html.login-page--paused .login-page__flare { 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; + overflow: visible; } .bs-cmp-sides__side { @@ -10139,9 +10139,11 @@ html.login-page--paused .login-page__flare { align-items: center; gap: 6px; min-width: 0; + max-width: 100%; padding: 0 6px; border-radius: 6px; height: 24px; + flex: 0 1 auto; } .bs-cmp-sides__side.is-before { @@ -10177,7 +10179,9 @@ html.login-page--paused .login-page__flare { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 10rem; + max-width: 8rem; + min-width: 0; + flex: 0 1 auto; line-height: 1.2; } @@ -10186,6 +10190,7 @@ html.login-page--paused .login-page__flare { font-variant-numeric: tabular-nums; white-space: nowrap; flex: 0 0 auto; + color: #94a3b8; } .bs-cmp-sides__arrow { @@ -10256,6 +10261,18 @@ html.login-page--paused .login-page__flare { line-height: 1.35; } +.bs-cmp-nav__legend { + display: grid; + grid-template-columns: repeat(4, 1.6em); + gap: 2px; + justify-items: end; + margin-top: 6px; + margin-left: auto; + width: fit-content; + font-size: 10px; + font-weight: 700; +} + .bs-cmp-nav__list { flex: 1 1 auto; min-height: 0; @@ -10269,10 +10286,9 @@ html.login-page--paused .login-page__flare { .bs-cmp-nav__item { appearance: none; display: grid; - grid-template-columns: 8px minmax(0, 1fr); - grid-template-rows: auto auto; - gap: 4px 8px; - align-items: start; + grid-template-columns: 8px minmax(0, 1fr) auto; + gap: 0 8px; + align-items: center; width: 100%; text-align: left; border: 1px solid transparent; @@ -10300,7 +10316,6 @@ html.login-page--paused .login-page__flare { background: #64748b; justify-self: center; align-self: center; - grid-row: 1; } .bs-cmp-nav__item.is-clean .bs-cmp-nav__dot { @@ -10318,81 +10333,45 @@ html.login-page--paused .login-page__flare { text-overflow: ellipsis; white-space: nowrap; color: #e2e8f0; - grid-column: 2; - grid-row: 1; - align-self: center; -} - -.bs-cmp-nav__badges { - display: flex; - flex-wrap: wrap; - gap: 4px; - grid-column: 2; - grid-row: 2; min-width: 0; } -.bs-cmp-nav__badge { +.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; - white-space: nowrap; - padding: 1px 6px; - border-radius: 4px; - font-size: 10px; font-variant-numeric: tabular-nums; - line-height: 1.45; - color: #94a3b8; - background: rgba(148, 163, 184, 0.1); - border: 1px solid rgba(148, 163, 184, 0.16); } -.bs-cmp-nav__badge b { +.bs-cmp-nav__num { + font-size: 11px; font-weight: 700; - margin-left: 2px; - color: #cbd5e1; + line-height: 1; + color: #475569; + opacity: 0.55; } -.bs-cmp-nav__badge--removed.is-hot { - color: #fecaca; - background: rgba(239, 68, 68, 0.14); - border-color: rgba(248, 113, 113, 0.35); +.bs-cmp-nav__num--removed.is-hot { + color: #f87171; + opacity: 1; } -.bs-cmp-nav__badge--removed.is-hot b { - color: #fca5a5; +.bs-cmp-nav__num--added.is-hot { + color: #4ade80; + opacity: 1; } -.bs-cmp-nav__badge--added.is-hot { - color: #bbf7d0; - background: rgba(34, 197, 94, 0.14); - border-color: rgba(74, 222, 128, 0.35); -} - -.bs-cmp-nav__badge--added.is-hot b { - color: #86efac; -} - -.bs-cmp-nav__badge--changed.is-hot { - color: #fde68a; - background: rgba(245, 158, 11, 0.14); - border-color: rgba(251, 191, 36, 0.35); -} - -.bs-cmp-nav__badge--changed.is-hot b { +.bs-cmp-nav__num--changed.is-hot { color: #fbbf24; + opacity: 1; } -.bs-cmp-nav__badge--unchanged b { +.bs-cmp-nav__num--unchanged { 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; + opacity: 0.85; } .bs-cmp-main { diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 4155356..38ee983 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -224,30 +224,61 @@ function sideDeviceName(side?: { } function sideCollectTime(side?: { started_at?: string | null } | null): string { - return fmtTime(side?.started_at); + const raw = side?.started_at; + if (!raw) return "—"; + const full = fmtTime(raw); + if (!full || full === raw) { + // Prefer compact local time when possible + try { + const d = new Date(/[zZ]$|[+-]\d{2}:\d{2}$/.test(String(raw)) ? raw : `${String(raw).replace(" ", "T")}Z`); + if (!Number.isNaN(d.getTime())) { + const m = d.getMonth() + 1; + const day = d.getDate(); + const hh = String(d.getHours()).padStart(2, "0"); + const mm = String(d.getMinutes()).padStart(2, "0"); + return `${m}/${day} ${hh}:${mm}`; + } + } catch { + /* fall through */ + } + return full || "—"; + } + // Compact: drop seconds / year noise from locale string when long + // e.g. "2026/9/18 09:35:53" -> "9/18 09:35" + const m = String(full).match(/(?:\d{4}[/-])?(\d{1,2})[/-](\d{1,2})\s+(\d{2}):(\d{2})/); + if (m) return `${m[1]}/${m[2]} ${m[3]}:${m[4]}`; + return full; } -function enrichSideFromTask( +type SideInfo = { + ne_name?: string; + ne_ip?: string; + label?: string; + batch_id?: string; + started_at?: string | null; +}; + +function enrichSide( 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, - }; + batches: BatchOpt[], +): SideInfo { + const base: SideInfo = { ...(side || {}) } as SideInfo; + const bid = String(base.batch_id || "").trim(); + if (!base.started_at && bid) { + const batch = batches.find((b) => b.id === bid); + if (batch?.started_at) base.started_at = batch.started_at; + } + if (sideDeviceName(base) === "—") { + const task = tasks.find((t) => t.id === taskId); + if (task) { + base.ne_name = task.ne_name || base.ne_name; + base.ne_ip = task.ne_ip || base.ne_ip; + base.label = task.ne_name || task.ne_ip || base.label; + } + } + return base; } function taskLabel(row: TaskOpt) { @@ -2113,15 +2144,17 @@ export function BizComparePage() { > {runs.map((r) => { - const before = enrichSideFromTask( + const before = enrichSide( (r as any).before, beforeTaskId, tasks, + beforeBatches, ); - const after = enrichSideFromTask( + const after = enrichSide( (r as any).after, afterTaskId || beforeTaskId, tasks, + afterBatches.length ? afterBatches : beforeBatches, ); const bl = sideDeviceName(before); const al = sideDeviceName(after); @@ -2135,28 +2168,31 @@ export function BizComparePage() { {runDetail ? (
{(() => { - const before = enrichSideFromTask( + const before = enrichSide( runDetail.before, beforeTaskId, tasks, + beforeBatches, ); - const after = enrichSideFromTask( + const after = enrichSide( runDetail.after, afterTaskId || beforeTaskId, tasks, + afterBatches.length ? afterBatches : beforeBatches, ); + const beforeName = sideDeviceName(before); + const afterName = sideDeviceName(after); + const beforeTime = sideCollectTime(before); + const afterTime = sideCollectTime(after); return ( <>
{t("bizCompare.sideBefore")} - - {sideDeviceName(before)} + + {beforeName} - - {sideCollectTime(before)} + + {beforeTime}
@@ -2164,14 +2200,11 @@ export function BizComparePage() {
{t("bizCompare.sideAfter")} - - {sideDeviceName(after)} + + {afterName} - - {sideCollectTime(after)} + + {afterTime}
@@ -2212,6 +2245,32 @@ export function BizComparePage() { ? ` · ${t("bizCompare.sheetFailCount", { n: String(sheetFailCount) })}` : ""}
+
+ + - + + + + + + + ~ + + + = + +
{(sheetCards.length @@ -2248,39 +2307,34 @@ export function BizComparePage() { > {c.metric_id} - - {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} - - )} + + 0 ? " is-hot" : "" + }`} + > + {Number(c.removed || 0)} + + 0 ? " is-hot" : "" + }`} + > + {Number(c.added || 0)} + + 0 ? " is-hot" : "" + }`} + > + {Number(c.changed || 0)} + + + {Number(c.unchanged || 0)} + );