Files
KeepItGoingServer/templates/sw.js
T
Keith SmithandClaude Sonnet 5 bde7be454b Show overdue styling on tasks in offline mode
renderTaskItem() never applied the "overdue" CSS class at all, so
overdue tasks in offline mode looked like any other task instead of
getting the red highlight/due-date treatment they get online. Adds
isTaskOverdue(), mirroring Task.is_overdue, applied the same way
_task_item.html does it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 00:15:22 -06:00

85 lines
2.9 KiB
JavaScript

{% load static %}const CACHE_NAME = 'keepitgoing-shell-v7';
const OFFLINE_URL = '{% url "offline" %}';
const OFFLINE_TASKS_URL = '{% url "offline-tasks" %}';
const PRECACHE_URLS = [
"{% static 'css/app.css' %}",
"{% static 'js/app.js' %}",
"{% static 'js/offline-db.js' %}",
"{% static 'js/offline-tasks.js' %}",
"{% static 'favicons/favicon.svg' %}",
"{% static 'favicons/icon-192.png' %}",
"{% static 'favicons/icon-512.png' %}",
"{% static 'favicons/icon-512-maskable.png' %}",
"{% static 'favicons/apple-touch-icon-180.png' %}",
OFFLINE_URL,
OFFLINE_TASKS_URL,
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
)
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const request = event.request;
// Navigations: try the network first, fall back to the offline page.
// The dashboard route falls back to the offline-capable task app instead
// of the generic offline page, since it can still show/edit cached tasks.
// A 5xx response (e.g. a reverse proxy up-front returning 502/503 while
// the app server itself is down) is treated the same as a network
// failure - fetch() only rejects on true network errors, not bad status
// codes, so that has to be checked explicitly.
if (request.mode === 'navigate') {
event.respondWith(
fetch(request).then((response) => {
if (response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
return response;
}).catch(() => {
const path = new URL(request.url).pathname;
return caches.match(path === '/' ? OFFLINE_TASKS_URL : OFFLINE_URL);
})
);
return;
}
// Precached shell assets: serve from cache first.
const path = new URL(request.url).pathname;
if (PRECACHE_URLS.includes(path)) {
event.respondWith(
caches.match(request).then((cached) => cached || fetch(request))
);
}
});
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(data.title || 'KeepItGoing', {
body: data.body || '',
icon: '{% static "favicons/icon-192.png" %}',
badge: '{% static "favicons/icon-192.png" %}',
data: { url: data.url || '/' },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url || '/'));
});