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:
2026-03-01 13:20:21 +03:00
parent 8fe9c6489b
commit 9ee6dcf94a
11 changed files with 715 additions and 10 deletions

View 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;
}
});