diff --git a/goodnews/queries.py b/goodnews/queries.py index 4d31761..7a51ab1 100644 --- a/goodnews/queries.py +++ b/goodnews/queries.py @@ -282,7 +282,7 @@ def content_stats(conn: sqlite3.Connection) -> dict: def source_health(conn: sqlite3.Connection) -> list[dict]: - """Per source (active AND paused), the data an operator needs to manage feeds: + """Per source (active, paused, AND retired), the data an operator needs to manage feeds: failure streak, last error/success/attempt, computed next poll, and the backing metrics (served/accepted/total counts + acceptance & duplicate rates) so Pause/Flag decisions aren't vibes-based. diff --git a/goodnews/sources.py b/goodnews/sources.py index 5ccb720..f33f68c 100644 --- a/goodnews/sources.py +++ b/goodnews/sources.py @@ -21,14 +21,20 @@ def load_sources(path: Path | str) -> list[dict]: def upsert_sources(conn: sqlite3.Connection, source_defs: list[dict]) -> int: count = 0 for source in source_defs: + # Keep status and the legacy `active` mirror in lockstep (Phase 1 rule): + # derive status from an explicit value or from active, then mirror active. + status = source.get("status") or ("active" if source.get("active", True) else "paused") + if status not in ("active", "paused", "retired"): + status = "active" + active = 1 if status == "active" else 0 conn.execute( """ INSERT INTO sources ( name, homepage_url, feed_url, source_type, default_category, - trust_score, pr_risk_score, active, poll_interval_minutes, notes, + trust_score, pr_risk_score, active, status, poll_interval_minutes, notes, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(feed_url) DO UPDATE SET name = excluded.name, homepage_url = excluded.homepage_url, @@ -37,6 +43,7 @@ def upsert_sources(conn: sqlite3.Connection, source_defs: list[dict]) -> int: trust_score = excluded.trust_score, pr_risk_score = excluded.pr_risk_score, active = excluded.active, + status = excluded.status, poll_interval_minutes = excluded.poll_interval_minutes, notes = excluded.notes, updated_at = CURRENT_TIMESTAMP @@ -49,7 +56,8 @@ def upsert_sources(conn: sqlite3.Connection, source_defs: list[dict]) -> int: source.get("default_category"), int(source.get("trust_score", 5)), int(source.get("pr_risk_score", 3)), - 1 if source.get("active", True) else 0, + active, + status, int(source.get("poll_interval_minutes", 60)), source.get("notes"), ), diff --git a/tests/test_candidates.py b/tests/test_candidates.py index 57c5217..e9fd349 100644 --- a/tests/test_candidates.py +++ b/tests/test_candidates.py @@ -36,9 +36,10 @@ def test_promote_creates_inactive_source_and_marks_promoted(conn): cand = save_candidate(conn, "http://x/feed", name="Lovely Feed") source_id = promote_candidate(conn, cand["id"]) # inactive by default - src = conn.execute("SELECT name, active FROM sources WHERE id = ?", (source_id,)).fetchone() + src = conn.execute("SELECT name, active, status FROM sources WHERE id = ?", (source_id,)).fetchone() assert src["name"] == "Lovely Feed" assert src["active"] == 0 # active-on-approval: not polled until activated + assert src["status"] == "paused" # status mirrors active — no drift (active=0 ⇒ paused) status = conn.execute("SELECT status FROM source_candidates WHERE id = ?", (cand["id"],)).fetchone()["status"] assert status == "promoted" @@ -47,7 +48,8 @@ def test_promote_creates_inactive_source_and_marks_promoted(conn): def test_promote_active_flag(conn): cand = save_candidate(conn, "http://y/feed", name="Y") sid = promote_candidate(conn, cand["id"], active=True) - assert conn.execute("SELECT active FROM sources WHERE id = ?", (sid,)).fetchone()["active"] == 1 + src = conn.execute("SELECT active, status FROM sources WHERE id = ?", (sid,)).fetchone() + assert src["active"] == 1 and src["status"] == "active" # both set together def test_promote_unknown_raises(conn):