394 lines
14 KiB
JavaScript
394 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;
|
|
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);
|