From b8b29b95e21939c2e05bc6327f1215a8c4a760fa Mon Sep 17 00:00:00 2001 From: AuthorTom <63931206+authorTom@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:23:53 +0100 Subject: [PATCH] chore: remove dead code and deduplicate helpers - Drop CSS for the demo-credentials login panel removed in the production-hardening work - Hoist the scenario wizard tab order into a single EDITOR_TABS constant (was declared four times, twice within one function) - Extract csvQuote helper shared by the two CSV exports - Extract eachProgramme helper for the four programme-directory scans in server.js (also gives the auth helpers consistent skip-and-log handling of unreadable files via readJsonSafe) - Remove unused runState.startTime and debriefState.timerRunning - Ignore .claude/ local assistant settings; declare the Node engine range package.json already advertises in the README badge No behaviour change. QA suite passes (37/37). Co-Authored-By: Claude Fable 5 --- .gitignore | 3 ++- package.json | 3 +++ public/css/style.css | 36 ----------------------------- public/js/components.js | 46 +++++++++++++++++-------------------- server.js | 51 +++++++++++++++++++---------------------- 5 files changed, 49 insertions(+), 90 deletions(-) diff --git a/.gitignore b/.gitignore index 29acd94..c4d411a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,9 +21,10 @@ lerna-debug.log* .env.production.local *.env -# IDE / Editor folders +# IDE / Editor / assistant folders .vscode/ .idea/ +.claude/ *.suo *.ntvs* *.njsproj diff --git a/package.json b/package.json index 7efbbb6..05601bb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "A beautiful, premium Clinical Simulation Scenario Storage & running system designed to align with ASPiH standards.", "main": "server.js", + "engines": { + "node": ">=16.0.0" + }, "scripts": { "start": "node server.js", "seed": "node seed.js", diff --git a/public/css/style.css b/public/css/style.css index 882577c..11826d6 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1289,22 +1289,6 @@ main { animation: modalIn 0.4s cubic-bezier(0.21, 1.02, 0.73, 1); } -/* Demo accounts behind a quiet disclosure */ -.login-credentials-helper summary { - cursor: pointer; - color: var(--text-muted); - font-size: 0.8rem; - user-select: none; - list-style: none; - display: flex; - align-items: center; - gap: 6px; - transition: color var(--transition-fast); -} -.login-credentials-helper summary::-webkit-details-marker { display: none; } -.login-credentials-helper summary:hover { color: var(--text-secondary); } -.login-credentials-helper[open] summary { margin-bottom: 10px; } - .login-logo { display: flex; flex-direction: column; @@ -1325,26 +1309,6 @@ main { font-size: 1.8rem; } -.login-credentials-helper { - margin-top: 24px; - padding: 16px; - background: rgba(255, 255, 255, 0.02); - border-radius: var(--border-radius-sm); - border: 1px solid var(--glass-border); - font-size: 0.8rem; -} - -.login-credentials-helper h5 { - margin-bottom: 8px; - color: var(--text-secondary); -} - -.login-credentials-helper p { - color: var(--text-muted); - font-family: var(--font-mono); - margin-bottom: 4px; -} - /* Toast notifications: stacked, animated, auto-dismiss progress bar */ #toast-container { position: fixed; diff --git a/public/js/components.js b/public/js/components.js index b2ad9f2..67cab67 100644 --- a/public/js/components.js +++ b/public/js/components.js @@ -34,12 +34,14 @@ const components = { allScenarios: [], allProgrammes: [], currentFormTab: 'tab-general', - + + // Ordered steps of the scenario editor wizard. + EDITOR_TABS: ['tab-general', 'tab-outcomes', 'tab-patient', 'tab-setup', 'tab-progression', 'tab-debrief', 'tab-aspih'], + // Interactive Run State runState: { scenario: null, activePhaseIndex: 0, - startTime: null, elapsedSeconds: 0, timerInterval: null, timerRunning: false, @@ -51,8 +53,7 @@ const components = { scenario: null, currentStep: 'reactions', // 'reactions', 'description', 'analysis', 'summary' elapsedSeconds: 0, - timerInterval: null, - timerRunning: false + timerInterval: null }, // Initialize and load data @@ -1163,8 +1164,7 @@ const components = { this.currentFormTab = tabId; // Stepper states: current step is active, earlier steps show completed - const stepList = ['tab-general', 'tab-outcomes', 'tab-patient', 'tab-setup', 'tab-progression', 'tab-debrief', 'tab-aspih']; - const currentIdx = stepList.indexOf(tabId); + const currentIdx = this.EDITOR_TABS.indexOf(tabId); const tabs = document.querySelectorAll('#editor-tabs-container .editor-tab'); tabs.forEach((tab, i) => { tab.classList.toggle('active', i === currentIdx); @@ -1186,17 +1186,14 @@ const components = { // Update wizard button displays const prevBtn = document.getElementById('btn-prev-tab'); const nextBtn = document.getElementById('btn-next-tab'); - - const tabList = ['tab-general', 'tab-outcomes', 'tab-patient', 'tab-setup', 'tab-progression', 'tab-debrief', 'tab-aspih']; - const idx = tabList.indexOf(tabId); - if (idx === 0) { + if (currentIdx === 0) { prevBtn.style.visibility = 'hidden'; } else { prevBtn.style.visibility = 'visible'; } - if (idx === tabList.length - 1) { + if (currentIdx === this.EDITOR_TABS.length - 1) { nextBtn.innerHTML = ' Save Scenario'; nextBtn.onclick = () => this.saveScenario(); } else { @@ -1206,18 +1203,16 @@ const components = { }, nextEditorTab() { - const tabList = ['tab-general', 'tab-outcomes', 'tab-patient', 'tab-setup', 'tab-progression', 'tab-debrief', 'tab-aspih']; - const idx = tabList.indexOf(this.currentFormTab); - if (idx < tabList.length - 1) { - this.switchEditorTab(tabList[idx + 1]); + const idx = this.EDITOR_TABS.indexOf(this.currentFormTab); + if (idx < this.EDITOR_TABS.length - 1) { + this.switchEditorTab(this.EDITOR_TABS[idx + 1]); } }, prevEditorTab() { - const tabList = ['tab-general', 'tab-outcomes', 'tab-patient', 'tab-setup', 'tab-progression', 'tab-debrief', 'tab-aspih']; - const idx = tabList.indexOf(this.currentFormTab); + const idx = this.EDITOR_TABS.indexOf(this.currentFormTab); if (idx > 0) { - this.switchEditorTab(tabList[idx - 1]); + this.switchEditorTab(this.EDITOR_TABS[idx - 1]); } }, @@ -2926,8 +2921,7 @@ const components = { this.debriefState.scenario = s; this.debriefState.currentStep = 'reactions'; this.debriefState.elapsedSeconds = 0; - this.debriefState.timerRunning = false; - + if (this.debriefState.timerInterval) clearInterval(this.debriefState.timerInterval); const container = document.getElementById('debrief-content'); @@ -3118,7 +3112,6 @@ const components = { }, startDebriefTimer() { - this.debriefState.timerRunning = true; this.debriefState.timerInterval = setInterval(() => { this.debriefState.elapsedSeconds++; this.updateDebriefTimerDisplay(); @@ -3407,6 +3400,11 @@ const components = { app.showToast(`${s.email}: ${s.reason}`, 'error')); }, + // Escape a value for a CSV cell (RFC 4180 double-quote style). + csvQuote(v) { + return `"${String(v).replace(/"/g, '""')}"`; + }, + // Download the account list (no credentials) as a CSV file exportUsersCsv() { const users = this.adminUsersState.users; @@ -3414,14 +3412,13 @@ const components = { app.showToast('No users loaded to export.', 'error'); return; } - const quote = v => `"${String(v).replace(/"/g, '""')}"`; const csv = ['email,name,role,status,created,last_login', ...users.map(u => [ u.email, u.name, u.role, u.disabled ? 'Disabled' : 'Active', u.createdAt || '', u.lastLogin || 'Never' - ].map(quote).join(','))].join('\r\n'); + ].map(v => this.csvQuote(v)).join(','))].join('\r\n'); this.downloadFile(csv, `simhub_users_${new Date().toISOString().slice(0, 10)}.csv`, 'text/csv'); app.showToast('User list exported.', 'success'); }, @@ -3552,9 +3549,8 @@ const components = { const dl = document.getElementById('bulk-creds-download'); if (dl) { dl.addEventListener('click', () => { - const quote = v => `"${String(v).replace(/"/g, '""')}"`; const csv = ['email,name,role,temporary_password', - ...created.map(c => [c.email, c.name, c.role, c.tempPassword || ''].map(quote).join(','))].join('\r\n'); + ...created.map(c => [c.email, c.name, c.role, c.tempPassword || ''].map(v => this.csvQuote(v)).join(','))].join('\r\n'); this.downloadFile(csv, `simhub_new_accounts_${new Date().toISOString().slice(0, 10)}.csv`, 'text/csv'); }); } diff --git a/server.js b/server.js index 2096b3f..39cd7dd 100644 --- a/server.js +++ b/server.js @@ -389,17 +389,23 @@ function editorProgrammeIds(user) { return Array.isArray(user && user.programmeIds) ? user.programmeIds : []; } -// Ids of every programme on disk (used to validate allocations). -function existingProgrammeIds() { - const set = new Set(); +// Iterate every readable programme file; unreadable files are skipped +// (readJsonSafe logs them). The file path is passed for callers that need +// to write the programme back. +function eachProgramme(cb) { fs.readdirSync(PROGRAMMES_DIR) .filter(f => f.endsWith('.json')) .forEach(f => { - try { - const p = JSON.parse(fs.readFileSync(path.join(PROGRAMMES_DIR, f), 'utf8')); - if (p && p.id) set.add(p.id); - } catch { /* ignore unreadable programme files */ } + const filePath = path.join(PROGRAMMES_DIR, f); + const prog = readJsonSafe(filePath); + if (prog) cb(prog, filePath); }); +} + +// Ids of every programme on disk (used to validate allocations). +function existingProgrammeIds() { + const set = new Set(); + eachProgramme(p => { if (p.id) set.add(p.id); }); return set; } @@ -418,14 +424,9 @@ function sanitizeProgrammeIds(input) { // Programme ids whose scenarioIds include the given scenario. function programmeIdsContainingScenario(scenarioId) { const ids = new Set(); - fs.readdirSync(PROGRAMMES_DIR) - .filter(f => f.endsWith('.json')) - .forEach(f => { - try { - const p = JSON.parse(fs.readFileSync(path.join(PROGRAMMES_DIR, f), 'utf8')); - if (Array.isArray(p.scenarioIds) && p.scenarioIds.includes(scenarioId)) ids.add(p.id); - } catch { /* ignore */ } - }); + eachProgramme(p => { + if (Array.isArray(p.scenarioIds) && p.scenarioIds.includes(scenarioId)) ids.add(p.id); + }); return ids; } @@ -738,14 +739,11 @@ app.delete('/api/scenarios/:id', authenticate, requireScenarioAccess, (req, res) writeJsonAtomic(binPath, data); fs.unlinkSync(filePath); - // Also remove scenario from any programmes. Per-file failures are logged - // and skipped: the scenario has already moved to the recycle bin, so one - // unreadable programme file must not turn a completed delete into a 500. - const progFiles = fs.readdirSync(PROGRAMMES_DIR); - progFiles.filter(file => file.endsWith('.json')).forEach(file => { - const pPath = path.join(PROGRAMMES_DIR, file); - const prog = readJsonSafe(pPath); - if (prog && prog.scenarioIds && prog.scenarioIds.includes(scenarioId)) { + // Also remove scenario from any programmes. Unreadable programme files + // are logged and skipped: the scenario has already moved to the recycle + // bin, so one bad file must not turn a completed delete into a 500. + eachProgramme((prog, pPath) => { + if (prog.scenarioIds && prog.scenarioIds.includes(scenarioId)) { prog.scenarioIds = prog.scenarioIds.filter(id => id !== scenarioId); writeJsonAtomic(pPath, prog); } @@ -763,11 +761,8 @@ app.delete('/api/scenarios/:id', authenticate, requireScenarioAccess, (req, res) // List programmes app.get('/api/programmes', authenticate, (req, res) => { try { - const files = fs.readdirSync(PROGRAMMES_DIR); - const programmes = files - .filter(file => file.endsWith('.json')) - .map(file => readJsonSafe(path.join(PROGRAMMES_DIR, file))) - .filter(Boolean); + const programmes = []; + eachProgramme(p => programmes.push(p)); res.json(programmes); } catch (err) { res.status(500).json({ error: 'Failed to retrieve programmes: ' + err.message });