pediatric-ai-scribe-v3/public/sw.js
Daniel 767821b810 fix(notes): 5 bugs found in post-commit review
1. flushAutosave dropped brand-new unsaved notes.
   The guard was `if (_dirty && _activeId != null)` but _activeId is
   null until the first POST succeeds. So tap-New-Note → type → tap-
   Back went through flushAutosave → skipped → closeToList → lost
   the typed content. saveNote() already handles isNew via POST;
   the guard only needs to check _dirty.

2. beforeunload sendBeacon used wrong HTTP method.
   navigator.sendBeacon is POST-only by spec, but my code used it to
   target PUT /api/notes/:id. Beacons silently hit a 404 route and
   the note didn't save. For brand-new notes the URL resolved to
   /api/notes/undefined. Replaced with fetch({ keepalive: true })
   which browsers queue + ship even across unload, supports PUT,
   and handles POST for new notes correctly.

3. Mobile: empty-state card stacked below the sidebar in list view.
   The mobile media-query hid .notes-reader + .notes-editor when
   data-view="list" but didn't include .notes-empty-state, so the
   big "new note" card appeared below the list on phones. Added
   the missing selector.

4. Delete fallback used window.confirm.
   The `else if (window.confirm(...))` branch in deleteActive was
   a rule violation even as a fallback — feedback_no_native_dialogs
   says no native dialogs anywhere in frontend. Dropped it;
   showConfirm is loaded by app.js before any tab activates so the
   fallback path is unreachable in practice. Also upgraded the
   confirm call to use the danger + confirmText options so the
   modal renders red "Delete" button instead of neutral "Confirm".

5. Recorder kept running after switching tabs or notes.
   Dictating, then tapping another clinical-tools tab (or opening
   another note) left the MediaRecorder active and the mic stream
   open — wasted battery, privacy surprise. Added stopRecording
   (silent=true) to the non-notes branch of tabChanged, to
   startCreate, and to openReader so any in-flight recording
   cancels cleanly when the user moves on.

Minor:
  • Dead "'Saving…' : 'Saving…'" ternary in saveNote simplified.

SW cache bumped to pedscribe-v12-notes4.
2026-04-24 13:22:26 +02:00

93 lines
2.8 KiB
JavaScript

// ============================================================
// SERVICE WORKER — Cache shell, network-first for API
// Provides offline fallback for app shell while keeping
// API calls always fresh (critical for medical data accuracy)
// ============================================================
var CACHE_NAME = 'pedscribe-v12-notes4';
var SHELL_ASSETS = [
'/',
'/index.html',
'/css/styles.css',
'/js/app.js',
'/js/auth.js',
'/manifest.json'
];
// Install: precache app shell
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(SHELL_ASSETS);
}).then(function() {
return self.skipWaiting();
})
);
});
// Activate: clear old caches
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(names) {
return Promise.all(
names.filter(function(name) { return name !== CACHE_NAME; })
.map(function(name) { return caches.delete(name); })
);
}).then(function() {
return self.clients.claim();
})
);
});
// Fetch: network-first for API, cache-first for static assets
self.addEventListener('fetch', function(event) {
var url = new URL(event.request.url);
// Only handle same-origin requests
if (url.origin !== self.location.origin) return;
// API calls — always network, never cache (medical data must be fresh)
if (url.pathname.startsWith('/api/')) {
event.respondWith(fetch(event.request));
return;
}
// Component HTML — network-first with cache fallback
if (url.pathname.startsWith('/components/')) {
event.respondWith(
fetch(event.request).then(function(response) {
var clone = response.clone();
caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, clone); });
return response;
}).catch(function() {
return caches.match(event.request);
})
);
return;
}
// Static assets (JS, CSS, icons) — network-first so code updates apply immediately
if (url.pathname.match(/\.(js|css|png|ico|woff2?)$/)) {
event.respondWith(
fetch(event.request).then(function(response) {
var clone = response.clone();
caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, clone); });
return response;
}).catch(function() {
return caches.match(event.request);
})
);
return;
}
// HTML pages — network-first
event.respondWith(
fetch(event.request).then(function(response) {
var clone = response.clone();
caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, clone); });
return response;
}).catch(function() {
return caches.match(event.request) || caches.match('/index.html');
})
);
});