diff --git a/e2e/tests/soap-hospital-workflow.spec.js b/e2e/tests/soap-hospital-workflow.spec.js
new file mode 100644
index 0000000..cfa0244
--- /dev/null
+++ b/e2e/tests/soap-hospital-workflow.spec.js
@@ -0,0 +1,75 @@
+// ============================================================
+// SOAP + HOSPITAL COURSE — detailed workflow for the two notes
+// tabs that previously only had smoke coverage.
+// ============================================================
+
+const { test, expect, E2E_BASE, mockAI } = require('../fixtures');
+
+async function openTab(page, name) {
+ await page.goto(E2E_BASE + '/');
+ await page.waitForSelector('button.tab-btn', { timeout: 15000 });
+ const vp = page.viewportSize();
+ if (vp && vp.width <= 768) {
+ await page.click('#btn-menu-toggle').catch(() => {});
+ }
+ await page.click(`button.tab-btn[data-tab="${name}"]`);
+ await page.waitForFunction((t) => {
+ const el = document.getElementById(t + '-tab');
+ return el && el.classList.contains('active') && el.innerHTML.trim().length > 100;
+ }, name, { timeout: 15000 });
+}
+
+test.describe('SOAP Note — generate + refine', () => {
+
+ test('fill transcript + generate → mocked SOAP renders', async ({ authedPage: _, page }) => {
+ await mockAI(page);
+ await openTab(page, 'soap');
+
+ await page.fill('#soap-age', '5 years');
+ await page.selectOption('#soap-gender', 'Male');
+ await page.locator('#soap-transcript').click();
+ await page.keyboard.type('Patient presents with 3 days of fever, cough, and runny nose.');
+
+ const [resp] = await Promise.all([
+ page.waitForResponse('**/api/generate-soap', { timeout: 15000 }),
+ page.click('#soap-generate-btn'),
+ ]);
+ expect(resp.status()).toBe(200);
+ await expect(page.locator('#soap-output')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('#soap-text')).toContainText('MOCK SOAP NOTE');
+ });
+
+ test('clear transcript button empties the contenteditable', async ({ authedPage: _, page }) => {
+ await mockAI(page);
+ await openTab(page, 'soap');
+ await page.locator('#soap-transcript').click();
+ await page.keyboard.type('some transcript');
+ await expect(page.locator('#soap-transcript')).toContainText('some transcript');
+ await page.click('#soap-clear');
+ const text = await page.locator('#soap-transcript').innerText();
+ expect(text.trim()).toBe('');
+ });
+
+ test('load popover opens and closes', async ({ authedPage: _, page }) => {
+ await openTab(page, 'soap');
+ await page.click('#btn-soap-load').catch(() => page.click('button[id*="soap-load"]'));
+ await expect(page.locator('#soap-load-popover')).not.toHaveClass(/hidden/, { timeout: 3000 });
+ });
+});
+
+test.describe('Hospital Course — tab loads with save/load bar', () => {
+
+ test('tab renders with label input + save/load/new buttons', async ({ authedPage: _, page }) => {
+ await openTab(page, 'hospital');
+ await expect(page.locator('#hosp-label')).toBeVisible();
+ await expect(page.locator('#hosp-save-bar')).toBeVisible();
+ });
+
+ test('load popover opens and closes', async ({ authedPage: _, page }) => {
+ await openTab(page, 'hospital');
+ // Any button with id that looks like load
+ const loadBtn = page.locator('button[id*="hosp-load"], button[id*="btn-hosp-load"]').first();
+ await loadBtn.click();
+ await expect(page.locator('#hosp-load-popover')).not.toHaveClass(/hidden/, { timeout: 3000 });
+ });
+});
diff --git a/e2e/tests/ui-state-persistence.spec.js b/e2e/tests/ui-state-persistence.spec.js
new file mode 100644
index 0000000..53c98d0
--- /dev/null
+++ b/e2e/tests/ui-state-persistence.spec.js
@@ -0,0 +1,89 @@
+// ============================================================
+// UI STATE PERSISTENCE — sub-pill / sub-tab choices survive a
+// full page reload (simulating sign-out / sign-in or browser
+// restart). Guards against regressions in the ui-state.js +
+// localStorage wiring.
+// ============================================================
+
+const { test, expect, E2E_BASE } = require('../fixtures');
+
+async function gotoHome(page) {
+ await page.goto(E2E_BASE + '/');
+ await page.waitForSelector('button.tab-btn', { timeout: 15000 });
+}
+
+async function openDesktopTab(page, name) {
+ const vp = page.viewportSize();
+ if (vp && vp.width <= 768) {
+ await page.click('#btn-menu-toggle').catch(() => {});
+ }
+ await page.click(`button.tab-btn[data-tab="${name}"]`);
+ await page.waitForFunction((t) => {
+ const el = document.getElementById(t + '-tab');
+ return el && el.classList.contains('active') && el.innerHTML.trim().length > 100;
+ }, name, { timeout: 15000 });
+}
+
+test.describe('UI state survives page reload', () => {
+
+ test('ped_last_tab → user lands on last-active tab after reload', async ({ authedPage: _, page }) => {
+ await gotoHome(page);
+ await openDesktopTab(page, 'calculators');
+ // Reload (keeps cookie, clears _componentCache + in-memory DOM state)
+ await page.reload();
+ await page.waitForSelector('button.tab-btn', { timeout: 15000 });
+ await expect.poll(async () => {
+ return await page.locator('#calculators-tab.active').count();
+ }, { timeout: 5000 }).toBe(1);
+ });
+
+ test('calculators nav pill persists across reload', async ({ authedPage: _, page }) => {
+ await gotoHome(page);
+ await openDesktopTab(page, 'calculators');
+ // Switch to GCS
+ await page.click('button.calc-nav-pill[data-calc="gcs"]');
+ await expect(page.locator('button.calc-nav-pill[data-calc="gcs"].active')).toBeVisible();
+ await page.reload();
+ await page.waitForSelector('button.calc-nav-pill[data-calc="gcs"]', { timeout: 15000 });
+ // Same pill should be active after reload
+ await expect(page.locator('button.calc-nav-pill[data-calc="gcs"].active')).toBeVisible({ timeout: 5000 });
+ // And the corresponding panel should be un-hidden
+ await expect(page.locator('#calc-gcs')).not.toHaveClass(/hidden/);
+ });
+
+ test('bedside sub-pill persists across reload', async ({ authedPage: _, page }) => {
+ await gotoHome(page);
+ await openDesktopTab(page, 'bedside');
+ await page.click('button.calc-pill[data-em="anaphylaxis"]');
+ await expect(page.locator('button.calc-pill[data-em="anaphylaxis"].active')).toBeVisible();
+ await page.reload();
+ await page.waitForSelector('button.calc-pill[data-em="anaphylaxis"]', { timeout: 15000 });
+ await expect(page.locator('button.calc-pill[data-em="anaphylaxis"].active')).toBeVisible({ timeout: 5000 });
+ // The anaphylaxis em-section should be the visible one
+ await expect.poll(async () => {
+ return await page.locator('#em-anaphylaxis').evaluate(el => el.style.display);
+ }, { timeout: 5000 }).not.toBe('none');
+ });
+
+ test('well-visit sub-tab persists across reload', async ({ authedPage: _, page }) => {
+ await gotoHome(page);
+ await openDesktopTab(page, 'wellvisit');
+ await page.click('button.wv-subtab-btn[data-subtab="milestones"]');
+ await expect(page.locator('#wv-panel-milestones')).not.toHaveClass(/hidden/);
+ await page.reload();
+ await page.waitForSelector('button.wv-subtab-btn[data-subtab="milestones"]', { timeout: 15000 });
+ await expect(page.locator('#wv-panel-milestones')).not.toHaveClass(/hidden/, { timeout: 5000 });
+ });
+
+ test('PE guide age group + system persist across reload', async ({ authedPage: _, page }) => {
+ await gotoHome(page);
+ await openDesktopTab(page, 'peguide');
+ await page.selectOption('#pe-age-group', 'adolescent');
+ await page.click('button[data-pesystem="neuro"]');
+ await expect(page.locator('button[data-pesystem="neuro"].active')).toBeVisible();
+ await page.reload();
+ await page.waitForSelector('#pe-age-group', { timeout: 15000 });
+ await expect(page.locator('#pe-age-group')).toHaveValue('adolescent', { timeout: 5000 });
+ await expect(page.locator('button[data-pesystem="neuro"].active')).toBeVisible({ timeout: 5000 });
+ });
+});
diff --git a/public/index.html b/public/index.html
index 3ede480..271e216 100644
--- a/public/index.html
+++ b/public/index.html
@@ -443,6 +443,7 @@
+
diff --git a/public/js/bedside/sub-nav.js b/public/js/bedside/sub-nav.js
index 841d0ed..11e7d15 100644
--- a/public/js/bedside/sub-nav.js
+++ b/public/js/bedside/sub-nav.js
@@ -1,18 +1,43 @@
// ============================================================
// bedside/sub-nav.js
// EMERGENCIES SUB-NAV — data-em pill switching.
+// Persists the active pill so reloads / sign-out-sign-in land on the
+// same sub-section the user was last looking at.
// ============================================================
+var SECTIONS = ['neonatal','airway','cardiac','respiratory','ventilation','seizure','sepsis','anaphylaxis','sedation','agitation','antiemetics','antimicrobials','burns','toxicology','trauma'];
+
+function activate(emKey) {
+ var pill = document.querySelector('[data-em="' + emKey + '"]');
+ if (!pill) return false;
+ document.querySelectorAll('[data-em]').forEach(function(p) { p.classList.remove('active'); });
+ pill.classList.add('active');
+ SECTIONS.forEach(function(s) {
+ var el = document.getElementById('em-' + s);
+ if (el) el.style.display = emKey === s ? '' : 'none';
+ });
+ return true;
+}
+
export function init() {
document.addEventListener('click', function(e) {
var pill = e.target.closest('[data-em]');
if (!pill) return;
- document.querySelectorAll('[data-em]').forEach(function(p) { p.classList.remove('active'); });
- pill.classList.add('active');
- var sections = ['neonatal','airway','cardiac','respiratory','ventilation','seizure','sepsis','anaphylaxis','sedation','agitation','antiemetics','antimicrobials','burns','toxicology','trauma'];
- sections.forEach(function(s) {
- var el = document.getElementById('em-' + s);
- if (el) el.style.display = pill.dataset.em === s ? '' : 'none';
- });
+ activate(pill.dataset.em);
+ if (window.UIState) window.UIState.set('bedside.pill', pill.dataset.em);
+ });
+ // Restore on load. The bedside component is lazily injected the first time
+ // the tab is activated, so try both an immediate pass (harness where the
+ // DOM is synchronously ready) and a tabChanged listener (live app where
+ // the component HTML arrives async). Either path reaches the same
+ // activate(saved) call — whichever fires first wins; the other is a noop.
+ function applySaved() {
+ if (!window.UIState) return;
+ var saved = window.UIState.get('bedside.pill');
+ if (saved) activate(saved);
+ }
+ applySaved();
+ document.addEventListener('tabChanged', function(e) {
+ if (e.detail && e.detail.tab === 'bedside') applySaved();
});
}
diff --git a/public/js/calculators.js b/public/js/calculators.js
index 9e92fdc..07f474f 100644
--- a/public/js/calculators.js
+++ b/public/js/calculators.js
@@ -58,15 +58,27 @@
function initCalculators() {
// ── Top-level pill navigation (scoped to .calc-nav-pill only) ──
+ function activateCalcPill(key) {
+ var pill = document.querySelector('.calc-nav-pill[data-calc="' + key + '"]');
+ if (!pill) return false;
+ document.querySelectorAll('.calc-nav-pill').forEach(function(p) { p.classList.remove('active'); });
+ document.querySelectorAll('.calc-panel').forEach(function(p) { p.classList.add('hidden'); });
+ pill.classList.add('active');
+ var panel = document.getElementById('calc-' + key);
+ if (panel) panel.classList.remove('hidden');
+ return true;
+ }
document.querySelectorAll('.calc-nav-pill').forEach(function(pill) {
pill.addEventListener('click', function() {
- document.querySelectorAll('.calc-nav-pill').forEach(function(p) { p.classList.remove('active'); });
- document.querySelectorAll('.calc-panel').forEach(function(p) { p.classList.add('hidden'); });
- pill.classList.add('active');
- var panel = document.getElementById('calc-' + pill.dataset.calc);
- if (panel) panel.classList.remove('hidden');
+ activateCalcPill(pill.dataset.calc);
+ if (window.UIState) window.UIState.set('calc.pill', pill.dataset.calc);
});
});
+ // Restore the last-selected pill across page reloads / sign-out-sign-in.
+ if (window.UIState) {
+ var saved = window.UIState.get('calc.pill');
+ if (saved) activateCalcPill(saved);
+ }
// ═══════════════════════════════════════════════════════════
// BP PERCENTILE — BCM/Rosner Quantile Spline Regression
diff --git a/public/js/peGuide.js b/public/js/peGuide.js
index e3346a7..f59dbde 100644
--- a/public/js/peGuide.js
+++ b/public/js/peGuide.js
@@ -1357,8 +1357,8 @@
var content = document.getElementById('pe-content');
var actions = document.getElementById('pe-actions');
- ageSelect.addEventListener('change', function () {
- currentAgeGroup = ageSelect.value;
+ function applyAgeGroup(ag) {
+ currentAgeGroup = ag;
state = {};
document.getElementById('pe-output').classList.add('hidden');
if (!currentAgeGroup || !PE_DATA[currentAgeGroup]) {
@@ -1368,19 +1368,42 @@
}
renderSystem();
actions.style.display = 'flex';
+ }
+ ageSelect.addEventListener('change', function () {
+ applyAgeGroup(ageSelect.value);
+ if (window.UIState) window.UIState.set('pe.ageGroup', ageSelect.value || '');
});
+ function applySystem(sys) {
+ document.querySelectorAll('[data-pesystem]').forEach(function (b) {
+ b.classList.toggle('active', b.dataset.pesystem === sys);
+ });
+ currentSystem = sys;
+ state = {};
+ document.getElementById('pe-output').classList.add('hidden');
+ if (currentAgeGroup) renderSystem();
+ }
document.querySelectorAll('[data-pesystem]').forEach(function (btn) {
btn.addEventListener('click', function () {
- document.querySelectorAll('[data-pesystem]').forEach(function (b) { b.classList.remove('active'); });
- btn.classList.add('active');
- currentSystem = btn.dataset.pesystem;
- state = {};
- document.getElementById('pe-output').classList.add('hidden');
- if (currentAgeGroup) renderSystem();
+ applySystem(btn.dataset.pesystem);
+ if (window.UIState) window.UIState.set('pe.system', btn.dataset.pesystem);
});
});
+ // Restore persisted selections. Age group must be restored first because
+ // the system render depends on it.
+ if (window.UIState) {
+ var savedAge = window.UIState.get('pe.ageGroup');
+ if (savedAge && PE_DATA[savedAge]) {
+ ageSelect.value = savedAge;
+ applyAgeGroup(savedAge);
+ }
+ var savedSys = window.UIState.get('pe.system');
+ if (savedSys && /^(msk|neuro|resp|cv)$/.test(savedSys)) {
+ applySystem(savedSys);
+ }
+ }
+
document.getElementById('pe-all-normal').addEventListener('click', setAllNormal);
document.getElementById('pe-all-clear').addEventListener('click', clearAll);
document.getElementById('pe-generate-btn').addEventListener('click', generate);
diff --git a/public/js/ui-state.js b/public/js/ui-state.js
new file mode 100644
index 0000000..9dc9d40
--- /dev/null
+++ b/public/js/ui-state.js
@@ -0,0 +1,35 @@
+// ============================================================
+// UI STATE — sub-pill / sub-tab persistence across browser restart
+// ============================================================
+// Tab-level selection (ped_last_tab) is handled in app.js. This file
+// persists the finer-grained choices that would otherwise reset to
+// defaults when the user closes the browser, signs out / back in, or
+// reloads the page — because _componentCache is in-memory and cleared
+// on reload.
+//
+// All keys live under the "ped_ui/" namespace in localStorage so they
+// are easy to enumerate and clear. Reads and writes are wrapped in
+// try/catch — localStorage can throw in some contexts (Safari private
+// mode, storage quotas), and a persistence failure must never break
+// the UI.
+// ============================================================
+
+(function () {
+ var PREFIX = 'ped_ui/';
+
+ function get(key) {
+ try { return localStorage.getItem(PREFIX + key); } catch (e) { return null; }
+ }
+ function set(key, value) {
+ try { localStorage.setItem(PREFIX + key, value); } catch (e) {}
+ }
+ function del(key) {
+ try { localStorage.removeItem(PREFIX + key); } catch (e) {}
+ }
+
+ window.UIState = {
+ get: get,
+ set: set,
+ del: del,
+ };
+})();
diff --git a/public/js/wellVisit.js b/public/js/wellVisit.js
index a485306..451512f 100644
--- a/public/js/wellVisit.js
+++ b/public/js/wellVisit.js
@@ -110,8 +110,16 @@
document.getElementById('wv-visit-select').addEventListener('change', onVisitChange);
document.querySelectorAll('.wv-subtab-btn').forEach(function (btn) {
- btn.addEventListener('click', function () { switchSubtab(btn.dataset.subtab); });
+ btn.addEventListener('click', function () {
+ switchSubtab(btn.dataset.subtab);
+ if (window.UIState) window.UIState.set('wv.subtab', btn.dataset.subtab);
+ });
});
+ // Restore persisted subtab so the user lands on the last-open pane.
+ if (window.UIState) {
+ var savedSubtab = window.UIState.get('wv.subtab');
+ if (savedSubtab) switchSubtab(savedSubtab);
+ }
// Wire the visit-detail panel click/input ONCE here (not inside renderVisitPanel)
// which would accumulate a new listener on every visit change — causing "can't click Done"