Add PWA support and mobile responsive layout
- PWA manifest, service worker (stale-while-revalidate for static assets, network-only for API), and app icons for installability - Root-scoped /manifest.json and /sw.js routes in FastAPI - New mobile.css with responsive breakpoints at 768/600/400px: fixed bottom tab bar on phones, single-column cards, full-screen modals, compact header toolbar, touch-friendly targets - Fix modal-content-wide min-width overflow on small screens - Update README with Camera, OpenRGB, and PWA features Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
90
server/src/wled_controller/static/sw.js
Normal file
90
server/src/wled_controller/static/sw.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Service Worker for LED Grab PWA.
|
||||
*
|
||||
* Strategy:
|
||||
* - Static assets (/static/): stale-while-revalidate
|
||||
* - API / config requests: network-only (device control must be live)
|
||||
* - Navigation: network-first with offline fallback
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'ledgrab-v1';
|
||||
|
||||
const PRECACHE_URLS = [
|
||||
'/',
|
||||
'/static/css/base.css',
|
||||
'/static/css/layout.css',
|
||||
'/static/css/components.css',
|
||||
'/static/css/cards.css',
|
||||
'/static/css/modal.css',
|
||||
'/static/css/calibration.css',
|
||||
'/static/css/dashboard.css',
|
||||
'/static/css/streams.css',
|
||||
'/static/css/patterns.css',
|
||||
'/static/css/automations.css',
|
||||
'/static/css/tutorials.css',
|
||||
'/static/css/mobile.css',
|
||||
'/static/icons/icon-192.png',
|
||||
'/static/icons/icon-512.png',
|
||||
];
|
||||
|
||||
// Install: pre-cache core shell
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => cache.addAll(PRECACHE_URLS))
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// Activate: clean old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(
|
||||
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch handler
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// API and config: always network (device control must be live)
|
||||
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/config/')) {
|
||||
return; // fall through to default network fetch
|
||||
}
|
||||
|
||||
// Static assets: stale-while-revalidate
|
||||
if (url.pathname.startsWith('/static/')) {
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then((cache) =>
|
||||
cache.match(event.request).then((cached) => {
|
||||
const fetchPromise = fetch(event.request).then((response) => {
|
||||
if (response.ok) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
return response;
|
||||
}).catch(() => cached);
|
||||
|
||||
return cached || fetchPromise;
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigation: network-first
|
||||
if (event.request.mode === 'navigate') {
|
||||
event.respondWith(
|
||||
fetch(event.request).catch(() =>
|
||||
caches.match('/') || new Response('Offline', {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
})
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user