Admin: tabbed operator console (Overview/Content/Sources/Audience/Feedback)

Reshape the long single-page dashboard into a sectioned console (one route,
?section= tabs, sticky subnav) focused on "what needs my attention" first.

* Overview: an "Attention Needed" strip (soft amber/blue, never alarming red)
  derived from the same data — sources resting/flagged, image coverage <70%,
  thin brief, recent feedback — plus at-a-glance pulse cards.
* Content: corpus health + image/summary coverage (with_image, summaries_with_
  image, brief image coverage, 24h image misses) + top opened / topics / tags.
* Sources: filterable table (All/Healthy/Resting/Flagged) — served, last
  success, next poll, failure streak, status — instead of a card pile.
* Audience: visitors, retention, accounts, funnel, sharing, daily trend.
* Feedback: inbox with category filter, newest first, quick mailto reply.

Backend: content_stats gains added_7d + image-coverage fields; source_health
gains review_flag; admin_stats adds attention[] + feedback_7d.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-08 13:03:23 -04:00
parent 489c34d2f2
commit 575f562ad5
3 changed files with 375 additions and 179 deletions
+66 -4
View File
@@ -233,17 +233,45 @@ def content_stats(conn: sqlite3.Connection) -> dict:
"SELECT brief_date, (SELECT COUNT(*) FROM daily_brief_items WHERE brief_id=daily_briefs.id) n "
"FROM daily_briefs ORDER BY brief_date DESC LIMIT 1"
).fetchone()
with_image = scalar(
"SELECT COUNT(*) FROM article_scores s JOIN articles a ON a.id=s.article_id "
"WHERE s.accepted=1 AND a.duplicate_of IS NULL AND a.image_url IS NOT NULL AND a.image_url!=''"
)
summaries = scalar("SELECT COUNT(*) FROM article_summaries")
summaries_with_image = scalar(
"SELECT COUNT(*) FROM article_summaries m JOIN articles a ON a.id=m.article_id "
"WHERE a.image_url IS NOT NULL AND a.image_url!=''"
)
brief_with_image = 0
if brief:
brief_with_image = scalar(
"SELECT COUNT(*) FROM daily_brief_items bi JOIN articles a ON a.id=bi.article_id "
"JOIN daily_briefs b ON b.id=bi.brief_id "
"WHERE b.brief_date=? AND a.image_url IS NOT NULL AND a.image_url!=''",
(brief["brief_date"],),
)
return {
"served": served,
"total": scalar("SELECT COUNT(*) FROM articles"),
"rejected": scalar("SELECT COUNT(*) FROM article_scores WHERE accepted=0"),
"accepted_7d": accepted_7d,
"added_24h": scalar("SELECT COUNT(*) FROM articles WHERE discovered_at >= datetime('now','-1 day')"),
"summaries": scalar("SELECT COUNT(*) FROM article_summaries"),
"added_7d": scalar("SELECT COUNT(*) FROM articles WHERE discovered_at >= datetime('now','-7 days')"),
"summaries": summaries,
"summaries_with_image": summaries_with_image,
"with_image": with_image,
# Accepted, imageless articles whose enrichment was tried recently and
# came up empty (no usable og:image) — the image "misses" to watch.
"recent_enrich_fail": scalar(
"SELECT COUNT(*) FROM article_scores s JOIN articles a ON a.id=s.article_id "
"WHERE s.accepted=1 AND a.duplicate_of IS NULL AND (a.image_url IS NULL OR a.image_url='') "
"AND a.image_checked_at >= datetime('now','-1 day')"
),
"active_sources": scalar("SELECT COUNT(*) FROM sources WHERE active=1"),
"total_sources": scalar("SELECT COUNT(*) FROM sources"),
"latest_brief_date": brief["brief_date"] if brief else None,
"latest_brief_size": brief["n"] if brief else 0,
"brief_with_image": brief_with_image,
}
@@ -258,7 +286,7 @@ def source_health(conn: sqlite3.Connection) -> list[dict]:
"""
SELECT
s.id, s.name, s.default_category AS category, s.active,
s.consecutive_failures AS failures,
s.consecutive_failures AS failures, s.review_flag,
s.poll_interval_minutes AS interval_minutes,
s.last_success_at, s.last_error_at, substr(s.last_error, 1, 160) AS last_error,
(SELECT MAX(r.finished_at) FROM ingest_runs r
@@ -279,6 +307,34 @@ def source_health(conn: sqlite3.Connection) -> list[dict]:
return [dict(r) for r in rows]
def _attention(content: dict, sources: list[dict], feedback_7d: int) -> list[dict]:
"""The 'Attention Needed' strip: what an operator should look at, soft-toned
(warn = act soon, info = worth a glance). Derived from the same data shown
elsewhere, so it never disagrees with the detail sections."""
items: list[dict] = []
n = lambda c: "" if c == 1 else "s" # noqa: E731 — tiny pluralizer
resting = [s for s in sources if (s.get("failures") or 0) > 0]
if resting:
items.append({"level": "warn", "text": f"{len(resting)} source{n(len(resting))} backing off after failures"})
flagged = [s for s in sources if s.get("review_flag")]
if flagged:
items.append({"level": "warn", "text": f"{len(flagged)} source{n(len(flagged))} flagged for review"})
served = content.get("served") or 0
with_image = content.get("with_image") or 0
if served and (with_image / served) < 0.70:
items.append({"level": "info", "text": f"Image coverage at {round(100 * with_image / served)}% (aim for 70%+)"})
brief_size = content.get("latest_brief_size") or 0
if brief_size < 7:
items.append({"level": "info", "text": f"Today's brief has only {brief_size} item{n(brief_size)}"})
if feedback_7d:
items.append({"level": "info", "text": f"{feedback_7d} feedback message{n(feedback_7d)} in the last 7 days"})
return items
def admin_stats(conn: sqlite3.Connection, days: int = 30) -> dict:
"""Aggregate, non-personal usage stats for the admin dashboard."""
since = f"-{days} days"
@@ -376,10 +432,16 @@ def admin_stats(conn: sqlite3.Connection, days: int = 30) -> dict:
"FROM events WHERE day>=date('now',?) GROUP BY day ORDER BY day", (since,),
)]
content = content_stats(conn)
sources = source_health(conn)
feedback_7d = scalar("SELECT COUNT(*) FROM feedback WHERE created_at >= datetime('now','-7 days')")
return {
"days": days,
"content": content_stats(conn),
"sources": source_health(conn),
"content": content,
"sources": sources,
"attention": _attention(content, sources, feedback_7d),
"feedback_7d": feedback_7d,
"visitors": visitors,
"returning": loyalty.get("returning", 0),
"once": loyalty.get("once", 0),