Merge pull request #4 from nmemmert/claude/ambient-station-overlay-m8dh80
Use station data for current conditions; persist keys server-side
This commit is contained in:
4 files changed
+151
-4
No files matched your search
+103
-1
@@ -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} <span id="cur-feels">${fT(data.feelsLike)}</span>°${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);
|
||||
}
|
||||
@@ -64,6 +64,7 @@
|
||||
<div id="cur-icon" class="hero-icon"></div>
|
||||
<div id="cur-desc" class="hero-desc"></div>
|
||||
<div id="cur-precip" class="hero-sub"></div>
|
||||
<div id="station-badge" class="station-badge hidden">📡 <span id="station-badge-name">My Station</span></div>
|
||||
</div>
|
||||
<div class="hero-center">
|
||||
<div class="hero-temp"><span id="cur-temp">--</span><span class="deg">°F</span></div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+32
-3
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user