Four changes landing together so the UX flows properly.
1. Fix voice-generation model error.
/api/notes/from-voice was passing `req.body.model` (undefined)
through to callAI, which resolved to LITELLM_DEFAULT_MODEL (empty
for LiteLLM deployments with no explicit default) → `model=""` →
LiteLLM 400 "Invalid model name". Server now reads the admin-
configured `models.default` setting, falls back to the
LITELLM_DEFAULT_MODEL env var, and only passes a model if one
resolved. Empty string never reaches the provider.
2. Read mode as the default post-save.
Opening an existing note or saving a new one now lands on a
clean, read-only rendering of the body (sanitized HTML with an
allowlist — p/h2/h3/strong/em/u/s/a/ul/ol/li/blockquote/code/pre).
Click the "Edit" button to switch to the Tiptap editor. Matches
the mental model of "notes are documents I review, not drafts I'm
always editing."
3. Autosave during edit.
Title-input + Tiptap onUpdate trigger a 1.2-second-debounced save.
In-flight saves coalesce: if the user types more while a save is
in progress, a follow-up fires right after it lands so the last
keystroke never gets stranded. beforeunload uses navigator.
sendBeacon to best-effort flush on tab close. The manual Save
button is kept as a "save + switch to reader" shortcut. Ctrl/
Cmd+S still works in the editor.
4. Mobile layout.
The two-pane layout collapses to a single-pane view below 900px,
driven by a data-view attribute on .notes-layout. Back buttons
appear in the reader + editor heads on mobile to return to the
list. Desktop layout unchanged (sidebar + right pane always
visible).
Also:
• CSS specificity fix — .hidden{display:none} was losing to
.notes-rec-indicator{display:inline-flex} on source order, so
the "Recording" indicator was showing when idle. Added explicit
.notes-rec-indicator.hidden + .notes-voice-bar .btn-sm.hidden
overrides at higher specificity.
• Reader-body sanitizer — allowlist of safe tags + attributes;
<a> links get target=_blank + rel=noopener.
• SW cache bumped to pedscribe-v12-notes2 so clients pick up the
new module / component / CSS.
93 lines
2.8 KiB
JavaScript
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-notes2';
|
|
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');
|
|
})
|
|
);
|
|
});
|