// Settings and configuration management const DEFAULT_SETTINGS = { triage_instruction: "Act as Lead Clinician. Priority: Life-saving protocols. Format: ## ASSESSMENT, ## PROTOCOL.", inquiry_instruction: "Act as Medical Librarian. Focus: Academic research and pharmacology.", tr_temp: 0.1, tr_tok: 1024, tr_p: 0.9, in_temp: 0.6, in_tok: 2048, in_p: 0.95, mission_context: "Isolated Medical Station offshore.", rep_penalty: 1.1, user_mode: "user", med_photo_model: "qwen", med_photo_prompt: "You are a pharmacy intake assistant on a sailing vessel. Look at the medication photo and return JSON only with keys: generic_name, brand_name, form, strength, expiry_date, batch_lot, storage_location, manufacturer, indication, allergy_warnings, dosage, notes.", vaccine_types: ["MMR", "DTaP", "HepB", "HepA", "Td/Tdap", "Influenza", "COVID-19"], pharmacy_labels: ["Antibiotic", "Analgesic", "Cardiac", "Respiratory", "Gastrointestinal", "Endocrine", "Emergency"] }; let settingsDirty = false; let settingsLoaded = false; let settingsAutoSaveTimer = null; let workspaceListLoaded = false; let offlineStatusCache = null; let vaccineTypeList = [...DEFAULT_SETTINGS.vaccine_types]; let pharmacyLabelList = [...DEFAULT_SETTINGS.pharmacy_labels]; function setUserMode(mode) { const body = document.body; const normalized = ['advanced', 'developer'].includes((mode || '').toLowerCase()) ? mode.toLowerCase() : 'user'; body.classList.remove('mode-user', 'mode-advanced', 'mode-developer'); body.classList.add(`mode-${normalized}`); document.documentElement.classList.remove('mode-user', 'mode-advanced', 'mode-developer'); document.documentElement.classList.add(`mode-${normalized}`); document.querySelectorAll('.dev-tag').forEach(el => { el.style.display = normalized === 'developer' ? 'inline-block' : ''; }); document.querySelectorAll('.developer-only').forEach(el => { el.style.display = normalized === 'developer' ? '' : 'none'; }); try { localStorage.setItem('user_mode', normalized); } catch (err) { /* ignore */ } } function applySettingsToUI(data = {}) { const merged = { ...DEFAULT_SETTINGS, ...(data || {}) }; vaccineTypeList = normalizeVaccineTypes(merged.vaccine_types); pharmacyLabelList = normalizePharmacyLabels(merged.pharmacy_labels); renderVaccineTypes(); renderPharmacyLabels(); Object.keys(merged).forEach(k => { const el = document.getElementById(k); if (el) { el.value = merged[k]; } }); setUserMode(merged.user_mode); try { localStorage.setItem('user_mode', merged.user_mode || 'user'); } catch (err) { /* ignore */ } window.CACHED_SETTINGS = merged; settingsDirty = false; settingsLoaded = true; } if (document.readyState !== 'loading') { applyStoredUserMode(); loadSettingsUI().catch(() => {}); loadWorkspaceSwitcher().catch(() => {}); bindSettingsDirtyTracking(); } else { document.addEventListener('DOMContentLoaded', () => { applyStoredUserMode(); loadSettingsUI().catch(() => {}); loadWorkspaceSwitcher().catch(() => {}); bindSettingsDirtyTracking(); }, { once: true }); } function bindSettingsDirtyTracking() { const fields = document.querySelectorAll('#Settings input, #Settings textarea, #Settings select'); fields.forEach(el => { if (el.dataset.settingsDirtyBound) return; el.dataset.settingsDirtyBound = 'true'; el.addEventListener('input', () => { settingsDirty = true; scheduleAutoSave('input-change'); if (el.id === 'user_mode') { setUserMode(el.value); // Persist immediately to avoid losing mode if navigation happens quickly saveSettings(false, 'user-mode-change').catch(() => {}); } if (el.classList.contains('developer-only')) { // Keep developer-only components in sync setUserMode(document.getElementById('user_mode')?.value || 'user'); } }); el.addEventListener('change', () => { settingsDirty = true; scheduleAutoSave('input-change'); if (el.id === 'user_mode') { setUserMode(el.value); saveSettings(false, 'user-mode-change').catch(() => {}); } if (el.classList.contains('developer-only')) { setUserMode(document.getElementById('user_mode')?.value || 'user'); } }); }); } async function loadSettingsUI() { console.log('[settings] loadSettingsUI called'); try { const workspaceLabel = window.WORKSPACE_LABEL || localStorage.getItem('workspace_label') || ''; const url = workspaceLabel ? `/api/data/settings?workspace=${encodeURIComponent(workspaceLabel)}` : '/api/data/settings'; const res = await fetch(url, { credentials: 'same-origin', headers: workspaceLabel ? { 'x-workspace': workspaceLabel, 'x-workspace-slug': workspaceLabel } : {}, }); console.log('[settings] fetch response status:', res.status); if (!res.ok) throw new Error(`Settings load failed (${res.status})`); const s = await res.json(); console.log('[settings] loaded settings:', s); applySettingsToUI(s); console.log('[settings] settings applied to UI'); try { localStorage.setItem('user_mode', s.user_mode || 'user'); } catch (err) { /* ignore */ } } catch (err) { console.error('[settings] load error', err); alert(`Unable to load settings: ${err.message}`); const localMode = (() => { try { return localStorage.getItem('user_mode') || 'user'; } catch (e) { return 'user'; } })(); applySettingsToUI({ ...DEFAULT_SETTINGS, user_mode: localMode }); if (String(err.message || '').toLowerCase().includes('workspace')) { window.location.href = '/workspace'; } } } function updateSettingsStatus(message, isError = false) { const el = document.getElementById('settings-save-status'); if (!el) return; el.textContent = message || ''; el.style.color = isError ? 'var(--red)' : '#2c3e50'; } function applyStoredUserMode() { try { const stored = localStorage.getItem('user_mode') || 'user'; setUserMode(stored); } catch (err) { /* ignore */ } } function scheduleAutoSave(reason = 'auto') { if (settingsAutoSaveTimer) clearTimeout(settingsAutoSaveTimer); settingsAutoSaveTimer = setTimeout(() => saveSettings(false, reason), 800); } async function saveSettings(showAlert = true, reason = 'manual') { try { const workspaceLabel = window.WORKSPACE_LABEL || localStorage.getItem('workspace_label') || ''; const s = {}; const numeric = new Set(['tr_temp','tr_tok','tr_p','in_temp','in_tok','in_p','rep_penalty']); ['triage_instruction','inquiry_instruction','tr_temp','tr_tok','tr_p','in_temp','in_tok','in_p','mission_context','rep_penalty','user_mode','med_photo_model','med_photo_prompt'].forEach(k => { const el = document.getElementById(k); if (!el) return; const val = el.value; if (numeric.has(k)) { const num = val === '' ? '' : Number(val); s[k] = Number.isFinite(num) ? num : DEFAULT_SETTINGS[k]; } else { s[k] = val; } }); s.vaccine_types = normalizeVaccineTypes(vaccineTypeList); s.pharmacy_labels = normalizePharmacyLabels(pharmacyLabelList); console.log('[settings] saving', { reason, payload: s }); updateSettingsStatus('Saving…', false); const headers = { 'Content-Type': 'application/json' }; if (workspaceLabel) { headers['x-workspace'] = workspaceLabel; } const url = workspaceLabel ? `/api/data/settings?workspace=${encodeURIComponent(workspaceLabel)}` : '/api/data/settings'; const res = await fetch(url, { method:'POST', headers, body:JSON.stringify(s), credentials:'same-origin' }); if (!res.ok) { let detail = ''; try { const err = await res.json(); detail = err?.error ? `: ${err.error}` : ''; } catch (_) { /* ignore parse errors */ } throw new Error(`Save failed (${res.status})${detail}`); } const updated = await res.json(); console.log('[settings] save response', updated); // Preserve the locally selected user_mode to avoid flicker if the server echoes stale data const merged = { ...updated, user_mode: s.user_mode || updated.user_mode, vaccine_types: s.vaccine_types || updated.vaccine_types }; applySettingsToUI(merged); try { localStorage.setItem('user_mode', updated.user_mode || 'user'); } catch (err) { /* ignore */ } settingsDirty = false; updateSettingsStatus(`Saved at ${new Date().toLocaleTimeString()}`); if (showAlert) { alert("Configuration synchronized."); } if (typeof refreshPromptPreview === 'function') { const promptBox = document.getElementById('prompt-preview'); // Only refresh if the user has not manually edited the prompt box if (!promptBox || promptBox.dataset.autofilled !== 'false') { refreshPromptPreview(true); } } } catch (err) { if (showAlert) { alert(`Unable to save settings: ${err.message}`); } updateSettingsStatus(`Save error: ${err.message}`, true); console.error('[settings] save error', err); } } function resetSection(section) { if (section === 'triage') { document.getElementById('triage_instruction').value = DEFAULT_SETTINGS.triage_instruction; document.getElementById('tr_temp').value = DEFAULT_SETTINGS.tr_temp; document.getElementById('tr_tok').value = DEFAULT_SETTINGS.tr_tok; document.getElementById('tr_p').value = DEFAULT_SETTINGS.tr_p; } else if (section === 'inquiry') { document.getElementById('inquiry_instruction').value = DEFAULT_SETTINGS.inquiry_instruction; document.getElementById('in_temp').value = DEFAULT_SETTINGS.in_temp; document.getElementById('in_tok').value = DEFAULT_SETTINGS.in_tok; document.getElementById('in_p').value = DEFAULT_SETTINGS.in_p; } else if (section === 'mission') { document.getElementById('mission_context').value = DEFAULT_SETTINGS.mission_context; } else if (section === 'med_photo') { const el = document.getElementById('med_photo_prompt'); if (el) el.value = DEFAULT_SETTINGS.med_photo_prompt; } saveSettings(); } function normalizeVaccineTypes(list) { if (!Array.isArray(list)) return [...DEFAULT_SETTINGS.vaccine_types]; const seen = new Set(); return list .map((v) => (typeof v === 'string' ? v.trim() : '')) .filter((v) => !!v) .filter((v) => { const key = v.toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; }); } function renderVaccineTypes() { const container = document.getElementById('vaccine-types-list'); if (!container) return; if (!vaccineTypeList.length) { container.innerHTML = '
No vaccine types defined. Add at least one to enable the dropdown.
'; return; } container.innerHTML = vaccineTypeList .map((v, idx) => `
${idx + 1}.
${v}
`).join(''); } function addVaccineType() { const input = document.getElementById('vaccine-type-input'); if (!input) return; const val = input.value.trim(); if (!val) { alert('Enter a vaccine type before adding.'); return; } vaccineTypeList = normalizeVaccineTypes([...vaccineTypeList, val]); input.value = ''; renderVaccineTypes(); settingsDirty = true; scheduleAutoSave('vaccine-type-add'); } function removeVaccineType(idx) { if (idx < 0 || idx >= vaccineTypeList.length) return; vaccineTypeList.splice(idx, 1); vaccineTypeList = normalizeVaccineTypes(vaccineTypeList); renderVaccineTypes(); settingsDirty = true; scheduleAutoSave('vaccine-type-remove'); } function moveVaccineType(idx, delta) { const newIndex = idx + delta; if (newIndex < 0 || newIndex >= vaccineTypeList.length) return; const nextList = [...vaccineTypeList]; const [item] = nextList.splice(idx, 1); nextList.splice(newIndex, 0, item); vaccineTypeList = normalizeVaccineTypes(nextList); renderVaccineTypes(); settingsDirty = true; scheduleAutoSave('vaccine-type-reorder'); } function normalizePharmacyLabels(list) { if (!Array.isArray(list)) return [...DEFAULT_SETTINGS.pharmacy_labels]; const seen = new Set(); return list .map((v) => (typeof v === 'string' ? v.trim() : '')) .filter((v) => !!v) .filter((v) => { const key = v.toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; }); } function renderPharmacyLabels() { const container = document.getElementById('pharmacy-labels-list'); if (!container) return; if (!pharmacyLabelList.length) { container.innerHTML = '
No user labels defined. Add at least one to enable the dropdown.
'; return; } container.innerHTML = pharmacyLabelList .map((v, idx) => `
${idx + 1}.
${v}
`).join(''); window.CACHED_SETTINGS = window.CACHED_SETTINGS || {}; window.CACHED_SETTINGS.pharmacy_labels = [...pharmacyLabelList]; if (typeof window.refreshPharmacyLabelsFromSettings === 'function') { window.refreshPharmacyLabelsFromSettings(pharmacyLabelList); } } function addPharmacyLabel() { const input = document.getElementById('pharmacy-label-input'); if (!input) return; const val = input.value.trim(); if (!val) { alert('Enter a label before adding.'); return; } pharmacyLabelList = normalizePharmacyLabels([...pharmacyLabelList, val]); input.value = ''; renderPharmacyLabels(); settingsDirty = true; scheduleAutoSave('pharmacy-label-add'); } function removePharmacyLabel(idx) { if (idx < 0 || idx >= pharmacyLabelList.length) return; pharmacyLabelList.splice(idx, 1); pharmacyLabelList = normalizePharmacyLabels(pharmacyLabelList); renderPharmacyLabels(); settingsDirty = true; scheduleAutoSave('pharmacy-label-remove'); } function movePharmacyLabel(idx, delta) { const newIndex = idx + delta; if (newIndex < 0 || newIndex >= pharmacyLabelList.length) return; const nextList = [...pharmacyLabelList]; const [item] = nextList.splice(idx, 1); nextList.splice(newIndex, 0, item); pharmacyLabelList = normalizePharmacyLabels(nextList); renderPharmacyLabels(); settingsDirty = true; scheduleAutoSave('pharmacy-label-reorder'); } function renderOfflineStatus(msg, isError = false) { const box = document.getElementById('offline-status'); if (!box) return; box.style.color = isError ? 'var(--red)' : '#2c3e50'; box.innerHTML = msg; } async function createOfflineBackup() { renderOfflineStatus('Creating offline backup…'); try { const res = await fetch('/api/offline/backup', { method: 'POST', credentials: 'same-origin' }); const data = await res.json(); if (!res.ok || data.error) throw new Error(data.error || `Status ${res.status}`); renderOfflineStatus(`Backup created: ${data.backup}`); } catch (err) { renderOfflineStatus(`Backup failed: ${err.message}`, true); } } async function restoreOfflineBackup() { renderOfflineStatus('Restoring latest backup…'); try { const res = await fetch('/api/offline/restore', { method: 'POST', credentials: 'same-origin' }); const data = await res.json(); if (!res.ok || data.error) throw new Error(data.error || `Status ${res.status}`); renderOfflineStatus(`Restored backup: ${data.restored}`); alert('Cache restored. Please reload the app.'); } catch (err) { renderOfflineStatus(`Restore failed: ${err.message}`, true); } } function formatOfflineStatus(data) { const models = data.models || []; const missing = data.missing || models.filter((m) => !m.cached); const offline = data.offline_mode; const env = data.env || {}; const disk = data.disk || {}; const modelLines = models .map((m) => `${m.model}: ${m.cached ? 'Cached ✅' : 'Missing ❌'}${m.downloaded ? ' (downloaded)' : ''}${m.error ? ` (err: ${m.error})` : ''}`) .join('
'); const envLines = Object.entries(env) .map(([k, v]) => `${k}: ${v || 'unset'}`) .join('
'); let note = ''; if (missing.length) { note = `
Missing ${missing.length} model(s). Click “Download missing models” while online.
`; } else { note = `
All required models cached.
`; } if (offline) { note += `
Offline mode flags detected.
`; } else { note += `
Offline flags not set; set HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 before going offline.
`; } const diskLine = disk.total_gb ? `
Cache disk (${disk.path || ''}): ${disk.free_gb || '?'}GB free / ${disk.total_gb || '?'}GB total.
` : ''; const howTo = `
Steps: 1) While online, click “Download missing models”. 2) Then click “Backup cache”. 3) Before sailing, set offline env flags and rerun “Check cache status”.
Required models: medgemma-1.5-4b-it, medgemma-27b-text-it, Qwen2.5-VL-7B-Instruct.
`; return ( `Models
${modelLines || 'None'}

` + `Env
${envLines}

` + `Cache
${data.cache_dir || ''}` + note + diskLine + howTo ); } function updateOfflineFlagButton() { const btn = document.getElementById('offline-flag-btn'); if (!btn) return; const offline = offlineStatusCache ? !!offlineStatusCache.offline_mode : false; btn.textContent = offline ? 'Disable offline flags' : 'Enable offline flags'; btn.style.background = offline ? '#555' : '#b26a00'; } async function runOfflineCheck(downloadMissing = false) { renderOfflineStatus(downloadMissing ? 'Checking and downloading missing models…' : 'Checking offline requirements…'); try { const endpoint = downloadMissing ? '/api/offline/ensure' : '/api/offline/check'; const res = await fetch(endpoint, { credentials: 'same-origin', method: downloadMissing ? 'POST' : 'GET' }); const data = await res.json(); if (!res.ok || data.error) throw new Error(data.error || `Status ${res.status}`); offlineStatusCache = data; renderOfflineStatus(formatOfflineStatus(data)); updateOfflineFlagButton(); } catch (err) { renderOfflineStatus(`Error: ${err.message}. If missing models persist, stay online and click “Download missing models”.`, true); } } async function toggleOfflineFlags(forceEnable) { const desired = typeof forceEnable === 'boolean' ? forceEnable : !(offlineStatusCache ? !!offlineStatusCache.offline_mode : false); renderOfflineStatus(desired ? 'Enabling offline flags…' : 'Disabling offline flags…'); try { const res = await fetch('/api/offline/flags', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enable: desired }) }); const data = await res.json(); if (!res.ok || data.error) throw new Error(data.error || `Status ${res.status}`); offlineStatusCache = data; renderOfflineStatus(formatOfflineStatus(data)); updateOfflineFlagButton(); } catch (err) { renderOfflineStatus(`Unable to set offline flags: ${err.message}`, true); } } async function loadWorkspaceSwitcher() { const row = document.getElementById('workspace-switch-row'); if (!row) return; row.textContent = 'Loading workspaces…'; const status = document.getElementById('workspace-switch-status'); try { const res = await fetch('/api/workspaces', { credentials: 'same-origin' }); const data = await res.json(); if (!res.ok || data.error) throw new Error(data.error || `Status ${res.status}`); const names = (data.workspaces || []).slice().sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); const current = data.current || ''; if (!names.length) { row.innerHTML = '
No workspaces configured.
'; return; } const select = document.createElement('select'); select.id = 'workspace-select'; select.style.padding = '10px'; select.style.minWidth = '200px'; names.forEach(n => { const opt = document.createElement('option'); opt.value = n; opt.textContent = n; if (n === current) opt.selected = true; select.appendChild(opt); }); const pwd = document.createElement('input'); pwd.type = 'password'; pwd.id = 'workspace-switch-pwd'; pwd.placeholder = 'Workspace password'; pwd.style.padding = '10px'; pwd.style.minWidth = '180px'; pwd.value = 'Aphrodite'; const btn = document.createElement('button'); btn.className = 'btn btn-sm'; btn.style.background = 'var(--inquiry)'; btn.textContent = 'Switch workspace'; btn.onclick = switchWorkspaceFromSettings; row.innerHTML = ''; row.appendChild(select); row.appendChild(pwd); row.appendChild(btn); workspaceListLoaded = true; } catch (err) { row.innerHTML = `
Unable to load workspaces: ${err.message}
`; if (status) { status.textContent = 'Refresh the Settings tab to retry.'; status.style.color = 'var(--red)'; } console.error('[settings] workspace load error', err); } } async function switchWorkspaceFromSettings() { const select = document.getElementById('workspace-select'); const pwdInput = document.getElementById('workspace-switch-pwd'); const status = document.getElementById('workspace-switch-status'); if (!select) return; const chosen = select.value; const pwdVal = (pwdInput?.value || '').trim() || 'Aphrodite'; status.textContent = ''; if (status) status.style.color = '#555'; const btn = select.nextElementSibling; if (btn) btn.disabled = true; try { const res = await fetch('/workspace', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ workspace: chosen, password: pwdVal }) }); const data = await res.json().catch(() => ({})); if (!res.ok || data.error) { throw new Error(data.error || `Status ${res.status}`); } status.textContent = 'Switching workspace…'; window.location.href = '/login'; } catch (err) { status.textContent = `Unable to switch: ${err.message}`; status.style.color = 'var(--red)'; if (btn) btn.disabled = false; console.error('[settings] switch workspace error', err); } } async function exportDefaultDataset() { const status = document.getElementById('default-export-status'); if (status) { status.textContent = 'Exporting default dataset…'; status.style.color = '#2c3e50'; } try { const res = await fetch('/api/default/export', { method: 'POST', credentials: 'same-origin' }); const data = await res.json().catch(() => ({})); if (!res.ok || data.error) { throw new Error(data.error || `Status ${res.status}`); } if (status) { status.textContent = `Exported: ${Array.isArray(data.written) ? data.written.join(', ') : 'default data'}`; status.style.color = '#2e7d32'; } else { alert('Default dataset exported.'); } } catch (err) { if (status) { status.textContent = `Export failed: ${err.message}`; status.style.color = 'var(--red)'; } else { alert(`Export failed: ${err.message}`); } } } // Load crew credentials list async function loadCrewCredentials() { const container = document.getElementById('crew-credentials'); if (!container) return; container.innerHTML = 'Loading crew...'; try { const data = await (await fetch('/api/data/patients', {credentials:'same-origin'})).json(); if (!data || data.length === 0) { container.innerHTML = '
No crew members found.
'; return; } const hasCreds = data.some(p => p.username || p.password); if (!hasCreds) { container.innerHTML = '
No credentials assigned yet. To enable username/password login per crew, assign them below.
' + data.map(p => `
${p.firstName || ''} ${p.lastName || p.name || ''}
`).join(''); return; } container.innerHTML = data.map(p => `
${p.firstName || ''} ${p.lastName || p.name || ''}
`).join(''); } catch (err) { container.innerHTML = `
Error loading crew: ${err.message}
`; } } // Save a single crew credential async function saveCrewCredential(id) { const userEl = document.getElementById(`cred-user-${id}`); const passEl = document.getElementById(`cred-pass-${id}`); if (!userEl || !passEl) return; try { const data = await (await fetch('/api/data/patients', {credentials:'same-origin'})).json(); const patient = data.find(p => p.id === id); if (!patient) throw new Error('Crew member not found'); patient.username = userEl.value; patient.password = passEl.value; const res = await fetch('/api/data/patients', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data), credentials:'same-origin'}); if (!res.ok) throw new Error(res.statusText || 'Save failed'); alert('Saved credentials for crew member.'); } catch (err) { alert(`Failed to save credentials: ${err.message}`); } } // Expose functions to window for inline onclick handlers window.saveSettings = saveSettings; window.resetSection = resetSection; window.loadCrewCredentials = loadCrewCredentials; window.saveCrewCredential = saveCrewCredential; window.setUserMode = setUserMode; window.runOfflineCheck = runOfflineCheck; window.createOfflineBackup = createOfflineBackup; window.restoreOfflineBackup = restoreOfflineBackup; window.addVaccineType = addVaccineType; window.removeVaccineType = removeVaccineType; window.moveVaccineType = moveVaccineType; window.addPharmacyLabel = addPharmacyLabel; window.removePharmacyLabel = removePharmacyLabel; window.movePharmacyLabel = movePharmacyLabel;