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:
Ulas
2026-04-01 09:57:48 +02:00
parent ac294628e8
commit be8801aef7
3 changed files with 33 additions and 4 deletions
+3 -3
View File
@@ -265,7 +265,7 @@ function renderPinnedNotes(notes) {
// Wetter-Widget
// --------------------------------------------------------
const WEATHER_ICON_BASE = 'https://openweathermap.org/img/wn/';
const WEATHER_ICON_BASE = '/api/v1/weather/icon/';
function renderWeatherWidget(weather) {
if (!weather) return '';
@@ -279,7 +279,7 @@ function renderWeatherWidget(weather) {
return `
<div class="weather-forecast__day${extraCls}">
<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">
<div class="weather-forecast__temps">
<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 })}
</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">
</div>
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
+1 -1
View File
@@ -43,7 +43,7 @@ app.use(helmet({
'https://cdn.jsdelivr.net',
],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https://openweathermap.org'],
imgSrc: ["'self'", 'data:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
+29
View File
@@ -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: 24 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;