Add weather sync job, API endpoints and settings UI

Fetches daily ERA5 weather from Open-Meteo (no API key).
Configurable location, timezone and sync time via Settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-13 21:10:03 +00:00
parent fc15bc5d9a
commit 5fbf155f62
4 changed files with 671 additions and 1 deletions

View file

@ -886,3 +886,110 @@ async def run_backfill_job(
raise
finally:
db.close()
# ============================================================
# WEATHER SYNC ENDPOINTS
# ============================================================
@router.post("/weather")
async def trigger_weather_sync(
background_tasks: BackgroundTasks,
from_date: Optional[date] = Query(None, description="Start date (default: 7 days ago)"),
to_date: Optional[date] = Query(None, description="End date (default: today)"),
current_user: dict = Depends(get_current_user)
):
"""
Trigger a manual weather data sync from Open-Meteo.
Defaults to last 7 days. Pass from_date/to_date for a custom range.
"""
from jobs.weather_sync import sync_weather_data
if from_date is None:
from_date = date.today() - timedelta(days=7)
if to_date is None:
to_date = date.today()
background_tasks.add_task(
sync_weather_data,
from_date=from_date,
to_date=to_date,
triggered_by=f"user:{current_user['username']}"
)
return {
"status": "started",
"source": "open_meteo",
"from_date": from_date,
"to_date": to_date,
"message": f"Weather sync {from_date} to {to_date} started in background"
}
@router.post("/weather/backfill")
async def trigger_weather_backfill(
background_tasks: BackgroundTasks,
from_date: date = Query(..., description="Start date for backfill"),
to_date: date = Query(..., description="End date for backfill"),
current_user: dict = Depends(get_current_user)
):
"""
Backfill weather data over an arbitrary date range.
Runs in 6-month chunks so progress is logged incrementally.
Open-Meteo has data from 1940-01-01 to present.
"""
from jobs.weather_sync import run_weather_backfill
background_tasks.add_task(
run_weather_backfill,
from_date=from_date,
to_date=to_date,
triggered_by=f"user:{current_user['username']}"
)
return {
"status": "started",
"source": "open_meteo",
"from_date": from_date,
"to_date": to_date,
"message": f"Weather backfill {from_date} to {to_date} started in background"
}
@router.get("/weather/status")
async def get_weather_status(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""Return weather data coverage and last sync status."""
coverage = await db.execute(text("""
SELECT
MIN(date) as first_date,
MAX(date) as last_date,
COUNT(*) as total_days
FROM weather_data
"""))
cov = coverage.fetchone()
last_sync = await db.execute(text("""
SELECT completed_at, status, records_fetched, records_created, date_from, date_to
FROM sync_log
WHERE source = 'open_meteo'
ORDER BY started_at DESC LIMIT 1
"""))
sync_row = last_sync.fetchone()
return {
"coverage": {
"first_date": cov.first_date,
"last_date": cov.last_date,
"total_days": cov.total_days,
},
"last_sync": {
"completed_at": sync_row.completed_at if sync_row else None,
"status": sync_row.status if sync_row else None,
"records_fetched": sync_row.records_fetched if sync_row else None,
"date_from": sync_row.date_from if sync_row else None,
"date_to": sync_row.date_to if sync_row else None,
}
}