Optimize cutover compare/monitor per the 2026-10-12 plan.

Correctness (A): normalize port-map names with the template rules (snapshot v2,
v1 replays unchanged); protect mappings/templates that are referenced; stop
dropping edited mapping text; accept pulled-but-not-shutdown ports as migrated;
validate baseline ownership on project create; one canonical key form; fix
built-in template tolerances and upgrade old packaged values in place.

Cross-vendor identity (B): single identity pipeline for every path, per-side and
per-vendor interface rules, value maps, filters after normalization, mapping
pre-flight for compare and cutover projects, SQL eligibility fixes.

Data quality (V): no_data / suspect_parse / not_collected sheet states, command
slice coverage, directional compare modes, value-sorted duplicate pairing,
before-side raw names in results and CSV.

Robustness (H): bounded auto-run history, background evaluate/finish with
polling, manual route tables out of the acceptance gate, baseline row cache,
scoped duplicate-key failures, merged HF tasks with a per-NE session cap,
same-device cutover, explicit scope_by_port_map, read-only GETs, resolvable red
tickets.

Frontend: pre-flight report, mapping table view, template editor support for the
new rules, data-state banners. Docs: canonical field contract.

Not included: Huawei status parsers (no real samples), SQL-path route compare.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-12 11:51:55 +08:00
parent fff7d63b9b
commit 315539e0fd
59 changed files with 6483 additions and 487 deletions

View file

