Admin step B: stats endpoint + /admin dashboard

- users.is_admin (+ migration); admin = is_admin OR email in GOODNEWS_ADMIN_EMAILS
  (normalized). is_admin exposed on /api/auth/me. Server-authorized GET
  /api/admin/stats (403 for non-admins).
- queries.admin_stats: visitors (today/7d/30d), returning vs one-and-done, top
  opened articles, popular groupings + topics (derived from article_id at query
  time), share breakdown, daily opens/visits trend — all aggregate, no PII.
- /admin page (gated, redirects non-admins): stat cards, CSS bar lists, a daily
  trend; "Admin dashboard" link on /account for admins. 129 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-03 18:25:46 +00:00
parent 1a778e1334
commit 762f121320
7 changed files with 334 additions and 2 deletions
+2 -1
View File
@@ -46,7 +46,8 @@ def normalize_email(email: str) -> str:
def get_user(conn: sqlite3.Connection, user_id: int) -> sqlite3.Row | None:
return conn.execute(
"SELECT id, email, display_name, avatar_url, created_at FROM users WHERE id = ?", (user_id,)
"SELECT id, email, display_name, avatar_url, is_admin, created_at FROM users WHERE id = ?",
(user_id,),
).fetchone()