admin: read/unread triage for load errors (unread by default, mark read/all)
The load-error log had no way to clear reviewed entries. Add a read_at column to
client_errors and a read/unread model mirroring the feedback inbox:
- GET /api/admin/client-errors?show=unread|read|all (default unread; returns id+read)
- POST /api/admin/client-errors/read-all (mark all unread read)
- POST /api/admin/client-errors/{id}/read {read: bool} (per-row toggle)
Headline stat is now "Unread load errors" (admin_stats.client_errors.unread), so the
red badge clears as you triage. Admin UI: Unread/Read/All tabs, a "Mark all read"
button, and a per-row ✓/↩ toggle; reading an entry drops it from the default view.
14-day auto-prune still bounds the table. Tests cover filter, toggle, mark-all,
404, and gating.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -484,6 +484,37 @@ def test_client_error_telemetry(tmp_path, monkeypatch):
|
||||
assert stats["today"] == 1 and stats["window"] == 1 # bot excluded from both
|
||||
|
||||
|
||||
def test_client_error_read_unread(tmp_path, monkeypatch):
|
||||
app, api = _make(tmp_path, monkeypatch, admin_email="boss@x.com")
|
||||
anon = TestClient(app)
|
||||
for r in ("boot-timeout", "preloadError", "boot-slow"):
|
||||
anon.post("/api/client-error", json={"reason": r, "path": "/"})
|
||||
tc = _signin(app, api, "boss@x.com")
|
||||
# Default view is unread; all three start unread and drive the headline count.
|
||||
unread = tc.get("/api/admin/client-errors").json()
|
||||
assert len(unread) == 3 and all(not e["read"] for e in unread)
|
||||
assert tc.get("/api/admin/stats").json()["client_errors"]["unread"] == 3
|
||||
# Mark one read → it leaves the unread view, appears under read, count drops.
|
||||
eid = unread[0]["id"]
|
||||
assert tc.post(f"/api/admin/client-errors/{eid}/read", json={"read": True}).json()["read"] is True
|
||||
assert len(tc.get("/api/admin/client-errors?show=unread").json()) == 2
|
||||
rd = tc.get("/api/admin/client-errors?show=read").json()
|
||||
assert len(rd) == 1 and rd[0]["id"] == eid and rd[0]["read"] is True
|
||||
assert len(tc.get("/api/admin/client-errors?show=all").json()) == 3
|
||||
assert tc.get("/api/admin/stats").json()["client_errors"]["unread"] == 2
|
||||
# Toggling back restores it to unread.
|
||||
assert tc.post(f"/api/admin/client-errors/{eid}/read", json={"read": False}).json()["read"] is False
|
||||
assert len(tc.get("/api/admin/client-errors?show=unread").json()) == 3
|
||||
# Mark-all clears the unread view in one go.
|
||||
assert tc.post("/api/admin/client-errors/read-all").json()["marked"] == 3
|
||||
assert tc.get("/api/admin/client-errors?show=unread").json() == []
|
||||
assert tc.get("/api/admin/stats").json()["client_errors"]["unread"] == 0
|
||||
# Unknown id 404s; both new routes are admin-gated.
|
||||
assert tc.post("/api/admin/client-errors/99999/read", json={"read": True}).status_code == 404
|
||||
assert anon.post("/api/admin/client-errors/read-all").status_code == 401
|
||||
assert anon.post(f"/api/admin/client-errors/{eid}/read", json={"read": True}).status_code == 401
|
||||
|
||||
|
||||
def test_wordsearch_theme_admin(tmp_path, monkeypatch):
|
||||
app, api = _make(tmp_path, monkeypatch, admin_email="boss@x.com")
|
||||
assert TestClient(app).get("/api/admin/wordsearch/themes").status_code == 401 # gated
|
||||
|
||||
Reference in New Issue
Block a user