pediatric-ai-scribe-v3/src/routes/wellVisit.js
Daniel 020e831b3c v6.2: Session management, password change, audit logging, refine context, UI fixes
Security:
- Add session management: users can view/revoke active sessions in Settings
- Add password change in Settings (requires current password, HIBP check)
- Force logout all sessions on password reset
- Fix logout to destroy server-side session (was only clearing cookie)
- Add trust proxy for correct client IP in rate limiting and audit logs
- Add CORS support for multiple domains (CORS_ORIGINS env var)
- Add HIBP breach check endpoint and inline warnings on password fields

Audit logging:
- Add audit logging to all 24 PHI-handling endpoints across 13 route files
- Covers: generation, transcription, TTS, refine, encounters, documents, Nextcloud
- All fire-and-forget (no response delay)

AI improvements:
- Refine now includes original source material (transcript, notes, labs)
  so AI can reference the full input when modifying output
- Add correction tracking (trackAIOutput) to sick visit and well visit tabs
- Fix sickvisit missing from encounter save noteIdMap

UI fixes:
- Non-blocking busy bar for transcription and AI generation (replaces full-screen overlay)
- Fix encounter recording: hide record button during recording (was showing two stop buttons)
- Fix ROS/PE "All WNL" stacking duplicate event handlers; add Clear buttons
- Enlarge AI instructions textarea in Learning Hub CMS

Domain:
- Primary domain now app.pedshub.com, with scribe.pedshub.com and peds.danvics.com as CORS origins
2026-04-08 20:27:45 +02:00

235 lines
8.9 KiB
JavaScript

