375 lines
14 KiB
JavaScript
375 lines
14 KiB
JavaScript
|
|
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();
|
|
});
|
|
});
|