@@ -0,0 +1,298 @@
|
||||
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 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;
|
||||
}
|
||||
|
||||
refreshStatus().catch(showStatusError);
|
||||
window.setInterval(function () {
|
||||
refreshStatus().catch(showStatusError);
|
||||
}, 30000);
|
||||
+1
-104
@@ -221,9 +221,7 @@ function formatGpsAge(seconds) {
|
||||
function renderGpsStatus(status) {
|
||||
const liveBadge = document.getElementById('gps-live-badge');
|
||||
const fixDetail = document.getElementById('gps-fix-detail');
|
||||
const healthStatus = document.getElementById('pi-gps-status');
|
||||
const healthDetail = document.getElementById('pi-gps-detail');
|
||||
if (!liveBadge || !fixDetail || !healthStatus || !healthDetail) return;
|
||||
if (!liveBadge || !fixDetail) return;
|
||||
|
||||
let label = 'Unavailable';
|
||||
let badgeClass = 'text-bg-secondary';
|
||||
@@ -248,9 +246,6 @@ function renderGpsStatus(status) {
|
||||
liveBadge.className = `badge ${badgeClass}`;
|
||||
liveBadge.textContent = label;
|
||||
fixDetail.textContent = detail;
|
||||
healthStatus.textContent = label;
|
||||
healthStatus.className = `h6 fw-bold mb-0 ${label === 'Live fix' ? 'text-success' : label === 'Offline' ? 'text-danger' : 'text-warning'}`;
|
||||
healthDetail.textContent = detail;
|
||||
}
|
||||
|
||||
async function refreshGpsStatus() {
|
||||
@@ -295,112 +290,14 @@ socket.on('gps_location', function(data) {
|
||||
}
|
||||
});
|
||||
|
||||
function formatUptime(seconds) {
|
||||
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 setHealthValue(id, value) {
|
||||
document.getElementById(id).textContent = value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function refreshHealth() {
|
||||
const response = await fetch("/api/health");
|
||||
const health = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(health.error || "Could not load Pi health.");
|
||||
}
|
||||
|
||||
setHealthValue(
|
||||
"pi-temperature",
|
||||
health.temperature_c == null ? "Unavailable" : `${health.temperature_c.toFixed(1)} °C`
|
||||
);
|
||||
setHealthValue(
|
||||
"pi-memory",
|
||||
health.memory == null ? "Unavailable" : `${health.memory.used_percent.toFixed(1)}%`
|
||||
);
|
||||
setHealthValue(
|
||||
"pi-disk",
|
||||
health.disk == null
|
||||
? "Unavailable"
|
||||
: `${health.disk.used_percent.toFixed(1)}% · ${YarisUI.formatBytes(health.disk.free_bytes)} free`
|
||||
);
|
||||
setHealthValue(
|
||||
"pi-uptime",
|
||||
health.uptime_seconds == null ? "Unavailable" : formatUptime(health.uptime_seconds)
|
||||
);
|
||||
|
||||
const warning = document.getElementById("pi-health-warning");
|
||||
const currentWarnings = health.warnings.map(healthWarningLabel);
|
||||
const historicalWarnings = health.historical_warnings.map(
|
||||
historicalHealthWarningLabel
|
||||
);
|
||||
const warnings = currentWarnings.concat(
|
||||
historicalWarnings.length
|
||||
? [`Previously detected: ${historicalWarnings.join(", ")}`]
|
||||
: []
|
||||
);
|
||||
warning.hidden = warnings.length === 0;
|
||||
warning.textContent = warnings.join(" · ");
|
||||
|
||||
const error = document.getElementById("pi-health-error");
|
||||
const unavailable = Object.keys(health.errors);
|
||||
error.hidden = unavailable.length === 0;
|
||||
error.textContent = unavailable.length
|
||||
? `Unavailable health sources: ${unavailable.join(", ")}`
|
||||
: "";
|
||||
document.getElementById("pi-health-updated").textContent =
|
||||
`Updated ${new Date().toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
refreshHealth().catch(function (error) {
|
||||
const message = document.getElementById("pi-health-error");
|
||||
message.textContent = error.message;
|
||||
message.hidden = false;
|
||||
});
|
||||
window.setInterval(function () {
|
||||
refreshHealth().catch(function (error) {
|
||||
const message = document.getElementById("pi-health-error");
|
||||
message.textContent = error.message;
|
||||
message.hidden = false;
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
refreshGpsStatus().catch(function (error) {
|
||||
renderGpsStatus({thread_alive: false, has_fix: false, fix_age_seconds: null});
|
||||
const fixDetail = document.getElementById('gps-fix-detail');
|
||||
const healthDetail = document.getElementById('pi-gps-detail');
|
||||
if (fixDetail) fixDetail.textContent = error.message;
|
||||
if (healthDetail) healthDetail.textContent = error.message;
|
||||
});
|
||||
window.setInterval(function () {
|
||||
refreshGpsStatus().catch(function (error) {
|
||||
const fixDetail = document.getElementById('gps-fix-detail');
|
||||
const healthDetail = document.getElementById('pi-gps-detail');
|
||||
if (fixDetail) fixDetail.textContent = error.message;
|
||||
if (healthDetail) healthDetail.textContent = error.message;
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
Reference in New Issue
Block a user