// ============================================================
// WELL VISIT ROUTES — SSHADESS assessment + note generation
// ============================================================
var express = require('express');
var router = express.Router();
var { callAI } = require('../utils/ai');
var PROMPTS = require('../utils/prompts');
var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
// Load growth reference and BMI classification data
var scheduleData;
try {
scheduleData = require('../../public/js/pediatricScheduleData');
} catch(e) {
scheduleData = {};
}
var GROWTH_REFERENCE = scheduleData.GROWTH_REFERENCE || {};
var BMI_CLASSIFICATION = scheduleData.BMI_CLASSIFICATION || {};
// Map visit age text to GROWTH_REFERENCE keys
function getGrowthRefForAge(ageStr) {
if (!ageStr) return null;
var a = ageStr.toLowerCase().trim();
// Direct key matches
var directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
for (var i = 0; i < directKeys.length; i++) {
if (a.indexOf(directKeys[i]) !== -1) return GROWTH_REFERENCE[directKeys[i]] || null;
}
// Age-based range matching
var ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
if (ageMatch) {
var num = parseInt(ageMatch[1]);
var unit = ageMatch[2].toLowerCase();
if (unit === 'month' || unit === 'mo') {
if (num <= 1) return GROWTH_REFERENCE['1mo'];
if (num <= 3) return GROWTH_REFERENCE['2mo'];
if (num <= 5) return GROWTH_REFERENCE['4mo'];
if (num <= 8) return GROWTH_REFERENCE['6mo'];
if (num <= 11) return GROWTH_REFERENCE['9mo'];
if (num <= 14) return GROWTH_REFERENCE['12mo'];
if (num <= 17) return GROWTH_REFERENCE['15mo'];
if (num <= 23) return GROWTH_REFERENCE['18mo'];
if (num <= 29) return GROWTH_REFERENCE['24mo'];
if (num <= 35) return GROWTH_REFERENCE['30mo'];
}
if (unit === 'year' || unit === 'y' || unit === 'yr') {
if (num <= 3) return GROWTH_REFERENCE['3y'];
if (num <= 4) return GROWTH_REFERENCE['4y'];
if (num <= 5) return GROWTH_REFERENCE['5y'];
if (num <= 10) return GROWTH_REFERENCE['6y_to_10y'];
if (num <= 14) return GROWTH_REFERENCE['11y_to_14y'];
return GROWTH_REFERENCE['15y_to_21y'];
}
}
if (a.indexOf('newborn') !== -1 || a.indexOf('birth') !== -1) return GROWTH_REFERENCE['newborn'];
return null;
}
// ── POST generate SSHADESS assessment summary ────────────────────────────
router.post('/well-visit/shadess', authMiddleware, async function(req, res) {
var start = Date.now();
try {
var { domains, patientAge, patientGender, dictationText, model } = req.body;
if (!domains && !dictationText) {
return res.status(400).json({ error: 'Provide domain responses or dictation text' });
}
// Build structured input text
var inputText = 'PATIENT: ' + (patientAge || 'Adolescent') + ', ' + (patientGender || 'Unknown gender') + '\n\n';
inputText += 'SSHADESS SCREENING DATA:\n\n';
if (dictationText) {
inputText += 'PHYSICIAN DICTATION:\n' + dictationText + '\n\n';
}
if (domains) {
var domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
var domainNames = {
strengths: 'STRENGTHS',
school: 'SCHOOL',
home: 'HOME',
activities: 'ACTIVITIES',
drugs: 'DRUGS/SUBSTANCES',
emotions: 'EMOTIONS/EATING',
sexuality: 'SEXUALITY',
safety: 'SAFETY'
};
domainOrder.forEach(function(key) {
var d = domains[key];
if (!d || d.skipped) return;
inputText += domainNames[key] + ':\n';
if (d.questions) {
d.questions.forEach(function(q) {
if (q.answer !== null && q.answer !== undefined && q.answer !== '') {
inputText += ' - ' + q.text + ': ' + (q.answer === true || q.answer === 'yes' ? 'YES' : (q.answer === false || q.answer === 'no' ? 'NO' : q.answer)) + '\n';
}
});
}
if (d.comment && d.comment.trim()) {
inputText += ' Comments: ' + d.comment + '\n';
}
if (d.concern) {
inputText += ' *** CONCERN FLAGGED ***\n';
}
inputText += '\n';
});
}
var result = await callAI([
{ role: 'system', content: PROMPTS.shadessAssessment },
{ role: 'user', content: inputText }
], { model });
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/well-visit/shadess', {
model: result.model, tokensInput: result.usage?.input_tokens,
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
});
res.json({ success: true, assessment: result.content, model: result.model });
logger.audit(req.user.id, 'generate_shadess', 'Generated SSHADESS assessment', req, { category: 'clinical' });
} catch (e) {
logger.error('[WellVisit] SHADESS generation failed', e.message);
res.status(500).json({ error: e.message });
}
});
// ── POST generate full well visit encounter note ─────────────────────────
router.post('/well-visit/note', authMiddleware, async function(req, res) {
var start = Date.now();
try {
var {
patientAge, patientGender, visitAge,
vitals, measurements,
transcript, dictation,
screenings, vaccines,
shadessAssessment,
parentConcerns,
ros, physicalExam, diagnoses,
physicianMemories,
noteStyle, // 'full' | 'short'
model
} = req.body;
if (!patientAge && !visitAge) {
return res.status(400).json({ error: 'Patient age or visit age required' });
}
// Assemble context
var context = 'WELL CHILD VISIT\n';
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n';
if (vitals) {
context += 'VITAL SIGNS:\n' + vitals + '\n\n';
}
if (measurements) {
context += 'MEASUREMENTS/GROWTH:\n' + measurements + '\n\n';
}
if (parentConcerns) {
context += 'PARENT/PATIENT CONCERNS:\n' + parentConcerns + '\n\n';
}
if (transcript) {
context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + transcript + '\n\n';
} else if (dictation) {
context += 'PHYSICIAN DICTATION:\n' + dictation + '\n\n';
}
if (screenings) {
context += 'SCREENINGS COMPLETED:\n' + screenings + '\n\n';
}
if (vaccines) {
context += 'IMMUNIZATIONS TODAY:\n' + vaccines + '\n\n';
}
if (shadessAssessment) {
context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + shadessAssessment + '\n\n';
}
if (ros) {
context += ros + '\n\n';
}
if (physicalExam) {
context += physicalExam + '\n\n';
}
if (diagnoses) {
context += diagnoses + '\n\n';
}
if (physicianMemories) {
context += '[STYLE HINTS (low priority — only apply if relevant to the current note, never copy content from these examples)]\n' + physicianMemories + '\n[END STYLE HINTS]\n\n';
}
// Add growth reference and feeding guidance for this age
var growthRef = getGrowthRefForAge(patientAge || visitAge);
if (growthRef) {
context += 'GROWTH/NUTRITION GUIDANCE (AAP reference for this age):\n';
if (growthRef.weight) context += 'Expected weight gain: ' + growthRef.weight + '\n';
if (growthRef.length) context += 'Expected length/height gain: ' + growthRef.length + '\n';
if (growthRef.headCirc) context += 'Expected head circumference gain: ' + growthRef.headCirc + '\n';
if (growthRef.feeding && growthRef.feeding.length) {
context += 'Feeding guidance:\n';
growthRef.feeding.forEach(function(f) { context += ' - ' + f + '\n'; });
}
if (growthRef.bmiClassification && BMI_CLASSIFICATION.categories) {
context += 'BMI Classification (AAP 2023 / CDC Extended BMI):\n';
BMI_CLASSIFICATION.categories.forEach(function(c) {
context += ' - ' + c.label + ': ' + c.range + ' → ' + c.action + '\n';
});
}
context += '\n';
}
var prompt = (noteStyle === 'short') ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
var result = await callAI([
{ role: 'system', content: prompt },
{ role: 'user', content: context }
], { model });
var dur = Date.now() - start;
logger.apiCall(req.user.id, '/api/well-visit/note', {
model: result.model, tokensInput: result.usage?.input_tokens,
tokensOutput: result.usage?.output_tokens, duration: dur, statusCode: 200
});
res.json({ success: true, note: result.content, model: result.model });
logger.audit(req.user.id, 'generate_well_visit', 'Generated well visit note', req, { category: 'clinical' });
} catch (e) {
logger.error('[WellVisit] Note generation failed', e.message);
res.status(500).json({ error: e.message });
}
});
module.exports = router;