From 776c63a1a93f2c53779c31e711bc187f95bb26ae Mon Sep 17 00:00:00 2001 From: ztimson Date: Mon, 6 Apr 2026 16:47:35 -0400 Subject: [PATCH] Customizable home screen --- client/public/code-book.html | 68 +- client/public/index.html | 1440 +++++++++++++---------------- client/public/notification-bar.js | 1041 +++++++++++++++++++++ 3 files changed, 1741 insertions(+), 808 deletions(-) create mode 100644 client/public/notification-bar.js diff --git a/client/public/code-book.html b/client/public/code-book.html index de0641b..562dd6a 100644 --- a/client/public/code-book.html +++ b/client/public/code-book.html @@ -239,12 +239,16 @@ border-color: rgba(16, 185, 129, 0.3); } + .auth-table-wrapper { + overflow-x: auto; + position: relative; + } + .auth-table { font-family: 'Courier New', monospace; font-size: 0.75rem; margin: 1rem 0; width: 100%; - overflow-x: auto; } .auth-table table { @@ -263,12 +267,26 @@ } .auth-table th { - background: rgba(255, 255, 255, 0.1); + background: #465570; font-weight: 600; } - .auth-table td.highlight, - .auth-table th.highlight { + .auth-table th.row-header { + position: sticky; + left: 0; + z-index: 10; + background: #465570; + } + + .auth-table th.row-header.highlight { + background: rgba(255, 255, 255, 0.3); + } + + .auth-table th.col-header.highlight { + background: rgba(255, 255, 255, 0.3); + } + + .auth-table td.highlight { background: rgba(255, 255, 255, 0.25); } @@ -478,16 +496,24 @@ } .hotp-row { - flex-wrap: wrap; + flex-direction: column; } .hotp-row button { - flex: 1 1 100%; + width: 100%; + order: 2; } .hotp-challenge-response { flex: 1 1 100%; + width: 100%; flex-wrap: nowrap; + order: 1; + } + + .hotp-challenge-response > div { + flex: 1; + min-width: 0; } .ascii-search-grid { @@ -555,6 +581,20 @@ .ascii-tables-grid { grid-template-columns: repeat(4, 1fr); } + + .hotp-row { + flex-direction: row; + } + + .hotp-row button { + width: auto; + order: 0; + } + + .hotp-challenge-response { + order: 0; + width: auto; + } } @media (min-width: 1200px) { @@ -569,14 +609,7 @@ -
-
-
- Logo - -
-
-
+
@@ -656,7 +689,9 @@
-
+
+
+
@@ -751,6 +786,7 @@ Message: H E L L O + + + + + diff --git a/client/public/notification-bar.js b/client/public/notification-bar.js new file mode 100644 index 0000000..d734375 --- /dev/null +++ b/client/public/notification-bar.js @@ -0,0 +1,1041 @@ +class DashboardHeader extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.hostname = 'localhost'; + this.serverTimeOffset = null; + this.isTimeServerSynced = false; + this.statusCache = null; + } + + async connectedCallback() { + await this.fetchHostname(); + this.render(); + this.initializeHeader(); + this.startTimeUpdates(); + this.startStatusUpdates(); + } + + 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); + this.hostname = 'localhost'; + } + } + + async fetchStatus() { + try { + 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) { + const tempElement = this.shadowRoot.getElementById('temp'); + if (tempElement && system.temperature) { + tempElement.textContent = `${system.temperature}°C`; + } + + const loadElement = this.shadowRoot.getElementById('load'); + if (loadElement && system.load !== undefined) { + loadElement.textContent = `${system.load.toFixed(1)} Load`; + } + + this.updateProgressBar('cpu-usage', system.cpu); + this.updateProgressBar('memory-usage', system.memory); + this.updateProgressBar('disk-usage', system.storage); + } + + updateProgressBar(id, percentage) { + const fillElement = this.shadowRoot.getElementById(`${id}-fill`); + const textElement = this.shadowRoot.getElementById(`${id}-text`); + + if (fillElement && textElement) { + const value = Math.round(percentage); + fillElement.style.width = `${value}%`; + textElement.textContent = `${value}%`; + + if (value > 80) { + fillElement.style.background = '#ef4444'; + } else if (value > 60) { + fillElement.style.background = '#f59e0b'; + } else { + fillElement.style.background = '#10b981'; + } + } + } + + updateGPSStatus(gps) { + const gpsStatus = this.shadowRoot.getElementById('gps-status'); + const gpsTile = this.shadowRoot.getElementById('gps-tile'); + + if (gpsStatus && gpsTile) { + const hasValidFix = gps.fix !== 'No Fix' && gps.latitude !== null; + + 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'); + } + + 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) { + const wifiTile = this.shadowRoot.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 || this.hostname}`; + } else { + wifiInfo.textContent = 'Client Mode'; + wifiDetail.textContent = `SSID: ${network.wifi.ssid}`; + } + } + } + + const ethernetStatus = this.shadowRoot.getElementById('ethernet-status'); + const ethernetTile = this.shadowRoot.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 = this.shadowRoot.getElementById('lora-status'); + const loraTile = this.shadowRoot.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) { + const networkLatency = (requestEnd - requestStart) / 2; + const serverTime = new Date(serverTimeString); + const adjustedServerTime = new Date(serverTime.getTime() + networkLatency); + const clientTime = new Date(); + this.serverTimeOffset = adjustedServerTime.getTime() - clientTime.getTime(); + this.isTimeServerSynced = true; + } + this.updateCurrentTime(); + } + + updateCurrentTime() { + const now = new Date(); + let displayTime; + + if (this.isTimeServerSynced && this.serverTimeOffset !== null) { + displayTime = new Date(now.getTime() + this.serverTimeOffset); + } else { + displayTime = now; + } + + this.updateTimeDisplay('local', displayTime, false); + this.updateTimeDisplay('zulu', displayTime, true); + } + + updateTimeDisplay(type, date, isUtc) { + const timeElement = this.shadowRoot.getElementById(`${type}-time`); + const dateElement = this.shadowRoot.getElementById(`${type}-date`); + const mobileTimeElement = this.shadowRoot.getElementById(`mobile-${type}-time`); + const mobileDateElement = this.shadowRoot.getElementById(`mobile-${type}-date`); + + if (timeElement && dateElement) { + const displayDate = isUtc ? new Date(date.getTime()) : date; + + const timeStr = isUtc ? + displayDate.toUTCString().split(' ')[4] : + displayDate.toLocaleTimeString('en-US', { hour12: false }); + + const dateStr = isUtc ? + displayDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' }) : + displayDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + + timeElement.textContent = timeStr; + dateElement.textContent = dateStr; + + if (mobileTimeElement && mobileDateElement) { + mobileTimeElement.textContent = timeStr; + mobileDateElement.textContent = dateStr; + } + + if (this.isTimeServerSynced) { + timeElement.style.color = '#fff'; + dateElement.style.color = '#64748b'; + if (mobileTimeElement) mobileTimeElement.style.color = '#fff'; + if (mobileDateElement) mobileDateElement.style.color = '#64748b'; + } else { + timeElement.style.color = '#f59e0b'; + dateElement.style.color = '#f59e0b'; + if (mobileTimeElement) mobileTimeElement.style.color = '#f59e0b'; + if (mobileDateElement) mobileDateElement.style.color = '#f59e0b'; + } + } + } + + showOfflineStatus() { + this.serverTimeOffset = null; + this.isTimeServerSynced = false; + } + + startStatusUpdates() { + this.fetchStatus(); + setInterval(() => this.fetchStatus(), 60000); + } + + startTimeUpdates() { + this.updateCurrentTime(); + setInterval(() => this.updateCurrentTime(), 1000); + } + + initializeHeader() { + const expandToggle = this.shadowRoot.getElementById('expand-toggle'); + const expandIcon = this.shadowRoot.getElementById('expand-icon'); + const expandedStatus = this.shadowRoot.getElementById('expanded-status'); + + expandToggle.addEventListener('click', () => { + expandIcon.classList.toggle('expanded'); + expandedStatus.classList.toggle('visible'); + }); + } + + render() { + const title = this.getAttribute('title') || 'DASHBOARD'; + + this.shadowRoot.innerHTML = ` + + ${this.getHTML(title)} + `; + } + + getStyles() { + return ` + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .header { + background: rgba(15, 23, 42, 0.95); + backdrop-filter: blur(10px); + padding: 1.5rem 2rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + position: sticky; + top: 0; + z-index: 100; + } + + .header-content { + max-width: 1400px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr auto 1fr; + grid-template-areas: "logo status clocks"; + align-items: center; + gap: 1rem; + } + + .logo-section { + grid-area: logo; + display: flex; + align-items: center; + gap: 1rem; + justify-self: start; + } + + .logo { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: 0.05rem; + color: #fff; + } + + .hostname { + color: #94a3b8; + } + + .header-info { + grid-area: clocks; + justify-self: end; + } + + .network-status-wrapper { + grid-area: status; + display: flex; + flex-direction: column; + align-items: center; + justify-self: center; + } + + .network-status { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: center; + padding: 0.5rem 0; + } + + .network-item { + display: flex; + align-items: center; + justify-content: center; + padding: 0.4rem; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + cursor: pointer; + transition: all 0.2s ease; + } + + .network-item:hover { + background: rgba(255, 255, 255, 0.1); + } + + .network-icon { + width: 20px; + height: 20px; + stroke-width: 2; + fill: none; + transition: all 0.3s ease; + } + + .expand-toggle { + cursor: pointer; + padding: 0.4rem; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + } + + .expand-toggle:hover { + background: rgba(255, 255, 255, 0.1); + } + + .expand-icon { + width: 20px; + height: 20px; + stroke: #94a3b8; + stroke-width: 2; + fill: none; + transition: transform 0.3s ease; + } + + .expand-icon.expanded { + transform: rotate(180deg); + } + + .expanded-status { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + } + + .expanded-status.visible { + max-height: 300px; + } + + .status-tiles { + display: flex; + justify-content: flex-start; + gap: 1rem; + padding: 1.5rem 0; + max-width: 1400px; + margin: 0 auto; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .status-tiles::-webkit-scrollbar { + height: 8px; + } + + .status-tiles::-webkit-scrollbar-track { + background: transparent; + } + + .status-tiles::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + } + + .status-tiles::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); + } + + .status-tile { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + justify-content: space-between; + flex: 0 0 auto; + width: 170px; + } + + .tile-header { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .tile-icon { + width: 24px; + height: 24px; + stroke-width: 2; + } + + .tile-name { + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05rem; + color: #fff; + } + + .tile-info { + font-size: 0.85rem; + color: #94a3b8; + margin-top: 0.25rem; + } + + .tile-detail { + font-size: 0.75rem; + color: #64748b; + } + + .network-item.disconnected .network-icon, + .status-tile.disconnected .tile-icon { + stroke: #64748b; + } + + .status-tile.disconnected .tile-name { + color: #64748b; + } + + .status-tile.connected { + border-color: rgba(16, 185, 129, 0.3); + } + + .network-item.connected .network-icon, + .status-tile.connected .tile-icon { + stroke: #10b981; + } + + .status-tile.connected .tile-name { + color: #10b981; + } + + .time-display { + display: flex; + gap: 2rem; + font-size: 0.85rem; + } + + .time-block { + display: flex; + flex-direction: column; + align-items: flex-end; + } + + .time-label { + color: #94a3b8; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05rem; + margin-bottom: 0.2rem; + } + + .time-value { + font-weight: 600; + font-family: 'Courier New', monospace; + color: #fff; + } + + .time-date { + color: #64748b; + font-size: 0.75rem; + margin-top: 0.1rem; + } + + #temp { + color: #94a3b8; + } + + .tile-wide { + width: 360px; + } + + .system-metrics { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; + } + + .system-metric { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .metric-label { + font-size: 0.8rem; + font-weight: 500; + color: #cdd5e0; + width: 80px; + } + + .metric-label span { + font-size: 0.7rem; + color: #94a3b8; + margin-left: 4px; + } + + .progress-bar { + flex-grow: 1; + height: 14px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 6px; + position: relative; + overflow: hidden; + } + + .progress-bar-fill { + height: 100%; + background: #10b981; + border-radius: 6px; + transition: width 0.5s ease-in-out; + } + + .progress-bar-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #fff; + font-size: 0.8rem; + font-weight: 600; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); + } + + .home-button { + display: none; + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + border: none; + color: #fff; + } + + .home-button:hover { + background: rgba(255, 255, 255, 0.1); + } + + .home-button svg { + width: 20px; + height: 20px; + stroke: #fff; + stroke-width: 2; + fill: none; + } + + .mobile-time-display { + display: none; + } + + @media (max-width: 800px) { + .header-content { + grid-template-columns: 1fr 1fr; + grid-template-areas: + "logo clocks" + "status status"; + justify-items: start; + } + + .logo-section { + justify-self: start; + } + + .header-info { + justify-self: end; + } + + .time-display { + gap: 1rem; + } + + .network-status-wrapper { + justify-self: center; + grid-column: 1 / -1; + } + } + + @media (max-width: 650px) { + .header { + padding: 0.75rem 1rem; + } + + .header-content { + grid-template-columns: auto 1fr; + grid-template-areas: "home status"; + gap: 0.75rem; + align-items: center; + } + + .home-button { + display: flex; + grid-area: home; + } + + .home-button svg { + width: 22px; + height: 22px; + } + + .logo-section { + display: none; + } + + .header-info { + display: none; + } + + .network-status-wrapper { + grid-area: status; + justify-self: end; + grid-column: unset; + } + + .network-status { + gap: 0.4rem; + padding: 0; + } + + .network-item { + padding: 0.35rem; + } + + .network-icon { + width: 18px; + height: 18px; + } + + .expand-toggle { + padding: 0.35rem; + } + + .expand-icon { + width: 18px; + height: 18px; + } + + .expanded-status.visible { + max-height: 400px; + } + + .status-tiles { + padding: 1rem 0; + gap: 1rem; + } + + .mobile-time-display { + display: flex; + gap: 1rem; + flex: 0 0 auto; + } + + .mobile-time-block { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(16, 185, 129, 0.3); + border-radius: 12px; + padding: 1rem; + display: flex; + flex-direction: column; + flex: 0 0 auto; + width: 170px; + } + + .mobile-time-label { + color: #94a3b8; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05rem; + } + + .mobile-time-value { + font-weight: 600; + font-family: 'Courier New', monospace; + color: #10b981; + font-size: 1.1rem; + line-height: 1.2; + } + + .mobile-time-date { + color: #64748b; + font-size: 0.75rem; + } + } + `; + } + + getHTML(title) { + return ` +
+
+ + +
+
+ +
${this.hostname}
+
+
+ +
+
+
+
Local Time
+
00:00:00
+
Jan 1, 2025
+
+
+
Zulu / UTC
+
00:00:00
+
Jan 1, 2025
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+ + + + +
+
+ + + +
+
+
+
+ +
+
+
+
+
Local Time
+
00:00:00
+
Jan 1, 2025
+
+
Zulu / UTC
+
00:00:00
+
Jan 1, 2025
+
+
+ +
+
+ + + + + + + + +
System ?°C
+
+
+
+
CPU - Load
+
+
+
0%
+
+
+
+
Memory
+
+
+
0%
+
+
+
+
Disk
+
+
+
0%
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + +
GPS
+
+
+
39.2000, -49.0000
+
No Fix
+
+
+ +
+
+ + + + + +
LoRa
+
+
+
0 / 0 Online
+
Ch 0% Air 0%
+
+
+ +
+
+ + + + + + + + + + + + + + + + +
Mesh
+
+
+
Connected
+
900 Mhz -45 dBm
+
+
+ +
+
+ + + + + + +
WiFi
+
+
+
Access Point
+
SSID: ${this.hostname}
+
+
+ +
+
+ + + + +
Ethernet
+
+
+
Disconnected
+
No cable detected
+
+
+
+
+
+ `; + } +} + +customElements.define('notification-bar', DashboardHeader);