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:
@@ -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 {
|
||||||
|
|||||||
@@ -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 ─────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user