import { apiPost } from '../api.js'; import { getRole, pageHeaderHTML, showToast } from '../app.js'; import { navigate } from '../router.js'; export function devPage() { if (getRole() !== 'admin') { navigate('#/'); return; } const app = document.getElementById('app'); app.innerHTML = ` ${pageHeaderHTML('Admin Settings')}

Export all response data (ZIP)

Download one CSV per questionnaire in a single ZIP (full server data). Current data uses live answers; All versions includes every upload (one row per submission).

Dev import

Local / test only. Imports prefixed dev users (dev_*), clients (DEV-CL-*), and completed questionnaire runs. Existing real users are not modified. Conflicts are skipped (usernames, client codes, completions). Default password: socialvrlab. Questionnaires must already exist in the database.

Use dev-test-fixture.json from the repo root (generate via nat-as-server/scripts/generate_dev_fixture.py; uneven coach load, multi-version uploads, natural timestamps).

Reset database

Destructive. Deletes every client, completion, coach, and supervisor (including non-dev accounts). Admin logins and questionnaire definitions are kept.

`; let parsedFixture = null; document.getElementById('devFixtureFile').addEventListener('change', async (e) => { const file = e.target.files?.[0]; const summary = document.getElementById('devFixtureSummary'); const btn = document.getElementById('devImportBtn'); parsedFixture = null; btn.disabled = true; summary.style.display = 'none'; if (!file) return; try { const text = await file.text(); parsedFixture = JSON.parse(text); const st = parsedFixture.stats || {}; summary.innerHTML = ` Preview: ${parsedFixture.admins?.length ?? st.admins ?? 0} admins, ${parsedFixture.supervisors?.length ?? st.supervisors ?? 0} supervisors, ${parsedFixture.coaches?.length ?? st.coaches ?? 0} coaches, ${parsedFixture.clients?.length ?? st.clients ?? 0} clients, ${parsedFixture.completions?.length ?? st.completionRecords ?? st.completions ?? 0} completions, ${st.totalUploads ?? '—'} uploads (${st.multiVersionPairs ?? '—'} re-uploaded) `; summary.style.display = ''; btn.disabled = false; } catch (err) { showToast('Invalid JSON: ' + err.message, 'error'); } }); document.getElementById('devRemoveBtn').addEventListener('click', async () => { if (!confirm( 'Remove all dev test data?\n\n' + 'Deletes users matching dev_*, clients DEV-CL-*, and their answers/completions.\n' + 'Real accounts are not affected.' )) { return; } const btn = document.getElementById('devRemoveBtn'); const pre = document.getElementById('devImportResult'); btn.disabled = true; btn.textContent = 'Removing…'; pre.style.display = 'none'; try { const res = await apiPost('dev', { action: 'remove', usernamePrefix: 'dev_', clientCodePrefix: 'DEV-CL-', }); if (!res.success) { showToast(res.error || res.message || 'Remove failed', 'error'); return; } pre.textContent = JSON.stringify(res, null, 2); pre.style.display = ''; const d = res.deleted || {}; showToast( `Removed ${d.clients ?? 0} clients, ${d.users ?? 0} users, ` + `${d.completed_questionnaires ?? 0} completions`, 'success' ); } catch (e) { showToast(e.message, 'error'); } finally { btn.disabled = false; btn.textContent = 'Remove test data'; } }); document.getElementById('devWipeBtn').addEventListener('click', async () => { if (!confirm( 'Remove ALL clients, completions, coaches, and supervisors?\n\n' + 'Admin accounts and questionnaires will NOT be deleted.\n' + 'This cannot be undone.' )) { return; } const btn = document.getElementById('devWipeBtn'); const pre = document.getElementById('devImportResult'); btn.disabled = true; btn.textContent = 'Removing…'; pre.style.display = 'none'; try { const res = await apiPost('dev', { action: 'wipeExceptAdmins' }); if (!res.success) { showToast(res.error || res.message || 'Wipe failed', 'error'); return; } pre.textContent = JSON.stringify(res, null, 2); pre.style.display = ''; const d = res.deleted || {}; const kept = res.kept?.admins ?? '?'; showToast( `Wiped ${d.clients ?? 0} clients, ${d.users ?? 0} users, ` + `${d.completed_questionnaires ?? 0} completions (${kept} admins kept)`, 'success' ); } catch (e) { showToast(e.message, 'error'); } finally { btn.disabled = false; btn.textContent = 'Remove all data (keep admins)'; } }); wireAdminZipExport(); document.getElementById('devImportBtn').addEventListener('click', async () => { if (!parsedFixture) return; const btn = document.getElementById('devImportBtn'); const pre = document.getElementById('devImportResult'); btn.disabled = true; btn.textContent = 'Importing…'; pre.style.display = 'none'; try { const res = await apiPost('dev', { fixture: parsedFixture }); if (!res.success) { showToast(res.error || 'Import failed', 'error'); return; } pre.textContent = JSON.stringify(res, null, 2); pre.style.display = ''; const imp = res.imported || {}; const sk = res.skipped || {}; showToast( `Imported: ${imp.completions ?? 0} completions, ${imp.submissions ?? 0} uploads, ` + `${imp.clients ?? 0} clients, ${imp.coaches ?? 0} coaches ` + `(skipped ${sk.completions ?? 0} existing)`, 'success' ); if (res.errors?.length) { showToast(`${res.errors.length} warning(s) — see log below`, 'error'); } } catch (e) { showToast(e.message, 'error'); } finally { btn.disabled = false; btn.textContent = 'Import fixture'; } }); } function filenameFromContentDisposition(res, fallback) { const disp = res.headers.get('Content-Disposition'); if (!disp) return fallback; const m = /filename="([^"]+)"/i.exec(disp); return m ? m[1] : fallback; } async function downloadExportZip(url, defaultFilename) { const token = localStorage.getItem('qdb_token'); const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { const err = await res.json().catch(() => ({ error: 'Download failed' })); throw new Error(err.error?.message || err.error || 'Download failed'); } const blob = await res.blob(); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = filenameFromContentDisposition(res, defaultFilename); a.click(); URL.revokeObjectURL(blobUrl); } function wireAdminZipExport() { document.getElementById('exportAllCurrentZipBtn')?.addEventListener('click', async () => { const btn = document.getElementById('exportAllCurrentZipBtn'); btn.disabled = true; const prev = btn.textContent; btn.textContent = 'Preparing ZIP…'; try { await downloadExportZip('../api/export?exportAll=1', 'responses_export_current.zip'); showToast('ZIP download started (current data)', 'success'); } catch (e) { showToast(e.message, 'error'); } finally { btn.disabled = false; btn.textContent = prev; } }); document.getElementById('exportAllVersionsZipBtn')?.addEventListener('click', async () => { const btn = document.getElementById('exportAllVersionsZipBtn'); btn.disabled = true; const prev = btn.textContent; btn.textContent = 'Preparing ZIP…'; try { await downloadExportZip('../api/export?exportAll=1&allVersions=1', 'responses_export_all_versions.zip'); showToast('ZIP download started (all versions)', 'success'); } catch (e) { showToast(e.message, 'error'); } finally { btn.disabled = false; btn.textContent = prev; } }); }