mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 14:33:19 +08:00
feat: refine cutover operations board and monitoring performance
Protect previous wave coverage, normalize batch identities, cache shared metric evidence, and batch diff inserts. Add live continuity KPIs, server paging and filters, scoped business selection, and preserve legacy monitor rule metadata. Validation: 198 backend tests passed with 1 opt-in benchmark skipped; 14 frontend unit tests and 24 browser checks passed; TypeScript and production build passed.
This commit is contained in:
parent
fe4c1db763
commit
32d7d969f6
12 changed files with 731 additions and 151 deletions
|
|
@ -7,9 +7,10 @@ from typing import Any
|
|||
from uuid import uuid4
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import insert
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..biz_state.compare_rules import apply_row_filters
|
||||
from ..biz_state.compare_rules import apply_row_filters, scalar_text
|
||||
from ..biz_state.compare_service import (
|
||||
_load_metric_rows,
|
||||
_port_map_dict,
|
||||
|
|
@ -949,6 +950,13 @@ def run_evaluate(
|
|||
from ..biz_state.compare_validation import validate_template_body
|
||||
|
||||
validate_template_body({"metrics": sheets, "iface_normalize_rules": iface_norm})
|
||||
from ..biz_state.iface_normalize import normalize_iface_name
|
||||
|
||||
normalized_map = {normalize_iface_name(k, iface_norm): normalize_iface_name(v, iface_norm)
|
||||
for k, v in port_map.items()}
|
||||
if len(normalized_map) != len(port_map) or len(set(normalized_map.values())) != len(normalized_map):
|
||||
raise HTTPException(status_code=400, detail="mapping_target_ambiguous")
|
||||
port_map = normalized_map
|
||||
known_sheets = {sheet_key(s) for s in sheets}
|
||||
known_metrics = {str(s.get("metric_id") or "") for s in sheets}
|
||||
for override in sheet_overrides:
|
||||
|
|
@ -967,7 +975,8 @@ def run_evaluate(
|
|||
rows_cache: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
||||
command_cache: dict[str, dict[str, Any]] = {}
|
||||
scope_ids = {str(s.get("metric_id") or "") for s in sheets} | {sheet_key(s) for s in sheets} | {"_ports"}
|
||||
uncovered_scope = sorted(k for k, values in expect.items() if values and k not in scope_ids)
|
||||
uncovered_scope = sorted({k for scope in (expect, previous_expect) for k, values in scope.items()
|
||||
if values and k not in scope_ids})
|
||||
|
||||
def metric_rows(bid: str, mid: str) -> list[dict[str, Any]]:
|
||||
key = (bid, mid)
|
||||
|
|
@ -1116,6 +1125,7 @@ def run_evaluate(
|
|||
"progress_total": one["progress_total"],
|
||||
"anomaly": one["anomaly"],
|
||||
"anomaly_in_expect": int(one.get("anomaly_in_expect") or 0),
|
||||
"steady_outside": int(one.get("steady_outside") or 0),
|
||||
"old_summary": one["old_summary"],
|
||||
"new_summary": one["new_summary"],
|
||||
"new_baseline_mode": one.get("new_baseline_mode") or "provided",
|
||||
|
|
@ -1189,12 +1199,14 @@ def run_evaluate(
|
|||
],
|
||||
"verdict_counts": verdict_counts,
|
||||
"anomaly_outside_expect": sum(int(c.get("anomaly") or 0) - int(c.get("anomaly_in_expect") or 0) for c in active_cards),
|
||||
"steady_outside": sum(int(c.get("steady_outside") or 0) for c in active_cards),
|
||||
"duplicate_keys": sum(int(c.get("duplicate_keys_before") or 0) + int(c.get("duplicate_keys_after") or 0) for c in active_cards),
|
||||
"uncovered_scope": uncovered_scope,
|
||||
"coverage_complete": bool(sheet_cards) and not uncovered_scope and not any(c.get("collect_skipped") or c.get("current_missing") or c.get("awaiting_peer") or c.get("duplicate_keys_before") or c.get("duplicate_keys_after") for c in sheet_cards),
|
||||
"config_snapshot": {"sheets": sheets, "sheet_overrides": sheet_overrides,
|
||||
"defaults": defaults, "iface_normalize": iface_norm,
|
||||
"port_map": port_map, "expect_set": mb.expect_set_json},
|
||||
"port_map": port_map, "expect_set": mb.expect_set_json,
|
||||
"previous_expect": {k: sorted(v) for k, v in previous_expect.items()}},
|
||||
"window_active": window_active,
|
||||
"expect_ports": sorted(expect.get("_ports") or ()),
|
||||
"old_current_task_id": current_task_id(proj, "old"),
|
||||
|
|
@ -1204,6 +1216,7 @@ def run_evaluate(
|
|||
)
|
||||
db.add(run)
|
||||
db.flush()
|
||||
pending_diffs: list[dict[str, Any]] = []
|
||||
for r in all_rows:
|
||||
key_list = r.get("key") or []
|
||||
ev = r.get("evidence") if isinstance(r.get("evidence"), dict) else {}
|
||||
|
|
@ -1222,8 +1235,8 @@ def run_evaluate(
|
|||
str(r.get("metric_id") or ""),
|
||||
]
|
||||
)
|
||||
db.add(
|
||||
BizMigrationDiff(
|
||||
pending_diffs.append(
|
||||
dict(
|
||||
id=uuid4().hex,
|
||||
run_id=run.id,
|
||||
metric_id=str(r.get("metric_id") or ""),
|
||||
|
|
@ -1254,6 +1267,11 @@ def run_evaluate(
|
|||
search_text=search[:2000],
|
||||
)
|
||||
)
|
||||
if len(pending_diffs) >= 500:
|
||||
db.execute(insert(BizMigrationDiff), pending_diffs)
|
||||
pending_diffs.clear()
|
||||
if pending_diffs:
|
||||
db.execute(insert(BizMigrationDiff), pending_diffs)
|
||||
db.flush()
|
||||
if _commit:
|
||||
db.commit()
|
||||
|
|
@ -1436,6 +1454,11 @@ def list_baseline_expect_objects(db: Session, project_id: str) -> dict[str, Any]
|
|||
collect_ids = set(resolve_collect_metric_ids(db, p))
|
||||
port_map = _port_map_dict(db, p.mapping_id)
|
||||
out_sheets: list[dict[str, Any]] = []
|
||||
from ..biz_state.iface_normalize import apply_iface_normalize, resolve_mapped_iface, normalize_iface_name
|
||||
from .evaluate import KEY_SEP
|
||||
|
||||
rows_cache: dict[str, list[dict[str, Any]]] = {}
|
||||
port_map = {normalize_iface_name(k, _norm): normalize_iface_name(v, _norm) for k, v in port_map.items()}
|
||||
for sheet in sheets:
|
||||
mid = str(sheet.get("metric_id") or "").strip()
|
||||
sid = sheet_key(sheet)
|
||||
|
|
@ -1449,25 +1472,32 @@ def list_baseline_expect_objects(db: Session, project_id: str) -> dict[str, Any]
|
|||
if sheet_ov.get("skip_dual"):
|
||||
continue
|
||||
iface_fields = [str(k) for k in (sheet.get("iface_fields") or []) if str(k).strip()]
|
||||
if mid not in rows_cache:
|
||||
rows_cache[mid] = _load_metric_rows(db, batch_id=p.old_baseline_batch_id, metric_id=mid)
|
||||
rows_raw = apply_row_filters(
|
||||
_load_metric_rows(db, batch_id=p.old_baseline_batch_id, metric_id=mid),
|
||||
rows_cache[mid],
|
||||
list(sheet.get("row_filters") or []),
|
||||
)
|
||||
items: list[dict[str, Any]] = []
|
||||
seen_keys: set[str] = set()
|
||||
for r in rows_raw:
|
||||
keys = [str(r.get(f) or "").strip() for f in key_fields]
|
||||
normalized = apply_iface_normalize(dict(r), iface_fields=iface_fields, rules=_norm)
|
||||
keys = [scalar_text(normalized.get(f)).strip() for f in key_fields]
|
||||
if not any(keys):
|
||||
continue
|
||||
key_str = "|".join(keys)
|
||||
key_str = KEY_SEP.join(keys)
|
||||
if key_str in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key_str)
|
||||
mapped = ""
|
||||
if iface_fields and key_fields and key_fields[0] in iface_fields:
|
||||
mapped = port_map.get(keys[0]) or ""
|
||||
mapped = resolve_mapped_iface(keys[0], port_map) if port_map else ""
|
||||
items.append(
|
||||
{
|
||||
"key": key_str,
|
||||
"keys": keys,
|
||||
"mapped_to": mapped,
|
||||
"label": key_str,
|
||||
"label": "|".join(scalar_text(r.get(f)).strip() for f in key_fields),
|
||||
"row": {f: r.get(f) for f in list(dict.fromkeys([*key_fields, *iface_fields, "description", "admin", "phy", "prot"])) if f in r},
|
||||
}
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue