Fix port traffic wall chart refresh and first-sample visibility.

Trigger collect on start, normalize sample time bounds, and keep the uPlot series updating live.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-07-31 15:10:33 +08:00
parent efe8f58cd2
commit eea4043848
9 changed files with 240 additions and 54 deletions

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter, Depends, Query, Request
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from .auth_service import write_audit
@ -56,10 +56,15 @@ def api_list_tasks(
@router.post("/tasks")
def api_create_task(
body: PortTrafficTaskCreate,
background_tasks: BackgroundTasks,
request: Request,
db: Session = Depends(get_db),
):
out = create_task(db, body)
if body.start_now and out.status == "running":
from .port_traffic_runner import dispatch_collect
background_tasks.add_task(dispatch_collect, out.id)
uid, uname = _actor(request)
write_audit(
db,
@ -119,8 +124,16 @@ def api_delete_task(task_id: str, request: Request, db: Session = Depends(get_db
@router.post("/tasks/{task_id}/start")
def api_start_task(task_id: str, request: Request, db: Session = Depends(get_db)):
def api_start_task(
task_id: str,
background_tasks: BackgroundTasks,
request: Request,
db: Session = Depends(get_db),
):
out = set_task_status(db, task_id, "running")
from .port_traffic_runner import dispatch_collect
background_tasks.add_task(dispatch_collect, task_id)
uid, uname = _actor(request)
write_audit(
db,
@ -135,6 +148,46 @@ def api_start_task(task_id: str, request: Request, db: Session = Depends(get_db)
return out.model_dump()
@router.post("/tasks/{task_id}/collect-now")
def api_collect_now(
task_id: str,
background_tasks: BackgroundTasks,
request: Request,
db: Session = Depends(get_db),
):
task = get_task(db, task_id)
if task.status not in ("running", "paused", "draft", "stopped"):
raise HTTPException(status_code=400, detail="invalid_status")
# Force due: clear last end so claim accepts, ensure running for this round.
from .models import PortTrafficTask
from .port_traffic_runner import dispatch_collect
row = db.get(PortTrafficTask, task_id)
if not row:
raise HTTPException(status_code=404, detail="task_not_found")
if bool(row.collect_running):
return {"ok": True, "started": False, "reason": "already_collecting", **task.model_dump()}
if str(row.status) != "running":
row.status = "running"
row.last_collect_ended_at = None
row.updated_at = datetime.utcnow()
db.commit()
background_tasks.add_task(dispatch_collect, task_id)
uid, uname = _actor(request)
write_audit(
db,
action="port_traffic.task.collect_now",
actor_user_id=uid,
actor_username=uname,
method="POST",
path=f"/v1/port-traffic/tasks/{task_id}/collect-now",
status_code=200,
detail={"id": task_id},
)
out = get_task(db, task_id)
return {"ok": True, "started": True, **out.model_dump()}
@router.post("/tasks/{task_id}/pause")
def api_pause_task(task_id: str, request: Request, db: Session = Depends(get_db)):
out = set_task_status(db, task_id, "paused")

View file

@ -3,7 +3,7 @@
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any
from uuid import uuid4
@ -204,11 +204,10 @@ def set_task_status(db: Session, task_id: str, status: str) -> PortTrafficTaskOu
)
if active <= 0:
raise HTTPException(status_code=400, detail="no_active_targets")
# Allow scheduler/collect-now to fire immediately after start/resume.
task.last_collect_ended_at = None
task.status = status
task.updated_at = _utcnow()
if status in ("stopped", "paused"):
# leave collect_running for runner to finish; recovery clears stuck flags
pass
db.commit()
db.refresh(task)
return _task_out(db, task)
@ -308,6 +307,15 @@ def discover_ports(db: Session, body: DiscoverPortsRequest) -> DiscoverPortsResp
)
def _as_naive_utc(value: datetime | None) -> datetime | None:
"""Normalize query bounds to naive UTC (DB columns are naive utcnow)."""
if value is None:
return None
if value.tzinfo is None:
return value
return value.astimezone(timezone.utc).replace(tzinfo=None)
def get_samples(
db: Session,
*,
@ -319,10 +327,10 @@ def get_samples(
if not target:
raise HTTPException(status_code=404, detail="target_not_found")
now = _utcnow()
if to_ts is None:
to_ts = now
if from_ts is None:
from_ts = to_ts - timedelta(hours=1)
to_ts = _as_naive_utc(to_ts) or now
from_ts = _as_naive_utc(from_ts) or (to_ts - timedelta(hours=1))
# Slight skew so just-written samples are not clipped by client clock.
to_ts = to_ts + timedelta(seconds=5)
rows = (
db.query(PortTrafficSample)
.filter(