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 saveCodeBook() { event.stopPropagation(); event.preventDefault(); window.location.href = '/code-book-print.html'; } function openKiwixm(event) { event.stopPropagation(); event.preventDefault(); window.location.href = window.location.origin.replace(/:\d+/g, '') + ':1400/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(); }); });