Add collect dashboard KPIs and create-job modal.

Align collection tasks with LLDP/config-sync chrome, and refresh active-task overview when jobs change.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-05 14:08:13 +08:00
parent 0100fb8430
commit 6642f4b8b5
11 changed files with 502 additions and 249 deletions

View file

@ -8,6 +8,7 @@ from .collection_service import (
build_collection_job_zip,
create_collection,
delete_collection_job,
get_collection_dashboard,
get_collection_job,
list_collection_jobs,
list_collection_runs,
@ -36,6 +37,11 @@ def api_eligible_ne(
return list_eligible_ne(db, page=page, page_size=page_size, keyword=keyword)
@router.get("/dashboard")
def api_collection_dashboard(db: Session = Depends(get_db)):
return get_collection_dashboard(db).model_dump()
@router.post("")
def api_create_collection(body: CollectionJobCreate, db: Session = Depends(get_db)):
return create_collection(db, body).model_dump()

View file

@ -41,3 +41,23 @@ class CollectionJobOut(BaseModel):
started_at: datetime | None
ended_at: datetime | None
last_run_at: datetime | None = None
class CollectionJobSummary(BaseModel):
id: str
title: str
status: str
ne_count: int
success_count: int
fail_count: int
created_at: datetime | None = None
started_at: datetime | None = None
ended_at: datetime | None = None
last_run_at: datetime | None = None
class CollectionDashboardOut(BaseModel):
job_count: int = 0
active_count: int = 0
running_job: CollectionJobSummary | None = None
last_job: CollectionJobSummary | None = None

View file

@ -20,7 +20,13 @@ from .collection_job_state import (
sync_job_progress,
_sync_job_counts,
)
from .collection_schemas import CollectionJobCreate, CollectionJobOut, CollectionRunOut
from .collection_schemas import (
CollectionDashboardOut,
CollectionJobCreate,
CollectionJobOut,
CollectionJobSummary,
CollectionRunOut,
)
from .ne_collection_paths import clear_run_output_files, collection_data_root
_log = logging.getLogger("netx.collection")
@ -94,6 +100,76 @@ def job_to_out(row: NeCollectionJob, *, output_count: int | None = None) -> Coll
)
def job_to_summary(row: NeCollectionJob | None) -> CollectionJobSummary | None:
if row is None:
return None
return CollectionJobSummary(
id=str(row.id),
title=str(row.title or "").strip() or str(row.id)[:8],
status=str(row.status or "pending"),
ne_count=int(row.ne_count or 0),
success_count=int(row.success_count or 0),
fail_count=int(row.fail_count or 0),
created_at=row.created_at,
started_at=row.started_at,
ended_at=row.ended_at,
last_run_at=row.last_run_at,
)
def has_active_collection_job(db: Session) -> NeCollectionJob | None:
return (
db.query(NeCollectionJob)
.filter(NeCollectionJob.status.in_(("pending", "running", "paused")))
.order_by(NeCollectionJob.created_at.desc())
.first()
)
def last_finished_collection_job(db: Session) -> NeCollectionJob | None:
return (
db.query(NeCollectionJob)
.filter(NeCollectionJob.status.in_(("done", "failed")))
.order_by(NeCollectionJob.created_at.desc())
.first()
)
def get_collection_dashboard(db: Session) -> CollectionDashboardOut:
job_count = int(db.query(NeCollectionJob).count() or 0)
active_count = int(
db.query(NeCollectionJob)
.filter(NeCollectionJob.status.in_(("pending", "running", "paused")))
.count()
or 0
)
running = has_active_collection_job(db)
if running is not None:
jid = str(running.id)
if str(running.status or "") not in ("done", "failed", "paused"):
reconcile_stale_collection_job(db, jid)
db.refresh(running)
if str(running.status or "") == "running":
sync_job_progress(db, jid)
finalize_collection_job(db, jid)
db.refresh(running)
if str(running.status or "") not in ("pending", "running", "paused"):
running = has_active_collection_job(db)
active_count = int(
db.query(NeCollectionJob)
.filter(NeCollectionJob.status.in_(("pending", "running", "paused")))
.count()
or 0
)
last = last_finished_collection_job(db)
return CollectionDashboardOut(
job_count=job_count,
active_count=active_count,
running_job=job_to_summary(running),
last_job=job_to_summary(last),
)
def run_to_out(row: NeCollectionRun) -> CollectionRunOut:
rel = str(row.output_rel_path or "").strip()
return CollectionRunOut(

View file

@ -185,6 +185,7 @@ def _collection_items(db: Session, actors: dict[str, str]) -> list[dict[str, Any
ok = int(row.success_count or 0)
fail = int(row.fail_count or 0)
total = int(row.ne_count or 0)
done = ok + fail
items.append(
_item(
kind="ne_collect",
@ -195,7 +196,7 @@ def _collection_items(db: Session, actors: dict[str, str]) -> list[dict[str, Any
actor=actor,
started_at=row.started_at or row.created_at,
updated_at=row.last_run_at or row.ended_at or row.started_at or row.created_at,
progress=f"{ok}/{fail}/{total}",
progress=f"{done}/{total}",
inflight=int(running),
detail=str(row.error_message or "")[:240],
href="/network/tasks/collect",