diff --git a/src/fabledassistant/routes/journal.py b/src/fabledassistant/routes/journal.py index ca9a859..8c4a6b5 100644 --- a/src/fabledassistant/routes/journal.py +++ b/src/fabledassistant/routes/journal.py @@ -8,6 +8,7 @@ The ambient endpoints read locations + temp_unit + topic preferences from the """ from __future__ import annotations +import asyncio import datetime import json import logging @@ -222,12 +223,50 @@ async def _journal_temp_unit(user_id: int) -> str: # ── Weather ─────────────────────────────────────────────────────────────────── +_STALE_THRESHOLD_SECONDS = 4 * 3600 # 4 hours — start refreshing well before the 7-day forecast window slides past today + + +def _is_stale(cache_row) -> bool: + if cache_row is None or cache_row.fetched_at is None: + return True + age = (datetime.datetime.now(datetime.timezone.utc) - cache_row.fetched_at).total_seconds() + return age > _STALE_THRESHOLD_SECONDS + + +async def _refresh_stale_in_background(user_id: int, stale_keys: set[str]) -> None: + """Best-effort refresh of stale cache rows. Silently no-ops if the user's + config has no usable lat/lon for a given location_key.""" + cfg = await _resolve_config(user_id) + locations = cfg.get("locations") or {} + for key in stale_keys: + loc = locations.get(key) + if not loc or not loc.get("lat") or not loc.get("lon"): + continue + try: + await weather_svc.refresh_location_cache( + user_id=user_id, + location_key=key, + location_label=loc.get("label", key), + lat=loc["lat"], + lon=loc["lon"], + ) + except Exception: + logger.warning("Background weather refresh failed for user %d / %s", user_id, key, exc_info=True) + + @journal_bp.get("/weather") @login_required async def get_weather(): user_id = get_current_user_id() rows = await weather_svc.get_cached_weather_rows(user_id) temp_unit = await _journal_temp_unit(user_id) + + # Kick off a best-effort background refresh for stale rows so the next page + # load gets fresh data; we still serve whatever's currently cached now. + stale_keys = {row.location_key for row in rows if _is_stale(row)} + if stale_keys: + asyncio.create_task(_refresh_stale_in_background(user_id, stale_keys)) + cards = [ card for row in rows if (card := weather_svc.parse_weather_card_data(row, temp_unit)) is not None diff --git a/src/fabledassistant/services/weather.py b/src/fabledassistant/services/weather.py index c734e00..34f7eb3 100644 --- a/src/fabledassistant/services/weather.py +++ b/src/fabledassistant/services/weather.py @@ -156,15 +156,13 @@ def parse_weather_card_data( ) -> dict | None: """ Parse a WeatherCache row into the metadata.weather card schema. - Returns None if the cache is stale (older than 24 hours) or unavailable. + Returns None if the cache row is missing or unparseable. Stale data is + returned as-is — the frontend uses `fetched_at` to convey freshness, and + WeatherCard handles missing today/forecast fields gracefully. """ from datetime import date, timedelta - if cache_row is None or cache_row.fetched_at is None: - return None - - age_seconds = (datetime.now(timezone.utc) - cache_row.fetched_at).total_seconds() - if age_seconds > 86400: + if cache_row is None or cache_row.fetched_at is None or not cache_row.forecast_json: return None raw = cache_row.forecast_json or {}