1042 lines
30 KiB
JavaScript
1042 lines
30 KiB
JavaScript
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 = `Alt: ${Math.round(gps.altitude)}m • Acc: ${Math.round(gps.accuracy)}m • ${gps.satellites} Sats`;
|
|
} else {
|
|
gpsInfo.textContent = gps.satellites > 0 ? 'Acquiring Fix...' : 'No Fix';
|
|
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 = `
|
|
<style>
|
|
${this.getStyles()}
|
|
</style>
|
|
${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 `
|
|
<div class="header">
|
|
<div class="header-content">
|
|
<button class="home-button" id="home-btn" onclick="window.location.href='/'">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
|
<polyline points="9 22 9 12 15 12 15 22"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="logo-section">
|
|
<div>
|
|
<div class="logo">${title}</div>
|
|
<div class="hostname">${this.hostname}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header-info">
|
|
<div class="time-display">
|
|
<div class="time-block">
|
|
<div class="time-label">Local Time</div>
|
|
<div class="time-value" id="local-time">00:00:00</div>
|
|
<div class="time-date" id="local-date">Jan 1, 2025</div>
|
|
</div>
|
|
<div class="time-block">
|
|
<div class="time-label">Zulu / UTC</div>
|
|
<div class="time-value" id="zulu-time">00:00:00</div>
|
|
<div class="time-date" id="zulu-date">Jan 1, 2025</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="network-status-wrapper">
|
|
<div class="network-status">
|
|
<div class="network-item disconnected" id="gps-status">
|
|
<svg class="network-icon" viewBox="0 0 24 24">
|
|
<g transform="translate(2,2) rotate(-45 12 12)">
|
|
<rect x="9.5" y="1" width="5" height="8.5" rx="1" ry="1" fill="transparent"/>
|
|
<rect x="1" y="6" width="6" height="4"/>
|
|
<rect x="17" y="6" width="6" height="4"/>
|
|
<line x1="1" y1="8" x2="23" y2="8"/>
|
|
<defs>
|
|
<clipPath id="halfCircle">
|
|
<rect x="0" y="5" width="100%" height="42%" />
|
|
</clipPath>
|
|
</defs>
|
|
<circle cx="12" cy="17.5" r="6px" clip-path="url(#halfCircle)" fill="transparent" />
|
|
<line x1="12" y1="12" x2="12" y2="17" stroke-linecap="round" />
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
<div class="network-item connected" id="lora-status">
|
|
<svg class="network-icon" viewBox="0 0 24 24">
|
|
<line x1="0" y1="18" x2="8" y2="6"/>
|
|
<line x1="8" y1="18" x2="16" y2="6"/>
|
|
<line x1="16" y1="6" x2="24" y2="18"/>
|
|
</svg>
|
|
</div>
|
|
<div class="network-item connected" id="mesh-status">
|
|
<svg class="network-icon" viewBox="0 0 24 24" fill="none">
|
|
<circle cx="5" cy="5" r="2" />
|
|
<line x1="6" y1="6" x2="15" y2="4" />
|
|
<line x1="6" y1="6" x2="20" y2="12" />
|
|
<line x1="6" y1="6" x2="13.5" y2="19" />
|
|
<line x1="6" y1="6" x2="5" y2="15" />
|
|
<circle cx="15.5" cy="3" r="2" />
|
|
<line x1="15" y1="4" x2="20" y2="12" />
|
|
<line x1="15" y1="4" x2="13.5" y2="19" />
|
|
<line x1="15" y1="4" x2="5" y2="15" />
|
|
<circle cx="21" cy="12" r="2" />
|
|
<line x1="20" y1="12" x2="13.5" y2="19" />
|
|
<circle cx="14" cy="20.5" r="2" />
|
|
<line x1="13.5" y1="19" x2="5" y2="15" />
|
|
<circle cx="4" cy="16" r="2" />
|
|
</svg>
|
|
</div>
|
|
<div class="network-item connected" id="wifi-status">
|
|
<svg class="network-icon" viewBox="0 0 24 24">
|
|
<path d="M5 12.55a11 11 0 0 1 14.08 0"/>
|
|
<path d="M1.42 9a16 16 0 0 1 21.16 0"/>
|
|
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
|
|
<line x1="12" y1="20" x2="12.01" y2="20"/>
|
|
</svg>
|
|
</div>
|
|
<div class="network-item disconnected" id="ethernet-status">
|
|
<svg class="network-icon" viewBox="0 0 24 24" fill="none">
|
|
<rect x="5" y="9" width="14" height="10" rx="1" ry="1"/>
|
|
<rect x="9" y="5" width="6" height="4" rx="0.5" ry="0.5"/>
|
|
</svg>
|
|
</div>
|
|
<div class="expand-toggle" id="expand-toggle">
|
|
<svg class="expand-icon" id="expand-icon" viewBox="0 0 24 24">
|
|
<polyline points="6 9 12 15 18 9"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expanded-status" id="expanded-status">
|
|
<div class="status-tiles">
|
|
<div class="mobile-time-display">
|
|
<div class="mobile-time-block">
|
|
<div class="mobile-time-label">Local Time</div>
|
|
<div class="mobile-time-value" id="mobile-local-time">00:00:00</div>
|
|
<div class="mobile-time-date" id="mobile-local-date">Jan 1, 2025</div>
|
|
<br>
|
|
<div class="mobile-time-label">Zulu / UTC</div>
|
|
<div class="mobile-time-value" id="mobile-zulu-time">00:00:00</div>
|
|
<div class="mobile-time-date" id="mobile-zulu-date">Jan 1, 2025</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-tile connected tile-wide" id="system-tile">
|
|
<div class="tile-header">
|
|
<svg class="tile-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
|
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
|
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
|
<line x1="10" y1="6" x2="10.01" y2="6"></line>
|
|
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
|
<line x1="10" y1="18" x2="10.01" y2="18"></line>
|
|
</svg>
|
|
<div class="tile-name">System <span id="temp">?°C</span></div>
|
|
</div>
|
|
<div class="system-metrics">
|
|
<div class="system-metric">
|
|
<div class="metric-label">CPU <span id="load">- Load</span></div>
|
|
<div class="progress-bar">
|
|
<div class="progress-bar-fill" id="cpu-usage-fill"></div>
|
|
<div class="progress-bar-text" id="cpu-usage-text">0%</div>
|
|
</div>
|
|
</div>
|
|
<div class="system-metric">
|
|
<div class="metric-label">Memory</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-bar-fill" id="memory-usage-fill"></div>
|
|
<div class="progress-bar-text" id="memory-usage-text">0%</div>
|
|
</div>
|
|
</div>
|
|
<div class="system-metric">
|
|
<div class="metric-label">Disk</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-bar-fill" id="disk-usage-fill"></div>
|
|
<div class="progress-bar-text" id="disk-usage-text">0%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-tile disconnected" id="gps-tile">
|
|
<div class="tile-header">
|
|
<svg class="tile-icon" viewBox="0 0 24 24">
|
|
<g transform="translate(2,2) rotate(-45 12 12)">
|
|
<rect x="9.5" y="1" width="5" height="8.5" rx="1" ry="1" fill="transparent" />
|
|
<rect x="1" y="6" width="6" height="4"/>
|
|
<rect x="17" y="6" width="6" height="4"/>
|
|
<line x1="1" y1="8" x2="23" y2="8"/>
|
|
<defs>
|
|
<clipPath id="halfCircle2">
|
|
<rect x="0" y="5" width="100%" height="42%" />
|
|
</clipPath>
|
|
</defs>
|
|
<circle cx="12" cy="17.5" r="6px" clip-path="url(#halfCircle2)" fill="transparent" />
|
|
<line x1="12" y1="12" x2="12" y2="17" stroke-linecap="round" />
|
|
</g>
|
|
</svg>
|
|
<div class="tile-name">GPS</div>
|
|
</div>
|
|
<div>
|
|
<div class="tile-info">No Fix</div>
|
|
<div class="tile-detail">0 Satellites</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-tile disconnected" id="lora-tile">
|
|
<div class="tile-header">
|
|
<svg class="tile-icon" viewBox="0 0 24 24">
|
|
<line x1="0" y1="18" x2="8" y2="6"/>
|
|
<line x1="8" y1="18" x2="16" y2="6"/>
|
|
<line x1="16" y1="6" x2="24" y2="18"/>
|
|
</svg>
|
|
<div class="tile-name">LoRa</div>
|
|
</div>
|
|
<div>
|
|
<div class="tile-info">0 / 0 Online</div>
|
|
<div class="tile-detail">Ch 0% Air 0%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-tile connected" id="mesh-tile">
|
|
<div class="tile-header">
|
|
<svg class="tile-icon" viewBox="0 0 24 24" fill="none">
|
|
<circle cx="5" cy="5" r="2" />
|
|
<line x1="6" y1="6" x2="15" y2="4" />
|
|
<line x1="6" y1="6" x2="20" y2="12" />
|
|
<line x1="6" y1="6" x2="13.5" y2="19" />
|
|
<line x1="6" y1="6" x2="5" y2="15" />
|
|
<circle cx="15.5" cy="3" r="2" />
|
|
<line x1="15" y1="4" x2="20" y2="12" />
|
|
<line x1="15" y1="4" x2="13.5" y2="19" />
|
|
<line x1="15" y1="4" x2="5" y2="15" />
|
|
<circle cx="21" cy="12" r="2" />
|
|
<line x1="20" y1="12" x2="13.5" y2="19" />
|
|
<circle cx="14" cy="20.5" r="2" />
|
|
<line x1="13.5" y1="19" x2="5" y2="15" />
|
|
<circle cx="4" cy="16" r="2" />
|
|
</svg>
|
|
<div class="tile-name">Mesh</div>
|
|
</div>
|
|
<div>
|
|
<div class="tile-info">Connected</div>
|
|
<div class="tile-detail">900 Mhz -45 dBm</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-tile connected" id="wifi-tile">
|
|
<div class="tile-header">
|
|
<svg class="tile-icon" viewBox="0 0 24 24">
|
|
<path d="M5 12.55a11 11 0 0 1 14.08 0"/>
|
|
<path d="M1.42 9a16 16 0 0 1 21.16 0"/>
|
|
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
|
|
<line x1="12" y1="20" x2="12.01" y2="20"/>
|
|
</svg>
|
|
<div class="tile-name">WiFi</div>
|
|
</div>
|
|
<div>
|
|
<div class="tile-info">Access Point</div>
|
|
<div class="tile-detail">SSID: ${this.hostname}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-tile disconnected" id="ethernet-tile">
|
|
<div class="tile-header">
|
|
<svg class="tile-icon" viewBox="0 0 24 24" fill="none">
|
|
<rect x="5" y="9" width="14" height="10" rx="1" ry="1"/>
|
|
<rect x="9" y="5" width="6" height="4" rx="0.5" ry="0.5"/>
|
|
</svg>
|
|
<div class="tile-name">Ethernet</div>
|
|
</div>
|
|
<div>
|
|
<div class="tile-info">Disconnected</div>
|
|
<div class="tile-detail">No cable detected</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('notification-bar', DashboardHeader);
|