(function () { 'use strict'; const loginForm = document.querySelector('#login-form'); const setupForm = document.querySelector('#setup-form'); const message = document.querySelector('#message'); function showMessage(text, kind = '') { message.textContent = text; message.className = kind; } async function api(url, options = {}) { const response = await fetch(url, { credentials: 'same-origin', ...options, headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } }); const data = await response.json(); if (!response.ok || data.ok === false) throw new Error(data.error || 'Request failed.'); return data; } function values(form) { return Object.fromEntries(new FormData(form).entries()); } async function initialize() { const auth = await api('/api/auth/status'); if (auth.authenticated) { location.replace('/'); return; } const status = await api('/api/setup/status'); if (status.configured) { loginForm.hidden = false; const authStatus = await api('/api/auth/status'); document.querySelector('#totp-row').hidden = !authStatus.totpRequired; loginForm.elements.totpCode.required = authStatus.totpRequired; document.querySelector('#intro').textContent = authStatus.totpRequired ? 'Sign in with your password and two-factor authentication.' : 'Sign in to your personal Chimera server.'; return; } setupForm.hidden = false; setupForm.elements.language.value = status.defaultLanguage || 'en'; document.querySelector('#intro').textContent = 'Complete the one-time secure Docker configuration.'; document.querySelector('#data-status').textContent = `Data root: ${status.dataRoot} — ${status.volumes.filter(v => v.writable).length}/${status.volumes.length} volumes writable.`; const roots = document.querySelector('#roots'); for (const volume of status.volumes) { const label = document.createElement('label'); label.className = 'check'; const input = document.createElement('input'); input.type = 'checkbox'; input.name = 'allowedRoots'; input.value = volume.path; input.checked = true; input.disabled = !volume.writable; label.append(input, document.createTextNode(` ${volume.path} ${volume.writable ? '' : '(read-only)'}`)); roots.append(label); } document.querySelector('#capabilities').textContent = Object.entries(status.capabilities.features).map(([name, feature]) => `${name}: ${feature.available ? 'available' : `unavailable — ${feature.reason}`}`).join('\n'); } loginForm.addEventListener('submit', async event => { event.preventDefault(); showMessage('Signing in…'); try { await api('/api/auth/login', { method: 'POST', body: JSON.stringify(values(loginForm)) }); location.replace('/'); } catch (error) { showMessage(error.message, 'error'); } }); document.querySelector('#test-ftp').addEventListener('click', async () => { const data = values(setupForm); if (!data.ftpHost) { showMessage('Enter a PS5 host or IP first.', 'error'); return; } showMessage('Testing FTP connection…'); try { await api('/api/setup/test-ftp', { method: 'POST', headers: { 'X-Chimera-Bootstrap-Token': data.bootstrapToken || '' }, body: JSON.stringify({ host: data.ftpHost, port: data.ftpPort, user: data.ftpUser, password: data.ftpPassword }) }); showMessage('FTP connection successful.', 'success'); } catch (error) { showMessage(error.message, 'error'); } }); setupForm.addEventListener('submit', async event => { event.preventDefault(); showMessage('Saving setup securely…'); const data = values(setupForm); data.allowedRoots = [...setupForm.querySelectorAll('input[name="allowedRoots"]:checked:not(:disabled)')].map(input => input.value); data.safetyConfirmed = setupForm.elements.safetyConfirmed.checked; try { await api('/api/setup', { method: 'POST', headers: { 'X-Chimera-Bootstrap-Token': data.bootstrapToken || '' }, body: JSON.stringify(data) }); location.replace('/'); } catch (error) { showMessage(error.message, 'error'); } }); initialize().catch(error => showMessage(error.message, 'error')); })();