De labels toonden de werkelijke meettijdstippen van Netatmo (11:41, 17:41, 23:41), wat onrustig leest. Nu markeringen op hele uren die op de klok zijn uitgelijnd: elke 6 uur vanaf middernacht, dus 00:00, 06:00, 12:00, 18:00, elk op zijn echte tijdspositie in het venster. "nu" blijft op de rechterrand; een markering die daar te dicht bij komt valt weg. axisMarks() is vervangen door hourTicks(), die uur voor uur door het venster loopt en telkens de lokale klok raadpleegt. Daardoor blijven de labels ook op de zomertijdwissel hele lokale uren, waar optellen in vaste stappen er een uur naast zou zitten; daar is een test voor. Een label vlak op de linkerrand wordt niet gecentreerd, zodat het binnen de grafiek blijft. Servicewerker naar v9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
2.7 KiB
JavaScript
91 lines
2.7 KiB
JavaScript
/* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */
|
|
|
|
const VERSION = 'weerscherm-v9';
|
|
const SHELL = [
|
|
'./',
|
|
'./index.html',
|
|
'./styles.css',
|
|
'./app.js',
|
|
'./format.js',
|
|
'./icons.js',
|
|
'./manifest.webmanifest',
|
|
'./icons/icon.svg',
|
|
'./icons/icon-180.png',
|
|
'./icons/icon-192.png',
|
|
'./icons/icon-512.png',
|
|
];
|
|
const API_PREFIX = '/api/';
|
|
const DATA_PATH = '/api/data.php';
|
|
const API_TIMEOUT_MS = 8000;
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(VERSION).then((cache) => cache.addAll(SHELL)).then(() => self.skipWaiting()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== VERSION).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request;
|
|
if (req.method !== 'GET') return;
|
|
const url = new URL(req.url);
|
|
if (url.origin !== self.location.origin) return;
|
|
|
|
if (url.pathname.includes(API_PREFIX)) {
|
|
event.respondWith(networkFirst(req));
|
|
} else {
|
|
event.respondWith(cacheFirst(req));
|
|
}
|
|
});
|
|
|
|
async function networkFirst(req) {
|
|
const cache = await caches.open(VERSION);
|
|
const url = new URL(req.url);
|
|
// Sleutel zonder querystring: het radarbeeld krijgt elke paar minuten een andere ?t=, en zonder
|
|
// normaliseren zou de cache vollopen met varianten en zou offline nooit een treffer geven.
|
|
const key = url.origin + url.pathname;
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
try {
|
|
const res = await fetch(req, { signal: controller.signal, cache: 'no-store' });
|
|
if (res.ok) {
|
|
cache.put(key, res.clone());
|
|
}
|
|
return res;
|
|
} catch (err) {
|
|
const cached = await cache.match(key);
|
|
if (cached) {
|
|
const headers = new Headers(cached.headers);
|
|
headers.set('X-From-Cache', '1');
|
|
return new Response(await cached.arrayBuffer(), { status: 200, statusText: 'OK', headers });
|
|
}
|
|
if (url.pathname.endsWith(DATA_PATH)) {
|
|
return new Response(JSON.stringify({ errors: ['offline'] }), {
|
|
status: 503,
|
|
headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
});
|
|
}
|
|
return new Response('offline', { status: 503, headers: { 'Cache-Control': 'no-store' } });
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
async function cacheFirst(req) {
|
|
const cache = await caches.open(VERSION);
|
|
const cached = await cache.match(req, { ignoreSearch: true });
|
|
if (cached) return cached;
|
|
const res = await fetch(req);
|
|
if (res.ok) {
|
|
cache.put(req, res.clone());
|
|
}
|
|
return res;
|
|
}
|