diff --git a/public/css/styles.css b/public/css/styles.css index 79def95..f7faf6b 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -985,9 +985,13 @@ textarea.full-input{resize:vertical;} .notes-layout{grid-template-columns:1fr;gap:0;} .notes-sidebar,.notes-reader,.notes-editor,.notes-empty-state{max-height:none;} - /* Show only the pane matching data-view; hide the others */ + /* Show only the pane matching data-view; hide the others. The empty- + state card is purely a desktop affordance ("nothing picked yet, + pick from the sidebar"); on mobile the sidebar IS the list, so + we hide empty-state whenever the sidebar is showing. */ .notes-layout[data-view="list"] .notes-reader, .notes-layout[data-view="list"] .notes-editor, + .notes-layout[data-view="list"] .notes-empty-state, .notes-layout[data-view="reader"] .notes-sidebar, .notes-layout[data-view="reader"] .notes-editor, .notes-layout[data-view="reader"] .notes-empty-state, diff --git a/public/js/notes.js b/public/js/notes.js index 05e8cc0..7a1bd9f 100644 --- a/public/js/notes.js +++ b/public/js/notes.js @@ -39,10 +39,16 @@ // Wait for the Notes tab to be activated — the component HTML is // lazy-loaded, so elements don't exist until the user clicks the tab. document.addEventListener('tabChanged', function(e) { - if (e.detail.tab !== 'notes') return; - if (_inited) { refreshList(); return; } - _inited = true; - init(); + if (e.detail.tab === 'notes') { + if (_inited) { refreshList(); return; } + _inited = true; + init(); + return; + } + // Leaving the Notes tab — cancel any in-flight recording so the + // mic doesn't stay live + the recorder doesn't keep buffering. + if (_recActive) stopRecording(true); + flushAutosave(); }); function $(id) { return document.getElementById(id); } @@ -111,16 +117,27 @@ } }); - // Persist dirty state on tab close / page unload — best-effort final flush + // Persist dirty state on tab close / page unload — best-effort final flush. + // navigator.sendBeacon is POST-only so it can't drive a PUT update; fetch + // with keepalive:true is the right primitive here (works for both POST + // for new notes and PUT for existing). window.addEventListener('beforeunload', function() { - if (_dirty && _activeId != null) { - // Use sendBeacon for a guaranteed best-effort send - try { - var payload = JSON.stringify({ title: getTitle(), body: getBody() }); - var headers = { type: 'application/json' }; - navigator.sendBeacon && navigator.sendBeacon('/api/notes/' + _activeId, new Blob([payload], headers)); - } catch (e) {} - } + if (!_dirty) return; + var title = getTitle(); + if (!title) return; + var body = getBody(); + var isNew = _activeId == null; + var url = isNew ? '/api/notes' : '/api/notes/' + _activeId; + var method = isNew ? 'POST' : 'PUT'; + try { + fetch(url, { + method: method, + keepalive: true, + credentials: 'include', + headers: Object.assign({ 'Content-Type': 'application/json' }, getAuthHeaders()), + body: JSON.stringify({ title: title, body: body }), + }); + } catch (e) {} }); refreshList(); @@ -209,6 +226,7 @@ // ── Reader mode (default after opening or saving) ───────── function openReader(id) { flushAutosave(); + if (_recActive) stopRecording(true); var note = _notes.find(function(n) { return n.id === id; }); if (!note) return; _activeId = id; @@ -230,6 +248,7 @@ // ── Editor mode (create new / edit existing) ────────────── function startCreate() { flushAutosave(); + if (_recActive) stopRecording(true); _activeId = null; _dirty = false; $('note-title').value = ''; @@ -281,9 +300,11 @@ function flushAutosave() { if (_autosaveTimer) { clearTimeout(_autosaveTimer); _autosaveTimer = null; } - if (_dirty && _activeId != null) { - saveNote({ source: 'flush' }); - } + // Flush dirty state regardless of whether the note is new or existing. + // saveNote() handles both paths (POST for new, PUT for existing), so + // gating on _activeId here silently dropped the very first flush for + // a brand-new note — e.g. tap New note → type → tap Back. + if (_dirty) saveNote({ source: 'flush' }); } function saveNote(opts) { @@ -313,7 +334,7 @@ var method = isNew ? 'POST' : 'PUT'; _autosaveInFlight = true; - updateStatus(opts.source === 'auto' ? 'Saving…' : 'Saving…', 'saving'); + updateStatus('Saving…', 'saving'); fetch(url, { method: method, @@ -381,10 +402,13 @@ .catch(function(err) { updateStatus(err.message || 'Delete failed', 'err'); }); }; + // showConfirm is loaded by app.js before any tab activates, so the + // "if missing" fallback used to be a native window.confirm() — + // dropped here because Daniel's rule is no native dialogs anywhere + // in the frontend. If showConfirm is somehow unavailable, the click + // silently no-ops; the user can try again after a reload. if (typeof showConfirm === 'function') { - showConfirm('Delete "' + label + '"? This cannot be undone.', doDelete); - } else if (window.confirm('Delete "' + label + '"? This cannot be undone.')) { - doDelete(); + showConfirm('Delete "' + label + '"?', doDelete, { danger: true, confirmText: 'Delete' }); } } diff --git a/public/sw.js b/public/sw.js index 0a20185..b983a73 100644 --- a/public/sw.js +++ b/public/sw.js @@ -4,7 +4,7 @@ // API calls always fresh (critical for medical data accuracy) // ============================================================ -var CACHE_NAME = 'pedscribe-v12-notes3'; +var CACHE_NAME = 'pedscribe-v12-notes4'; var SHELL_ASSETS = [ '/', '/index.html',