// Configuration tab: frame settings + firmware card + take control. // window.FRAME_API is set by the template. Checkboxes are always sent // explicitly as "true"/"false" -- the server treats absent fields as // "leave unchanged", so a checkbox must never be simply omitted. async function saveConfig() { const minutes = parseInt(document.getElementById('refresh_interval_minutes').value, 10) || 60; const body = new URLSearchParams({ name: document.getElementById('frame_name').value || '', order: document.getElementById('order').value, orientation: document.getElementById('orientation').value, refresh_interval_s: String(minutes * 60), smart_crop_faces: String(document.getElementById('smart_crop_faces').checked), quiet_hours_enabled: String(document.getElementById('quiet_hours_enabled').checked), quiet_hours_start: document.getElementById('quiet_hours_start').value || '22:00', quiet_hours_end: document.getElementById('quiet_hours_end').value || '07:00', timezone: document.getElementById('timezone').value || 'UTC', }); const resp = await fetch(`${window.FRAME_API}/config`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, }); if (!resp.ok) { throw new Error(await apiError(resp)); } } document.getElementById('config-form').addEventListener('submit', async (e) => { e.preventDefault(); try { await saveConfig(); showStatus(true, 'Saved.'); } catch (e) { showStatus(false, e.message); } }); async function takeControl() { try { const resp = await fetch(`${window.FRAME_API}/take-control`, { method: 'POST' }); if (!resp.ok) throw new Error(await apiError(resp)); showStatus(true, 'You have control now.'); loadControl(); } catch (e) { showStatus(false, e.message); } } async function loadControl() { const banner = document.getElementById('control-banner'); try { const resp = await fetch(`${window.FRAME_API}/queue`); if (!resp.ok) return; // unconfigured frame: control still works via 409s const data = await resp.json(); if (data.control && !data.control.you) { banner.style.display = 'flex'; document.getElementById('control-holder').textContent = data.control.controller ? `${data.control.controller} currently has control of this frame.` : 'Nobody has control of this frame yet.'; } else { banner.style.display = 'none'; } } catch (e) { /* banner is best-effort */ } } document.getElementById('take-control').addEventListener('click', takeControl); // ---- Firmware card ---- document.getElementById('firmware-upload').addEventListener('click', async () => { const input = document.getElementById('firmware-file'); if (!input.files.length) { showStatus(false, 'Pick a firmware .bin first.'); return; } const form = new FormData(); form.append('file', input.files[0]); try { const resp = await fetch(`${window.FRAME_API}/firmware`, { method: 'POST', body: form }); if (!resp.ok) { throw new Error(await apiError(resp)); } const result = await resp.json(); document.getElementById('firmware-available').textContent = `Uploaded: v${result.version} -- the frame updates itself on its next wake if it's running something else.`; showStatus(true, `Firmware v${result.version} uploaded (${result.size} bytes).`); } catch (e) { showStatus(false, e.message); } }); function showRepoDisplayMode(url) { document.getElementById('firmware-repo-text').textContent = url; document.getElementById('firmware-repo-display').style.display = url ? 'block' : 'none'; document.getElementById('firmware-repo-edit').style.display = url ? 'none' : 'block'; } document.getElementById('firmware-repo-edit-btn').addEventListener('click', () => { document.getElementById('firmware-repo-display').style.display = 'none'; document.getElementById('firmware-repo-edit').style.display = 'block'; document.getElementById('firmware_update_repo_url').focus(); }); document.getElementById('firmware-settings-save').addEventListener('click', async () => { try { const body = new URLSearchParams({ firmware_update_repo_url: document.getElementById('firmware_update_repo_url').value || '', firmware_auto_update: String(document.getElementById('firmware_auto_update').checked), }); const resp = await fetch(`${window.FRAME_API}/config`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, }); if (!resp.ok) throw new Error(await apiError(resp)); showStatus(true, 'Saved.'); showRepoDisplayMode(document.getElementById('firmware_update_repo_url').value.trim()); loadFirmwareCheck(); } catch (e) { showStatus(false, e.message); } }); async function loadFirmwareCheck(force) { const statusEl = document.getElementById('firmware-gitea-status'); const btn = document.getElementById('firmware-update-btn'); const boardEl = document.getElementById('firmware-board'); try { const resp = await fetch(`${window.FRAME_API}/firmware/check` + (force ? '?force=true' : '')); if (!resp.ok) { if (force) { showStatus(false, await apiError(resp)); } return; } const data = await resp.json(); if (data.board) { boardEl.textContent = `Detected board: ${data.board}`; } if (!data.enabled) { statusEl.style.display = 'none'; btn.style.display = 'none'; if (force) { showStatus(false, 'No Gitea repo URL configured.'); } return; } statusEl.style.display = 'block'; if (!data.board) { statusEl.textContent = 'Waiting for the frame to check in before it can look up the right build.'; btn.style.display = 'none'; } else if (data.update_available) { statusEl.textContent = `Update available: v${data.latest_version}.`; btn.style.display = 'inline-block'; } else if (data.latest_version) { statusEl.textContent = `Up to date (v${data.latest_version}).`; btn.style.display = 'none'; } else { statusEl.textContent = 'No releases found yet.'; btn.style.display = 'none'; } if (force) { showStatus(true, 'Checked.'); } } catch (e) { // A failed passive poll is silent; an explicit "Check now" click // still surfaces the error. if (force) { showStatus(false, e.message); } } } document.getElementById('firmware-check-now').addEventListener('click', () => loadFirmwareCheck(true)); document.getElementById('firmware-update-btn').addEventListener('click', async () => { const btn = document.getElementById('firmware-update-btn'); btn.disabled = true; try { const resp = await fetch(`${window.FRAME_API}/firmware/apply-latest`, { method: 'POST' }); if (!resp.ok) { throw new Error(await apiError(resp)); } const result = await resp.json(); document.getElementById('firmware-available').textContent = `Uploaded: v${result.version} -- the frame updates itself on its next wake if it's running something else.`; showStatus(true, `Firmware v${result.version} staged from Gitea.`); loadFirmwareCheck(); } catch (e) { showStatus(false, e.message); } finally { btn.disabled = false; } }); loadControl(); loadFirmwareCheck(); // The server throttles actual Gitea API calls itself, so this poll is // cheap either way. setInterval(loadFirmwareCheck, 60000);