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.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: s = db.query(Settings).filter(Settings.id == 1).first() if not s or not s.ntfy_topic: logger.info("Daily notification skipped: ntfy not configured") return summary = build_daily_summary(db) ok, detail = await send_ntfy(s, "Sproutly Daily Summary", summary, db) logger.info(f"Daily notification: {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]: try: s = db.query(Settings).filter(Settings.id == 1).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="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) 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"}