From 7719e564be6d2ee5c9ae561f55ffdb4fc796b54e Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Sun, 22 Mar 2026 16:43:39 -0400 Subject: [PATCH] Feat: admin CMS, save/resume encounters, user templates, SSHADESS, well visit notes, sick visit, ROS/PE checklists, ICD-10 diagnoses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin CMS: announcements, feature flags, email templates, AI prompts, SMTP, model management, reset-to-defaults - Save/resume encounter progress with 7-day auto-expiry and patient labels - Inline searchable load popover (replaces settings-modal redirect) - User memory/template system (physical exam, ROS, encounter format, etc.) - SSHADESS psychosocial screening form (8 domains, listen-in recording) - Well visit note generation with vitals, vaccines, screenings, SSHADESS - Interactive screenings/vaccines status buttons (Done/Refused/Already Given/N/A) - ROS 15-system checklist + PE 17-system checklist in well visit note tab - ICD-10 diagnoses tag picker (28 common pediatric codes + free text) - Simple sick visit tab: chief complaint, inferred ROS (4) + PE (7), diagnoses, generate note - Settings converted from modal to full-page tab - Fix: /api/models moved before authenticated routes (was returning 401 → empty model selector) - Fix: getUserMemoryContext always fetches from API regardless of cache state - Pause/resume recording on all recording tabs - Site-level SMTP config stored in DB; ElevenLabs TTS with browser fallback --- docker-compose.local.yml | 41 ++ public/css/styles.css | 95 ++++ public/index.html | 588 +++++++++++++++++++++- public/js/admin.js | 405 +++++++++++++++ public/js/app.js | 34 ++ public/js/auth.js | 25 +- public/js/encounters.js | 342 +++++++++++++ public/js/liveEncounter.js | 59 ++- public/js/memories.js | 174 +++++++ public/js/shadess.js | 970 ++++++++++++++++++++++++++++++++++++ public/js/sickVisit.js | 423 ++++++++++++++++ public/js/voiceDictation.js | 58 ++- public/js/wellVisit.js | 183 ++++++- server.js | 44 +- src/db/database.js | 80 ++- src/routes/adminConfig.js | 300 +++++++++++ src/routes/auth.js | 60 ++- src/routes/encounters.js | 90 ++++ src/routes/memories.js | 90 ++++ src/routes/sickVisit.js | 73 +++ src/routes/wellVisit.js | 164 ++++++ src/utils/config.js | 52 ++ src/utils/models.js | 22 + src/utils/prompts.js | 90 +++- 24 files changed, 4381 insertions(+), 81 deletions(-) create mode 100644 docker-compose.local.yml create mode 100644 public/js/encounters.js create mode 100644 public/js/memories.js create mode 100644 public/js/shadess.js create mode 100644 public/js/sickVisit.js create mode 100644 src/routes/adminConfig.js create mode 100644 src/routes/encounters.js create mode 100644 src/routes/memories.js create mode 100644 src/routes/sickVisit.js create mode 100644 src/routes/wellVisit.js create mode 100644 src/utils/config.js diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..85fae15 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,41 @@ +services: + pediatric-scribe: + build: . + ports: + - "3552:3000" + env_file: + - .env + volumes: + - scribe-logs:/app/data/logs + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + container_name: pediatric-ai-scribe + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 20s + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: pedscribe + POSTGRES_USER: pedscribe + POSTGRES_PASSWORD: ${DB_PASSWORD:-pedscribe_secret_change_me} + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + container_name: pedscribe-db + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pedscribe"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +volumes: + pgdata: + scribe-logs: diff --git a/public/css/styles.css b/public/css/styles.css index d63f279..3421f53 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -345,3 +345,98 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(-- .wv-sched-has{background:var(--green-light);color:#065f46;font-weight:700;border-radius:4px;cursor:help;} .wv-sched-legend{font-size:11px;color:var(--g400);margin-top:10px;display:flex;align-items:center;gap:6px;} .wv-sched-leg-dot{display:inline-block;width:12px;height:12px;background:var(--green-light);border-radius:2px;} + +/* Announcement Banner */ +.announcement-banner{display:flex;align-items:center;gap:10px;padding:10px 16px;font-size:13px;font-weight:500;border-bottom:1px solid transparent;} +.announcement-banner.hidden{display:none;} +.announcement-banner.ann-info{background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe;} +.announcement-banner.ann-warning{background:#fffbeb;color:#92400e;border-color:#fcd34d;} +.announcement-banner.ann-error{background:#fef2f2;color:#991b1b;border-color:#fca5a5;} +.announcement-banner.ann-success{background:#f0fdf4;color:#166534;border-color:#86efac;} +.announcement-banner span{flex:1;} +.announcement-close{background:none;border:none;cursor:pointer;opacity:0.6;font-size:14px;padding:0 4px;color:inherit;} +.announcement-close:hover{opacity:1;} + +/* Save bar (encounter label + save/load) */ +.save-bar{display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--g50);border:1px solid var(--g200);border-radius:var(--radius);margin-bottom:10px;flex-wrap:wrap;position:relative;} +.save-label-input{flex:1;min-width:160px;font-size:13px;padding:5px 8px;border:1px solid var(--g200);border-radius:6px;background:white;} +.save-label-input:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 2px rgba(37,99,235,.12);} + +/* Saved encounters list */ +.saved-enc-item{display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--g50);border:1px solid var(--g200);border-radius:8px;font-size:13px;} +.saved-enc-label{flex:1;font-weight:600;color:var(--g800);} +.saved-enc-meta{font-size:11px;color:var(--g500);} +.saved-enc-type{font-size:11px;background:var(--blue-light);color:var(--blue);padding:1px 6px;border-radius:10px;font-weight:600;} + +/* Memory list item */ +.mem-item{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;background:var(--g50);border:1px solid var(--g200);border-radius:8px;} +.mem-item-info{flex:1;} +.mem-item-name{font-weight:600;font-size:13px;color:var(--g800);} +.mem-item-cat{font-size:11px;background:var(--green-light);color:#166534;padding:1px 6px;border-radius:10px;font-weight:600;} +.mem-item-preview{font-size:12px;color:var(--g500);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:320px;} + +/* Models list in admin CMS */ +.model-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--g100);} +.model-row:last-child{border-bottom:none;} +.model-row-name{flex:1;font-size:13px;font-weight:500;color:var(--g800);} +.model-row-cost{font-size:12px;color:var(--g500);} +.model-row-tag{font-size:10px;background:var(--g100);color:var(--g600);padding:1px 5px;border-radius:8px;} +.model-row-toggle{font-size:12px;} + +/* SSHADESS form */ +.shadess-domain{margin-bottom:12px;padding:10px 12px;border-radius:8px;background:white;box-shadow:var(--shadow);} +.shadess-domain-header{display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap;} +.shadess-domain-body{display:flex;flex-direction:column;gap:6px;} +.shadess-q-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:3px 0;} +.shadess-q-text{font-size:13px;color:var(--g700);flex:1;min-width:200px;} +.shadess-concern-badge{font-size:11px;background:var(--red-light);color:var(--red);padding:2px 8px;border-radius:10px;font-weight:600;} +.shadess-flag-btn{font-size:11px;padding:2px 6px;} + +/* Settings page */ +.settings-page{display:flex;flex-direction:column;gap:16px;max-width:800px;} +.settings-page .settings-section{padding:20px;} +.settings-page .settings-section h3{font-size:16px;font-weight:700;color:var(--g800);margin-bottom:12px;display:flex;align-items:center;gap:8px;} +.settings-page .settings-section p{font-size:13px;color:var(--g600);margin-bottom:12px;} +.settings-page .hipaa-info ul{margin:10px 0 10px 20px;font-size:13px;} +.settings-page .hipaa-info li{margin-bottom:4px;} + +/* Inline load popover — compact dropdown, right-aligned under Load button */ +.enc-load-popover{position:absolute;right:0;top:calc(100% + 4px);width:360px;max-width:calc(100vw - 24px);background:white;border:1px solid var(--g300);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:600;display:flex;flex-direction:column;} +.enc-load-popover-inner{display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid var(--g200);} +.enc-load-search{flex:1;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;font-size:13px;font-family:inherit;} +.enc-load-search:focus{outline:none;border-color:var(--blue);} +.enc-pop-list{overflow-y:auto;max-height:260px;} +.enc-pop-item{display:flex;align-items:center;gap:8px;padding:9px 12px;cursor:pointer;border-bottom:1px solid var(--g100);} +.enc-pop-item:hover{background:var(--g50);} +.enc-pop-item:last-child{border-bottom:none;} + +/* ===== ROS / PE / DX — Tasks 2, 3, 4, 6 ===== */ +.ros-system-row{display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1px solid var(--g100);} +.ros-system-row:last-child{border-bottom:none;} +.ros-system-name{flex:1;font-size:13px;color:var(--g700);} +.ros-status-btn{font-size:11px;padding:2px 8px;border-radius:12px;border:1px solid var(--g300);background:white;cursor:pointer;transition:all 0.15s;font-family:inherit;} +.ros-status-btn:hover{background:var(--g100);} +.ros-status-btn.wnl{background:var(--green-light);color:#166534;border-color:var(--green);} +.ros-status-btn.abnormal{background:var(--red-light);color:#991b1b;border-color:var(--red);} +.ros-status-btn.notrev{background:var(--g100);color:var(--g500);border-color:var(--g300);} +.ros-note-input{font-size:12px;padding:3px 8px;border:1px solid var(--g300);border-radius:6px;width:180px;display:none;font-family:inherit;} +.ros-note-input.visible{display:inline-block;} +.dx-tag{display:inline-flex;align-items:center;gap:4px;background:var(--blue-light);color:#1d4ed8;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:600;margin:2px;} +.dx-tag .dx-remove{cursor:pointer;opacity:0.7;font-size:14px;line-height:1;} +.dx-tag .dx-remove:hover{opacity:1;} +.dx-chip{cursor:pointer;padding:3px 10px;background:var(--g100);border:1px solid var(--g200);border-radius:12px;font-size:11px;color:var(--g700);display:inline-block;margin:2px;transition:all 0.15s;} +.dx-chip:hover{background:var(--blue-light);color:var(--blue);border-color:var(--blue);} +.sv-section-card{background:white;border-radius:var(--radius);border:1px solid var(--g200);padding:0;margin-bottom:10px;box-shadow:var(--shadow);} +.sv-section-card .card-header{padding:10px 16px;border-bottom:1px solid var(--g100);display:flex;align-items:center;gap:8px;} +.sv-section-card .card-header h3{font-size:14px;font-weight:600;color:var(--g800);display:flex;align-items:center;gap:6px;} +.sv-section-card .card-body{padding:10px 16px;} +.visit-status-btn{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid var(--g300);background:white;cursor:pointer;transition:all 0.15s;font-family:inherit;} +.visit-status-btn.given,.visit-status-btn.done{background:var(--green-light);color:#166534;border-color:var(--green);} +.visit-status-btn.refused{background:var(--red-light);color:#991b1b;border-color:var(--red);} +.visit-status-btn.deferred,.visit-status-btn.not-due{background:var(--amber-light);color:#92400e;border-color:var(--amber);} +.visit-status-btn.already-done{background:var(--blue-light);color:#1e40af;border-color:var(--blue);} +@media(max-width:640px){ + .ros-system-row{flex-wrap:wrap;} + .ros-note-input{width:100%;} + .dx-chip{font-size:10px;} +} diff --git a/public/index.html b/public/index.html index 43e8766..2c5a38d 100644 --- a/public/index.html +++ b/public/index.html @@ -128,6 +128,13 @@ + + +