Files
mrs/code/public/index.js
2026-04-05 20:27:43 -04:00

369 lines
14 KiB
JavaScript
Executable File

class Dashboard {
constructor() {
this.statusCache = null;
this.lastFetch = 0;
this.hostname = 'localhost';
this.serverTimeOffset = null; // Track difference between server and client time
this.isTimeServerSynced = false; // Track if we're using server time
this.init();
}
async init() {
await this.fetchHostname();
this.updateHostname();
this.startTimeUpdates(); // Start with client time immediately
this.startStatusUpdates();
this.initializeEventListeners();
}
async fetchHostname() {
try {
const response = await fetch('/api/hostname');
const data = await response.json();
this.hostname = data.hostname.toLowerCase();
} catch (error) {
console.warn('Failed to fetch hostname:', error);
}
}
updateHostname() {
const hostnameElement = document.querySelector('.hostname');
if (hostnameElement) {
hostnameElement.textContent = this.hostname;
}
}
async fetchStatus() {
try {
// Record start time for latency calculation
const requestStart = performance.now();
const response = await fetch('/api/status');
const requestEnd = performance.now();
if (!response.ok) throw new Error('Status fetch failed');
const status = await response.json();
this.statusCache = status;
this.updateUI(status, requestStart, requestEnd);
} catch (error) {
console.error('Failed to fetch status:', error);
this.showOfflineStatus();
}
}
updateUI(status, requestStart, requestEnd) {
this.updateSystemMetrics(status.system);
this.updateGPSStatus(status.gps);
this.updateNetworkStatus(status.network);
this.updateLoRaStatus(status.lora);
this.updateTimeSync(status.time, requestStart, requestEnd);
}
updateSystemMetrics(system) {
// Update temperature
const tempElement = document.getElementById('temp');
if (tempElement && system.temperature) {
tempElement.textContent = `${system.temperature}°C`;
}
// Update load average
const loadElement = document.getElementById('load');
if (loadElement && system.load !== undefined) {
loadElement.textContent = `${system.load.toFixed(1)} Load`;
}
// Update progress bars
this.updateProgressBar('cpu-usage', system.cpu);
this.updateProgressBar('memory-usage', system.memory);
this.updateProgressBar('disk-usage', system.storage);
}
updateProgressBar(id, percentage) {
const fillElement = document.getElementById(`${id}-fill`);
const textElement = document.getElementById(`${id}-text`);
if (fillElement && textElement) {
const value = Math.round(percentage);
fillElement.style.width = `${value}%`;
textElement.textContent = `${value}%`;
// Update color based on usage
if (value > 80) {
fillElement.style.background = '#ef4444'; // Red
} else if (value > 60) {
fillElement.style.background = '#f59e0b'; // Orange
} else {
fillElement.style.background = '#10b981'; // Green
}
}
}
updateGPSStatus(gps) {
const gpsStatus = document.getElementById('gps-status');
const gpsTile = document.getElementById('gps-tile');
if (gpsStatus && gpsTile) {
const hasValidFix = gps.fix !== 'No Fix' && gps.latitude !== null;
// Update status indicator
if (hasValidFix) {
gpsStatus.classList.remove('disconnected');
gpsStatus.classList.add('connected');
gpsTile.classList.remove('disconnected');
gpsTile.classList.add('connected');
} else {
gpsStatus.classList.remove('connected');
gpsStatus.classList.add('disconnected');
gpsTile.classList.remove('connected');
gpsTile.classList.add('disconnected');
}
// Update GPS tile info
const gpsInfo = gpsTile.querySelector('.tile-info');
const gpsDetail = gpsTile.querySelector('.tile-detail');
if (gpsInfo && gpsDetail) {
if (hasValidFix) {
gpsInfo.textContent = `${gps.latitude.toFixed(4)}, ${gps.longitude.toFixed(4)}`;
gpsDetail.textContent = `${gps.fix}${gps.satellites} sats • Alt: ${Math.round(gps.altitude)}m`;
} else {
gpsInfo.textContent = gps.satellites > 0 ? 'Acquiring Fix...' : 'No Signal';
gpsDetail.textContent = `${gps.satellites} satellites`;
}
}
}
}
updateNetworkStatus(network) {
// Update WiFi status
const wifiTile = document.getElementById('wifi-tile');
if (wifiTile) {
const wifiInfo = wifiTile.querySelector('.tile-info');
const wifiDetail = wifiTile.querySelector('.tile-detail');
if (wifiInfo && wifiDetail) {
if (network.wifi.mode === 'ap') {
wifiInfo.textContent = 'Access Point';
wifiDetail.textContent = `SSID: ${network.wifi.ssid}`;
} else {
wifiInfo.textContent = 'Client Mode';
wifiDetail.textContent = `SSID: ${network.wifi.ssid}`;
}
}
}
// Update Ethernet status
const ethernetStatus = document.getElementById('ethernet-status');
const ethernetTile = document.getElementById('ethernet-tile');
if (ethernetStatus && ethernetTile) {
if (network.ethernet.connected) {
ethernetStatus.classList.remove('disconnected');
ethernetStatus.classList.add('connected');
ethernetTile.classList.remove('disconnected');
ethernetTile.classList.add('connected');
const ethernetInfo = ethernetTile.querySelector('.tile-info');
const ethernetDetail = ethernetTile.querySelector('.tile-detail');
if (ethernetInfo && ethernetDetail) {
ethernetInfo.textContent = 'Connected';
ethernetDetail.textContent = 'Link detected';
}
} else {
ethernetStatus.classList.remove('connected');
ethernetStatus.classList.add('disconnected');
ethernetTile.classList.remove('connected');
ethernetTile.classList.add('disconnected');
const ethernetInfo = ethernetTile.querySelector('.tile-info');
const ethernetDetail = ethernetTile.querySelector('.tile-detail');
if (ethernetInfo && ethernetDetail) {
ethernetInfo.textContent = 'Disconnected';
ethernetDetail.textContent = 'No cable detected';
}
}
}
}
updateLoRaStatus(lora) {
const loraStatus = document.getElementById('lora-status');
const loraTile = document.getElementById('lora-tile');
if (loraStatus && loraTile) {
if (lora.connected) {
loraStatus.classList.remove('disconnected');
loraStatus.classList.add('connected');
loraTile.classList.remove('disconnected');
loraTile.classList.add('connected');
const loraInfo = loraTile.querySelector('.tile-info');
const loraDetail = loraTile.querySelector('.tile-detail');
if (loraInfo && loraDetail) {
loraInfo.textContent = `${lora.onlineNodes}/${lora.totalNodes} Online`;
loraDetail.textContent = `CH: ${lora.channelUtilization.toFixed(1)}% • Air: ${lora.airtimeUtilization.toFixed(1)}%`;
}
} else {
loraStatus.classList.remove('connected');
loraStatus.classList.add('disconnected');
loraTile.classList.remove('connected');
loraTile.classList.add('disconnected');
const loraInfo = loraTile.querySelector('.tile-info');
const loraDetail = loraTile.querySelector('.tile-detail');
if (loraInfo && loraDetail) {
loraInfo.textContent = 'Disconnected';
loraDetail.textContent = 'No device found';
}
}
}
}
updateTimeSync(serverTimeString, requestStart, requestEnd) {
if (serverTimeString && requestStart && requestEnd) {
// Calculate network latency (round-trip time / 2)
const networkLatency = (requestEnd - requestStart) / 2;
// Parse server time and adjust for network latency
const serverTime = new Date(serverTimeString);
const adjustedServerTime = new Date(serverTime.getTime() + networkLatency);
// Calculate the offset between adjusted server time and client time
const clientTime = new Date();
this.serverTimeOffset = adjustedServerTime.getTime() - clientTime.getTime();
this.isTimeServerSynced = true;
console.log(`Time synchronized with server (latency: ${networkLatency.toFixed(1)}ms, offset: ${this.serverTimeOffset.toFixed(1)}ms)`);
}
// Update time displays
this.updateCurrentTime();
}
updateCurrentTime() {
const now = new Date();
let displayTime;
if (this.isTimeServerSynced && this.serverTimeOffset !== null) {
// Use server-synchronized time
displayTime = new Date(now.getTime() + this.serverTimeOffset);
} else {
// Use client time (default)
displayTime = now;
}
this.updateTimeDisplay('local', displayTime, false);
this.updateTimeDisplay('zulu', displayTime, true);
}
updateTimeDisplay(type, date, isUtc) {
const timeElement = document.getElementById(`${type}-time`);
const dateElement = document.getElementById(`${type}-date`);
if (timeElement && dateElement) {
const displayDate = isUtc ? new Date(date.getTime()) : date;
const timeStr = isUtc ?
displayDate.toUTCString().split(' ')[4] :
displayDate.toLocaleTimeString();
const dateStr = isUtc ?
displayDate.toUTCString().split(' ').slice(0, 4).join(' ') :
displayDate.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
timeElement.textContent = timeStr;
dateElement.textContent = dateStr;
// Color time orange when using client time, white when server-synced
if (this.isTimeServerSynced) {
timeElement.style.color = '#fff';
dateElement.style.color = '#64748b';
} else {
timeElement.style.color = '#f59e0b';
dateElement.style.color = '#f59e0b';
}
}
}
showOfflineStatus() {
// Reset to client time and show orange color
this.serverTimeOffset = null;
this.isTimeServerSynced = false;
console.warn('System appears offline - falling back to client time');
}
startStatusUpdates() {
// Initial fetch
this.fetchStatus();
// Update every 60 seconds
setInterval(() => {
this.fetchStatus();
}, 60000);
}
startTimeUpdates() {
// Start immediately with client time
this.updateCurrentTime();
// Update time every second
setInterval(() => {
this.updateCurrentTime();
}, 1000);
}
initializeEventListeners() {
// Expand/collapse status
const expandToggle = document.getElementById('expand-toggle');
const expandedStatus = document.getElementById('expanded-status');
const expandIcon = document.getElementById('expand-icon');
if (expandToggle && expandedStatus && expandIcon) {
expandToggle.addEventListener('click', () => {
expandedStatus.classList.toggle('visible');
expandIcon.classList.toggle('expanded');
});
}
// Search functionality
const searchInput = document.getElementById('search-input');
const appsGrid = document.getElementById('apps-grid');
if (searchInput && appsGrid) {
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const apps = appsGrid.querySelectorAll('.app-item');
apps.forEach(app => {
const name = app.getAttribute('data-name')?.toLowerCase() || '';
app.style.display = name.includes(query) ? 'flex' : 'none';
});
});
}
}
}
function openKiwixm(event) {
event.stopPropagation();
event.preventDefault();
window.location.href = window.location.origin.replace(/:\d+/g, '') + ':1300/admin';
}
// Initialize dashboard when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new Dashboard();
document.querySelectorAll('.app-item[href^=":"]').forEach(link => {
const port = link.getAttribute('href').substring(1); // Remove the ':'
const url = new URL(location.origin);
url.port = port;
link.href = url.toString();
});
});