Closes the migration. Every line of vanilla JS/CSS/HTML that used to
render the user-facing app is gone. The React bundle in public/app/
is the entire client.
Deleted (everything was 100% covered by ported React equivalents):
• public/index.html (vanilla shell — auth screen + tab loader)
• public/js/ (~30 vanilla modules: app.js, auth.js,
admin.js, calculators.js, peGuide.js,
bedside/*, learningHub.js, encounters.js,
etc. — all replaced by client/src/pages/*
and client/src/data/*)
• public/components/ (18 lazy-loaded HTML fragments — not
loaded by any React route)
• public/css/styles.css (vanilla design system — superseded by
Tailwind + shadcn classes throughout
the React tree)
• public/e2e-harness.html (vanilla-only Playwright bootstrapper)
• e2e/tests/bedside-smoke.spec.js
• e2e/tests/top-calculators.spec.js
(the two e2e specs that exercised
/e2e-harness.html and the vanilla
calculators directly — replaced by
calculators-react.spec.js +
bedside-react.spec.js + the 136
vitest parity tests in
shared/clinical/*.test.ts)
Moved (still needed by the backend):
• public/js/pediatricScheduleData.js → src/data/pediatric-schedule-data.js
Required by src/routes/wellVisit.ts at runtime; should never have been
in public/ anyway since it carries server-side schedule + growth +
BMI reference tables and was being served as a 2120-line public JS
blob to every browser. Not in public/ means it's not exposed to
anonymous web requests anymore.
server.ts
• Removed the dead getTemplatedIndex() / INDEX_PATH machinery that
used to BUILD_ID-stamp /js/* and /css/* references in the vanilla
HTML. Vite's hashed asset URLs already do that job for the React
bundle.
• express.static cache header: removed the /components/ branch
(folder no longer exists) and changed /js/ + /css/ from 1-hour
to 1-day immutable cache — safe because Vite hashes the
filenames on every build.
Backend tsc + client tsc + vite build all green. 136/136 vitest
parity tests still pass against the captured calc-vectors.json.
Initial bundle unchanged at 343.97 kB / 106.59 kB gz.
249 lines
11 KiB
TypeScript
249 lines
11 KiB
TypeScript
// ============================================================
|
|
// WELL VISIT ROUTES — SSHADESS assessment + note generation
|
|
// ============================================================
|
|
|
|
import express = require('express');
|
|
import type { Request, Response } from 'express';
|
|
const { callAI } = require('../utils/ai');
|
|
const PROMPTS = require('../utils/prompts');
|
|
const { authMiddleware } = require('../middleware/auth');
|
|
const logger = require('../utils/logger');
|
|
const { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
|
|
|
|
const router = express.Router();
|
|
|
|
// Load growth reference and BMI classification data
|
|
let scheduleData: any;
|
|
try {
|
|
scheduleData = require('../data/pediatric-schedule-data');
|
|
} catch (e) {
|
|
scheduleData = {};
|
|
}
|
|
const GROWTH_REFERENCE: Record<string, any> = scheduleData.GROWTH_REFERENCE || {};
|
|
const BMI_CLASSIFICATION: any = scheduleData.BMI_CLASSIFICATION || {};
|
|
|
|
// Vaccine full names — sourced from public/js/wellVisit.js where they were
|
|
// inlined. Mirrored here so a single backend response can power the React
|
|
// vax schedule page without a second round trip.
|
|
const VACCINE_FULL_NAMES: Record<string, string> = {
|
|
HepB: 'Hepatitis B (HepB)',
|
|
RV: 'Rotavirus (RV)',
|
|
DTaP: 'DTaP (Diphtheria, Tetanus, Pertussis)',
|
|
Hib: 'Hib (Haemophilus influenzae type b)',
|
|
PCV: 'Pneumococcal (PCV)',
|
|
IPV: 'Polio (IPV)',
|
|
Flu_IIV: 'Influenza (IIV)',
|
|
MMR: 'MMR (Measles, Mumps, Rubella)',
|
|
VAR: 'Varicella (VAR)',
|
|
HepA: 'Hepatitis A (HepA)',
|
|
Tdap: 'Tdap (Tetanus, Diphtheria, Pertussis booster)',
|
|
HPV: 'HPV (Human Papillomavirus)',
|
|
MenACWY: 'Meningococcal ACWY (MenACWY)',
|
|
MenB: 'Meningococcal B (MenB)',
|
|
RSV_mAb: 'RSV Monoclonal Antibody (Nirsevimab/Beyfortus)',
|
|
COVID: 'COVID-19',
|
|
Dengue: 'Dengue (DEN4CYD / Dengvaxia)',
|
|
Mpox: 'Mpox (JYNNEOS)',
|
|
};
|
|
|
|
// ── GET schedule data — used by the React client to render vaccine
|
|
// ── + catch-up tables without duplicating the 2000-line schedule module.
|
|
router.get('/schedule-data', authMiddleware, function (_req: Request, res: Response) {
|
|
res.json({
|
|
success: true,
|
|
visitAges: scheduleData.VISIT_AGES || [],
|
|
periodicity: scheduleData.PERIODICITY || {},
|
|
catchUpSchedule: scheduleData.CATCH_UP_SCHEDULE || {},
|
|
vaccineFullNames: VACCINE_FULL_NAMES,
|
|
});
|
|
});
|
|
|
|
// Map visit age text to GROWTH_REFERENCE keys
|
|
function getGrowthRefForAge(ageStr?: string): any {
|
|
if (!ageStr) return null;
|
|
const a = ageStr.toLowerCase().trim();
|
|
const directKeys = ['newborn', '1mo', '2mo', '4mo', '6mo', '9mo', '12mo', '15mo', '18mo', '24mo', '30mo', '3y', '4y', '5y'];
|
|
for (let i = 0; i < directKeys.length; i++) {
|
|
if (a.indexOf(directKeys[i]) !== -1) return GROWTH_REFERENCE[directKeys[i]] || null;
|
|
}
|
|
const ageMatch = a.match(/(\d+)\s*(month|mo|year|y|yr)/i);
|
|
if (ageMatch) {
|
|
const num = parseInt(ageMatch[1]);
|
|
const 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: Request, res: Response) {
|
|
const start = Date.now();
|
|
try {
|
|
const { domains, patientAge, patientGender, dictationText, model } = req.body;
|
|
|
|
if (!domains && !dictationText) {
|
|
return res.status(400).json({ error: 'Provide domain responses or dictation text' });
|
|
}
|
|
|
|
let 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) {
|
|
const domainOrder = ['strengths', 'school', 'home', 'activities', 'drugs', 'emotions', 'sexuality', 'safety'];
|
|
const domainNames: Record<string, string> = {
|
|
strengths: 'STRENGTHS',
|
|
school: 'SCHOOL',
|
|
home: 'HOME',
|
|
activities: 'ACTIVITIES',
|
|
drugs: 'DRUGS/SUBSTANCES',
|
|
emotions: 'EMOTIONS/EATING',
|
|
sexuality: 'SEXUALITY',
|
|
safety: 'SAFETY',
|
|
};
|
|
|
|
domainOrder.forEach((key) => {
|
|
const d = domains[key];
|
|
if (!d || d.skipped) return;
|
|
|
|
inputText += domainNames[key] + ':\n';
|
|
if (d.questions) {
|
|
d.questions.forEach((q: any) => {
|
|
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';
|
|
});
|
|
}
|
|
|
|
const result = await callAI([
|
|
{ role: 'system', content: PROMPTS.shadessAssessment + INJECTION_GUARD },
|
|
{ role: 'user', content: wrapUserText('sshadess_answers', inputText) },
|
|
], { model });
|
|
|
|
const 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: any) {
|
|
logger.error('[WellVisit] SHADESS generation failed', e.message);
|
|
res.status(500).json({ error: 'Request failed' });
|
|
}
|
|
});
|
|
|
|
// ── POST generate full well visit encounter note ─────────────────────────
|
|
router.post('/well-visit/note', authMiddleware, async function (req: Request, res: Response) {
|
|
const start = Date.now();
|
|
try {
|
|
const {
|
|
patientAge, patientGender, visitAge,
|
|
vitals, measurements,
|
|
transcript, dictation,
|
|
screenings, vaccines,
|
|
shadessAssessment,
|
|
parentConcerns,
|
|
ros, physicalExam, diagnoses,
|
|
physicianMemories,
|
|
noteStyle,
|
|
model,
|
|
} = req.body;
|
|
|
|
if (!patientAge && !visitAge) {
|
|
return res.status(400).json({ error: 'Patient age or visit age required' });
|
|
}
|
|
|
|
let context = 'WELL CHILD VISIT\n';
|
|
context += 'Patient: ' + (patientAge || visitAge) + ', ' + (patientGender || 'Unknown') + '\n\n';
|
|
|
|
if (vitals) context += 'VITAL SIGNS:\n' + wrapUserText('vitals', vitals) + '\n\n';
|
|
if (measurements) context += 'MEASUREMENTS/GROWTH:\n' + wrapUserText('measurements', measurements) + '\n\n';
|
|
if (parentConcerns) context += 'PARENT/PATIENT CONCERNS:\n' + wrapUserText('concerns', parentConcerns) + '\n\n';
|
|
if (transcript) context += 'ENCOUNTER TRANSCRIPT/DICTATION:\n' + wrapUserText('transcript', transcript) + '\n\n';
|
|
else if (dictation) context += 'PHYSICIAN DICTATION:\n' + wrapUserText('dictation', dictation) + '\n\n';
|
|
if (screenings) context += 'SCREENINGS COMPLETED:\n' + wrapUserText('screenings', screenings) + '\n\n';
|
|
if (vaccines) context += 'IMMUNIZATIONS TODAY:\n' + wrapUserText('vaccines', vaccines) + '\n\n';
|
|
if (shadessAssessment) context += 'SSHADESS PSYCHOSOCIAL ASSESSMENT:\n' + wrapUserText('sshadess', shadessAssessment) + '\n\n';
|
|
if (ros) context += wrapUserText('ros', ros) + '\n\n';
|
|
if (physicalExam) context += wrapUserText('physical_exam', physicalExam) + '\n\n';
|
|
if (diagnoses) context += wrapUserText('diagnoses', 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'
|
|
+ wrapUserText('style_hints', physicianMemories) + '\n[END STYLE HINTS]\n\n';
|
|
}
|
|
|
|
const 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((f: string) => { context += ' - ' + f + '\n'; });
|
|
}
|
|
if (growthRef.bmiClassification && BMI_CLASSIFICATION.categories) {
|
|
context += 'BMI Classification (AAP 2023 / CDC Extended BMI):\n';
|
|
BMI_CLASSIFICATION.categories.forEach((c: any) => {
|
|
context += ' - ' + c.label + ': ' + c.range + ' → ' + c.action + '\n';
|
|
});
|
|
}
|
|
context += '\n';
|
|
}
|
|
|
|
const prompt = noteStyle === 'short' ? PROMPTS.wellVisitShort : PROMPTS.wellVisitNote;
|
|
|
|
const result = await callAI([
|
|
{ role: 'system', content: prompt + INJECTION_GUARD },
|
|
{ role: 'user', content: context },
|
|
], { model });
|
|
|
|
const 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: any) {
|
|
logger.error('[WellVisit] Note generation failed', e.message);
|
|
res.status(500).json({ error: 'Request failed' });
|
|
}
|
|
});
|
|
|
|
export = router;
|