Add system alerts log with nav badge and Admin Alerts tab.
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:
@@ -60,3 +60,30 @@ async def test_empty_fetch_with_existing_history_is_up_to_date(session):
|
||||
|
||||
assert result.status == "complete"
|
||||
assert result.records_ingested == 0
|
||||
|
||||
|
||||
async def test_empty_fetch_with_stale_history_reports_stale(session):
|
||||
# Provider returns nothing but last stored bar is weeks old — classic
|
||||
# rename/delist case (e.g. SATS after it became ECHO). Must not look like success.
|
||||
await _add_ticker(session, "SATS")
|
||||
today = date.today()
|
||||
old = [
|
||||
OHLCVData(
|
||||
ticker="SATS",
|
||||
date=today - timedelta(days=20 + i),
|
||||
open=100.0,
|
||||
high=101.0,
|
||||
low=99.0,
|
||||
close=100.0,
|
||||
volume=1000,
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=old), "SATS")
|
||||
|
||||
result = await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=[]), "SATS")
|
||||
|
||||
assert result.status == "stale"
|
||||
assert result.records_ingested == 0
|
||||
assert result.last_date is not None
|
||||
assert "renamed" in (result.message or "").lower() or "halted" in (result.message or "").lower()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""System event log: persist, dedup, summary, acknowledge."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services import system_event_service as svc
|
||||
from tests.conftest import _test_session_factory # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def session():
|
||||
async with _test_session_factory() as s:
|
||||
yield s
|
||||
|
||||
|
||||
async def test_log_and_list(session):
|
||||
row = await svc.log_event(
|
||||
session,
|
||||
severity="warning",
|
||||
source="ingestion",
|
||||
code="ohlcv_stale",
|
||||
message="No new bars since 2026-06-23",
|
||||
symbol="SATS",
|
||||
dedup_key="ohlcv_stale:SATS",
|
||||
)
|
||||
assert row is not None
|
||||
assert row.severity == "warning"
|
||||
|
||||
events = await svc.list_events(session, days=7)
|
||||
assert len(events) == 1
|
||||
assert events[0].symbol == "SATS"
|
||||
|
||||
|
||||
async def test_dedup_suppresses_repeat(session):
|
||||
first = await svc.log_event(
|
||||
session,
|
||||
severity="error",
|
||||
source="data_collector",
|
||||
code="job_error",
|
||||
message="boom",
|
||||
dedup_key="job:data_collector:error",
|
||||
)
|
||||
second = await svc.log_event(
|
||||
session,
|
||||
severity="error",
|
||||
source="data_collector",
|
||||
code="job_error",
|
||||
message="boom again",
|
||||
dedup_key="job:data_collector:error",
|
||||
)
|
||||
assert first is not None
|
||||
assert second is None
|
||||
events = await svc.list_events(session, days=7)
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
async def test_summary_and_acknowledge(session):
|
||||
await svc.log_event(
|
||||
session,
|
||||
severity="error",
|
||||
source="job",
|
||||
code="job_error",
|
||||
message="fail",
|
||||
dedup_key="a",
|
||||
)
|
||||
await svc.log_event(
|
||||
session,
|
||||
severity="warning",
|
||||
source="job",
|
||||
code="job_rate_limited",
|
||||
message="slow",
|
||||
dedup_key="b",
|
||||
)
|
||||
s = await svc.summary(session, days=7)
|
||||
assert s["total"] == 2
|
||||
assert s["unacknowledged"] == 2
|
||||
assert s["unacknowledged_errors"] == 1
|
||||
assert s["unacknowledged_warnings"] == 1
|
||||
|
||||
n = await svc.acknowledge_all(session, days=7)
|
||||
assert n == 2
|
||||
s2 = await svc.summary(session, days=7)
|
||||
assert s2["unacknowledged"] == 0
|
||||
assert s2["total"] == 2
|
||||
Reference in New Issue
Block a user