Admin: source Articles inspector (verify metrics against real evidence)
New per-row "Articles" button on the Sources table expands a read-only inline
panel of the source's ACTUAL ingested articles — so the automated metrics
(paywall/image/acceptance/duplicate) can be verified against evidence instead of
trusted blind. Distinct from "Check" (which re-samples the LIVE feed for
would-pass quality); this shows what's already in the DB, which is what the table
metrics are computed from.
- Backend: GET /api/admin/sources/{id}/articles?filter=&limit=&offset= (admin,
read-only). queries.source_articles + source_articles_summary — per article:
title, url, date, accepted, reason (the "why"), topic/flavor, paywalled
(domain rule), has_image, duplicate. Summary = counts + source-level paywall
rule.
- Frontend: expandable panel with a summary header ("27 ingested · 18 accepted
· … · paywall rule: ON (domain)"), filter chips (All/Accepted/Rejected/No
image/Duplicates), compact rows with title→link + badges + reason, Load more.
So "100% paywall" or "0% images" becomes clickable evidence: open two articles
to tell a real paywall from a mis-flagged domain, or a true image gap from an
enrichment failure. Test: test_source_articles_inspector. 241 pytest + 11 vitest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1146,6 +1146,24 @@ def create_app() -> FastAPI:
|
||||
url = src["feed_url"]
|
||||
return _preview_or_502(url) # safe fetch, no DB connection held
|
||||
|
||||
@app.get("/api/admin/sources/{sid}/articles")
|
||||
def admin_source_articles(sid: int, request: Request, filter: str = "all",
|
||||
limit: int = 25, offset: int = 0) -> dict:
|
||||
# Read-only inspector: the REAL ingested articles behind a source's metrics,
|
||||
# so paywall/image/acceptance/duplicate signals can be verified against evidence.
|
||||
limit = max(1, min(int(limit), 100))
|
||||
offset = max(0, int(offset))
|
||||
with get_conn() as conn:
|
||||
_require_admin(conn, request)
|
||||
if not conn.execute("SELECT 1 FROM sources WHERE id = ?", (sid,)).fetchone():
|
||||
raise HTTPException(status_code=404, detail="source not found")
|
||||
arts = queries.source_articles(conn, sid, filter, limit, offset)
|
||||
return {
|
||||
"articles": arts,
|
||||
"summary": queries.source_articles_summary(conn, sid) if offset == 0 else None,
|
||||
"has_more": len(arts) == limit,
|
||||
}
|
||||
|
||||
# --- Source candidates (supervised add-a-source pipeline) ----------------
|
||||
|
||||
def _candidate_dict(row) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user