pediatric-ai-scribe-v3/public/sw.js
Daniel df1a6613dd fix(notes): voice → AI note now produces HTML the editor can render
Symptom Daniel reported: "note recording, not working nor going into
textbox". Root cause was on the server side — /api/notes/from-voice
asked the AI for HTML in its prompt, but real-world models return
markdown ~25% of the time. Tiptap's setContent only renders HTML;
markdown comes through as literal text or a partial render, looking
like the textbox didn't fill.

Server (src/routes/notes.js):
  • New toHtmlBody() helper. If the AI returned real HTML (any
    block tag), pass through. Otherwise run through `marked` so
    markdown becomes <p>/<h*>/<strong>/etc.
  • Strips ```json / ```html code fences before JSON parsing.
  • Stricter JSON-recovery: only accepts {title|body} parsed shape;
    falls back to wrapping the AI's full reply via toHtmlBody().
  • Final guard: if body would be empty after sanitisation, wrap
    the raw transcript so the user can at least edit it manually.

Client (public/js/notes.js):
  • applyGeneratedNote prefers _editor.commands.setContent over a
    full remount — avoids the toolbar-reattach flicker + the brief
    window where the body looked empty.
  • Logs to console when the editor target is missing or Tiptap
    setContent throws, so a future regression is greppable.

Plus the two infra fixes Daniel approved earlier in the same
session — keeping them in this commit since they're already
deployed and tested:

  • src/db/database.js: cleanup interval handle exposed; server
    shutdown now clearInterval()s it before pool.end(). Removes
    the SIGTERM → 9-second-hang → Docker SIGKILL race.
  • src/routes/audioBackups.js: switch multer.memoryStorage() to
    diskStorage with cleanup. 10 concurrent 25 MB uploads no
    longer pin 250 MB of RAM. Identical user-visible perf since
    upload is wire-bound.

New dep: marked@latest (used server-side only in toHtmlBody).
2026-04-25 01:35:10 +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-notes6';
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');
})
);
});