Add system alerts log with nav badge and Admin Alerts tab.
Deploy / lint (push) Successful in 8s
Deploy / test (push) Failing after 1m4s
Deploy / deploy (push) Has been skipped

Persist job and ingestion warnings/errors for 7 days, surface a dismissible top-nav badge, treat stale OHLCV as a warning (e.g. ticker renames), and show market bar age on the ticker freshness chip.
This commit is contained in:
2026-07-14 13:38:04 +02:00
parent ed82d0a665
commit f59c0c3484
16 changed files with 825 additions and 7 deletions
+49 -1
View File
@@ -6,7 +6,7 @@ All endpoints require admin role.
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, require_admin
from app.dependencies import get_db, require_access, require_admin
from app.models.user import User
from app.schemas.admin import (
ActivationConfigUpdate,
@@ -29,6 +29,7 @@ from app.schemas.common import APIEnvelope
from app.services import admin_service
from app.services import alert_service
from app.services import sentiment_provider_service
from app.services import system_event_service
from app.services import ticker_universe_service
router = APIRouter(tags=["admin"])
@@ -403,3 +404,50 @@ async def toggle_job(
status="success",
data={"key": setting.key, "value": setting.value},
)
# ---------------------------------------------------------------------------
# System events (operational warnings / errors)
# ---------------------------------------------------------------------------
@router.get("/admin/system-events", response_model=APIEnvelope)
async def list_system_events(
days: int = Query(7, ge=1, le=30),
severity: str | None = Query(None, description="warning | error"),
unacknowledged_only: bool = Query(False),
_user: User = Depends(require_access),
db: AsyncSession = Depends(get_db),
):
"""List recent system events (default last 7 days)."""
rows = await system_event_service.list_events(
db,
days=days,
severity=severity,
unacknowledged_only=unacknowledged_only,
)
return APIEnvelope(
status="success",
data=[system_event_service.event_to_dict(r) for r in rows],
)
@router.get("/admin/system-events/summary", response_model=APIEnvelope)
async def system_events_summary(
days: int = Query(7, ge=1, le=30),
_user: User = Depends(require_access),
db: AsyncSession = Depends(get_db),
):
"""Badge counts for the top nav."""
data = await system_event_service.summary(db, days=days)
return APIEnvelope(status="success", data=data)
@router.post("/admin/system-events/acknowledge", response_model=APIEnvelope)
async def acknowledge_system_events(
days: int = Query(7, ge=1, le=30),
_user: User = Depends(require_access),
db: AsyncSession = Depends(get_db),
):
"""Dismiss unacknowledged events in the lookback window (clears the badge)."""
count = await system_event_service.acknowledge_all(db, days=days)
return APIEnvelope(status="success", data={"acknowledged": count})
+21 -1
View File
@@ -117,12 +117,32 @@ async def fetch_symbol(
result = await ingestion_service.fetch_and_ingest(
db, provider, symbol_upper, start_date, end_date
)
status_map = {"complete": "ok", "partial": "ok", "no_data": "warning"}
# "stale" = provider returned nothing but our last bar is old
# (rename/delist/halt) — must not look like a successful refresh.
status_map = {
"complete": "ok",
"partial": "ok",
"no_data": "warning",
"stale": "warning",
}
sources_out["ohlcv"] = {
"status": status_map.get(result.status, "error"),
"records": result.records_ingested,
"message": result.message,
"last_date": result.last_date.isoformat() if result.last_date else None,
}
if result.status in ("stale", "no_data", "error"):
from app.services.system_event_service import log_event
await log_event(
db,
severity="warning" if result.status != "error" else "error",
source="ingestion",
code=f"ohlcv_{result.status}",
message=result.message or f"OHLCV fetch {result.status} for {symbol_upper}",
symbol=symbol_upper,
dedup_key=f"ohlcv_{result.status}:{symbol_upper}",
)
except Exception as exc:
logger.error("OHLCV fetch failed for %s: %s", symbol_upper, exc)
sources_out["ohlcv"] = {"status": "error", "records": 0, "message": str(exc)}