diff --git a/package-lock.json b/package-lock.json
index 00479d5..e91a14a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"openai": "^4.73.0",
- "pdf-parse": "^2.4.5",
+ "pdf-parse": "^1.1.1",
"pg": "^8.13.0",
"pptxgenjs": "^4.0.1",
"qrcode": "^1.5.4",
@@ -1010,190 +1010,6 @@
}
}
},
- "node_modules/@napi-rs/canvas": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
- "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
- "license": "MIT",
- "workspaces": [
- "e2e/*"
- ],
- "engines": {
- "node": ">= 10"
- },
- "optionalDependencies": {
- "@napi-rs/canvas-android-arm64": "0.1.80",
- "@napi-rs/canvas-darwin-arm64": "0.1.80",
- "@napi-rs/canvas-darwin-x64": "0.1.80",
- "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
- "@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
- "@napi-rs/canvas-linux-arm64-musl": "0.1.80",
- "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
- "@napi-rs/canvas-linux-x64-gnu": "0.1.80",
- "@napi-rs/canvas-linux-x64-musl": "0.1.80",
- "@napi-rs/canvas-win32-x64-msvc": "0.1.80"
- }
- },
- "node_modules/@napi-rs/canvas-android-arm64": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
- "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-darwin-arm64": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
- "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-darwin-x64": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
- "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
- "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
- "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-linux-arm64-musl": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
- "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
- "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-linux-x64-gnu": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
- "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-linux-x64-musl": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
- "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@napi-rs/canvas-win32-x64-msvc": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
- "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@puppeteer/browsers": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
@@ -4468,6 +4284,12 @@
"node": ">=10.5.0"
}
},
+ "node_modules/node-ensure": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
+ "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
+ "license": "MIT"
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -4743,37 +4565,33 @@
"license": "MIT"
},
"node_modules/pdf-parse": {
- "version": "2.4.5",
- "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz",
- "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==",
- "license": "Apache-2.0",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
+ "integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
+ "license": "MIT",
"dependencies": {
- "@napi-rs/canvas": "0.1.80",
- "pdfjs-dist": "5.4.296"
- },
- "bin": {
- "pdf-parse": "bin/cli.mjs"
+ "debug": "^3.1.0",
+ "node-ensure": "^0.0.0"
},
"engines": {
- "node": ">=20.16.0 <21 || >=22.3.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/mehmet-kozan"
+ "node": ">=6.8.1"
}
},
- "node_modules/pdfjs-dist": {
- "version": "5.4.296",
- "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
- "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=20.16.0 || >=22.3.0"
- },
- "optionalDependencies": {
- "@napi-rs/canvas": "^0.1.80"
+ "node_modules/pdf-parse/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
}
},
+ "node_modules/pdf-parse/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
diff --git a/package.json b/package.json
index b060740..7eb9c62 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"openai": "^4.73.0",
- "pdf-parse": "^2.4.5",
+ "pdf-parse": "^1.1.1",
"pg": "^8.13.0",
"pptxgenjs": "^4.0.1",
"qrcode": "^1.5.4",
diff --git a/public/components/chart.html b/public/components/chart.html
index e91a6e9..cb0c928 100644
--- a/public/components/chart.html
+++ b/public/components/chart.html
@@ -90,9 +90,9 @@
Chart Review
-
-
-
+
+
+
diff --git a/public/components/cms.html b/public/components/cms.html
index 514b9b7..be4a985 100644
--- a/public/components/cms.html
+++ b/public/components/cms.html
@@ -157,10 +157,15 @@
PDF, TXT, MD, HTML — max 20 MB
+
+
+
+
+
/
@@ -169,10 +174,17 @@
Click to browse your Nextcloud files
-
+
+
+
+
-
+
+
+
+
+
@@ -238,9 +250,15 @@
Generate Content
+
+
+
+
+
+
diff --git a/public/components/dictation.html b/public/components/dictation.html
index 86303b4..8603e94 100644
--- a/public/components/dictation.html
+++ b/public/components/dictation.html
@@ -69,9 +69,9 @@
Output
-
-
-
+
+
+
diff --git a/public/components/encounter.html b/public/components/encounter.html
index f8111be..ca368ac 100644
--- a/public/components/encounter.html
+++ b/public/components/encounter.html
@@ -66,9 +66,9 @@
Generated HPI
-
-
-
+
+
+
diff --git a/public/components/hospital.html b/public/components/hospital.html
index dc61649..386b90d 100644
--- a/public/components/hospital.html
+++ b/public/components/hospital.html
@@ -138,9 +138,9 @@
-
-
-
+
+
+
diff --git a/public/components/sickvisit.html b/public/components/sickvisit.html
index 52b4dc5..edafb0e 100644
--- a/public/components/sickvisit.html
+++ b/public/components/sickvisit.html
@@ -93,8 +93,8 @@
-
-
+
+
diff --git a/public/components/soap.html b/public/components/soap.html
index 524f067..74e813e 100644
--- a/public/components/soap.html
+++ b/public/components/soap.html
@@ -43,9 +43,9 @@
SOAP Note
-
-
-
+
+
+
diff --git a/public/components/wellvisit.html b/public/components/wellvisit.html
index 92fed34..52ffba2 100644
--- a/public/components/wellvisit.html
+++ b/public/components/wellvisit.html
@@ -93,10 +93,10 @@
Developmental Assessment
-
+
-
-
+
+
@@ -109,7 +109,7 @@
@@ -149,8 +149,8 @@
-
-
+
+
@@ -290,9 +290,9 @@
-
-
-
+
+
+
diff --git a/public/js/app.js b/public/js/app.js
index 502c47e..cb30b96 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -234,6 +234,31 @@ function setOutputText(el, text) {
el.innerHTML = safe.replace(/\n/g, '
');
}
+// ── Data-action event delegation (replaces inline onclick handlers) ──
+// Handles data-action="copy|speak|nc-export" on any element,
+// allowing removal of 'unsafe-inline' from the Content Security Policy.
+document.addEventListener('click', function(e) {
+ var btn = e.target.closest('[data-action]');
+ if (!btn) return;
+ var action = btn.getAttribute('data-action');
+ var targetId = btn.getAttribute('data-target');
+ if (!action) return;
+
+ if (action === 'copy' && targetId) {
+ if (typeof copyText === 'function') copyText(targetId);
+ return;
+ }
+ if (action === 'speak' && targetId) {
+ if (typeof speakText === 'function') speakText(targetId);
+ return;
+ }
+ if (action === 'nc-export' && targetId) {
+ var label = btn.getAttribute('data-label') || 'export';
+ if (typeof exportToNextcloud === 'function') exportToNextcloud(targetId, label);
+ return;
+ }
+});
+
// ── Announcement banner ────────────────────────────────────
function loadAnnouncement() {
fetch('/api/admin/config/announcement', { headers: getAuthHeaders() })
diff --git a/public/js/learningHub.js b/public/js/learningHub.js
index b1cc5cf..b7350e0 100644
--- a/public/js/learningHub.js
+++ b/public/js/learningHub.js
@@ -83,7 +83,9 @@
if (e.target.closest('#btn-lh-ai-open')) { openAiPanel(); return; }
if (e.target.closest('#btn-lh-ai-close')) { closeAiPanel(); return; }
if (e.target.closest('#btn-lh-ai-generate')) { runAiGenerate(); return; }
- if (e.target.closest('#btn-lh-ai-refine-body')) { runAiRefineBody(); return; }
+ if (e.target.closest('#btn-lh-ai-refine-body')) { toggleRefineBar(); return; }
+ if (e.target.closest('#btn-lh-refine-submit')) { submitRefineBody(); return; }
+ if (e.target.closest('#btn-lh-refine-cancel')) { toggleRefineBar(false); return; }
if (e.target.closest('#btn-lh-webdav-refresh')) { browseWebdav(_aiWebdavCurrentPath); return; }
if (e.target.closest('#btn-lh-webdav-deselect')) { deselectWebdavFile(); return; }
@@ -671,17 +673,22 @@
function selectWebdavFile(path, name) {
_aiWebdavSelectedPath = path;
_aiWebdavSelectedName = name;
- var selDiv = document.getElementById('lh-ai-webdav-selected');
+ var browser = document.getElementById('lh-ai-webdav-browser');
+ var selDiv = document.getElementById('lh-ai-webdav-selected');
var selName = document.getElementById('lh-ai-webdav-selected-name');
- if (selDiv) selDiv.classList.remove('hidden');
- if (selName) selName.textContent = name;
+ // Hide browser, show selected indicator (use style.display — class="hidden" is overridden by inline styles)
+ if (browser) browser.style.display = 'none';
+ if (selDiv) selDiv.style.display = 'flex';
+ if (selName) selName.textContent = name;
}
function deselectWebdavFile() {
_aiWebdavSelectedPath = '';
_aiWebdavSelectedName = '';
- var selDiv = document.getElementById('lh-ai-webdav-selected');
- if (selDiv) selDiv.classList.add('hidden');
+ var browser = document.getElementById('lh-ai-webdav-browser');
+ var selDiv = document.getElementById('lh-ai-webdav-selected');
+ if (selDiv) selDiv.style.display = 'none';
+ if (browser) browser.style.display = '';
}
// ── Generate ────────────────────────────────────────────────
@@ -713,9 +720,13 @@
var fileInput = document.getElementById('lh-ai-file');
if (!fileInput || !fileInput.files[0]) { showToast('Select a file to upload', 'error'); return; }
formData.append('file', fileInput.files[0]);
+ var uploadCtx = document.getElementById('lh-ai-upload-context');
+ if (uploadCtx && uploadCtx.value.trim()) formData.append('topic', uploadCtx.value.trim());
} else if (tabName === 'webdav') {
if (!_aiWebdavSelectedPath) { showToast('Select a file from Nextcloud', 'error'); return; }
formData.append('webdavPath', _aiWebdavSelectedPath);
+ var webdavCtx = document.getElementById('lh-ai-webdav-context');
+ if (webdavCtx && webdavCtx.value.trim()) formData.append('topic', webdavCtx.value.trim());
}
var btn = document.getElementById('btn-lh-ai-generate');
@@ -784,21 +795,31 @@
}
}
- function runAiRefineBody() {
- var instructions = prompt('How should the body be refined?\n(e.g., "Make it more concise", "Add a clinical case", "Focus on management")');
- if (!instructions || !instructions.trim()) return;
+ function toggleRefineBar(forceShow) {
+ var bar = document.getElementById('lh-ai-refine-bar');
+ var input = document.getElementById('lh-ai-refine-input');
+ if (!bar) return;
+ var show = forceShow !== undefined ? forceShow : bar.style.display === 'none';
+ bar.style.display = show ? 'flex' : 'none';
+ if (show && input) { input.value = ''; input.focus(); }
+ }
+
+ function submitRefineBody() {
+ var input = document.getElementById('lh-ai-refine-input');
+ var instructions = input ? input.value.trim() : '';
+ if (!instructions) { showToast('Enter instructions for refinement', 'error'); return; }
var currentBody = _bodyEditor ? getTpHTML(_bodyEditor) : '';
if (!currentBody) { showToast('No body content to refine', 'error'); return; }
var model = document.getElementById('lh-ai-model') ? document.getElementById('lh-ai-model').value : getSelectedModel();
-
+ toggleRefineBar(false);
showLoading('Refining content…');
fetch('/api/admin/learning/ai-refine', {
method: 'POST',
headers: getAuthHeaders(),
- body: JSON.stringify({ content: currentBody, instructions: instructions.trim(), model: model })
+ body: JSON.stringify({ content: currentBody, instructions: instructions, model: model })
})
.then(function(r) { return r.json(); })
.then(function(data) {
diff --git a/public/js/nextcloud.js b/public/js/nextcloud.js
index 9451be0..1304245 100644
--- a/public/js/nextcloud.js
+++ b/public/js/nextcloud.js
@@ -63,7 +63,7 @@
document.getElementById('btn-nc-save-path').addEventListener('click', function() {
var p = (document.getElementById('nc-webdav-path').value || '').trim();
- fetch('/api/admin/learning/webdav-path', {
+ fetch('/api/user/webdav-path', {
method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ path: p })
}).then(function(r) { return r.json(); })
.then(function(d) { d.success ? showToast('Path saved', 'success') : showToast(d.error || 'Failed', 'error'); })
diff --git a/server.js b/server.js
index a042a4f..8c2fe29 100644
--- a/server.js
+++ b/server.js
@@ -19,8 +19,8 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'"],
- scriptSrcAttr: ["'unsafe-inline'"], // allow onclick/onchange attribute handlers
+ scriptSrc: ["'self'"],
+ scriptSrcAttr: ["'none'"],
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://cdnjs.cloudflare.com'],
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'],
imgSrc: ["'self'", 'data:', 'blob:'],
@@ -156,6 +156,18 @@ app.use('/api', require('./src/routes/wellVisit'));
app.use('/api', require('./src/routes/sickVisit'));
app.use('/api/admin/learning', require('./src/routes/learningAI'));
+// User-level preference: save WebDAV learning path (auth only, not moderator-only)
+(function() {
+ var { authMiddleware } = require('./src/middleware/auth');
+ var db = require('./src/db/database');
+ app.post('/api/user/webdav-path', authMiddleware, async function(req, res) {
+ try {
+ await db.run('UPDATE users SET webdav_learning_path = ? WHERE id = ?', [req.body.path || null, req.user.id]);
+ res.json({ success: true });
+ } catch(e) { res.status(500).json({ error: e.message }); }
+ });
+})();
+
app.get('/', (req, res) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');