const form = document.getElementById('settings-form'); const saveButtons = Array.from(document.querySelectorAll('.settings-save')); const message = document.getElementById('settings-message'); const fields = { dashcamUpload: document.getElementById('dashcam-upload-enabled'), retention: document.getElementById('retention-days'), cellularEnabled: document.getElementById('cellular-enabled'), cellularApn: document.getElementById('cellular-apn'), ntfyEnabled: document.getElementById('ntfy-enabled'), ntfyUrl: document.getElementById('ntfy-url'), ntfyToken: document.getElementById('ntfy-token'), clearNtfyUrl: document.getElementById('clear-ntfy-url'), clearNtfyToken: document.getElementById('clear-ntfy-token'), temperature: document.getElementById('temperature-c'), disk: document.getElementById('disk-percent'), trackingBacklog: document.getElementById('tracking-backlog'), videoBacklog: document.getElementById('video-backlog'), gpsLoss: document.getElementById('gps-loss-seconds'), repeat: document.getElementById('repeat-seconds'), }; function showMessage(text, kind) { message.textContent = text; message.className = `alert alert-${kind}`; message.hidden = false; } function setSaving(disabled) { saveButtons.forEach(button => { button.disabled = disabled; }); } function clearErrors() { form.querySelectorAll('.is-invalid').forEach(element => element.classList.remove('is-invalid')); form.querySelectorAll('[data-error-for]').forEach(element => { element.textContent = ''; }); } function applyErrors(errors) { const errorElements = { local_video_retention_days: fields.retention, cellular_apn: fields.cellularApn, ntfy_url: fields.ntfyUrl, ntfy_token: fields.ntfyToken, temperature_c: fields.temperature, disk_percent: fields.disk, tracking_backlog: fields.trackingBacklog, video_backlog: fields.videoBacklog, gps_loss_seconds: fields.gpsLoss, repeat_seconds: fields.repeat, }; Object.entries(errors).forEach(([name, text]) => { const input = errorElements[name]; const feedback = form.querySelector(`[data-error-for="${name}"]`); if (input) input.classList.add('is-invalid'); if (feedback) feedback.textContent = text; }); } function populate(settings) { const thresholds = settings.alert_thresholds; fields.dashcamUpload.checked = settings.dashcam_upload_enabled; fields.retention.value = settings.local_video_retention_days; fields.cellularEnabled.checked = settings.cellular_enabled; fields.cellularApn.value = settings.cellular_apn; fields.ntfyEnabled.checked = settings.ntfy_enabled; fields.temperature.value = thresholds.temperature_c; fields.disk.value = thresholds.disk_percent; fields.trackingBacklog.value = thresholds.tracking_backlog; fields.videoBacklog.value = thresholds.video_backlog; fields.gpsLoss.value = thresholds.gps_loss_seconds; fields.repeat.value = thresholds.repeat_seconds; document.getElementById('ntfy-url-state').textContent = settings.ntfy_url_configured ? 'A URL is saved. Leave blank to keep it unchanged.' : 'No URL is saved.'; document.getElementById('ntfy-token-state').textContent = settings.ntfy_token_configured ? 'A token is saved. Leave blank to keep it unchanged.' : 'No token is saved; public ntfy topics do not require one.'; fields.ntfyUrl.value = ''; fields.ntfyToken.value = ''; fields.clearNtfyUrl.checked = false; fields.clearNtfyToken.checked = false; } function payload() { return { dashcam_upload_enabled: fields.dashcamUpload.checked, local_video_retention_days: Number(fields.retention.value), cellular_enabled: fields.cellularEnabled.checked, cellular_apn: fields.cellularApn.value, ntfy_enabled: fields.ntfyEnabled.checked, ntfy_url: fields.ntfyUrl.value, ntfy_token: fields.ntfyToken.value, clear_ntfy_url: fields.clearNtfyUrl.checked, clear_ntfy_token: fields.clearNtfyToken.checked, alert_thresholds: { temperature_c: Number(fields.temperature.value), disk_percent: Number(fields.disk.value), tracking_backlog: Number(fields.trackingBacklog.value), video_backlog: Number(fields.videoBacklog.value), gps_loss_seconds: Number(fields.gpsLoss.value), repeat_seconds: Number(fields.repeat.value), }, }; } async function loadUploadHealth() { const element = document.getElementById('upload-health'); try { const response = await fetch('/api/upload/status'); if (!response.ok) throw new Error(`HTTP ${response.status}`); const status = await response.json(); if (!status.configured) { element.textContent = 'Remote server upload is not configured on this Pi.'; element.className = 'small text-muted mb-3'; return; } const parts = [`${status.pending_tracking_rows} tracking row(s) pending.`]; if (status.last_success_at) { parts.push(`Last success: ${status.last_success_at}${status.last_network_path ? ` via ${status.last_network_path}.` : '.'}`); } if (status.last_error) parts.push(`Last error: ${status.last_error}`); element.textContent = parts.join(' '); element.className = `small mb-3 ${status.last_error ? 'text-danger' : 'text-muted'}`; } catch (error) { element.textContent = `Could not load upload status: ${error.message}`; element.className = 'small text-danger mb-3'; } } async function loadSettings() { setSaving(true); try { const response = await fetch('/api/settings'); if (!response.ok) throw new Error(`HTTP ${response.status}`); populate(await response.json()); setSaving(false); } catch (error) { showMessage(`Could not load settings: ${error.message}`, 'danger'); } } form.addEventListener('submit', async event => { event.preventDefault(); clearErrors(); message.hidden = true; setSaving(true); try { const response = await fetch('/api/settings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload()), }); const result = await response.json(); if (!response.ok) { applyErrors(result.fields || {}); throw new Error(result.error || `HTTP ${response.status}`); } populate(result); showMessage('Settings saved.', 'success'); } catch (error) { if (message.hidden) showMessage(`Could not save settings: ${error.message}`, 'danger'); } finally { setSaving(false); } }); loadSettings(); loadUploadHealth();