UMBRA/backend/app/routers/weather.py
Kyle Pope ca8b654471 Dashboard Phase 2: weather widget, starred events, quick add, thinner events
- Add weather router with OpenWeatherMap integration and 1-hour cache
- Add is_starred column to calendar events with countdown widget
- Add weather_city setting with Settings page input
- Replace people/locations stats with open todos count + weather card
- Add quick-add dropdown (event/todo/reminder) to dashboard header
- Make CalendarWidget events single-line thin rows
- Add rain warnings to smart briefing when chance > 40%
- Invalidate dashboard/upcoming queries on form mutations
- Migration 004: is_starred + weather_city columns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:15:43 +08:00

79 lines
2.8 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import datetime, timedelta
import urllib.request
import urllib.parse
import urllib.error
import json
from app.database import get_db
from app.models.settings import Settings
from app.config import settings as app_settings
from app.routers.auth import get_current_session
router = APIRouter()
_cache: dict = {}
@router.get("/weather")
async def get_weather(
db: AsyncSession = Depends(get_db),
current_user: Settings = Depends(get_current_session)
):
# Check cache
now = datetime.now()
if _cache.get("expires_at") and now < _cache["expires_at"]:
return _cache["data"]
# Get city from settings
result = await db.execute(select(Settings))
settings_row = result.scalar_one_or_none()
city = settings_row.weather_city if settings_row else None
if not city:
raise HTTPException(status_code=400, detail="No weather city configured")
api_key = app_settings.OPENWEATHERMAP_API_KEY
if not api_key:
raise HTTPException(status_code=500, detail="Weather API key not configured")
try:
# Current weather
current_url = f"https://api.openweathermap.org/data/2.5/weather?q={urllib.parse.quote(city)}&units=metric&appid={api_key}"
with urllib.request.urlopen(current_url, timeout=10) as resp:
current_data = json.loads(resp.read().decode())
# Forecast for rain probability
forecast_url = f"https://api.openweathermap.org/data/2.5/forecast?q={urllib.parse.quote(city)}&units=metric&cnt=8&appid={api_key}"
with urllib.request.urlopen(forecast_url, timeout=10) as resp:
forecast_data = json.loads(resp.read().decode())
rain_chance = 0
for item in forecast_data.get("list", []):
pop = item.get("pop", 0)
if pop > rain_chance:
rain_chance = pop
weather_result = {
"temp": round(current_data["main"]["temp"]),
"temp_min": round(current_data["main"]["temp_min"]),
"temp_max": round(current_data["main"]["temp_max"]),
"description": current_data["weather"][0]["description"],
"rain_chance": round(rain_chance * 100),
"sunrise": current_data["sys"]["sunrise"],
"sunset": current_data["sys"]["sunset"],
"city": current_data["name"],
}
# Cache for 1 hour
_cache["data"] = weather_result
_cache["expires_at"] = now + timedelta(hours=1)
return weather_result
except urllib.error.URLError as e:
raise HTTPException(status_code=502, detail=f"Weather service unavailable: {str(e)}")
except (KeyError, json.JSONDecodeError) as e:
raise HTTPException(status_code=502, detail=f"Invalid weather data: {str(e)}")