@ -67,7 +67,7 @@ const bizCompare = {
rowFilters: "Row filter rules",
rowFiltersHint:
"Filter rows before compare. Conditions in a group are AND; groups are OR. Selecting ARP seeds editable default rules you can change.",
rowFiltersHintShort: "AND in group · OR across groups · empty = all rows",
rowFiltersHintShort: "AND in group · OR across groups · empty = all rows · filters run after normalization: write canonical values",
rowFiltersEmpty: "No filters → compare all rows.",
filterClear: "Clear",
filterAddOr: "Add group (OR)",
@ -111,12 +111,82 @@ const bizCompare = {
validateNeed: "Select mapping and before/after batches first",
mapHint:
"One pair per line: before_if,after_if (comma / tab / pipe). Example:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# comments start with #. Not slash-grouped A,B/C,D.",
templateMonitorUsage: "Used by {{n}} monitor templates",
mapParsed: "{{n}} pairs parsed",
mapInvalidLines: "{{m}} invalid (line {{lines}})",
mapDuplicate: "duplicate before port: {{list}}",
mapUnsaved: "unsaved",
mapInvalid: "Mapping text has errors and was not saved. Invalid lines: {{lines}}; duplicate before ports: {{dup}}",
mapEmpty: "Mapping text is empty. Pick \"New mapping…\" if you want no mapping, otherwise restore the pairs before saving.",
mapSharedConfirm: "This mapping is also used by {{jobs}} other compare jobs and {{projects}} cutover projects; saving affects them too. Continue?",
mappingAutoSaved: "Mapping saved automatically",
dataState: {
"ok": "OK",
"no_data": "No data",
"suspect_parse": "Suspect parse",
"not_collected": "Not collected",
"manual_pending": "Manual pending"
},
stateBanner: {
"no_data": "Both sides are empty: nothing to compare, excluded from the pass rate.",
"suspect_parse": "The device printed output but 0 rows were parsed. Failures may be a parsing problem rather than lost business; check the parser/template first.",
"not_collected": "One side never collected this check (or the slice). Not judged, excluded from the pass rate."
},
rawKeyTitle: "Before-side name as printed by the device",
scopeBoth: "Both sides",
scopeBefore: "Before only",
scopeAfter: "After only",
valueMap: "Value map",
valueMapScope: "value map scope",
valueMapPlaceholder: "One per line: source,canonical\nestab,established",
rankLabel: "best→worst",
rankPlaceholder: "up,down (earlier is better)",
modeNotWorse: "Not worse",
modeNumericMin: "Not below (%)",
modeNumericMax: "Not above (%)",
filterSide: "filter side",
unmappedPortPolicy: "Unmapped ports",
unmappedByName: "Match by name (default)",
unmappedIgnorePort: "Ignore port, match by content",
unmappedPortPolicyHint: "With a partial port map, how ports that have no mapping are paired.",
duplicatePairing: "Same-key pairing",
pairSorted: "Pair by sorted value (recommended)",
pairOrdered: "Pair by collection order (legacy)",
duplicatePairingHint: "When one key has several rows (ECMP, multipath), CLI ordering changes must not create false differences.",
builtinVendorCanonical: "Built-in per-vendor interface names (GigabitEthernet↔GE …)",
mappingNeedBatches: "Pick the before and after batches first",
preflight: {
"run": "Validate mapping",
"ready": "Mapping looks usable",
"risk": "Risks found — fix before running",
"coverage": "{{mapped}} mapped · {{suspect}} suspected unmapped · coverage {{rate}}",
"pairs": "{{n}} map pairs",
"suspect": "Suspected unmapped (on before, no mapping, absent on after)",
"addSuspects": "Add to the mapping editor (targets to fill in)",
"commandDiff": "Collected commands differ between sides",
"rows": "Rows (before→after)",
"state": "Data state",
"mapping": "Mapping",
"skippedHeavy": "Large table, interfaces not enumerated",
"sheetCounts": "{{exact}} mapped · {{same}} same name · {{suspect}} suspected unmapped",
"issue": {
"duplicate": "Duplicate before port (first one wins)",
"conflicts": "Before ports that collide after normalizing",
"unused": "Map keys that match no interface",
"missingTargets": "Map targets missing on the after device",
"nTo1": "Several before ports map to one target (N:1)"
}
},
coverageBanner: "Mapping coverage: {{n}} before-side ports have no mapping and do not exist on the after device; the gaps may be missing map rows: ",
mapViewText: "Text",
mapViewTable: "Table",
mapTable: {"before": "Before port", "after": "After port", "status": "Status", "remove": "Remove row", "add": "Add row", "empty": "No mapping rows yet", "ok": "OK", "todo": "Target to fill in", "duplicate": "Duplicate before port", "invalid": "Before port missing"},
mappingSaved: "Mapping saved",
mappingOptionalHint:
"Optional. Empty = ignore local port renames (match on non-interface keys). Fill to apply port map. Main-port pairs also cover .subinterfaces (suffix kept).",
ifaceNormalize: "Interface normalize",
ifaceNormalizeHint:
"Template rules, one per line: alias,canonical. Applied before port mapping. Example:\nGE,gei\nSG,smartgroup\nXGE,xgei",
"Template rules, one per line: alias,canonical. Applied before port mapping. Example:; optional 3rd/4th columns scope a rule: before/after and vendor, e.g. Eth-Trunk,ae,after,huawei\nGE,gei\nSG,smartgroup\nXGE,xgei",
createJob: "Create compare job",
defaultJobName: "Cutover compare",
jobName: "Name",

View file

@ -203,6 +203,18 @@ const bizMigration = {
startBatch: "Start",
finishBatch: "Finish",
evaluate: "Evaluate",
evalProgress: "{{label}} {{i}}/{{n}} {{title}}",
evalQueued: "{{label}} queued…",
preflight: "Mapping pre-flight",
preflightHint: "Checks mapping coverage and collection differences on the two portrait baselines before the window.",
manualPendingBanner: "Waiting for manual collection (not part of the acceptance gate): {{metrics}}",
notCollectedBanner: "Collected differently on the two devices, not judged (not red): {{metrics}}",
duplicateWarningBanner: "Duplicate keys outside this wave (do not block acceptance; refine key fields): {{metrics}}",
redResolvableHint: "{{n}} red tickets belong to objects that turned green in the latest acceptance.",
redResolveAllResolvable: "Close all that turned green",
redNowGreen: "now green",
redResolvableClosed: "{{n}} red tickets closed",
portSuccessHint: "A port counts as migrated when the old port is gone, physically down or admin down (any one) and the new port is admin and physically up. Pulling the fibre without shutdown counts.",
evaluated: "Done",
statusUpdated: "Status updated",
board: "Board",

View file

@ -66,7 +66,7 @@ const bizCompare = {
rowFilters: "行过滤规则",
rowFiltersHint:
"比对前按规则筛选行。组内条件为「且」,组与组之间为「或」。勾选 ARP 等监控项时会带上默认可编辑规则,可按需改删。",
rowFiltersHintShort: "组内且 · 组间或 · 无过滤则比对全部行",
rowFiltersHintShort: "组内且 · 组间或 · 无过滤则比对全部行 · 过滤在归一化之后执行,请按规范值书写",
rowFiltersEmpty: "无过滤则比对全部行。",
filterClear: "清空",
filterAddOr: "添加条件组(或)",
@ -110,12 +110,82 @@ const bizCompare = {
validateNeed: "请先选择映射与前后批次",
mapHint:
"每行一对:操作前接口,操作后接口(逗号/Tab/竖线分隔)。例:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# 井号开头为注释。不是 A,B/C,D 这种斜杠分组。",
templateMonitorUsage: "已被 {{n}} 个监控模板引用",
mapParsed: "已解析 {{n}} 对",
mapInvalidLines: "无效 {{m}} 行(第 {{lines}} 行)",
mapDuplicate: "前接口重复:{{list}}",
mapUnsaved: "未保存",
mapInvalid: "映射文本有误,未保存。无效行:{{lines}};重复的前接口:{{dup}}",
mapEmpty: "映射文本为空。若不需要映射,请选择“新建映射…”;否则请恢复内容后再保存。",
mapSharedConfirm: "该映射还被 {{jobs}} 个其他比对任务、{{projects}} 个割接项目使用,保存会同时影响它们。继续?",
mappingAutoSaved: "已自动保存映射",
dataState: {
"ok": "正常",
"no_data": "无数据",
"suspect_parse": "疑似解析失败",
"not_collected": "未采集",
"manual_pending": "待手动采集"
},
stateBanner: {
"no_data": "两侧都没有数据,没有可比较的内容,不计入通过率。",
"suspect_parse": "设备输出了内容但解析出 0 行,失败行可能是解析问题而非业务丢失,请先检查解析/模板。",
"not_collected": "某一侧没有采集该检查项(或缺少对应切片),不做判定,不计入通过率。"
},
rawKeyTitle: "操作前设备原始名称",
scopeBoth: "两侧",
scopeBefore: "仅操作前",
scopeAfter: "仅操作后",
valueMap: "值映射",
valueMapScope: "值映射范围",
valueMapPlaceholder: "每行:原值,规范值\nestab,established",
rankLabel: "好→坏顺序",
rankPlaceholder: "up,down(靠前更好)",
modeNotWorse: "不劣于",
modeNumericMin: "不低于(%)",
modeNumericMax: "不高于(%)",
filterSide: "过滤作用侧",
unmappedPortPolicy: "未映射端口",
unmappedByName: "按名称匹配(默认)",
unmappedIgnorePort: "忽略端口,按内容匹配",
unmappedPortPolicyHint: "只配了部分端口映射时,没有映射的端口如何配对。",
duplicatePairing: "同键多行配对",
pairSorted: "按取值排序后配对(推荐)",
pairOrdered: "按采集顺序配对(旧行为)",
duplicatePairingHint: "ECMP、多路径等同一个键出现多行时,CLI 输出顺序变化不应产生假差异。",
builtinVendorCanonical: "启用内置厂商接口名称统一(GigabitEthernet↔GE 等)",
mappingNeedBatches: "请先选择前后批次",
preflight: {
"run": "校验映射",
"ready": "映射可用",
"risk": "存在风险,请先处理",
"coverage": "已映射 {{mapped}} · 疑似漏配 {{suspect}} · 覆盖率 {{rate}}",
"pairs": "{{n}} 条映射",
"suspect": "疑似漏配(操作前有、未映射、操作后也不存在)",
"addSuspects": "加入映射编辑区(目标待填)",
"commandDiff": "两侧采集命令不一致",
"rows": "行数(前→后)",
"state": "数据状态",
"mapping": "映射",
"skippedHeavy": "大表,未逐个检查接口",
"sheetCounts": "已映射 {{exact}} · 同名 {{same}} · 疑似漏配 {{suspect}}",
"issue": {
"duplicate": "前接口重复(只取第一条)",
"conflicts": "归一化后前接口冲突",
"unused": "未命中任何接口的映射键",
"missingTargets": "映射目标在操作后设备上不存在",
"nTo1": "多个前接口指向同一目标(N:1)"
}
},
coverageBanner: "映射覆盖:{{n}} 个操作前端口没有映射且在操作后设备上不存在,缺失可能是漏配映射:",
mapViewText: "文本",
mapViewTable: "表格",
mapTable: {"before": "操作前接口", "after": "操作后接口", "status": "状态", "remove": "删除行", "add": "添加一行", "empty": "还没有映射行", "ok": "正常", "todo": "待填目标", "duplicate": "前接口重复", "invalid": "缺少前接口"},
mappingSaved: "映射已保存",
mappingOptionalHint:
"可选。留空=忽略本端端口改名,按非接口 Key 对齐。填写后才做端口映射校验。主接口映射会自动覆盖 .子接口(后缀不变)。",
ifaceNormalize: "接口归一化",
ifaceNormalizeHint:
"模板级规则,每行:简写,规范名。先归一化再端口映射。例:\nGE,gei\nSG,smartgroup\nXGE,xgei",
"模板级规则,每行:简写,规范名。先归一化再端口映射。例:;可选第 3/4 列限定:before/after 与厂商,如 Eth-Trunk,ae,after,huawei\nGE,gei\nSG,smartgroup\nXGE,xgei",
createJob: "建立比对任务",
defaultJobName: "割接比对",
jobName: "名称",

View file

@ -200,6 +200,18 @@ const bizMigration = {
startBatch: "启动",
finishBatch: "收工",
evaluate: "立即判定",
evalProgress: "{{label}}中 {{i}}/{{n}} {{title}}",
evalQueued: "{{label}}排队中…",
preflight: "映射预检",
preflightHint: "按两台设备的画像基线检查映射覆盖率与采集差异,割接前先看一眼。",
manualPendingBanner: "待手动采集(不计入验收门槛):{{metrics}}",
notCollectedBanner: "两侧采集不一致、不做判定(不判红):{{metrics}}",
duplicateWarningBanner: "本批范围外存在重复键(不影响验收,建议完善 key 字段):{{metrics}}",
redResolvableHint: "{{n}} 张红单对应的对象在最近一次验收中已转绿。",
redResolveAllResolvable: "一键关闭已转绿红单",
redNowGreen: "已转绿",
redResolvableClosed: "已关闭 {{n}} 张红单",
portSuccessHint: "端口迁移成功 = 旧端口(已删除 / 物理 down / 管理 down 任一)且新端口管理、物理都 up。拔纤但未 shutdown 也算完成。",
evaluated: "判定完成",
statusUpdated: "状态已更新",
board: "进度看板",