feat: RDG session flap rule, qwinsta/logoff queue and dashboard UI (0.9.13)

Detect 302→303 within 1–10s, Problem with 30s dedup, rdg_flap flag.
Agent command poll API, admin qwinsta/logoff from Overview. Migration 016.
This commit is contained in:
2026-06-19 23:34:01 +10:00
parent b328d32f97
commit 2aec488226
24 changed files with 1012 additions and 25 deletions
+95 -1
View File
@@ -9,11 +9,17 @@ from sqlalchemy import func, select
from sqlalchemy.orm import Session, joinedload
from app.auth.api_key import get_api_key_auth
from app.auth.jwt_auth import get_current_user
from app.auth.jwt_auth import get_current_user, require_admin
from app.config import get_settings
from app.database import get_db
from app.models import Event, Host
from app.schemas.list_models import EventDetail, EventListResponse, EventSummary
from app.services.agent_commands import (
command_to_dict,
get_command_by_uuid,
queue_logoff,
queue_qwinsta,
)
from app.services.ingest import ingest_event
from app.services.event_summary import event_to_summary
from app.services.problems import maybe_create_problem
@@ -168,6 +174,94 @@ def list_events(
return EventListResponse(items=items, total=total, page=page, page_size=page_size)
class AgentCommandResponse(BaseModel):
command_uuid: str
command_type: str
status: str
result_stdout: str | None = None
result_stderr: str | None = None
created_at: str | None = None
completed_at: str | None = None
class LogoffActionBody(BaseModel):
session_id: int
@router.post("/{event_db_id}/actions/qwinsta", response_model=AgentCommandResponse)
def post_event_qwinsta(
event_db_id: int,
db: Session = Depends(get_db),
user=Depends(require_admin),
) -> AgentCommandResponse:
event = db.get(Event, event_db_id)
if event is None:
raise HTTPException(status_code=404, detail="Event not found")
cmd = queue_qwinsta(db, event, requested_by=str(user))
db.commit()
data = command_to_dict(cmd)
return AgentCommandResponse(
command_uuid=data["id"],
command_type=data["type"],
status=data["status"],
result_stdout=data.get("result_stdout"),
result_stderr=data.get("result_stderr"),
created_at=data.get("created_at"),
completed_at=data.get("completed_at"),
)
@router.post("/{event_db_id}/actions/logoff", response_model=AgentCommandResponse)
def post_event_logoff(
event_db_id: int,
body: LogoffActionBody,
db: Session = Depends(get_db),
user=Depends(require_admin),
) -> AgentCommandResponse:
event = db.get(Event, event_db_id)
if event is None:
raise HTTPException(status_code=404, detail="Event not found")
cmd = queue_logoff(
db,
event,
session_id=body.session_id,
requested_by=str(user),
)
db.commit()
data = command_to_dict(cmd)
return AgentCommandResponse(
command_uuid=data["id"],
command_type=data["type"],
status=data["status"],
result_stdout=data.get("result_stdout"),
result_stderr=data.get("result_stderr"),
created_at=data.get("created_at"),
completed_at=data.get("completed_at"),
)
@router.get("/{event_db_id}/actions/{command_uuid}", response_model=AgentCommandResponse)
def get_event_action_status(
event_db_id: int,
command_uuid: str,
db: Session = Depends(get_db),
_user=Depends(get_current_user),
) -> AgentCommandResponse:
cmd = get_command_by_uuid(db, command_uuid)
if cmd is None or cmd.event_id != event_db_id:
raise HTTPException(status_code=404, detail="Command not found")
data = command_to_dict(cmd)
return AgentCommandResponse(
command_uuid=data["id"],
command_type=data["type"],
status=data["status"],
result_stdout=data.get("result_stdout"),
result_stderr=data.get("result_stderr"),
created_at=data.get("created_at"),
completed_at=data.get("completed_at"),
)
@router.get("/{event_db_id}", response_model=EventDetail)
def get_event(
event_db_id: int,