feat(dashboards): scheduler health strip, failing-source rollup, 24h activity sparkline, credential staleness nudge
D1 scheduler visibility: AppSetting last-tick stamp on every Beat tick + GET /api/sources/schedule-status (last_tick_at/next_due_at/due_now/auto_sources) + SchedulerStatusBar on the Subscriptions tab (re-polled every 30s). D2 failing-source rollup: ?failing=true on the sources list + FailingSourcesCard on Downloads with per-source and bulk "retry" (re-runs the feed via /check). D3 activity sparkline: GET /api/downloads/activity hourly buckets + CSS bar chart by the stat chips (failures stacked in error color); refreshes on live poll. D4 credential staleness: surface last_verified age + "re-verify recommended" warning past 30d; also fixes the dead last_verified_at field-name mismatch so the verification row renders at all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,3 +126,23 @@ async def test_stats_window_hours_rejects_out_of_range(client):
|
||||
assert resp.status_code == 400
|
||||
resp = await client.get("/api/downloads/stats?window_hours=bogus")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activity_returns_fixed_bucket_array(client, seed):
|
||||
resp = await client.get("/api/downloads/activity")
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["hours"] == 24
|
||||
assert len(body["buckets"]) == 24
|
||||
assert sum(b["total"] for b in body["buckets"]) == 2
|
||||
# Both seed events were created "now" → land in the newest bucket.
|
||||
newest = body["buckets"][-1]
|
||||
assert newest["ok"] == 1
|
||||
assert newest["error"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activity_rejects_bogus_hours(client):
|
||||
resp = await client.get("/api/downloads/activity?hours=bogus")
|
||||
assert resp.status_code == 400
|
||||
|
||||
Reference in New Issue
Block a user