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:
+66
-4
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user