fix(journal): wire weather refresh on config save; drop orphaned cache rows

Two related gaps in the journal weather panel:

1. Saving locations via PUT /journal/config didn't trigger a weather
   fetch, so newly-entered sites had no cache row (or a stale one) until
   the user manually clicked the panel's refresh button. The panel
   rendered "two sites with empty values" against pre-existing cache
   rows that no longer matched what the user had configured.

2. get_cached_weather_rows returned every WeatherCache row for the user
   regardless of whether the location was still in journal_config.
   Briefing-era rows survived migration 0040 (which only deleted the
   briefing_config setting, not the cache table) and showed up as
   ghost tabs in the UI.

Changes:
- get_cached_weather_rows accepts an optional valid_keys filter; rows
  whose location_key is not in the set are excluded.
- routes/journal.py:
  - put_config kicks off a background refresh_location_cache for any
    saved location with valid lat/lon.
  - GET /weather and POST /weather/refresh both pass valid_keys derived
    from the current config so orphaned rows don't surface.
- services/journal_prep.py filters the weather section to currently-
  configured locations as well; uses a lazy import of get_journal_config
  to avoid a cycle (journal_scheduler imports journal_prep).

153 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 20:37:23 -04:00
parent 36cd08c236
commit c33cab7020
3 changed files with 73 additions and 8 deletions
+9 -1
View File
@@ -178,7 +178,15 @@ async def gather_daily_sections(
sections["events"] = []
try:
weather_rows = await get_cached_weather_rows(user_id)
# Lazy import: journal_scheduler imports this module for prep generation,
# so a top-level import would cycle.
from fabledassistant.services.journal_scheduler import get_journal_config
cfg = await get_journal_config(user_id)
valid_weather_keys = {
key for key, loc in (cfg.get("locations") or {}).items()
if isinstance(loc, dict) and loc.get("lat") is not None and loc.get("lon") is not None
}
weather_rows = await get_cached_weather_rows(user_id, valid_weather_keys)
sections["weather"] = [w.to_dict() for w in weather_rows]
except Exception:
logger.exception("daily_prep weather section failed for user %d", user_id)
+16 -5
View File
@@ -232,12 +232,23 @@ def parse_weather_card_data(
}
async def get_cached_weather_rows(user_id: int) -> list:
"""Return raw WeatherCache ORM rows for a user (for card parsing)."""
async def get_cached_weather_rows(
user_id: int,
valid_keys: set[str] | None = None,
) -> list:
"""Return raw WeatherCache ORM rows for a user (for card parsing).
If ``valid_keys`` is provided, only rows whose ``location_key`` is in the
set are returned. This is how callers drop orphaned cache rows whose
location is no longer in the user's ``journal_config.locations`` (e.g.
leftovers from the briefing era, or a location that's been removed).
Passing an empty set returns no rows.
"""
async with async_session() as session:
result = await session.execute(
select(WeatherCache).where(WeatherCache.user_id == user_id)
)
stmt = select(WeatherCache).where(WeatherCache.user_id == user_id)
if valid_keys is not None:
stmt = stmt.where(WeatherCache.location_key.in_(list(valid_keys)))
result = await session.execute(stmt)
return list(result.scalars().all())