function setBadge(id, label, kind) { const badge = document.getElementById(id); if (!badge) return; badge.className = `badge ${kind}`; badge.textContent = label; } function setText(id, value) { const element = document.getElementById(id); if (element) { element.textContent = value; } } function showActionMessage(text, kind) { const banner = document.getElementById('status-action-message'); banner.textContent = text; banner.className = `alert alert-${kind} py-2`; banner.hidden = false; } function setActionButtonsDisabled(disabled) { [ 'status-action-test-server', 'status-action-sync-now', 'status-action-reconnect-uplink', 'status-action-restart-app', ].forEach((id) => { const button = document.getElementById(id); if (button) { button.disabled = disabled; } }); } function formatUptime(seconds) { if (seconds == null) return 'Unavailable'; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); return days ? `${days}d ${hours}h` : `${hours}h ${minutes}m`; } function formatGpsAge(seconds) { if (seconds == null) return 'No fix yet'; if (seconds < 10) return 'Just now'; if (seconds < 60) return `${Math.round(seconds)}s`; const mins = Math.floor(seconds / 60); const secs = Math.round(seconds % 60); return secs ? `${mins}m ${secs}s` : `${mins}m`; } function healthWarningLabel(warning) { return { under_voltage: 'Power supply undervoltage', frequency_capped: 'CPU frequency capped', throttled: 'CPU currently throttled', soft_temperature_limit: 'CPU temperature limit active', high_temperature: 'CPU temperature is high', disk_nearly_full: 'Disk is nearly full', memory_pressure: 'Memory usage is high', }[warning] || warning; } function historicalHealthWarningLabel(warning) { return { under_voltage: 'Power supply undervoltage', frequency_capped: 'CPU frequency capping', throttled: 'CPU throttling', soft_temperature_limit: 'CPU temperature limiting', }[warning] || warning; } function renderServer(server) { setText('server-url', server.server_url || 'Not configured'); setText('server-device', server.device_name || 'Not configured'); setText( 'server-detail', server.reachable ? `HTTP ${server.health_http_status} · ${server.latency_ms} ms` : 'Unavailable' ); setText('server-message', server.error || (server.authenticated ? 'Server reachable and device credentials are valid.' : server.reachable ? 'Server reachable.' : 'Server is not reachable.')); if (!server.configured) { setBadge('server-badge', 'Not configured', 'text-bg-secondary'); } else if (server.authenticated) { setBadge('server-badge', 'Online', 'text-bg-success'); } else if (server.reachable) { setBadge('server-badge', 'Reachable', 'text-bg-warning'); } else { setBadge('server-badge', 'Offline', 'text-bg-danger'); } } function renderUplink(uplink) { if (!uplink.ok) { setBadge('uplink-badge', 'Unavailable', 'text-bg-danger'); setText('uplink-mode', 'Unavailable'); setText('uplink-interface', '—'); setText('uplink-connection', '—'); setText('uplink-ip', '—'); setText('uplink-message', uplink.error || 'Could not load uplink status.'); return; } const modeLabel = { phone: 'Phone hotspot', local_hotspot: 'Local hotspot', wifi: 'Wi-Fi / Ethernet', disconnected: 'Disconnected', }[uplink.mode] || uplink.mode || 'Unknown'; if (uplink.internet_reachable) { setBadge('uplink-badge', 'Internet up', 'text-bg-success'); } else if (uplink.mode === 'local_hotspot') { setBadge('uplink-badge', 'Hotspot only', 'text-bg-warning'); } else { setBadge('uplink-badge', 'Offline', 'text-bg-danger'); } setText('uplink-mode', modeLabel); setText('uplink-interface', uplink.interface || '—'); setText('uplink-connection', uplink.active_connection || 'None'); setText('uplink-ip', uplink.ip_address || '—'); const details = []; if (uplink.phone_configured && uplink.phone_ssid) { details.push(`Saved phone SSID: ${uplink.phone_ssid}`); } if (uplink.connectivity) { details.push(`Connectivity: ${uplink.connectivity}`); } if (uplink.last_transition && uplink.last_transition.action) { details.push(`Last action: ${uplink.last_transition.action}`); } setText('uplink-message', details.join(' · ') || 'No uplink details available.'); } function renderUpload(upload) { const configured = upload.configured ? 'Configured' : 'Not configured'; const dashcam = upload.dashcam_upload_enabled ? 'Enabled' : 'Paused'; setText('upload-pending', String(upload.pending_tracking_rows)); setText('upload-path', upload.last_network_path || '—'); setText('upload-configured', configured); setText('upload-dashcam', dashcam); setText( 'upload-message', upload.last_error ? `Last error: ${upload.last_error}` : upload.last_success_at ? `Last success: ${new Date(upload.last_success_at * 1000).toLocaleString()}` : 'No successful upload yet in this session.' ); if (upload.last_error) { setBadge('upload-badge', 'Error', 'text-bg-danger'); } else if (upload.pending_tracking_rows > 0) { setBadge('upload-badge', 'Queued', 'text-bg-warning'); } else { setBadge('upload-badge', 'Idle', 'text-bg-success'); } } function renderHealth(health) { setText( 'health-temperature', health.temperature_c == null ? 'Unavailable' : `${health.temperature_c.toFixed(1)} °C` ); setText( 'health-memory', health.memory == null ? 'Unavailable' : `${health.memory.used_percent.toFixed(1)}%` ); setText( 'health-disk', health.disk == null ? 'Unavailable' : `${health.disk.used_percent.toFixed(1)}% · ${YarisUI.formatBytes(health.disk.free_bytes)} free` ); setText('health-uptime', formatUptime(health.uptime_seconds)); const warning = document.getElementById('health-warning'); const error = document.getElementById('health-error'); const currentWarnings = (health.warnings || []).map(healthWarningLabel); const historicalWarnings = (health.historical_warnings || []).map( historicalHealthWarningLabel ); const combinedWarnings = currentWarnings.concat( historicalWarnings.length ? [`Previously detected: ${historicalWarnings.join(', ')}`] : [] ); warning.hidden = combinedWarnings.length === 0; warning.textContent = combinedWarnings.join(' · '); const unavailable = Object.keys(health.errors || {}); error.hidden = unavailable.length === 0; error.textContent = unavailable.length ? `Unavailable health sources: ${unavailable.join(', ')}` : ''; if (health.status === 'warning') { setBadge('health-badge', 'Warning', 'text-bg-warning'); } else if (health.status === 'degraded') { setBadge('health-badge', 'Degraded', 'text-bg-danger'); } else { setBadge('health-badge', 'Healthy', 'text-bg-success'); } } function renderGpsAndCellular(gps, cellularHat) { const liveFix = gps.thread_alive && gps.has_fix; const staleFix = liveFix && gps.fix_age_seconds != null && gps.fix_age_seconds > 30; if (staleFix) { setBadge('gps-badge', 'Stale fix', 'text-bg-warning'); } else if (liveFix) { setBadge('gps-badge', 'Live fix', 'text-bg-success'); } else if (gps.thread_alive) { setBadge('gps-badge', 'No fix', 'text-bg-warning'); } else { setBadge('gps-badge', 'Offline', 'text-bg-danger'); } setText('gps-fix', liveFix ? 'Available' : gps.thread_alive ? 'Searching' : 'Offline'); setText('gps-age', formatGpsAge(gps.fix_age_seconds)); setText('gps-latitude', gps.latitude || '—'); setText('gps-longitude', gps.longitude || '—'); setText( 'cellular-uart', cellularHat.serial_port_present ? cellularHat.serial_port : 'Missing' ); setText( 'cellular-modem', cellularHat.module_responding ? 'Responding' : 'Not responding' ); const details = []; details.push(cellularHat.configured ? 'Cellular fallback configured' : 'Cellular fallback disabled'); details.push(cellularHat.apn_configured ? 'APN set' : 'No APN'); setText('cellular-message', details.join(' · ')); } function renderObd(obd) { if (obd.car_connected) { setBadge('obd-badge', 'Connected', 'text-bg-success'); } else if (obd.adapter_present) { setBadge('obd-badge', 'Adapter only', 'text-bg-warning'); } else { setBadge('obd-badge', 'Missing', 'text-bg-danger'); } setText('obd-adapter', obd.adapter_present ? 'Detected' : 'Not detected'); setText('obd-port', obd.port || '—'); setText('obd-status', obd.status_label || 'Unknown'); } function renderServices(services) { const table = document.getElementById('services-table'); table.replaceChildren(); services.forEach((service) => { const row = document.createElement('tr'); const statusLabel = service.ok ? `${service.active_state}` : 'Unavailable'; const statusClass = !service.ok ? 'text-danger' : service.active_state === 'active' ? 'text-success' : service.active_state === 'inactive' ? 'text-muted' : 'text-warning'; row.innerHTML = `
${service.label}
${service.unit}
${statusLabel} ${service.ok ? service.sub_state : '—'} ${service.ok ? service.unit_file_state : '—'} ${service.ok && service.restarts != null ? service.restarts : '—'} ${service.ok ? service.result : (service.error || '—')} `; table.appendChild(row); }); } async function refreshStatus() { const response = await fetch('/api/status'); const status = await response.json(); if (!response.ok) { throw new Error(status.error || 'Could not load status page data.'); } document.getElementById('status-error').hidden = true; document.getElementById('status-updated').textContent = `Updated ${new Date(status.generated_at * 1000).toLocaleTimeString()}`; renderServer(status.server); renderUplink(status.uplink); renderUpload(status.upload); renderHealth(status.health); renderGpsAndCellular(status.gps, status.cellular_hat); renderObd(status.obd); renderServices(status.services); } function showStatusError(error) { const banner = document.getElementById('status-error'); banner.textContent = error.message; banner.hidden = false; } async function runStatusAction(buttonId, requestFactory, successMessage, options = {}) { const button = document.getElementById(buttonId); const actionBanner = document.getElementById('status-action-message'); actionBanner.hidden = true; button.disabled = true; try { const response = await requestFactory(); const result = await response.json(); if (!response.ok) { throw new Error(result.error || result.message || `HTTP ${response.status}`); } showActionMessage(result.message || successMessage, options.successKind || 'success'); if (options.disableAllOnSuccess) { setActionButtonsDisabled(true); } if (options.refreshAfter !== false) { await refreshStatus(); } } catch (error) { showActionMessage(error.message, 'danger'); } finally { if (!options.keepDisabled) { button.disabled = false; } } } document.getElementById('status-action-test-server').addEventListener('click', function () { runStatusAction( 'status-action-test-server', function () { return fetch('/api/remote-config/test', { method: 'POST' }); }, 'Remote server connection looks good.' ); }); document.getElementById('status-action-sync-now').addEventListener('click', function () { runStatusAction( 'status-action-sync-now', function () { return fetch('/api/remote-config/sync-now', { method: 'POST' }); }, 'Sync completed.' ); }); document.getElementById('status-action-reconnect-uplink').addEventListener('click', function () { runStatusAction( 'status-action-reconnect-uplink', function () { return fetch('/api/uplink/connect', { method: 'POST' }); }, 'Waiting for the saved hotspot. This page may disconnect while networks switch.', { successKind: 'info' } ); }); document.getElementById('status-action-restart-app').addEventListener('click', function () { runStatusAction( 'status-action-restart-app', function () { return fetch('/api/status/restart-app', { method: 'POST' }); }, 'App restart requested. The page may disconnect for a few seconds.', { successKind: 'warning', refreshAfter: false, keepDisabled: true, disableAllOnSuccess: true, } ); }); refreshStatus().catch(showStatusError); window.setInterval(function () { refreshStatus().catch(showStatusError); }, 30000);