Show full copyable batch ids in biz-state/cutover UI and document batch list tools.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-20 15:26:46 +08:00
parent 04850b942c
commit 83cf086d8c
6 changed files with 123 additions and 22 deletions

View file

@ -2,10 +2,10 @@
name: netx-biz-monitor name: netx-biz-monitor
description: >- description: >-
Overnight cutover / biz_state monitor analysis via netxops host tools Overnight cutover / biz_state monitor analysis via netxops host tools
(netx__listBizMonitors / netx__getBizMonitor*). List projects/tasks first (netx__listBizMonitors / listBizMonitorBatches / getBizMonitor*). List
when no id is given; then read definitions, board, reds/diffs with evidence projects/tasks then batches when no id is given; read definitions, board,
(device-raw A/B, show commands), and judge tool false-positive vs real reds/diffs with evidence (device-raw A/B, show commands), and judge tool
business fault. Does not create templates/tasks. false-positive vs real business fault. Does not create templates/tasks.
--- ---
# netx-biz-monitor(割接 / 业务监控只读分析) # netx-biz-monitor(割接 / 业务监控只读分析)
@ -18,6 +18,7 @@ description: >-
| 工具 | 用途 | | 工具 | 用途 |
|------|------| |------|------|
| `netx__listBizMonitors` | **入口目录**:列出割接/监控对比项目 + 业务监控任务(**无需** project_id) | | `netx__listBizMonitors` | **入口目录**:列出割接/监控对比项目 + 业务监控任务(**无需** project_id) |
| `netx__listBizMonitorBatches` | **批次目录**:`project_id`→割接批次;`task_id`→采集批次;`kind=runs`→evaluate run |
| `netx__getBizMonitorContext` | 任务定义:项目、监控/对比模板、归一化规则、端口映射、关联任务与命令项 | | `netx__getBizMonitorContext` | 任务定义:项目、监控/对比模板、归一化规则、端口映射、关联任务与命令项 |
| `netx__getBizMonitorBoard` | 批次进度、evaluate run、sheet 卡、verdict 统计 | | `netx__getBizMonitorBoard` | 批次进度、evaluate run、sheet 卡、verdict 统计 |
| `netx__listBizMonitorReds` | 红单 + **evidence** | | `netx__listBizMonitorReds` | 红单 + **evidence** |
@ -37,12 +38,13 @@ description: >-
## 推荐顺序 ## 推荐顺序
1. 无 id → `listBizMonitors`(`kind=projects|tasks|all`,可加 `q` / `purpose`) 1. 无 id → `listBizMonitors`(`kind=projects|tasks|all`,可加 `q` / `purpose`)
2. `getBizMonitorContext(project_id)` — 弄清盯什么 2. `listBizMonitorBatches(project_id)` 或 `(task_id)` — 拿到 **完整 batch_id**;需要 run 时再 `kind=runs` / `include_runs`
3. `getBizMonitorBoard(batch_id)` — 进度 / missing / skipped 3. `getBizMonitorContext(project_id)` — 弄清盯什么
4. `listBizMonitorReds` 或 `getBizMonitorDiffs(color=red)` 4. `getBizMonitorBoard(batch_id)` — 进度 / missing / skipped
5. 看 evidence:parse 失败?current_missing?映射 miss?归一化误伤? 5. `listBizMonitorReds` 或 `getBizMonitorDiffs(color=red)`
6. 需要原文 → `getBizCollectCommandRaw` 6. 看 evidence:parse 失败?current_missing?映射 miss?归一化误伤?
7. 像真障 → `execManagedNe` 现场核对 7. 需要原文 → `getBizCollectCommandRaw`
8. 像真障 → `execManagedNe` 现场核对
## Verdict 速查(skill 解释,工具不硬编码) ## Verdict 速查(skill 解释,工具不硬编码)

View file

