Add Reddit monitoring bot — backend, frontend, and Docker config

Python/FastAPI backend with PostgreSQL for collecting Reddit data via
public .json endpoints. React/Vite dashboard for analytics. Docker Compose
setup with API and worker services connecting to shared PostgreSQL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 19:29:58 -05:00
parent aaa240dbf0
commit bc2203524f
76 changed files with 7570 additions and 0 deletions

90
backend/worker/main.py Normal file
View File

@@ -0,0 +1,90 @@
import logging
import signal
import sys
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from backend.config import settings
from backend.worker.monitor import poll_new_posts, poll_hot_posts, collect_comments, update_scores
from backend.worker.snapshot import take_metric_snapshots
from backend.worker.digest_job import generate_daily_digests
from backend.worker.summary_job import generate_summaries
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
logger = logging.getLogger(__name__)
def seed_subreddits():
"""Add seed subreddits on first startup if configured."""
if not settings.seed_subreddits:
return
from sqlalchemy import select, create_engine
from sqlalchemy.orm import sessionmaker
from backend.models.subreddit import MonitoredSubreddit
engine = create_engine(settings.database_url_sync)
Session = sessionmaker(engine)
names = [s.strip().lower() for s in settings.seed_subreddits.split(",") if s.strip()]
with Session() as db:
for name in names:
existing = db.execute(
select(MonitoredSubreddit).where(MonitoredSubreddit.name == name)
).scalar_one_or_none()
if not existing:
db.add(MonitoredSubreddit(name=name))
logger.info(f"Seeded subreddit: r/{name}")
db.commit()
engine.dispose()
def main():
logger.info("Starting Reddit monitor worker")
seed_subreddits()
scheduler = BlockingScheduler()
# Reddit polling jobs
scheduler.add_job(poll_new_posts, IntervalTrigger(minutes=2), id="poll_new", max_instances=1)
scheduler.add_job(poll_hot_posts, IntervalTrigger(minutes=2), id="poll_hot", max_instances=1)
scheduler.add_job(collect_comments, IntervalTrigger(minutes=5), id="comments", max_instances=1)
scheduler.add_job(update_scores, IntervalTrigger(minutes=15), id="scores", max_instances=1)
# Metric snapshots
scheduler.add_job(take_metric_snapshots, IntervalTrigger(minutes=30), id="snapshots", max_instances=1)
# Daily digest
scheduler.add_job(
generate_daily_digests,
CronTrigger(hour=settings.digest_hour_utc, minute=0),
id="digest",
max_instances=1,
)
# AI summary stub
scheduler.add_job(
generate_summaries,
CronTrigger(hour=settings.digest_hour_utc, minute=30),
id="summary",
max_instances=1,
)
def shutdown(signum, frame):
logger.info("Shutting down worker...")
scheduler.shutdown(wait=False)
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
logger.info("Worker started. Scheduled jobs are running.")
scheduler.start()
if __name__ == "__main__":
main()