From 13722f04a8baeac5f5927fe0e508a0f12d7cab5c Mon Sep 17 00:00:00 2001 From: jay Date: Mon, 8 Jun 2026 13:19:15 -0400 Subject: [PATCH] Admin polish: section fallback, live-scoped coverage, dual source status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Codex audit: * Unknown ?section= values now clamp to Overview, so the page never renders the tabs with an empty body. * Summary/image coverage counts join through articles+scores and require accepted=1 AND duplicate_of IS NULL, so percentages stay ≤100% and honest as rejected/duplicate rows accrue summaries over time. * A source that's both resting and flagged now shows "⚠ resting · review" rather than hiding the review flag behind the resting state. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/routes/admin/+page.svelte | 8 ++++++-- goodnews/queries.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 516220d..c5503f7 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -30,7 +30,11 @@ { key: 'audience', label: 'Audience' }, { key: 'feedback', label: 'Feedback' }, ]; - let section = $derived($page.url.searchParams.get('section') || 'overview'); + const VALID_SECTIONS = new Set(TABS.map((t) => t.key)); + // Unknown ?section= values fall back to Overview so the page never renders blank. + let section = $derived( + VALID_SECTIONS.has($page.url.searchParams.get('section')) ? $page.url.searchParams.get('section') : 'overview' + ); function fdate(s) { try { return new Date(s + 'Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } @@ -208,7 +212,7 @@ {fwhen(s.next_due_at)} {s.failures || ''} - {#if s.failures > 0}⚠ resting + {#if s.failures > 0}⚠ resting{#if s.review_flag} · review{/if} {:else if s.review_flag}⚑ review {:else}✓ ok{/if} diff --git a/goodnews/queries.py b/goodnews/queries.py index b7d55d8..2f6071e 100644 --- a/goodnews/queries.py +++ b/goodnews/queries.py @@ -237,10 +237,16 @@ def content_stats(conn: sqlite3.Connection) -> dict: "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") + # Coverage is scoped to LIVE articles (accepted, non-duplicate) so the + # percentages can't drift past 100% as rejected/duplicate rows accrue summaries. + summaries = scalar( + "SELECT COUNT(*) FROM article_summaries m JOIN articles a ON a.id=m.article_id " + "JOIN article_scores s ON s.article_id=a.id WHERE s.accepted=1 AND a.duplicate_of IS NULL" + ) 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!=''" + "JOIN article_scores s ON s.article_id=a.id WHERE s.accepted=1 AND a.duplicate_of IS NULL " + "AND a.image_url IS NOT NULL AND a.image_url!=''" ) brief_with_image = 0 if brief: