import { api } from '../api.js'; import { showToast } from '../components/toast.js'; import { getLanguage, setLanguage, getAvailableLanguages, t, tn } from '../i18n.js'; import { esc, isPlatformAdmin } from '../utils.js'; import { resetBranding } from '../branding.js'; export async function render(container) { const serverUrl = `${window.location.protocol}//${window.location.host}`; // Fetch fresh user from the server — plan_id and role may have been changed // by an admin since login. Fall back to localStorage if the request fails. let user; try { user = await api.getMe(); localStorage.setItem('user', JSON.stringify(user)); } catch { user = JSON.parse(localStorage.getItem('user') || '{}'); } const isSuperAdmin = isPlatformAdmin(user); const isAdmin = user.role === 'admin' || isSuperAdmin; container.innerHTML = `

${t('settings.account')}

${user.auth_provider === 'local' ? `

${t('settings.change_password')}

${t('settings.password_min_8')}

` : `

${t('settings.sso_note', { provider: esc(user.auth_provider || 'SSO') })}

`}
${isAdmin ? `

${t('settings.license')}

${t('settings.license_mit')}

${isSuperAdmin ? `

${t('settings.platform_admin_link')} ${t('nav.admin')} ${t('settings.platform_admin_page_suffix')}

` : ''}

${t('settings.user_management')}

${t('settings.loading_users')}

${t('settings.white_label')}

${t('settings.white_label_desc')}

` : ''}

${t('settings.server_info')}

${t('settings.server_url')}
${serverUrl}

${t('settings.server_url_hint')}

${t('settings.api_endpoint')}
${serverUrl}/api

${t('settings.setup_guide')}

  1. ${t('settings.setup_step_1')}
  2. ${t('settings.setup_step_2_prefix')} ${serverUrl}
  3. ${t('settings.setup_step_3')}
  4. ${t('settings.setup_step_4')}
  5. ${t('settings.setup_step_5')}
  6. ${t('settings.setup_step_6')}

${t('settings.your_data')}

${t('settings.your_data_desc')}

${t('settings.language')}

${t('settings.about')}

ScreenTinker v1.4.1

${t('settings.about_tagline')}

${t('auth.terms')}  ·  ${t('auth.privacy')}  ·  ${t('settings.third_party_licenses')}

`; if (isAdmin) { loadUsers(); loadWhiteLabel(); // Support token generator document.getElementById('generateSupportBtn')?.addEventListener('click', async () => { const org = document.getElementById('supportOrg').value.trim() || 'Customer'; const hours = parseInt(document.getElementById('supportHours').value) || 4; try { const token = localStorage.getItem('token'); const res = await fetch('/api/auth/support/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ org, hours, reason: 'Support session' }) }); const data = await res.json(); if (res.ok) { document.getElementById('supportTokenOutput').value = data.token; document.getElementById('supportTokenResult').style.display = 'block'; showToast(t('settings.toast.support_token_generated', { hours }), 'success'); } else showToast(data.error, 'error'); } catch (err) { showToast(err.message, 'error'); } }); } // Export data handler document.getElementById('exportDataBtn')?.addEventListener('click', () => { const includeFiles = document.getElementById('exportIncludeFiles')?.checked; const token = localStorage.getItem('token'); const url = `/api/status/export?token=${token}${includeFiles ? '&include_files=true' : ''}`; window.location.href = url; }); // Import data handler document.getElementById('importDataBtn')?.addEventListener('click', () => { document.getElementById('importFileInput').click(); }); document.getElementById('importFileInput')?.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const isZip = file.name.endsWith('.zip') || file.type === 'application/zip'; const statusEl = document.getElementById('importStatus'); statusEl.style.display = 'block'; statusEl.style.background = 'var(--bg-secondary)'; statusEl.style.border = '1px solid var(--border)'; statusEl.style.color = 'var(--text-secondary)'; statusEl.textContent = t('settings.import.reading_file'); try { let data; if (isZip) { // For ZIP, show basic info and skip preview parsing data = { format: 'screentinker-export-v1', _isZip: true }; statusEl.innerHTML = `${t('settings.import.zip_detected', { name: esc(file.name), size: (file.size / 1048576).toFixed(1) })}

`; } else { const text = await file.text(); data = JSON.parse(text); if (!data.format || !data.format.startsWith('screentinker-export')) { statusEl.style.color = 'var(--danger)'; statusEl.textContent = t('settings.import.invalid_file'); return; } const summary = [ data.devices?.length ? t('settings.import.summary_devices', { n: data.devices.length }) : null, data.content?.length ? t('settings.import.summary_content', { n: data.content.length }) : null, data.widgets?.length ? t('settings.import.summary_widgets', { n: data.widgets.length }) : null, data.layouts?.length ? t('settings.import.summary_layouts', { n: data.layouts.length }) : null, data.schedules?.length ? t('settings.import.summary_schedules', { n: data.schedules.length }) : null, data.video_walls?.length ? t('settings.import.summary_walls', { n: data.video_walls.length }) : null, data.kiosk_pages?.length ? t('settings.import.summary_kiosk', { n: data.kiosk_pages.length }) : null, ].filter(Boolean).join(', '); statusEl.innerHTML = `${t('settings.import.found_summary', { summary: esc(summary) || t('settings.import.empty_export'), email: esc(data.user?.email) || t('common.unknown'), date: esc(data.exported_at?.split('T')[0]) || t('common.unknown') })}

`; } document.getElementById('cancelImportBtn').onclick = () => { statusEl.style.display = 'none'; e.target.value = ''; }; document.getElementById('confirmImportBtn').onclick = async () => { statusEl.innerHTML = isZip ? t('settings.import.uploading_zip') : t('settings.import.importing'); try { const token = localStorage.getItem('token'); let res; if (isZip) { const formData = new FormData(); formData.append('file', file); res = await fetch('/api/status/import', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData, }); } else { res = await fetch('/api/status/import', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify(data), }); } const result = await res.json(); if (res.ok) { const imported = Object.entries(result.stats).filter(([k,v]) => v > 0 && k !== 'files_restored').map(([k,v]) => `${v} ${k}`).join(', '); statusEl.style.color = 'var(--success)'; let html = t('settings.import.complete', { imported }); if (result.device_pairings?.length) { html += `

${t('settings.import.pairing_codes_title')}
` + result.device_pairings.map(d => ``).join('') + `
${d.name}${d.pairing_code}

${t('settings.import.pairing_codes_hint')}`; } html += `

${(result.notes || []).map(n => '• ' + n).join('
')}`; statusEl.innerHTML = html; showToast(t('settings.toast.import_success'), 'success'); } else { statusEl.style.color = 'var(--danger)'; statusEl.textContent = result.error || t('settings.import.failed'); } } catch (err) { statusEl.style.color = 'var(--danger)'; statusEl.textContent = t('settings.import.failed_with_error', { error: err.message }); } e.target.value = ''; }; } catch (err) { statusEl.style.color = 'var(--danger)'; statusEl.textContent = t('settings.import.read_failed', { error: err.message }); } }); document.getElementById('langSelect')?.addEventListener('change', (e) => { // setLanguage dispatches hashchange so the router re-renders the current // view (including this settings page) with new strings — no refresh needed. setLanguage(e.target.value); }); document.getElementById('saveAcctBtn')?.addEventListener('click', async () => { const name = document.getElementById('acctName').value.trim(); if (!name) return showToast(t('settings.toast.name_required'), 'error'); const email_alerts = !!document.getElementById('acctEmailAlerts')?.checked; const btn = document.getElementById('saveAcctBtn'); btn.disabled = true; try { const updated = await api.updateMe({ name, email_alerts }); const stored = JSON.parse(localStorage.getItem('user') || '{}'); localStorage.setItem('user', JSON.stringify({ ...stored, ...updated })); showToast(t('settings.toast.profile_saved'), 'success'); } catch (err) { showToast(err.message, 'error'); } finally { btn.disabled = false; } }); document.getElementById('changePwBtn')?.addEventListener('click', async () => { const current = document.getElementById('acctCurrentPw').value; const next = document.getElementById('acctNewPw').value; const confirm = document.getElementById('acctConfirmPw').value; if (!current) return showToast(t('settings.toast.current_password_required'), 'error'); if (next.length < 8) return showToast(t('settings.toast.new_password_min_8'), 'error'); if (next !== confirm) return showToast(t('settings.toast.passwords_dont_match'), 'error'); const btn = document.getElementById('changePwBtn'); btn.disabled = true; try { await api.updateMe({ current_password: current, password: next }); document.getElementById('acctCurrentPw').value = ''; document.getElementById('acctNewPw').value = ''; document.getElementById('acctConfirmPw').value = ''; showToast(t('settings.toast.password_changed'), 'success'); } catch (err) { showToast(err.message, 'error'); } finally { btn.disabled = false; } }); } async function loadWhiteLabel() { const token = localStorage.getItem('token'); const headers = { Authorization: `Bearer ${token}` }; // Only show white-label for enterprise plans or platform admins. // Use the fresh user cached by render() above, which called api.getMe(). const user = JSON.parse(localStorage.getItem('user') || '{}'); const section = document.getElementById('whiteLabelSection'); if (section && user.plan_id !== 'enterprise' && !isPlatformAdmin(user)) { section.innerHTML = `

${t('settings.white_label')}

${t('settings.white_label_enterprise_only')}

${t('settings.view_plans')}
`; return; } try { const res = await fetch('/api/white-label', { headers }); const wl = await res.json(); if (wl.brand_name) document.getElementById('wlBrandName').value = wl.brand_name; if (wl.logo_url) document.getElementById('wlLogoUrl').value = wl.logo_url; if (wl.primary_color) document.getElementById('wlPrimaryColor').value = wl.primary_color; if (wl.bg_color) document.getElementById('wlBgColor').value = wl.bg_color; if (wl.custom_domain) document.getElementById('wlDomain').value = wl.custom_domain; if (wl.favicon_url) document.getElementById('wlFavicon').value = wl.favicon_url; if (wl.custom_css) document.getElementById('wlCustomCss').value = wl.custom_css; if (wl.hide_branding) document.getElementById('wlHideBranding').checked = true; } catch {} document.getElementById('saveWhiteLabelBtn')?.addEventListener('click', async () => { try { await fetch('/api/white-label', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ brand_name: document.getElementById('wlBrandName').value, logo_url: document.getElementById('wlLogoUrl').value, primary_color: document.getElementById('wlPrimaryColor').value, bg_color: document.getElementById('wlBgColor').value, custom_domain: document.getElementById('wlDomain').value, favicon_url: document.getElementById('wlFavicon').value, custom_css: document.getElementById('wlCustomCss').value, hide_branding: document.getElementById('wlHideBranding').checked ? 1 : 0, }) }); await resetBranding(); showToast(t('settings.toast.branding_saved'), 'success'); } catch (err) { showToast(err.message, 'error'); } }); document.getElementById('previewWhiteLabelBtn')?.addEventListener('click', () => { const primary = document.getElementById('wlPrimaryColor').value; const bg = document.getElementById('wlBgColor').value; document.documentElement.style.setProperty('--accent', primary); document.documentElement.style.setProperty('--bg-primary', bg); showToast(t('settings.toast.preview_applied'), 'info'); }); } async function loadUsers() { const el = document.getElementById('userManagement'); if (!el) return; try { const [users, plans] = await Promise.all([ api.getUsers(), fetch('/api/subscription/plans').then(r => r.json()) ]); const currentUser = JSON.parse(localStorage.getItem('user') || '{}'); el.innerHTML = `
${users.map(u => ` `).join('')}
${t('settings.user.col_user')} ${t('settings.user.col_auth')} ${t('settings.user.col_role')} ${t('settings.user.col_plan')} ${t('settings.user.col_actions')}
${u.name || u.email}
${u.email}
${u.auth_provider} ${u.role} ${u.auth_provider === 'local' && u.id !== currentUser.id ? `` : ''} ${u.id !== currentUser.id ? `` : `${t('settings.user.you')}`}

${tn('settings.user.count', users.length)}

`; // Plan change handlers el.querySelectorAll('.plan-select').forEach(select => { select.addEventListener('change', async () => { const userId = select.dataset.userId; const planId = select.value; try { await api.assignPlan(userId, planId); showToast(t('settings.toast.plan_updated'), 'success'); } catch (err) { showToast(err.message, 'error'); loadUsers(); // Revert } }); }); // Reset password handlers el.querySelectorAll('.reset-user-pw-btn').forEach(btn => { btn.addEventListener('click', async () => { const email = btn.dataset.userEmail; const pw = prompt(t('settings.user.prompt_reset_password', { email })); if (pw === null) return; if (pw.length < 8) { showToast(t('settings.toast.new_password_min_8'), 'error'); return; } try { await api.resetUserPassword(btn.dataset.userId, pw); showToast(t('settings.toast.password_reset_for_user'), 'success'); } catch (err) { showToast(err.message, 'error'); } }); }); // Delete user handlers el.querySelectorAll('.delete-user-btn').forEach(btn => { let confirming = false; btn.addEventListener('click', async () => { if (confirming) { try { await api.deleteUser(btn.dataset.userId); showToast(t('settings.toast.user_removed'), 'success'); loadUsers(); } catch (err) { showToast(err.message, 'error'); } return; } confirming = true; btn.textContent = t('settings.user.confirm'); btn.style.background = 'var(--danger)'; btn.style.color = 'white'; setTimeout(() => { confirming = false; btn.textContent = t('settings.user.remove'); btn.style.background = ''; btn.style.color = ''; }, 3000); }); }); } catch (err) { el.innerHTML = `

${esc(err.message)}

`; } } export function cleanup() {}