Files
weerscherm/public/sw.js
T
theoandClaude Opus 5 bfc96acb5a feat: temperatuurverloop van vandaag ook bij de binnenmodules
Naast het getal in elke modulekaart staat nu hetzelfde soort lijntje als
bij buiten (200x40). Zonder labels: daar is in zo'n kaart geen ruimte
voor, de schaal staat al op de regel eronder en het venster is hetzelfde
"vandaag" als bij het grafiekje van buiten ernaast.

Per module een eigen getmeasure, gecachet onder temperature-<module> met
een gedeelde TTL; fetch() heeft daarvoor een losse ttlKey gekregen, want
anders zou de TTL-lookup per module mislukken en zou er helemaal niet
gecachet worden. Het basisstation meet zelf temperatuur en wordt zonder
module_id opgevraagd, dus die parameter is optioneel geworden. In de
payload als indoor[].history; mislukt er een, dan blijft die lijst leeg
en verdwijnt alleen dat lijntje, met de foutcode in errors. Een
stale-vlag per kamer zou het contract nodeloos zwaar maken.

De hoogte is overal 40px, ook bij een vastgezette module: gelijke hoogte
houdt de kamers onderling vergelijkbaar, en een hoger lijntje zou de
toch al krappe speling onder de kolom opeten. De grafiekjes voegen geen
hoogte toe aan de kaarten.

FakeSource houdt nu alle aanroepen bij in plaats van alleen de laatste,
zodat een test de volgorde van de vier temperatuur-calls kan nalopen.
Servicewerker naar v11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:10:40 +02:00

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-v11';
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;
}