fix: proxy weather icons through server to fix PWA standalone on Android
External image requests to openweathermap.org fail silently in Chrome Android PWA standalone mode. Icons are now proxied via GET /api/v1/weather/icon/:code, making them same-origin — cacheable by the service worker and free of CORS/CSP issues. Tightened CSP: removed openweathermap.org from imgSrc (no longer needed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -265,7 +265,7 @@ function renderPinnedNotes(notes) {
|
|||||||
// Wetter-Widget
|
// Wetter-Widget
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
const WEATHER_ICON_BASE = 'https://openweathermap.org/img/wn/';
|
const WEATHER_ICON_BASE = '/api/v1/weather/icon/';
|
||||||
|
|
||||||
function renderWeatherWidget(weather) {
|
function renderWeatherWidget(weather) {
|
||||||
if (!weather) return '';
|
if (!weather) return '';
|
||||||
@@ -279,7 +279,7 @@ function renderWeatherWidget(weather) {
|
|||||||
return `
|
return `
|
||||||
<div class="weather-forecast__day${extraCls}">
|
<div class="weather-forecast__day${extraCls}">
|
||||||
<div class="weather-forecast__label">${label}</div>
|
<div class="weather-forecast__label">${label}</div>
|
||||||
<img class="weather-forecast__icon" src="${WEATHER_ICON_BASE}${d.icon}@2x.png"
|
<img class="weather-forecast__icon" src="${WEATHER_ICON_BASE}${d.icon}"
|
||||||
alt="${d.desc}" width="32" height="32" loading="lazy">
|
alt="${d.desc}" width="32" height="32" loading="lazy">
|
||||||
<div class="weather-forecast__temps">
|
<div class="weather-forecast__temps">
|
||||||
<span class="weather-forecast__high">${d.temp_max}°</span>
|
<span class="weather-forecast__high">${d.temp_max}°</span>
|
||||||
@@ -303,7 +303,7 @@ function renderWeatherWidget(weather) {
|
|||||||
${t('dashboard.weatherFeelsLike', { temp: current.feels_like, humidity: current.humidity, wind: current.wind_speed })}
|
${t('dashboard.weatherFeelsLike', { temp: current.feels_like, humidity: current.humidity, wind: current.wind_speed })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img class="weather-widget__icon" src="${WEATHER_ICON_BASE}${current.icon}@2x.png"
|
<img class="weather-widget__icon" src="${WEATHER_ICON_BASE}${current.icon}"
|
||||||
alt="${current.desc}" width="80" height="80" loading="lazy">
|
alt="${current.desc}" width="80" height="80" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
|
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ app.use(helmet({
|
|||||||
'https://cdn.jsdelivr.net',
|
'https://cdn.jsdelivr.net',
|
||||||
],
|
],
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
imgSrc: ["'self'", 'data:', 'https://openweathermap.org'],
|
imgSrc: ["'self'", 'data:'],
|
||||||
connectSrc: ["'self'"],
|
connectSrc: ["'self'"],
|
||||||
fontSrc: ["'self'"],
|
fontSrc: ["'self'"],
|
||||||
objectSrc: ["'none'"],
|
objectSrc: ["'none'"],
|
||||||
|
|||||||
@@ -92,4 +92,33 @@ router.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// GET /api/v1/weather/icon/:code
|
||||||
|
// Proxy für OpenWeatherMap-Icons — vermeidet externe Bild-Requests
|
||||||
|
// im PWA-Standalone-Modus (CORS/CSP-Probleme auf Android Chrome).
|
||||||
|
// Erlaubte Codes: 2–4 alphanumerische Zeichen (z.B. "01d", "10n").
|
||||||
|
// Response: PNG-Bild mit 24h-Cache
|
||||||
|
// --------------------------------------------------------
|
||||||
|
router.get('/icon/:code', async (req, res) => {
|
||||||
|
const { code } = req.params;
|
||||||
|
if (!/^[a-zA-Z0-9]{2,4}$/.test(code)) {
|
||||||
|
return res.status(400).json({ error: 'Ungültiger Icon-Code.', code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { default: fetch } = await import('node-fetch');
|
||||||
|
const url = `https://openweathermap.org/img/wn/${code}@2x.png`;
|
||||||
|
const upstream = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
||||||
|
if (!upstream.ok) {
|
||||||
|
return res.status(502).json({ error: 'Icon nicht verfügbar.', code: 502 });
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', 'image/png');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 Stunden
|
||||||
|
upstream.body.pipe(res);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Weather] Icon-Proxy Fehler:', err.message);
|
||||||
|
res.status(502).json({ error: 'Icon-Proxy fehlgeschlagen.', code: 502 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user