Improve biz-compare UX with guided create, sheet toggles, and per-run delete.

Also sticky template field headers and biz-state pause/start endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-20 16:29:50 +08:00
parent 9b3e98ede5
commit 3d2489041a
12 changed files with 669 additions and 155 deletions

View file

@ -1248,11 +1248,22 @@ def _job_out(j: BizCompareJob) -> dict[str, Any]:
"after_batch_id": j.after_batch_id,
"mode": j.mode,
"status": j.status,
"enabled_sheet_ids": _str_list(getattr(j, "enabled_sheet_ids", None)),
"note": j.note,
"updated_at": j.updated_at.isoformat() + "Z" if j.updated_at else None,
}
def _filter_enabled_sheets(
sheets_cfg: list[dict[str, Any]], enabled_sheet_ids: list[str] | None
) -> list[dict[str, Any]]:
"""Empty enabled list → all sheets; else keep matching sheet_id only."""
allowed = set(_str_list(enabled_sheet_ids))
if not allowed:
return sheets_cfg
return [s for s in sheets_cfg if sheet_key(s) in allowed]
def list_jobs(db: Session) -> list[dict[str, Any]]:
rows = db.query(BizCompareJob).order_by(BizCompareJob.updated_at.desc()).all()
return [_job_out(j) for j in rows]
@ -1267,6 +1278,7 @@ def create_job(db: Session, body: dict[str, Any]) -> dict[str, Any]:
else:
if not db.get(BizCompareTemplate, template_id):
raise HTTPException(status_code=404, detail="template_not_found")
enabled = _str_list(body.get("enabled_sheet_ids"))
j = BizCompareJob(
id=uuid4().hex,
name=str(body.get("name") or "compare")[:256],
@ -1278,6 +1290,7 @@ def create_job(db: Session, body: dict[str, Any]) -> dict[str, Any]:
after_batch_id=str(body.get("after_batch_id") or ""),
mode=str(body.get("mode") or "manual")[:16],
status="ready",
enabled_sheet_ids=enabled,
note=str(body.get("note") or "")[:512],
created_at=_utcnow(),
updated_at=_utcnow(),
@ -1307,6 +1320,8 @@ def update_job(db: Session, job_id: str, body: dict[str, Any]) -> dict[str, Any]
):
if key in body and body.get(key) is not None:
setattr(j, key, str(body.get(key) or ""))
if "enabled_sheet_ids" in body:
j.enabled_sheet_ids = _str_list(body.get("enabled_sheet_ids"))
j.updated_at = _utcnow()
db.commit()
return _job_out(j)
@ -1328,6 +1343,20 @@ def delete_job(db: Session, job_id: str) -> None:
db.commit()
def delete_run(db: Session, run_id: str) -> dict[str, Any]:
"""Delete one compare run and its diffs; leave the job intact."""
r = db.get(BizCompareRun, run_id)
if not r:
raise HTTPException(status_code=404, detail="run_not_found")
job_id = str(r.job_id or "")
db.query(BizCompareDiff).filter(BizCompareDiff.run_id == run_id).delete(
synchronize_session=False
)
db.delete(r)
db.commit()
return {"ok": True, "job_id": job_id, "run_id": run_id}
def _resolve_after_batch(db: Session, job: BizCompareJob) -> str:
if job.mode != "auto":
return str(job.after_batch_id or "")
@ -1423,6 +1452,9 @@ def run_compare(db: Session, job_id: str, *, force_after_batch_id: str = "") ->
sheets_cfg = template_metrics(tpl)
if not sheets_cfg:
raise HTTPException(status_code=400, detail="template_has_no_metrics")
sheets_cfg = _filter_enabled_sheets(sheets_cfg, getattr(j, "enabled_sheet_ids", None))
if not sheets_cfg:
raise HTTPException(status_code=400, detail="no_enabled_sheets")
pmap = _port_map_dict(db, j.mapping_id)
norm_rules = template_iface_normalize(tpl)

View file

@ -22,6 +22,7 @@ def apply_biz_state_schema(conn: Connection) -> None:
"CREATE INDEX IF NOT EXISTS ix_biz_state_batch_task_id ON biz_state_batch (task_id)",
"CREATE INDEX IF NOT EXISTS ix_biz_state_batch_command_batch_id ON biz_state_batch_command (batch_id)",
"CREATE INDEX IF NOT EXISTS ix_biz_state_lldp_neighbor_batch_id ON biz_state_lldp_neighbor (batch_id)",
"ALTER TABLE biz_compare_job ADD COLUMN IF NOT EXISTS enabled_sheet_ids JSON DEFAULT '[]'",
"CREATE INDEX IF NOT EXISTS ix_biz_compare_job_status ON biz_compare_job (status)",
"CREATE INDEX IF NOT EXISTS ix_biz_compare_run_job_id ON biz_compare_run (job_id)",
"CREATE INDEX IF NOT EXISTS ix_biz_state_vrf_route_batch_id ON biz_state_vrf_route_summary (batch_id)",

View file

@ -235,6 +235,11 @@ def _replace_items(db: Session, task_id: str, items_in: list[dict[str, Any]]) ->
)
def set_task_status(db: Session, task_id: str, status: str) -> dict[str, Any]:
"""Set lifecycle status: draft | running | paused | stopped."""
return update_task(db, task_id, {"status": status})
def update_task(db: Session, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
task = db.get(BizStateTask, task_id)
if not task:

View file

@ -174,6 +174,18 @@ def api_patch_task(
return svc.update_task(db, task_id, body.model_dump(exclude_unset=True))
@router.post("/tasks/{task_id}/pause")
def api_pause_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
"""Pause periodic schedule; manual collect remains allowed."""
return svc.set_task_status(db, task_id, "paused")
@router.post("/tasks/{task_id}/start")
def api_start_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
"""Enable periodic schedule (requires bindings for non–cutover-HF tasks)."""
return svc.set_task_status(db, task_id, "running")
@router.delete("/tasks/{task_id}")
def api_delete_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
svc.delete_task(db, task_id)
@ -374,6 +386,8 @@ class CompareJobIn(BaseModel):
before_batch_id: str = ""
after_batch_id: str = ""
mode: str = "manual"
# Empty = all template sheets; non-empty = only these sheet_id values
enabled_sheet_ids: list[str] = Field(default_factory=list)
note: str = ""
@ -478,6 +492,11 @@ def api_get_run(run_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.get_run(db, run_id)
@router.delete("/compare/runs/{run_id}")
def api_delete_run(run_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
return cmp_svc.delete_run(db, run_id)
@router.get("/compare/runs/{run_id}/diffs")
def api_list_run_diffs(
run_id: str,

View file

@ -293,6 +293,8 @@ class BizCompareJob(Base):
# manual: fixed after_batch; auto: after_batch_id empty → use latest after task batch
mode: Mapped[str] = mapped_column(String(16), default="manual", index=True)
status: Mapped[str] = mapped_column(String(32), default="draft", index=True) # draft|ready|auto
# Empty = all template sheets; non-empty = only these sheet_id values
enabled_sheet_ids: Mapped[list] = mapped_column(_JsonType, default=list)
note: Mapped[str] = mapped_column(String(512), default="")
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)