435cc1aca9
Security:
- Eliminate DOM-based XSS: renderMacros/renderTabs rebuilt with
createElement/textContent/setAttribute + addEventListener. No macro name,
id, image_path, or category ever reaches innerHTML.
- Remove all inline on*= handlers; add a strict Content-Security-Policy meta
(script-src 'self', object-src 'none', base-uri 'none', frame-ancestors
'none').
- Guard JSON.parse on WebSocket frames.
Robustness:
- Service worker: network-first for the app shell + JS/CSS so fixes actually
ship; synthetic 503 for offline API calls; drop no-op /ws intercept; bump
cache version.
- WebSocket reconnect: exponential backoff (1s→30s), tear down old socket
before reconnect, stop on pagehide/beforeunload.
- Tab-switch race fixed with a request-id guard; executing state cleared in
finally.
Accessibility:
- Macro cards and tabs are real buttons with aria-labels/roles; toast +
connection status are aria-live; removed user-scalable=no.
Feature — web-based macro creation/editing:
- Floating "+" button + modal command-sequence builder (text/key/hotkey/
wait/app) with reorder/remove; create via POST, edit via PUT (prefilled
from GET /api/macro/{id}), delete with a named confirm. Works in both local
and relay auth modes. Empty state now offers "Create your first macro".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
2.8 KiB
JavaScript
91 lines
2.8 KiB
JavaScript
// MacroPad PWA Service Worker
|
|
const CACHE_NAME = 'macropad-v4';
|
|
const ASSETS_TO_CACHE = [
|
|
'/',
|
|
'/static/css/styles.css',
|
|
'/static/js/app.js',
|
|
'/static/icons/icon-192.png',
|
|
'/static/icons/icon-512.png',
|
|
'/manifest.json'
|
|
];
|
|
|
|
// Install event - pre-cache the app shell
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME)
|
|
.then((cache) => {
|
|
console.log('Caching app assets');
|
|
return cache.addAll(ASSETS_TO_CACHE);
|
|
})
|
|
.then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
|
|
// Activate event - clean old caches
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter((name) => name !== CACHE_NAME)
|
|
.map((name) => caches.delete(name))
|
|
);
|
|
}).then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// Network-first for navigations + app code so deployed fixes reach clients.
|
|
async function networkFirst(request) {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
|
|
cache.put(request, networkResponse.clone());
|
|
}
|
|
return networkResponse;
|
|
} catch (err) {
|
|
const cached = await cache.match(request);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
// Offline fallback for navigations: serve the cached shell.
|
|
if (request.mode === 'navigate') {
|
|
const shell = await cache.match('/');
|
|
if (shell) {
|
|
return shell;
|
|
}
|
|
}
|
|
return new Response('Offline', {
|
|
status: 503,
|
|
statusText: 'Offline',
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
});
|
|
}
|
|
}
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const request = event.request;
|
|
|
|
// Only handle GET; let the browser deal with POST/PUT/DELETE directly.
|
|
if (request.method !== 'GET') {
|
|
return;
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
|
|
// API requests always go to the network; never serve stale data.
|
|
// Return a synthetic offline JSON response instead of an unhandled undefined.
|
|
if (url.pathname.startsWith('/api/')) {
|
|
event.respondWith(
|
|
fetch(request).catch(() => new Response(
|
|
JSON.stringify({ error: 'offline', detail: 'Network unavailable' }),
|
|
{ status: 503, headers: { 'Content-Type': 'application/json' } }
|
|
))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Navigations and static app assets: network-first with cache fallback.
|
|
event.respondWith(networkFirst(request));
|
|
});
|