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 */ }
}
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)
const newsItems = ref<NewsItem[]>([])
@@ -279,14 +290,23 @@ onMounted(async () => {
<div class="briefing-right">
<!-- Weather section (sticky) -->
<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
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>
class="weather-refresh-btn"
:class="{ spinning: refreshingWeather }"
:disabled="refreshingWeather"
@click="refreshWeather"
title="Refresh weather"
></button>
</div>
<WeatherCard
:weather="weatherData[selectedWeatherIdx]"
@@ -464,10 +484,37 @@ onMounted(async () => {
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 {
display: flex;
gap: 0.25rem;
margin-bottom: 0.5rem;
}
.weather-tab {
+11 -3
View File
@@ -167,6 +167,7 @@ async def get_weather():
return jsonify({"locations": cards, "temp_unit": temp_unit})
@briefing_bp.route("/weather/current", methods=["GET"])
@_REQUIRE
async def get_current_weather():
@@ -225,11 +226,14 @@ async def refresh_weather():
raw = await get_setting(g.user.id, "briefing_config", "{}")
try:
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:
config = {}
temp_unit = "C"
locations = config.get("locations", {})
refreshed = []
for key, loc in locations.items():
if not loc.get("lat") or not loc.get("lon"):
continue
@@ -241,11 +245,15 @@ async def refresh_weather():
lat=loc["lat"],
lon=loc["lon"],
)
refreshed.append(key)
except Exception:
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 ─────────────────────────────────────────────────────