Fix XSS, validation, and CSS regressions; refactor proxy retry logic
- Sanitize geocode suggestion rendering to prevent XSS - Add lat/lon validation and cache eviction to weather proxy endpoints - Extract shared fetchWithRetry helper, dedupe retry/timeout logic - Stop leaking upstream error details to clients - Fix radar provider metadata and drop dead nowcast field - Fix missing --surface-radius-sm var and duplicate/conflicting CSS rules
This commit is contained in:
1 parent
49b54e0fe4
commit
1863f6784b
4 files changed
+104
-144
No files matched your search
+7
-10
@@ -425,7 +425,6 @@ async function fetchWeather(lat, lon, tz) {
|
||||
|
||||
async function fetchAirQuality(lat, lon, tz) {
|
||||
const url = `/api/air-quality?lat=${lat}&lon=${lon}&tz=${encodeURIComponent(tz||'auto')}`;
|
||||
console.log('Fetching air quality from:', url);
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`Air quality fetch failed with status ${r.status}`);
|
||||
return r.json();
|
||||
@@ -641,13 +640,6 @@ function createRadar(mapEl, timelineEl, playBtn, tsEl, layerBtn) {
|
||||
maxNativeZoom: f.maxNativeZoom,
|
||||
isForecast: false,
|
||||
}));
|
||||
(data.radar?.nowcast || []).forEach(f => radarFrames.push({
|
||||
time: f.time,
|
||||
path: f.path,
|
||||
url: f.url,
|
||||
maxNativeZoom: f.maxNativeZoom,
|
||||
isForecast: true,
|
||||
}));
|
||||
(data.satellite?.infrared || []).forEach(f => satelliteFrames.push({
|
||||
time: f.time,
|
||||
path: f.path,
|
||||
@@ -1076,7 +1068,7 @@ function renderWeather(data, city) {
|
||||
document.getElementById('cur-desc').textContent = w.desc;
|
||||
document.getElementById('cur-temp').textContent = tempDisplay(c.temperature_2m);
|
||||
document.querySelector('.deg').textContent = `°${tempUnit()}`;
|
||||
document.querySelector('.feels').innerHTML = `${apparentLabel(c.temperature_2m, c.apparent_temperature)} <span id="cur-feels">${tempDisplay(c.apparent_temperature)}</span>°${tempUnit()}`;
|
||||
document.getElementById('feels-line').innerHTML = `${apparentLabel(c.temperature_2m, c.apparent_temperature)} <span id="cur-feels">${tempDisplay(c.apparent_temperature)}</span>°${tempUnit()}`;
|
||||
document.getElementById('cur-hi').textContent = tempDisplay(d.temperature_2m_max[0]);
|
||||
document.getElementById('cur-lo').textContent = tempDisplay(d.temperature_2m_min[0]);
|
||||
document.getElementById('cur-precip').textContent = c.precipitation > 0 ? `${precipDisplay(c.precipitation)} ${state.units === 'us' ? 'in' : 'mm'} precip` : 'No precipitation';
|
||||
@@ -1668,7 +1660,12 @@ searchInput.addEventListener('input', () => {
|
||||
|
||||
results.forEach(r => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `${r.name}<span class="sug-country">${r.admin1 ? r.admin1 + ', ' : ''}${r.country || ''}</span>`;
|
||||
const nameText = document.createTextNode(r.name);
|
||||
const countrySpan = document.createElement('span');
|
||||
countrySpan.className = 'sug-country';
|
||||
countrySpan.textContent = `${r.admin1 ? r.admin1 + ', ' : ''}${r.country || ''}`;
|
||||
li.appendChild(nameText);
|
||||
li.appendChild(countrySpan);
|
||||
li.addEventListener('click', () => {
|
||||
searchInput.value = r.name;
|
||||
hide(suggestEl);
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
<div class="hero-center">
|
||||
<div class="hero-temp"><span id="cur-temp">--</span><span class="deg">°F</span></div>
|
||||
<div class="feels">Feels like <span id="cur-feels">--</span>°F</div>
|
||||
<div class="feels" id="feels-line">Feels like <span id="cur-feels">--</span>°F</div>
|
||||
<button id="share-hero-btn" class="hero-share-btn" type="button">Share hero card</button>
|
||||
<p id="share-hero-status" class="hero-share-status hidden"></p>
|
||||
</div>
|
||||
|
||||
+5
-27
@@ -19,6 +19,7 @@
|
||||
--surface-border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
--surface-radius-lg: 16px;
|
||||
--surface-radius-md: 12px;
|
||||
--surface-radius-sm: 8px;
|
||||
--surface-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
@@ -338,6 +339,10 @@ a:hover { text-decoration: underline; }
|
||||
color: var(--ink-mid);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab:last-child { border-right: none; }
|
||||
@@ -498,25 +503,6 @@ a:hover { text-decoration: underline; }
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.45rem;
|
||||
font-size: 0.68rem;
|
||||
color: var(--ink-mid);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toggle-label input {
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.toggle-label span {
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.saved-item {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border);
|
||||
@@ -1581,8 +1567,6 @@ details[open] .drawer-chevron { transform: rotate(90deg); }
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-label { font-size: 0.58rem; letter-spacing: 0.1em; color: var(--ink-light); text-transform: uppercase; margin-bottom: 0.3rem; }
|
||||
|
||||
/* ── Section heads ── */
|
||||
.section-head {
|
||||
display: flex;
|
||||
@@ -1813,12 +1797,6 @@ details[open] .drawer-chevron { transform: rotate(90deg); }
|
||||
}
|
||||
|
||||
/* ── Touch-friendly tap targets (min 44 × 44 px) ── */
|
||||
.tab {
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.search-row input,
|
||||
.search-row button { min-height: 44px; }
|
||||
.radar-btn { width: 44px; height: 44px; font-size: 0.8rem; }
|
||||
|
||||
Reference in new issue
Block a user