feat(weather): add refresh button and return card data from refresh endpoint

Add a refresh button (↻) to the weather section header in BriefingView so
users can manually re-fetch weather data (needed after deploying the hourly
precip changes to populate the cache). Update the existing POST
/api/briefing/weather/refresh endpoint to return full card data instead of
just location keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 16:40:57 -04:00
parent ddab0db781
commit 619f069358
2 changed files with 66 additions and 11 deletions
+55 -8
View File
@@ -92,6 +92,17 @@ async function loadWeather() {
} catch { /* silent */ } } catch { /* silent */ }
} }
const refreshingWeather = ref(false)
async function refreshWeather() {
refreshingWeather.value = true
try {
const data = await apiPost<{ locations: WeatherData[]; temp_unit: string }>('/api/briefing/weather/refresh', {})
weatherData.value = data.locations ?? []
tempUnit.value = data.temp_unit ?? 'C'
} catch { /* silent */ }
finally { refreshingWeather.value = false }
}
// News panel (right column) // News panel (right column)
const newsItems = ref<NewsItem[]>([]) const newsItems = ref<NewsItem[]>([])
@@ -279,14 +290,23 @@ onMounted(async () => {
<div class="briefing-right"> <div class="briefing-right">
<!-- Weather section (sticky) --> <!-- Weather section (sticky) -->
<div class="weather-section" v-if="weatherData.length"> <div class="weather-section" v-if="weatherData.length">
<div class="weather-tabs" v-if="weatherData.length > 1"> <div class="weather-section-header">
<div class="weather-tabs" v-if="weatherData.length > 1">
<button
v-for="(loc, i) in weatherData"
:key="(loc as WeatherData).location"
class="weather-tab"
:class="{ active: selectedWeatherIdx === i }"
@click="selectedWeatherIdx = i"
>{{ (loc as WeatherData).location }}</button>
</div>
<button <button
v-for="(loc, i) in weatherData" class="weather-refresh-btn"
:key="(loc as WeatherData).location" :class="{ spinning: refreshingWeather }"
class="weather-tab" :disabled="refreshingWeather"
:class="{ active: selectedWeatherIdx === i }" @click="refreshWeather"
@click="selectedWeatherIdx = i" title="Refresh weather"
>{{ (loc as WeatherData).location }}</button> ></button>
</div> </div>
<WeatherCard <WeatherCard
:weather="weatherData[selectedWeatherIdx]" :weather="weatherData[selectedWeatherIdx]"
@@ -464,10 +484,37 @@ onMounted(async () => {
margin-bottom: 0; margin-bottom: 0;
} }
.weather-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.weather-refresh-btn {
background: none;
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-text-muted);
font-size: 1rem;
cursor: pointer;
padding: 0.2rem 0.45rem;
line-height: 1;
transition: all 0.15s;
}
.weather-refresh-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.weather-refresh-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.weather-refresh-btn.spinning {
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.weather-tabs { .weather-tabs {
display: flex; display: flex;
gap: 0.25rem; gap: 0.25rem;
margin-bottom: 0.5rem;
} }
.weather-tab { .weather-tab {
+11 -3
View File
@@ -167,6 +167,7 @@ async def get_weather():
return jsonify({"locations": cards, "temp_unit": temp_unit}) return jsonify({"locations": cards, "temp_unit": temp_unit})
@briefing_bp.route("/weather/current", methods=["GET"]) @briefing_bp.route("/weather/current", methods=["GET"])
@_REQUIRE @_REQUIRE
async def get_current_weather(): async def get_current_weather():
@@ -225,11 +226,14 @@ async def refresh_weather():
raw = await get_setting(g.user.id, "briefing_config", "{}") raw = await get_setting(g.user.id, "briefing_config", "{}")
try: try:
config = json.loads(raw) if isinstance(raw, str) else {} config = json.loads(raw) if isinstance(raw, str) else {}
temp_unit = config.get("temp_unit", "C")
if temp_unit not in ("C", "F"):
temp_unit = "C"
except Exception: except Exception:
config = {} config = {}
temp_unit = "C"
locations = config.get("locations", {}) locations = config.get("locations", {})
refreshed = []
for key, loc in locations.items(): for key, loc in locations.items():
if not loc.get("lat") or not loc.get("lon"): if not loc.get("lat") or not loc.get("lon"):
continue continue
@@ -241,11 +245,15 @@ async def refresh_weather():
lat=loc["lat"], lat=loc["lat"],
lon=loc["lon"], lon=loc["lon"],
) )
refreshed.append(key)
except Exception: except Exception:
logger.warning("Failed to refresh weather for %s", key, exc_info=True) logger.warning("Failed to refresh weather for %s", key, exc_info=True)
return jsonify({"refreshed": refreshed}) rows = await weather_svc.get_cached_weather_rows(g.user.id)
cards = [
card for row in rows
if (card := weather_svc.parse_weather_card_data(row, temp_unit)) is not None
]
return jsonify({"locations": cards, "temp_unit": temp_unit})
# ── Briefing Conversations ───────────────────────────────────────────────────── # ── Briefing Conversations ─────────────────────────────────────────────────────