diff --git a/app/public/app.js b/app/public/app.js index cf856e8..e657898 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -348,6 +348,7 @@ const state = { pwsUrl: localStorage.getItem(STORAGE_KEYS.pwsUrl) || '', ambientApiKey: localStorage.getItem(STORAGE_KEYS.ambientApiKey) || '', ambientAppKey: localStorage.getItem(STORAGE_KEYS.ambientAppKey) || '', + ambientDevice: null, pushSub: null, hourlyExtended: false, hourlyFeelsLike: false, @@ -1503,6 +1504,9 @@ function renderWeather(data, city) { show(weatherDiv); setTimeout(() => { radarEmbed.refresh && radarEmbed.refresh(); }, 80); + + // Apply station overlay if we have fresh station data + if (state.ambientDevice) applyStationOverlay(state.ambientDevice); } function renderDailyForecast(daily) { @@ -2684,8 +2688,14 @@ if (ambientSaveBtn) { state.ambientAppKey = appKey; localStorage.setItem(STORAGE_KEYS.ambientApiKey, apiKey); localStorage.setItem(STORAGE_KEYS.ambientAppKey, appKey); + // Also persist keys server-side so all devices share them + fetch('/api/ambient-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ apiKey, appKey }), + }).catch(() => {}); const stationName = devices?.[0]?.info?.name || devices?.[0]?.macAddress || 'station'; - if (ambientStatusEl) { ambientStatusEl.textContent = `✓ Connected to ${stationName}`; show(ambientStatusEl); } + if (ambientStatusEl) { ambientStatusEl.textContent = `✓ Connected to ${stationName} — saved for all devices`; show(ambientStatusEl); } fetchAmbientStation(); } else { const { error } = await res.json().catch(() => ({})); @@ -2698,6 +2708,14 @@ if (ambientSaveBtn) { }); } +// On load, check if server already has keys configured (other device saved them) +fetch('/api/ambient-config').then(r => r.json()).then(({ configured }) => { + if (configured && !state.ambientApiKey) { + if (ambientStatusEl) { ambientStatusEl.textContent = '✓ Keys configured on server'; show(ambientStatusEl); } + fetchAmbientStation(); + } +}).catch(() => {}); + const pwsUrlInput = document.getElementById('pws-url-input'); const pwsSaveBtn = document.getElementById('pws-save-btn'); const pwsStatusEl = document.getElementById('pws-status'); @@ -2859,7 +2877,88 @@ async function fetchAmbientStation() { } } +function applyStationOverlay(device) { + if (!device) return; + const data = device.lastData || {}; + if (!data.dateutc || Date.now() - data.dateutc > 15 * 60 * 1000) return; // stale > 15 min + + const isMetric = state.units !== 'us'; + const fT = f => f == null ? null : (isMetric ? Math.round((f - 32) * 5 / 9) : Math.round(f)); + const fS = mph => mph == null ? null : (isMetric ? Math.round(mph * 1.60934) : Math.round(mph)); + + const setEl = (id, val) => { if (val == null) return; const el = document.getElementById(id); if (el) el.textContent = val; }; + + if (data.tempf != null) { + setEl('cur-temp', fT(data.tempf)); + const panelTemp = document.getElementById('panel-temp'); + if (panelTemp) panelTemp.textContent = `${fT(data.tempf)}°${tempUnit()}`; + } + + if (data.feelsLike != null) { + setEl('cur-feels', fT(data.feelsLike)); + const feelsLine = document.getElementById('feels-line'); + if (feelsLine) { + const label = apparentLabel(data.tempf, data.feelsLike); + feelsLine.innerHTML = `${label} ${fT(data.feelsLike)}°${tempUnit()}`; + } + } + + if (data.humidity != null) { + setEl('cur-hum', data.humidity); + setEl('hum-feel', humidityFeel(data.humidity)); + } + + if (data.tempf != null && data.humidity != null) { + const tc = (data.tempf - 32) * 5 / 9; + const alpha = Math.log(data.humidity / 100) + (17.62 * tc) / (243.12 + tc); + const dewC = (243.12 * alpha) / (17.62 - alpha); + setEl('cur-dew', isMetric ? Math.round(dewC) : Math.round(dewC * 9 / 5 + 32)); + } + + if (data.windspeedmph != null) { + setEl('cur-wind', fS(data.windspeedmph)); + const panelWind = document.getElementById('panel-wind-speed'); + if (panelWind) panelWind.textContent = `${fS(data.windspeedmph)} ${speedUnit()}`; + } + if (data.winddir != null) { + setEl('cur-wind-dir', compassDirFull(data.winddir)); + const panelDir = document.getElementById('panel-wind-dir'); + if (panelDir) panelDir.textContent = compassDirFull(data.winddir); + } + if (data.windgustmph != null) setEl('cur-gust', fS(data.windgustmph)); + + // Pressure: Ambient gives inHg, display expects hPa + if (data.baromrelin != null) setEl('cur-pres', Math.round(data.baromrelin * 33.8639)); + + if (data.uv != null) setEl('cur-uv', data.uv); + + // Rain summary in hero sub-line + const rainEl = document.getElementById('cur-precip'); + if (rainEl && data.dailyrainin != null) { + const rainVal = isMetric ? (data.dailyrainin * 25.4).toFixed(1) : data.dailyrainin.toFixed(2); + const rainUnit2 = isMetric ? 'mm' : 'in'; + rainEl.textContent = data.dailyrainin > 0 + ? `${rainVal} ${rainUnit2} rain today` + : 'No rain today'; + } + + // Wind peek summary + const peekEl = document.getElementById('wind-drawer-peek'); + if (peekEl && data.windspeedmph != null) { + peekEl.textContent = `· ${fS(data.windspeedmph)} ${speedUnit()} ${compassDir(data.winddir || 0)} · ${data.humidity ?? '--'}% humidity`; + } + + // Station badge + const badge = document.getElementById('station-badge'); + const badgeName = document.getElementById('station-badge-name'); + if (badge) { + if (badgeName) badgeName.textContent = device.info?.name || 'My Station'; + show(badge); + } +} + function renderAmbientStation(device) { + state.ambientDevice = device; const data = device?.lastData || {}; const info = device?.info || {}; const contentEl = document.getElementById('station-content'); @@ -2934,4 +3033,7 @@ function renderAmbientStation(device) { } if (contentEl) show(contentEl); + + // Update the main conditions display with real station readings + if (state.currentWeather) applyStationOverlay(device); } diff --git a/app/public/index.html b/app/public/index.html index 794c42e..294123a 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -64,6 +64,7 @@
+
--°F
diff --git a/app/public/style.css b/app/public/style.css index 9944cad..bccda0a 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -2487,3 +2487,18 @@ body.map-fs #unified-map { } .station-name { font-weight: 600; font-size: 0.95rem; } .station-updated { font-size: 0.75rem; color: var(--ink-light); } + +.station-badge { + display: inline-flex; + align-items: center; + gap: 0.3rem; + margin-top: 0.4rem; + font-size: 0.72rem; + font-weight: 600; + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, transparent); + border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); + border-radius: 999px; + padding: 0.15rem 0.55rem; + letter-spacing: 0.02em; +} diff --git a/app/server.js b/app/server.js index 34a7806..8fc1374 100644 --- a/app/server.js +++ b/app/server.js @@ -441,10 +441,39 @@ app.get('/api/spc/reports', async (_req, res) => { }); // ── Ambient Weather personal station ───────────────────────────────────────── -const ambientCaches = new Map(); // keyed by apiKey so each user's cache is independent +const AMBIENT_CONFIG_FILE = path.join(DATA_DIR, 'ambient-config.json'); +let ambientServerConfig = { apiKey: '', appKey: '' }; +try { + if (fs.existsSync(AMBIENT_CONFIG_FILE)) { + ambientServerConfig = JSON.parse(fs.readFileSync(AMBIENT_CONFIG_FILE, 'utf8')); + } +} catch {} + +function saveAmbientConfig() { + try { fs.writeFileSync(AMBIENT_CONFIG_FILE, JSON.stringify(ambientServerConfig)); } catch {} +} + +const ambientCaches = new Map(); + +// Save keys server-side so all devices share them +app.post('/api/ambient-config', (req, res) => { + const { apiKey, appKey } = req.body || {}; + if (!apiKey || !appKey) return res.status(400).json({ error: 'apiKey and appKey required' }); + ambientServerConfig = { apiKey, appKey }; + saveAmbientConfig(); + res.json({ ok: true }); +}); + +// Return whether server-side keys exist (never expose the actual keys) +app.get('/api/ambient-config', (_req, res) => { + const hasKeys = !!(ambientServerConfig.apiKey && ambientServerConfig.appKey); + res.json({ configured: hasKeys }); +}); + app.get('/api/ambient', async (req, res) => { - const apiKey = req.query.apiKey || process.env.AMBIENT_API_KEY; - const appKey = req.query.appKey || process.env.AMBIENT_APP_KEY; + // Priority: query params (client override) → server-saved → env vars + const apiKey = req.query.apiKey || ambientServerConfig.apiKey || process.env.AMBIENT_API_KEY; + const appKey = req.query.appKey || ambientServerConfig.appKey || process.env.AMBIENT_APP_KEY; if (!apiKey || !appKey) return res.status(503).json({ error: 'Ambient Weather keys not configured' }); const cacheKey = apiKey;