Files
yaris-obd/static/js/status.js
T

397 lines
14 KiB
JavaScript

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 = `
<td class="ps-3">
<div class="fw-semibold">${service.label}</div>
<div class="small text-muted">${service.unit}</div>
</td>
<td class="${statusClass} fw-semibold">${statusLabel}</td>
<td>${service.ok ? service.sub_state : '—'}</td>
<td>${service.ok ? service.unit_file_state : '—'}</td>
<td>${service.ok && service.restarts != null ? service.restarts : '—'}</td>
<td class="pe-3">${service.ok ? service.result : (service.error || '—')}</td>
`;
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);