feat(news): F2 — scheduler automático del monitor RSS (inerte)
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s

Engancha el monitor de noticias al scheduler loop EXISTENTE de
watched_topics (sin segunda task asyncio). Cada tick, si NEWS_ENABLED,
y si ha pasado NEWS_POLL_INTERVAL_HOURS desde el último poll (estado en
memoria), pollea los feeds, empuja los pendientes (notified=0) al chat
destino (NEWS_CHAT_ID o 1er TELEGRAM_ALLOWED_USERS) y los marca.

Best-effort: la rama de noticias va en su propio try/except — un fallo
del news-poll NUNCA tumba el scheduler de watched_topics. Deploy inerte:
NEWS_ENABLED sigue False; el flip se hará aparte en k8s-manifests.

- db: get_unnotified(limit=None) — pendientes, recientes primero (NULLS LAST)
- bot: rama news al final del tick del scheduler, reusa poll_feeds /
  item_from_row / format_digest de F1; "…y N más" si excede NEWS_MAX_ITEMS

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-06-27 10:16:07 +00:00
co-authored by Claude Opus 4.8
parent c0b0c1ba0a
commit b1c5bc737d
2 changed files with 55 additions and 1 deletions
+17
View File
@@ -562,6 +562,23 @@ class ResearchDB:
rows = await cursor.fetchall()
return [dict(r) for r in rows]
async def get_unnotified(self, limit: Optional[int] = None) -> list[dict]:
"""Items aún sin notificar (notified=0), más recientes primero. El
scheduler (F2) los empuja a Telegram y luego los marca con
mark_news_notified. Aplica LIMIT solo si `limit` no es None."""
sql = (
"SELECT id, source, title, link, published_at, matched_keywords "
"FROM news_seen WHERE notified = 0 "
"ORDER BY published_at DESC NULLS LAST, seen_at DESC"
)
params: tuple = ()
if limit is not None:
sql += " LIMIT ?"
params = (int(limit),)
cursor = await self.db.execute(sql, params)
rows = await cursor.fetchall()
return [dict(r) for r in rows]
# --- Maintenance ---
async def purge_old_sessions(self, max_age_days: int = 30) -> dict: