Rick
Adjust crew health layout and user label handling
1da89be
// 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 = '<div style="color:#666; font-size:12px;">No vaccine types defined. Add at least one to enable the dropdown.</div>';
return;
}
container.innerHTML = vaccineTypeList
.map((v, idx) => `
<div style="display:flex; align-items:center; gap:8px; padding:6px 0; border-bottom:1px solid #eee;">
<div style="width:28px; text-align:right; font-weight:700; color:#666;">${idx + 1}.</div>
<div style="flex:1; font-weight:600;">${v}</div>
<div style="display:flex; gap:6px; align-items:center;">
<button class="btn btn-sm" style="background:#607d8b;" ${idx === 0 ? 'disabled' : ''} onclick="moveVaccineType(${idx}, -1)">↑</button>
<button class="btn btn-sm" style="background:#607d8b;" ${idx === vaccineTypeList.length - 1 ? 'disabled' : ''} onclick="moveVaccineType(${idx}, 1)">↓</button>
<button class="btn btn-sm" style="background:var(--red);" onclick="removeVaccineType(${idx})">🗑 Remove</button>
</div>
</div>
`).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 = '<div style="color:#666; font-size:12px;">No user labels defined. Add at least one to enable the dropdown.</div>';
return;
}
container.innerHTML = pharmacyLabelList
.map((v, idx) => `
<div style="display:flex; align-items:center; gap:8px; padding:6px 0; border-bottom:1px solid #eee;">
<div style="width:28px; text-align:right; font-weight:700; color:#666;">${idx + 1}.</div>
<div style="flex:1; font-weight:600;">${v}</div>
<div style="display:flex; gap:6px; align-items:center;">
<button class="btn btn-sm" style="background:#607d8b;" ${idx === 0 ? 'disabled' : ''} onclick="movePharmacyLabel(${idx}, -1)">↑</button>
<button class="btn btn-sm" style="background:#607d8b;" ${idx === pharmacyLabelList.length - 1 ? 'disabled' : ''} onclick="movePharmacyLabel(${idx}, 1)">↓</button>
<button class="btn btn-sm" style="background:var(--red);" onclick="removePharmacyLabel(${idx})">🗑 Remove</button>
</div>
</div>
`).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('<br>');
const envLines = Object.entries(env)
.map(([k, v]) => `${k}: ${v || 'unset'}`)
.join('<br>');
let note = '';
if (missing.length) {
note = `<div style="color:var(--red); margin-top:6px;">Missing ${missing.length} model(s). Click “Download missing models” while online.</div>`;
} else {
note = `<div style="color:var(--green); margin-top:6px;">All required models cached.</div>`;
}
if (offline) {
note += `<div style="margin-top:6px;">Offline mode flags detected.</div>`;
} else {
note += `<div style="margin-top:6px; color:#b26a00;">Offline flags not set; set HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 before going offline.</div>`;
}
const diskLine = disk.total_gb ? `<div style="margin-top:6px; font-size:12px;">Cache disk (${disk.path || ''}): ${disk.free_gb || '?'}GB free / ${disk.total_gb || '?'}GB total.</div>` : '';
const howTo = `<div style="margin-top:8px; font-size:12px; color:#333;">
<strong>Steps:</strong> 1) While online, click “Download missing models”. 2) Then click “Backup cache”. 3) Before sailing, set offline env flags and rerun “Check cache status”.<br>
<strong>Required models:</strong> medgemma-1.5-4b-it, medgemma-27b-text-it, Qwen2.5-VL-7B-Instruct.
</div>`;
return (
`<strong>Models</strong><br>${modelLines || 'None'}<br><br>` +
`<strong>Env</strong><br>${envLines}<br><br>` +
`<strong>Cache</strong><br>${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 = '<div style="color:#666;">No workspaces configured.</div>';
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 = `<div style="color:var(--red);">Unable to load workspaces: ${err.message}</div>`;
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 = '<div style="color:#666;">No crew members found.</div>';
return;
}
const hasCreds = data.some(p => p.username || p.password);
if (!hasCreds) {
container.innerHTML = '<div style="color:#666;">No credentials assigned yet. To enable username/password login per crew, assign them below.</div>' +
data.map(p => `
<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px; flex-wrap:wrap;">
<div style="min-width:180px; font-weight:bold;">${p.firstName || ''} ${p.lastName || p.name || ''}</div>
<input type="text" id="cred-user-${p.id}" value="${p.username || ''}" placeholder="Username" style="padding:8px; flex:1; min-width:140px;">
<input type="text" id="cred-pass-${p.id}" value="${p.password || ''}" placeholder="Password" style="padding:8px; flex:1; min-width:140px;">
<button class="btn btn-sm" style="background:var(--inquiry);" onclick="saveCrewCredential('${p.id}')">Save</button>
</div>
`).join('');
return;
}
container.innerHTML = data.map(p => `
<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px; flex-wrap:wrap;">
<div style="min-width:180px; font-weight:bold;">${p.firstName || ''} ${p.lastName || p.name || ''}</div>
<input type="text" id="cred-user-${p.id}" value="${p.username || ''}" placeholder="Username" style="padding:8px; flex:1; min-width:140px;">
<input type="text" id="cred-pass-${p.id}" value="${p.password || ''}" placeholder="Password" style="padding:8px; flex:1; min-width:140px;">
<button class="btn btn-sm" style="background:var(--inquiry);" onclick="saveCrewCredential('${p.id}')">Save</button>
</div>
`).join('');
} catch (err) {
container.innerHTML = `<div style="color:red;">Error loading crew: ${err.message}</div>`;
}
}
// 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;