diff --git a/docker-compose.yml b/docker-compose.yml
index f348549..40a2327 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
pediatric-scribe:
- image: danielonyejesi/pediatric-ai-scribe-v3:v1.8
+ image: danielonyejesi/pediatric-ai-scribe-v3:v1.9
ports:
- "3552:3000"
env_file:
diff --git a/public/js/app.js b/public/js/app.js
index 2935bd9..7629ff7 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -2,6 +2,28 @@
// APP.JS — Core utilities, tabs, model selector, helpers
// ============================================================
+// ── Client-side error logging ──────────────────────────────
+(function() {
+ function sendError(data) {
+ try {
+ var token = localStorage.getItem('ped_scribe_token') || '';
+ navigator.sendBeacon('/api/logs/client-error', new Blob(
+ [JSON.stringify(data)], { type: 'application/json' }
+ ));
+ } catch(e) {}
+ }
+ window.onerror = function(msg, src, line, col, err) {
+ // Ignore errors from browser extensions
+ if (src && src.indexOf('moz-extension') !== -1) return;
+ if (src && src.indexOf('chrome-extension') !== -1) return;
+ sendError({ type: 'uncaught', message: String(msg), source: src, line: line, col: col, stack: err && err.stack });
+ };
+ window.addEventListener('unhandledrejection', function(e) {
+ var msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled promise rejection';
+ sendError({ type: 'unhandledrejection', message: msg, stack: e.reason && e.reason.stack });
+ });
+})();
+
document.addEventListener('DOMContentLoaded', function() {
// --- TAB NAVIGATION ---
@@ -179,17 +201,44 @@ function speakText(elementId) {
if (!el) return;
var text = (el.innerText || el.textContent).trim();
if (!text) { showToast('Nothing to read', 'error'); return; }
- if (!('speechSynthesis' in window)) { showToast('TTS not supported', 'error'); return; }
- window.speechSynthesis.cancel();
- var utter = new SpeechSynthesisUtterance(text);
- utter.rate = 0.9;
- utter.onend = function() { stopReading(); };
- utter.onerror = function() { stopReading(); };
+
currentlyReadingId = elementId;
var btn = findReadButton(elementId);
- if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; }
- window.speechSynthesis.speak(utter);
- showToast('Reading — click Stop to end', 'info');
+ if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Loading...'; }
+
+ fetch('/api/text-to-speech', {
+ method: 'POST',
+ headers: window.getAuthHeaders ? window.getAuthHeaders() : { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text: text })
+ })
+ .then(function(r) {
+ if (!r.ok) throw new Error('TTS request failed (' + r.status + ')');
+ return r.blob();
+ })
+ .then(function(blob) {
+ var url = URL.createObjectURL(blob);
+ currentAudio = new Audio(url);
+ currentAudio.onended = function() { URL.revokeObjectURL(url); stopReading(); };
+ currentAudio.onerror = function() { URL.revokeObjectURL(url); stopReading(); showToast('Audio playback error', 'error'); };
+ currentAudio.play();
+ if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; }
+ showToast('Reading aloud (Adam/ElevenLabs)', 'info');
+ })
+ .catch(function(err) {
+ stopReading();
+ // Fallback to browser TTS if ElevenLabs fails
+ if ('speechSynthesis' in window) {
+ var utter = new SpeechSynthesisUtterance(text);
+ utter.rate = 0.9;
+ utter.onend = function() { stopReading(); };
+ currentlyReadingId = elementId;
+ if (btn) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; }
+ window.speechSynthesis.speak(utter);
+ showToast('ElevenLabs unavailable — using browser voice', 'info');
+ } else {
+ showToast('Read aloud failed: ' + err.message, 'error');
+ }
+ });
}
function stopReading() {
diff --git a/src/routes/logs.js b/src/routes/logs.js
index fee0539..b1ef8fd 100644
--- a/src/routes/logs.js
+++ b/src/routes/logs.js
@@ -32,4 +32,23 @@ router.get('/logs/access', authMiddleware, async function(req, res) {
} catch (err) { res.status(500).json({ error: err.message }); }
});
+// Client-side error receiver — no auth required (sendBeacon can't send auth headers)
+router.post('/logs/client-error', function(req, res) {
+ try {
+ var e = req.body || {};
+ console.error('[CLIENT ERROR]', JSON.stringify({
+ type: e.type,
+ message: e.message,
+ source: e.source,
+ line: e.line,
+ col: e.col,
+ stack: e.stack,
+ ip: req.ip,
+ ua: req.headers['user-agent'],
+ time: new Date().toISOString()
+ }));
+ res.status(204).end();
+ } catch(err) { res.status(204).end(); }
+});
+
module.exports = router;