pediatric-ai-scribe-v3/public/js/milestones.js
Daniel d12d4f4e63 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

240 lines
9.9 KiB
JavaScript

(function() {
var _inited = false;
var MILESTONES_DATA = {}; // Will be loaded from API
document.addEventListener('tabChanged', function(e) {
if (e.detail.tab !== 'wellvisit' || _inited) return;
_inited = true;
initMilestones();
});
function initMilestones() {
var ageSelect = document.getElementById('ms-age-group');
var checklist = document.getElementById('milestone-checklist');
var actionsBar = document.getElementById('milestone-actions');
var allYesBtn = document.getElementById('ms-all-yes');
var allClearBtn = document.getElementById('ms-all-clear');
var generateBtn = document.getElementById('ms-generate-btn');
var outputCard = document.getElementById('ms-output');
var summaryBar = document.getElementById('ms-summary-bar');
var narrativeText = document.getElementById('ms-narrative-text');
var modelTag = document.getElementById('ms-model-tag');
var state = {};
// Load milestones from API (database), fallback to static data if empty
fetch('/api/milestones-data', {
headers: getAuthHeaders()
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success && data.milestones && Object.keys(data.milestones).length > 0) {
// Database has milestones - use them
MILESTONES_DATA = data.milestones;
populateAgeGroups();
} else {
// Database empty - fallback to static data
console.log('[Milestones] Database empty, using static data');
if (typeof window.MILESTONES_DATA_STATIC !== 'undefined') {
MILESTONES_DATA = window.MILESTONES_DATA_STATIC;
populateAgeGroups();
} else {
showToast('Milestones data not available', 'error');
}
}
})
.catch(function(err) {
console.error('[Milestones] Load error, falling back to static:', err);
// Fallback to static data on API error
if (typeof window.MILESTONES_DATA_STATIC !== 'undefined') {
MILESTONES_DATA = window.MILESTONES_DATA_STATIC;
populateAgeGroups();
} else {
showToast('Failed to load milestones: ' + err.message, 'error');
}
});
function populateAgeGroups() {
// Populate age group dropdown
if (ageSelect && Object.keys(MILESTONES_DATA).length > 0) {
ageSelect.innerHTML = '<option value="">Select age group</option>';
Object.keys(MILESTONES_DATA).forEach(function(age) {
var opt = document.createElement('option');
opt.value = age;
opt.textContent = age;
ageSelect.appendChild(opt);
});
}
}
ageSelect.addEventListener('change', function() {
var age = ageSelect.value;
state = {};
outputCard.classList.add('hidden');
if (!age || !MILESTONES_DATA[age]) {
checklist.innerHTML = '<p style="text-align:center;color:#9ca3af;padding:40px;">Select an age group.</p>';
actionsBar.style.display = 'none';
return;
}
renderChecklist(MILESTONES_DATA[age]);
actionsBar.style.display = 'flex';
});
function safeId(s) { return s.replace(/[^a-zA-Z0-9]/g, '_'); }
function renderChecklist(data) {
checklist.innerHTML = '';
Object.keys(data).forEach(function(domain) {
var items = data[domain];
var config = DOMAIN_CONFIG[domain] || { icon: '📋', css: '' };
var section = document.createElement('div');
section.className = 'domain-section ' + config.css;
var html = '<div class="domain-header"><span>' + config.icon + '</span><span>' + domain +
'</span><span class="badge" id="badge-' + safeId(domain) + '">' + items.length + ' items</span></div><div class="ms-items">';
items.forEach(function(m, idx) {
var id = domain + '-' + idx;
state[id] = { domain: domain, milestone: m, status: null };
html += '<div class="ms-item" id="item-' + safeId(id) + '">' +
'<div class="ms-toggle">' +
'<button class="toggle-btn" data-id="' + id + '" data-action="yes">✓</button>' +
'<button class="toggle-btn" data-id="' + id + '" data-action="no">✗</button>' +
'</div><span class="ms-text">' + m + '</span></div>';
});
section.innerHTML = html + '</div>';
checklist.appendChild(section);
});
checklist.querySelectorAll('.toggle-btn').forEach(function(btn) {
btn.addEventListener('click', function() { handleToggle(btn.getAttribute('data-id'), btn.getAttribute('data-action')); });
});
}
function handleToggle(id, action) {
var sid = safeId(id);
var item = document.getElementById('item-' + sid);
var yesBtn = item.querySelector('[data-action="yes"]');
var noBtn = item.querySelector('[data-action="no"]');
if (action === 'yes') {
if (state[id].status === 'yes') { state[id].status = null; yesBtn.classList.remove('yes-on'); item.classList.remove('is-yes','is-no'); }
else { state[id].status = 'yes'; yesBtn.classList.add('yes-on'); noBtn.classList.remove('no-on'); item.classList.add('is-yes'); item.classList.remove('is-no'); }
} else {
if (state[id].status === 'no') { state[id].status = null; noBtn.classList.remove('no-on'); item.classList.remove('is-yes','is-no'); }
else { state[id].status = 'no'; noBtn.classList.add('no-on'); yesBtn.classList.remove('yes-on'); item.classList.remove('is-yes'); item.classList.add('is-no'); }
}
updateBadges();
}
function updateBadges() {
var counts = {};
Object.keys(state).forEach(function(id) {
var d = state[id].domain;
if (!counts[d]) counts[d] = { total: 0, yes: 0, no: 0 };
counts[d].total++;
if (state[id].status === 'yes') counts[d].yes++;
if (state[id].status === 'no') counts[d].no++;
});
Object.keys(counts).forEach(function(d) {
var b = document.getElementById('badge-' + safeId(d));
if (b) { var c = counts[d]; b.textContent = (c.yes + c.no) > 0 ? c.yes + '✓ ' + c.no + '✗ / ' + c.total : c.total + ' items'; }
});
}
allYesBtn.addEventListener('click', function() {
Object.keys(state).forEach(function(id) {
state[id].status = 'yes';
var item = document.getElementById('item-' + safeId(id));
if (item) { item.querySelector('[data-action="yes"]').classList.add('yes-on'); item.querySelector('[data-action="no"]').classList.remove('no-on'); item.classList.add('is-yes'); item.classList.remove('is-no'); }
});
updateBadges();
});
allClearBtn.addEventListener('click', function() {
Object.keys(state).forEach(function(id) {
state[id].status = null;
var item = document.getElementById('item-' + safeId(id));
if (item) { item.querySelector('[data-action="yes"]').classList.remove('yes-on'); item.querySelector('[data-action="no"]').classList.remove('no-on'); item.classList.remove('is-yes','is-no'); }
});
updateBadges();
outputCard.classList.add('hidden');
});
generateBtn.addEventListener('click', function() {
if (!ageSelect.value) { showToast('Select age group', 'error'); return; }
var assessed = Object.values(state).filter(function(m) { return m.status !== null; });
if (assessed.length === 0) { showToast('Assess at least one milestone', 'error'); return; }
showBusy('Generating...');
fetch('/api/generate-milestone-narrative', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
milestones: Object.values(state),
ageGroup: ageSelect.value,
patientAge: document.getElementById('ms-age').value,
patientGender: document.getElementById('ms-gender').value,
format: document.getElementById('ms-format').value,
model: getSelectedModel()
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideBusy();
if (data.success) {
setOutputText(narrativeText, data.narrative);
modelTag.textContent = (data.model || '').split('/').pop();
if (data.summary) {
summaryBar.innerHTML = '<div class="stat"><span class="dot dot-green"></span> Achieved: ' + data.summary.achieved + '</div>' +
'<div class="stat"><span class="dot dot-red"></span> Not Yet: ' + data.summary.notAchieved + '</div>' +
'<div class="stat"><span class="dot dot-gray"></span> Not Assessed: ' + data.summary.notAssessed + ' (omitted)</div>';
summaryBar.classList.remove('hidden');
}
outputCard.classList.remove('hidden');
document.getElementById('ms-quick-summary').classList.add('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });
showToast('Generated!', 'success');
} else showToast(data.error || 'Failed', 'error');
})
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
});
// 3-sentence summary
document.getElementById('ms-quick-summary-btn').addEventListener('click', function() {
var narrative = narrativeText.innerText.trim();
if (!narrative) { showToast('Generate narrative first', 'error'); return; }
var btn = document.getElementById('ms-quick-summary-btn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Summarizing...';
fetch('/api/generate-milestone-summary', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
narrative: narrative,
ageGroup: ageSelect.value,
patientAge: document.getElementById('ms-age').value,
patientGender: document.getElementById('ms-gender').value,
model: getSelectedModel()
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-compress"></i> Regenerate Summary';
if (data.success) {
setOutputText('ms-summary-text', data.summary);
document.getElementById('ms-quick-summary').classList.remove('hidden');
showToast('Summary generated!', 'success');
}
})
.catch(function(err) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-compress"></i> 3-Sentence Summary'; showToast(err.message, 'error'); });
});
console.log('✅ Milestones module loaded');
} // end initMilestones
})();