@ -19,6 +19,8 @@ const en = {
opFailed: "Operation failed", opFailed: "Operation failed",
empty: "-", empty: "-",
cancel: "Cancel", cancel: "Cancel",
copy: "Copy",
copied: "Copied",
}, },
workbench: { workbench: {
title: "Workbench", title: "Workbench",
@ -240,6 +242,8 @@ const en = {
noBatches: "No batches yet", noBatches: "No batches yet",
batchDetail: "Batch detail", batchDetail: "Batch detail",
batchWorkbook: "Batch workbook", batchWorkbook: "Batch workbook",
batchId: "Batch ID",
copyBatchId: "Copy batch ID",
sheetCommands: "Commands", sheetCommands: "Commands",
sheetLldp: "LLDP neighbors", sheetLldp: "LLDP neighbors",
sheetVrfRoute: "VRF route summary", sheetVrfRoute: "VRF route summary",
@ -593,6 +597,8 @@ const en = {
portFilterPh: "Filter", portFilterPh: "Filter",
emptyBaselinePorts: "No baseline data", emptyBaselinePorts: "No baseline data",
batchLabel: "Batch label", batchLabel: "Batch label",
batchId: "Batch ID",
copyBatchId: "Copy batch ID",
defaultBatchLabel: "Night N", defaultBatchLabel: "Night N",
expectPorts: "Expected ports (old side)", expectPorts: "Expected ports (old side)",
expectPortsPh: "Comma or newline separated", expectPortsPh: "Comma or newline separated",

View file

@ -19,6 +19,8 @@ const zh = {
opFailed: "操作失败", opFailed: "操作失败",
empty: "-", empty: "-",
cancel: "取消", cancel: "取消",
copy: "复制",
copied: "已复制",
}, },
workbench: { workbench: {
title: "工作台", title: "工作台",
@ -240,6 +242,8 @@ const zh = {
noBatches: "尚无批次", noBatches: "尚无批次",
batchDetail: "批次详情", batchDetail: "批次详情",
batchWorkbook: "批次工作簿", batchWorkbook: "批次工作簿",
batchId: "批次 ID",
copyBatchId: "复制批次 ID",
sheetCommands: "命令", sheetCommands: "命令",
sheetLldp: "LLDP 邻居", sheetLldp: "LLDP 邻居",
sheetVrfRoute: "VRF 路由摘要", sheetVrfRoute: "VRF 路由摘要",
@ -586,6 +590,8 @@ const zh = {
portFilterPh: "筛选", portFilterPh: "筛选",
emptyBaselinePorts: "基线无数据", emptyBaselinePorts: "基线无数据",
batchLabel: "批次标签", batchLabel: "批次标签",
batchId: "批次 ID",
copyBatchId: "复制批次 ID",
defaultBatchLabel: "第N晚", defaultBatchLabel: "第N晚",
expectPorts: "本批预期端口(老侧)", expectPorts: "本批预期端口(老侧)",
expectPortsPh: "逗号或换行分隔", expectPortsPh: "逗号或换行分隔",

View file

@ -9953,6 +9953,45 @@ html.login-page--paused .login-page__flare {
gap: 8px 12px; gap: 8px 12px;
} }
.bs-id-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid rgba(148, 163, 184, 0.2);
background: rgba(2, 6, 23, 0.45);
}
.bs-id-row--inline {
width: auto;
flex: 1 1 220px;
}
.bs-id-row__label {
flex: 0 0 auto;
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #94a3b8;
}
.bs-id-row__value {
flex: 1 1 auto;
min-width: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.4;
color: #e2e8f0;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
word-break: normal;
}
.bs-sheet-cmd-bar { .bs-sheet-cmd-bar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -37,6 +37,7 @@ import {
} from "../../services/api"; } from "../../services/api";
// bizStateListTasks not needed for create — NEs + auto HF // bizStateListTasks not needed for create — NEs + auto HF
import type { CliTargetItem } from "../../types"; import type { CliTargetItem } from "../../types";
import { writeClipboardText } from "../../utils/clipboard";
import { pageCount } from "../../utils/display"; import { pageCount } from "../../utils/display";
import { formatSystemTime, localDatetimeInputToUtcIso, utcIsoToLocalDatetimeInput } from "../../utils/time"; import { formatSystemTime, localDatetimeInputToUtcIso, utcIsoToLocalDatetimeInput } from "../../utils/time";
import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips"; import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
@ -1426,14 +1427,26 @@ export function BizMigrationPage() {
const batchOptions = (items: { id: string; started_at?: string | null }[]) => ( const batchOptions = (items: { id: string; started_at?: string | null }[]) => (
<> <>
<option value="">—</option> <option value="">—</option>
{items.map((x) => ( {items.map((x) => {
<option key={x.id} value={x.id}> const when = fmtTime(x.started_at);
{fmtTime(x.started_at) !== "—" ? fmtTime(x.started_at) : x.id.slice(0, 12)} const label = when !== "—" ? `${when} · ${x.id}` : x.id;
</option> return (
))} <option key={x.id} value={x.id} title={x.id}>
{label}
</option>
);
})}
</> </>
); );
const copyBatchId = (id: string) => {
void (async () => {
const ok = await writeClipboardText(id);
if (ok) showOk(t("common.copied"));
else showError(t("common.opFailed"));
})();
};
return ( return (
<section className="panel nm-page-panel"> <section className="panel nm-page-panel">
<div className="panel__toolbar"> <div className="panel__toolbar">
@ -2340,8 +2353,8 @@ export function BizMigrationPage() {
aria-label={t("bizMigration.batches")} aria-label={t("bizMigration.batches")}
> >
{batches.map((b) => ( {batches.map((b) => (
<option key={b.id} value={b.id}> <option key={b.id} value={b.id} title={b.id}>
{b.batch_label} ({statusLabel(b.status)}) {b.batch_label} ({statusLabel(b.status)}) · {b.id}
</option> </option>
))} ))}
</FieldSelect> </FieldSelect>
@ -2397,6 +2410,15 @@ export function BizMigrationPage() {
) : null} ) : null}
</div> </div>
</div> </div>
{batch ? (
<div className="bs-id-row" title={batch.id}>
<span className="bs-id-row__label">{t("bizMigration.batchId")}</span>
<code className="bs-id-row__value">{batch.id}</code>
<Button size="sm" variant="ghost" onPress={() => copyBatchId(batch.id)}>
{t("bizMigration.copyBatchId")}
</Button>
</div>
) : null}
<div className="ct-editor__main bm-batches__main"> <div className="ct-editor__main bm-batches__main">
<aside className="ct-editor__nav" aria-label={t("bizMigration.pickExpectPorts")}> <aside className="ct-editor__nav" aria-label={t("bizMigration.pickExpectPorts")}>
@ -2530,11 +2552,20 @@ export function BizMigrationPage() {
aria-label={t("bizMigration.batches")} aria-label={t("bizMigration.batches")}
> >
{batches.map((b) => ( {batches.map((b) => (
<option key={b.id} value={b.id}> <option key={b.id} value={b.id} title={b.id}>
{b.batch_label} ({statusLabel(b.status)}) {b.batch_label} ({statusLabel(b.status)}) · {b.id}
</option> </option>
))} ))}
</FieldSelect> </FieldSelect>
{batchId ? (
<div className="bs-id-row bs-id-row--inline" title={batchId}>
<span className="bs-id-row__label">{t("bizMigration.batchId")}</span>
<code className="bs-id-row__value">{batchId}</code>
<Button size="sm" variant="ghost" onPress={() => copyBatchId(batchId)}>
{t("bizMigration.copyBatchId")}
</Button>
</div>
) : null}
<FieldSelect <FieldSelect
value={pinOldBatchId} value={pinOldBatchId}
onChange={(e) => setPinOldBatchId(e.target.value)} onChange={(e) => setPinOldBatchId(e.target.value)}

View file

@ -30,6 +30,7 @@ import {
} from "../../services/api"; } from "../../services/api";
import type { CliTargetItem } from "../../types"; import type { CliTargetItem } from "../../types";
import { pageCount } from "../../utils/display"; import { pageCount } from "../../utils/display";
import { writeClipboardText } from "../../utils/clipboard";
import { formatSystemTime } from "../../utils/time"; import { formatSystemTime } from "../../utils/time";
import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips"; import { jobChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
@ -1502,9 +1503,7 @@ export function BizStatePage() {
className="app-heroui-modal--xl bs-workbook-modal" className="app-heroui-modal--xl bs-workbook-modal"
> >
<Modal.Header> <Modal.Header>
<Modal.Heading> <Modal.Heading>{t("bizState.batchWorkbook")}</Modal.Heading>
{t("bizState.batchWorkbook")} · {batchDetail?.id?.slice(0, 8)}…
</Modal.Heading>
<Modal.CloseTrigger /> <Modal.CloseTrigger />
</Modal.Header> </Modal.Header>
<Modal.Body className="flex flex-col gap-2 bs-workbook-body"> <Modal.Body className="flex flex-col gap-2 bs-workbook-body">
@ -1517,6 +1516,24 @@ export function BizStatePage() {
{batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "} {batchDetail.command_count} cmd · {batchDetail.row_count} rows ·{" "}
{fmtTime(batchDetail.started_at)} {fmtTime(batchDetail.started_at)}
</span> </span>
<div className="bs-id-row" title={String(batchDetail.id || "")}>
<span className="bs-id-row__label">{t("bizState.batchId")}</span>
<code className="bs-id-row__value">{String(batchDetail.id || "")}</code>
<Button
size="sm"
variant="ghost"
isDisabled={!batchDetail.id}
onPress={() => {
void (async () => {
const ok = await writeClipboardText(String(batchDetail.id || ""));
if (ok) showOk(t("common.copied"));
else showError(t("common.opFailed"));
})();
}}
>
{t("bizState.copyBatchId")}
</Button>
</div>
</div> </div>
) : null} ) : null}