Files
stealergram/web/routes/dashboard.py
anti 741e6bb0d3 Rename to stealergram, add pyproject.toml, purge em-dashes
- Rename project to stealergram throughout
- Add pyproject.toml (replaces requirements.txt split, folds pytest.ini)
- Replace all em-dashes with hyphens across all source files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 10:06:30 -04:00

109 lines
3.5 KiB
Python

"""
web/routes/dashboard.py - Dashboard views and SSE live stream.
GET / → redirect to /dashboard
GET /dashboard → overview: all groups, stats, live hit feed
GET /dashboard/groups/{group_id} → per-group hits + filtered SSE
GET /api/stream → SSE event stream (one hit per event)
GET /api/stats → JSON severity counts (for hx-trigger refresh)
"""
import asyncio
import json
import queue
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse, StreamingResponse
import config
from tui import events as bus
from utils import database as hitdb
from web.dependencies import require_role
router = APIRouter()
def _templates(request: Request):
return request.app.state.templates
def _hit_to_dict(hit) -> dict:
reasons_raw = hit["reasons"] or ""
reasons = [r.strip() for r in reasons_raw.split("|") if r.strip()]
return {
"severity": hit["severity"],
"score": hit["score"],
"raw": hit["raw"],
"source": hit["source"],
"filename": hit["filename"],
"reasons": reasons,
"timestamp": hit["timestamp"],
}
@router.get("/")
async def root():
return RedirectResponse(url="/dashboard")
@router.get("/dashboard")
async def dashboard(request: Request, user=Depends(require_role("reader"))):
hits = [_hit_to_dict(h) for h in hitdb.recent(limit=50)]
counts = hitdb.count_by_severity()
groups = config.KEYWORD_GROUPS
return _templates(request).TemplateResponse(
request, "dashboard.html",
{"user": dict(user), "hits": hits, "counts": counts, "groups": groups},
)
@router.get("/dashboard/groups/{group_id}")
async def group_detail(request: Request, group_id: str, user=Depends(require_role("reader"))):
groups = config.KEYWORD_GROUPS
group = next((g for g in groups if g["id"] == group_id), None)
if group is None:
raise HTTPException(status_code=404, detail="Group not found")
patterns = [p["regex"] for p in group.get("patterns", [])]
hits = [_hit_to_dict(h) for h in hitdb.recent_for_domains(patterns, limit=100)]
counts = hitdb.count_by_severity_for_domains(patterns)
return _templates(request).TemplateResponse(
request, "group_detail.html",
{"user": dict(user), "group": group, "hits": hits, "counts": counts},
)
@router.get("/api/stream")
async def event_stream(request: Request, _user=Depends(require_role("reader"))):
"""Server-Sent Events: one data frame per EvHit."""
q = bus.subscribe()
async def generate():
try:
while True:
if await request.is_disconnected():
break
try:
ev = q.get_nowait()
if isinstance(ev, bus.EvHit):
payload = json.dumps({
"severity": ev.severity,
"raw": ev.raw,
"source": ev.source,
"filename": ev.filename,
"reasons": ev.reasons,
})
yield f"data: {payload}\n\n"
except queue.Empty:
await asyncio.sleep(0.1)
finally:
bus.unsubscribe(q)
return StreamingResponse(generate(), media_type="text/event-stream")
@router.get("/api/stats")
async def stats(_user=Depends(require_role("reader"))):
return hitdb.count_by_severity()