mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-20 13:20:38 -05:00
8fc823c252
Add app/static/manifest.json (TimeTracker / Tracker, indigo theme) and PNG install icons via scripts/generate_pwa_icons.py. Replace inline Flask service worker with app/static/js/sw.js served at /service-worker.js for full-site scope. Cache name timetracker-v1: cache-first for /static, network-first for HTML and non-v1 /api, no interception of /api/v1/* (preserves Authorization). Add public GET /offline and offline.html for SW navigation fallback; redirect /manifest.webmanifest to the static manifest. Wire base.html (manifest link, theme-color #4F46E5, SW registration) and pwa-enhancements.js (ready/update/push without duplicate registration). Remove legacy app/static/service-worker.js and manifest.webmanifest. Tests: service worker and offline routes, manifest redirect, TestPWA expectations; drop duplicate test_enhanced_ui app/client fixtures in favor of conftest. Docs: ASSETS.md, BUILD_CONFIGURATION.md, implementation notes, and incomplete-features analysis updated for new paths.
124 lines
2.9 KiB
JavaScript
124 lines
2.9 KiB
JavaScript
/* TimeTracker service worker — cache static assets; do not touch /api/v1/* (token auth). */
|
|
const CACHE_NAME = 'timetracker-v1';
|
|
|
|
const PRECACHE_URLS = [
|
|
'/offline',
|
|
'/static/manifest.json',
|
|
'/static/dist/output.css',
|
|
'/static/enhanced-ui.css',
|
|
'/static/enhanced-ui.js',
|
|
'/static/charts.js',
|
|
'/static/interactions.js',
|
|
'/static/images/timetracker-logo.svg',
|
|
];
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
(async () => {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
try {
|
|
await cache.addAll(PRECACHE_URLS);
|
|
} catch (e) {
|
|
console.warn('[SW] precache partial failure', e);
|
|
}
|
|
self.skipWaiting();
|
|
})()
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
(async () => {
|
|
const keys = await caches.keys();
|
|
await Promise.all(
|
|
keys.map((k) => {
|
|
if (k !== CACHE_NAME) return caches.delete(k);
|
|
return undefined;
|
|
})
|
|
);
|
|
await self.clients.claim();
|
|
})()
|
|
);
|
|
});
|
|
|
|
function isSameOrigin(url) {
|
|
return url.origin === self.location.origin;
|
|
}
|
|
|
|
async function cacheFirst(request) {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
const cached = await cache.match(request);
|
|
if (cached) return cached;
|
|
try {
|
|
const response = await fetch(request);
|
|
if (response.ok && request.method === 'GET') {
|
|
const clone = response.clone();
|
|
try {
|
|
await cache.put(request, clone);
|
|
} catch (_) {}
|
|
}
|
|
return response;
|
|
} catch (e) {
|
|
return new Response('Offline', { status: 503, statusText: 'Service Unavailable' });
|
|
}
|
|
}
|
|
|
|
async function networkFirstDocument(request) {
|
|
try {
|
|
return await fetch(request);
|
|
} catch (_) {
|
|
const fallback = await caches.match('/offline');
|
|
if (fallback) return fallback;
|
|
return new Response(
|
|
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>Offline</title></head><body><p>You are offline.</p></body></html>',
|
|
{ status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } }
|
|
);
|
|
}
|
|
}
|
|
|
|
async function networkFirstApi(request) {
|
|
try {
|
|
return await fetch(request);
|
|
} catch (_) {
|
|
return new Response('Offline', { status: 503, statusText: 'Service Unavailable' });
|
|
}
|
|
}
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
if (request.method !== 'GET') {
|
|
return;
|
|
}
|
|
let url;
|
|
try {
|
|
url = new URL(request.url);
|
|
} catch (_) {
|
|
return;
|
|
}
|
|
if (!isSameOrigin(url)) {
|
|
return;
|
|
}
|
|
|
|
const path = url.pathname;
|
|
|
|
// Never intercept token-auth API — browser handles the request unchanged.
|
|
if (path.startsWith('/api/v1/')) {
|
|
return;
|
|
}
|
|
|
|
if (path.startsWith('/static/')) {
|
|
event.respondWith(cacheFirst(request));
|
|
return;
|
|
}
|
|
|
|
if (path.startsWith('/api/')) {
|
|
event.respondWith(networkFirstApi(request));
|
|
return;
|
|
}
|
|
|
|
if (request.mode === 'navigate' || request.destination === 'document') {
|
|
event.respondWith(networkFirstDocument(request));
|
|
return;
|
|
}
|
|
});
|