import base64 from datetime import date, timedelta from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session import httpx from auth import get_current_user from database import get_db from models import Batch, BatchStatus, NotificationLog, Settings, User, Variety router = APIRouter(prefix="/notifications", tags=["notifications"]) ACTIVE_STATUSES = [ BatchStatus.planned, BatchStatus.germinating, BatchStatus.seedling, BatchStatus.potted_up, BatchStatus.hardening, BatchStatus.garden, ] def build_daily_summary(db: Session, user_id: int) -> str: settings = db.query(Settings).filter(Settings.user_id == user_id).first() today = date.today() lines = [f"Sproutly Daily Summary — {today.strftime('%A, %B %d')}"] lines.append("") tasks = [] if settings and settings.last_frost_date: last_frost = settings.last_frost_date.replace(year=today.year) if last_frost < today - timedelta(days=60): last_frost = last_frost.replace(year=today.year + 1) days_to_frost = (last_frost - today).days if 0 <= days_to_frost <= 14: lines.append(f"Last frost in {days_to_frost} days ({last_frost.strftime('%B %d')})!") lines.append("") varieties = db.query(Variety).filter(Variety.user_id == user_id).all() for v in varieties: full_name = f"{v.name} ({v.variety_name})" if v.variety_name else v.name if v.weeks_to_start: sd = last_frost - timedelta(weeks=v.weeks_to_start) d = (sd - today).days if -1 <= d <= 3: tasks.append(f"Start seeds: {full_name} (due {sd.strftime('%b %d')})") if v.weeks_to_greenhouse: gd = last_frost - timedelta(weeks=v.weeks_to_greenhouse) d = (gd - today).days if -1 <= d <= 3: tasks.append(f"Pot up: {full_name} (due {gd.strftime('%b %d')})") if v.weeks_to_garden is not None: if v.weeks_to_garden >= 0: td = last_frost + timedelta(weeks=v.weeks_to_garden) else: td = last_frost - timedelta(weeks=abs(v.weeks_to_garden)) d = (td - today).days if -1 <= d <= 3: tasks.append(f"Transplant to garden: {full_name} (due {td.strftime('%b %d')})") batches = ( db.query(Batch) .filter(Batch.user_id == user_id, Batch.status.in_(ACTIVE_STATUSES)) .all() ) for b in batches: v = b.variety full_name = f"{v.name} ({v.variety_name})" if v.variety_name else v.name label = b.label or full_name if b.status == BatchStatus.hardening: tasks.append(f"Harden off: {label}") if b.status == BatchStatus.germinating and b.sow_date: expected = b.sow_date + timedelta(days=v.days_to_germinate or 7) d = (expected - today).days if -2 <= d <= 1: tasks.append(f"Check germination: {label}") if tasks: lines.append("Today's tasks:") for t in tasks: lines.append(f" - {t}") else: lines.append("No urgent tasks today. Keep it up!") active_count = sum(1 for b in batches if b.status not in [BatchStatus.harvested, BatchStatus.failed]) lines.append("") lines.append(f"Active batches: {active_count}") return "\n".join(lines) async def send_ntfy(settings: Settings, title: str, message: str, db: Session, priority: str = "default"): if not settings or not settings.ntfy_topic: return False, "ntfy topic not configured" server = (settings.ntfy_server or "https://ntfy.sh").rstrip("/") url = f"{server}/{settings.ntfy_topic}" headers = { "Title": title, "Priority": priority, "Tags": "seedling", } if settings.ntfy_api_key: headers["Authorization"] = f"Bearer {settings.ntfy_api_key}" elif settings.ntfy_username: creds = base64.b64encode( f"{settings.ntfy_username}:{settings.ntfy_password or ''}".encode() ).decode() headers["Authorization"] = f"Basic {creds}" try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.post(url, content=message.encode("utf-8"), headers=headers) resp.raise_for_status() log = NotificationLog(message=message, status="sent", user_id=settings.user_id) db.add(log) db.commit() return True, "sent" except Exception as e: log = NotificationLog(message=message, status="failed", error=str(e), user_id=settings.user_id) db.add(log) db.commit() return False, str(e) @router.post("/test") async def send_test_notification(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): settings = db.query(Settings).filter(Settings.user_id == current_user.id).first() ok, detail = await send_ntfy(settings, "Sproutly Test", "Your Sproutly notifications are working!", db) if not ok: raise HTTPException(status_code=400, detail=detail) return {"status": "sent"} @router.post("/daily") async def send_daily_summary(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): settings = db.query(Settings).filter(Settings.user_id == current_user.id).first() summary = build_daily_summary(db, current_user.id) ok, detail = await send_ntfy(settings, "Sproutly Daily Summary", summary, db) if not ok: raise HTTPException(status_code=400, detail=detail) return {"status": "sent", "message": summary} @router.get("/log") def get_notification_log(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): logs = ( db.query(NotificationLog) .filter(NotificationLog.user_id == current_user.id) .order_by(NotificationLog.sent_at.desc()) .limit(50) .all() ) return [{"id": l.id, "sent_at": l.sent_at, "status": l.status, "message": l.message, "error": l.error} for l in logs]