diff --git a/app/public/app.js b/app/public/app.js
index 5fbeec1..a5059f1 100644
--- a/app/public/app.js
+++ b/app/public/app.js
@@ -48,6 +48,7 @@ const state = {
lastRefreshAt: 0,
lightningMarkers: [],
windArrows: [],
+ dailyExpanded: false,
};
// ── DOM ──────────────────────────────────────────────────────────────────────
@@ -73,6 +74,8 @@ const lightningToggle = document.getElementById('lightning-toggle');
const windToggle = document.getElementById('wind-toggle');
const lightningStatus = document.getElementById('lightning-status');
const windStatus = document.getElementById('wind-status');
+const dailyForecastLabel = document.getElementById('daily-forecast-label');
+const dailyForecastToggle = document.getElementById('daily-forecast-toggle');
// ── Helpers ──────────────────────────────────────────────────────────────────
const show = el => el.classList.remove('hidden');
@@ -310,7 +313,7 @@ async function fetchAlerts(lat, lon) {
function detectedCityFallback(lat, lon) {
return {
- name: `Detected location (${lat.toFixed(2)}, ${lon.toFixed(2)})`,
+ name: `Auto-detected (${lat.toFixed(2)}, ${lon.toFixed(2)})`,
latitude: lat,
longitude: lon,
country: '',
@@ -319,6 +322,17 @@ function detectedCityFallback(lat, lon) {
};
}
+function resolveDetectedName(place, fallback) {
+ const raw = (place?.name || '').trim();
+ const genericName = !raw || /current\s*location|detected\s*location/i.test(raw);
+ if (!genericName) return raw;
+
+ const displayRoot = (place?.display_name || '').split(',')[0]?.trim();
+ if (displayRoot) return displayRoot;
+
+ return fallback.name;
+}
+
async function buildDetectedCity(lat, lon) {
const fallback = detectedCityFallback(lat, lon);
@@ -327,6 +341,7 @@ async function buildDetectedCity(lat, lon) {
return {
...fallback,
...place,
+ name: resolveDetectedName(place, fallback),
latitude: lat,
longitude: lon,
timezone: place.timezone || 'auto',
@@ -639,6 +654,43 @@ function renderWindArrows(city) {
const mapsOverview = createOverviewMap(document.getElementById('map-overview'));
// ── Rendering ────────────────────────────────────────────────────────────────
+function renderDailyForecast(daily) {
+ const list = document.getElementById('daily-list');
+ const totalDays = daily?.time?.length || 0;
+ const defaultDays = Math.min(7, totalDays);
+ const visibleDays = state.dailyExpanded ? totalDays : defaultDays;
+
+ list.innerHTML = '';
+ for (let i = 0; i < visibleDays; i++) {
+ const dw = wmo(daily.weather_code[i]);
+ const row = document.createElement('div');
+ row.className = 'day-row' + (i === 0 ? ' today' : '');
+ row.innerHTML = `
+
${formatDayShort(daily.time[i])}
+ ${dw.icon}
+ ${dw.desc}
+ ${tempDisplay(daily.temperature_2m_max[i])}° / ${tempDisplay(daily.temperature_2m_min[i])}°
+ ${daily.precipitation_probability_max[i] > 0 ? daily.precipitation_probability_max[i] + '% rain
' : ''}${speedDisplay(daily.wind_speed_10m_max[i])} ${speedUnit()}
+ `;
+ list.appendChild(row);
+ }
+
+ if (dailyForecastLabel) {
+ dailyForecastLabel.textContent = `${visibleDays}-Day Forecast`;
+ }
+
+ if (dailyForecastToggle) {
+ if (totalDays > defaultDays) {
+ show(dailyForecastToggle);
+ dailyForecastToggle.textContent = state.dailyExpanded
+ ? 'Show 7-day view'
+ : `Show full ${totalDays}-day forecast`;
+ } else {
+ hide(dailyForecastToggle);
+ }
+ }
+}
+
function renderWeather(data, city) {
const c = data.current;
const h = data.hourly;
@@ -695,21 +747,7 @@ function renderWeather(data, city) {
track.appendChild(cell);
}
- const list = document.getElementById('daily-list');
- list.innerHTML = '';
- for (let i = 0; i < d.time.length; i++) {
- const dw = wmo(d.weather_code[i]);
- const row = document.createElement('div');
- row.className = 'day-row' + (i === 0 ? ' today' : '');
- row.innerHTML = `
- ${formatDayShort(d.time[i])}
- ${dw.icon}
- ${dw.desc}
- ${tempDisplay(d.temperature_2m_max[i])}° / ${tempDisplay(d.temperature_2m_min[i])}°
- ${d.precipitation_probability_max[i] > 0 ? d.precipitation_probability_max[i] + '% rain
' : ''}${speedDisplay(d.wind_speed_10m_max[i])} ${speedUnit()}
- `;
- list.appendChild(row);
- }
+ renderDailyForecast(d);
document.getElementById('updated-at').textContent =
new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
@@ -996,6 +1034,14 @@ windToggle.addEventListener('change', () => {
}
});
+if (dailyForecastToggle) {
+ dailyForecastToggle.addEventListener('click', () => {
+ if (!state.currentWeather?.daily) return;
+ state.dailyExpanded = !state.dailyExpanded;
+ renderDailyForecast(state.currentWeather.daily);
+ });
+}
+
function startAutoRefresh() {
clearInterval(state.autoRefreshTimer);
state.autoRefreshTimer = setInterval(() => {
diff --git a/app/public/index.html b/app/public/index.html
index d45ef07..c1bd440 100644
--- a/app/public/index.html
+++ b/app/public/index.html
@@ -128,7 +128,10 @@
- 7-Day Forecast
+ 7-Day Forecast
+
+
+
diff --git a/app/public/style.css b/app/public/style.css
index 546c81a..bf1d62d 100644
--- a/app/public/style.css
+++ b/app/public/style.css
@@ -652,6 +652,12 @@ a:hover { text-decoration: underline; }
.day-lo { color: var(--ink-light); }
.day-meta { text-align: right; font-size: 0.62rem; color: var(--ink-light); line-height: 1.6; }
+#daily-forecast-toggle {
+ font-size: 0.66rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
/* ── Footer ── */
.footer-note {
font-size: 0.62rem;
diff --git a/app/server.js b/app/server.js
index cb2bb0d..c938732 100644
--- a/app/server.js
+++ b/app/server.js
@@ -26,6 +26,32 @@ app.get('/api/reverse-geocode', async (req, res) => {
if (!lat || !lon) return res.status(400).json({ error: 'Missing lat/lon' });
try {
+ // Prefer Open-Meteo reverse geocoding for stable city/region labels.
+ const omParams = new URLSearchParams({
+ latitude: String(lat),
+ longitude: String(lon),
+ count: '1',
+ language: 'en',
+ format: 'json',
+ });
+
+ const omResp = await fetch(`https://geocoding-api.open-meteo.com/v1/reverse?${omParams}`);
+ if (omResp.ok) {
+ const omData = await omResp.json();
+ const top = omData?.results?.[0];
+ if (top) {
+ return res.json({
+ name: top.name || top.admin2 || top.admin1 || top.country || 'Detected location',
+ admin1: top.admin1 || top.admin2 || '',
+ country: top.country || '',
+ latitude: Number(lat),
+ longitude: Number(lon),
+ timezone: top.timezone || 'auto',
+ display_name: [top.name, top.admin1, top.country].filter(Boolean).join(', '),
+ });
+ }
+ }
+
const params = new URLSearchParams({
format: 'jsonv2',
lat: String(lat),