import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from database import SessionLocal from models import Settings, NotificationLog from routers import varieties, batches, dashboard, settings, notifications from routers import auth as auth_router from routers.notifications import build_daily_summary, send_ntfy logging.basicConfig(level=logging.INFO) logger = logging.getLogger("sproutly") scheduler = AsyncIOScheduler() async def scheduled_daily_notification(): db = SessionLocal() try: all_settings = db.query(Settings).filter(Settings.ntfy_topic.isnot(None)).all() for s in all_settings: if not s.ntfy_topic: continue summary = build_daily_summary(db, s.user_id) ok, detail = await send_ntfy(s, "Sproutly Daily Summary", summary, db) logger.info(f"Daily notification for user {s.user_id}: {detail}") except Exception as e: logger.error(f"Daily notification error: {e}") log = NotificationLog(message="scheduler error", status="failed", error=str(e)) db.add(log) db.commit() finally: db.close() def get_notification_schedule(db) -> tuple[int, int]: """Use the earliest configured notification time across all users.""" try: s = db.query(Settings).filter(Settings.ntfy_topic.isnot(None)).first() if s and s.notification_time: h, m = s.notification_time.split(":") return int(h), int(m) except Exception: pass return 7, 0 @asynccontextmanager async def lifespan(app: FastAPI): db = SessionLocal() hour, minute = get_notification_schedule(db) db.close() scheduler.add_job( scheduled_daily_notification, CronTrigger(hour=hour, minute=minute), id="daily_summary", replace_existing=True, ) scheduler.start() logger.info(f"Scheduler started — daily notification at {hour:02d}:{minute:02d}") yield scheduler.shutdown() app = FastAPI(title="Sproutly API", version="2.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) app.include_router(auth_router.router) app.include_router(varieties.router) app.include_router(batches.router) app.include_router(dashboard.router) app.include_router(settings.router) app.include_router(notifications.router) @app.get("/health") def health(): return {"status": "ok", "service": "sproutly"}