Source Retire lifecycle (Phase 1: status + content_visible, active mirrored)

Per Codex's plan — introduce a lifecycle without a risky "change the source of
truth everywhere" moment.

* Schema: sources.status (active|paused|retired) + content_visible; migration
  backfills status from active (active=1→active, else paused), content_visible=1.
* `active` is kept as a SYNCED MIRROR: status active→active=1, paused/retired→0,
  so the scheduler/CLI/legacy code keep working unchanged.
* Retire stops polling but keeps articles visible (non-destructive). Hiding is a
  separate, reversible lever: content_visible=0 drops a source's articles from
  the public feed + brief (read AND build), behind a confirm. Personal saved/
  history are untouched.
* API: /sources/{id}/status (validates, mirrors active) + /visibility, replacing
  /active. source_health returns status + content_visible.
* Admin: status column (active/paused/retired + "hidden"), Retired filter,
  Pause/Resume · Retire/Restore · Hide/Show actions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-09 09:58:15 -04:00
parent ba92c0a04b
commit 9ed817c051
6 changed files with 124 additions and 49 deletions
+3 -2
View File
@@ -72,7 +72,7 @@ def feed(
items (e.g. 'discovery' for a Wonder lane) fall outside any over-fetch window.
Word-boundary avoid-terms remain a Python pass on the caller side.
"""
clauses = ["a.duplicate_of IS NULL"]
clauses = ["a.duplicate_of IS NULL", "src.content_visible = 1"]
params: list = []
if accepted_only:
clauses.append("s.accepted = 1")
@@ -157,7 +157,7 @@ def brief(conn: sqlite3.Connection, brief_date: str | None = None, limit: int =
JOIN articles a ON a.id = bi.article_id
JOIN sources src ON src.id = a.source_id
LEFT JOIN article_scores s ON s.article_id = a.id
WHERE b.brief_date = ?
WHERE b.brief_date = ? AND src.content_visible = 1
ORDER BY bi.rank
LIMIT ?
""",
@@ -295,6 +295,7 @@ def source_health(conn: sqlite3.Connection) -> list[dict]:
"""
SELECT
s.id, s.name, s.default_category AS category, s.active,
s.status, s.content_visible,
s.consecutive_failures AS failures, s.review_flag, s.review_reason,
s.poll_interval_minutes AS interval_minutes,
s.last_success_at, s.last_error_at, substr(s.last_error, 1, 160) AS last_error,