This commit is contained in:
2026-04-05 20:27:43 -04:00
commit 32a690d85b
32 changed files with 5325 additions and 0 deletions
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+953
View File
@@ -0,0 +1,953 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>MRS Dashboard</title>
<link rel="icon" href="/favicon.png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
min-height: 100vh;
color: #fff;
overflow-x: hidden;
}
.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-section img {
height: 3rem;
width: auto;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 0.05rem;
}
.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: center;
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;
}
.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;
}
.time-date {
color: #64748b;
font-size: 0.75rem;
margin-top: 0.1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.search-bar {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1rem 1.5rem;
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.3s ease;
}
.search-bar:focus-within {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
}
.search-icon {
width: 20px;
height: 20px;
stroke: #64748b;
stroke-width: 2;
fill: none;
}
.search-input {
flex: 1;
background: none;
border: none;
color: #fff;
font-size: 1rem;
outline: none;
}
.search-input::placeholder {
color: #64748b;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1.5rem;
}
.app-item {
text-decoration: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
transition: all 0.2s ease;
position: relative;
}
.second-action {
position: absolute;
top: -6px;
right: -10px;
display: flex;
align-items: center;
padding: 5px;
border-radius: 50%;
background: #5a6b63;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.2s ease;
}
.second-action:hover {
background: #6a7b73;
transform: scale(1.05);
}
.second-action:active {
transform: scale(0.95);
}
.second-action svg {
stroke: white;
width: 18px;
height: 18px;
}
.app-badge svg {
width: 14px;
height: 14px;
stroke: #fff;
stroke-width: 2;
fill: none;
}
.app-icon-wrapper {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
position: relative;
}
.app-item:hover .app-icon-wrapper {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.app-item:active .app-icon-wrapper {
transform: scale(0.95);
}
#temp {
color: #94a3b8;
}
.app-icon {
width: 40px;
height: 40px;
stroke: #fff;
stroke-width: 1.5;
fill: none;
}
.app-name {
color: #fff;
font-size: 0.9rem;
font-weight: 500;
text-align: center;
line-height: 1.2;
}
.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);
}
.hidden {
display: none;
}
/* Responsive breakpoints */
@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;
}
.network-status-wrapper {
justify-self: center;
grid-column: 1 / -1;
}
}
@media (max-width: 550px) {
.header-content {
grid-template-columns: 1fr;
grid-template-areas:
"logo"
"clocks"
"status";
justify-items: center;
}
.logo-section {
justify-self: center;
}
.header-info {
justify-self: center;
}
.time-display {
justify-content: center;
}
.time-block {
align-items: center;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div class="logo-section">
<img src="/favicon.png" alt="Logo">
<div>
<div class="logo">DASHBOARD</div>
<div class="hostname">Hostname</div>
</div>
</div>
<div class="header-info">
<div class="time-display">
<div class="time-block">
<div class="time-label">Local</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="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="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 class="tile-name">GPS</div>
</div>
<div>
<div class="tile-info">39.2000, -49.0000</div>
<div class="tile-detail">No Fix</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: 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>
<div class="container">
<div class="search-bar">
<svg class="search-icon" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<input type="text" class="search-input" placeholder="Search apps..." id="search-input">
</div>
<div class="apps-grid" id="apps-grid">
<a href=":1000" class="app-item" data-name="AI Assistant">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<rect x="6" y="6" width="12" height="12" rx="2" ry="2"/>
<line x1="2" y1="9" x2="6" y2="9"/>
<line x1="2" y1="12" x2="6" y2="12"/>
<line x1="2" y1="15" x2="6" y2="15"/>
<line x1="18" y1="9" x2="22" y2="9"/>
<line x1="18" y1="12" x2="22" y2="12"/>
<line x1="18" y1="15" x2="22" y2="15"/>
</svg>
</div>
<div class="app-name">AI Assistant</div>
</a>
<a href=":1100" class="app-item" data-name="ATAK">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<div class="second-action">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
<line x1="12" y1="0" x2="12" y2="24" />
<line x1="4" y1="11" x2="12" y2="21" />
<line x1="20" y1="11" x2="12" y2="21" />
<line x1="1" y1="22" x2="23" y2="22" />
</svg>
</div>
</div>
<div class="app-name">ATAK</div>
</a>
<a href=":1200" class="app-item" data-name="Remote Desktop">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
<div class="second-action">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
<line x1="12" y1="0" x2="12" y2="24" />
<line x1="4" y1="11" x2="12" y2="21" />
<line x1="20" y1="11" x2="12" y2="21" />
<line x1="1" y1="22" x2="23" y2="22" />
</svg>
</div>
</div>
<div class="app-name">Desktop</div>
</a>
<a href=":1300" class="app-item" data-name="File Browser">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
</div>
<div class="app-name">File Browser</div>
</a>
<a href=":1400" class="app-item" data-name="Library">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
</svg>
<div class="second-action" onclick="openKiwixm(event)">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
<circle cx="12" cy="12" r="6.5" stroke-width="5" />
<line x1="12" y1="7" x2="12" y2=".5" />
<line x1="12" y1="17" x2="12" y2="23.5" />
<line x1="0.5" y1="12" x2="7" y2="12" />
<line x1="17" y1="12" x2="23.5" y2="12" />
<line x1="15" y1="15" x2="20" y2="20" />
<line x1="4" y1="4" x2="9" y2="9" />
<line x1="20" y1="4" x2="15" y2="9" />
<line x1="4" y1="20" x2="9" y2="15" />
</svg>
</div>
</div>
<div class="app-name">Library</div>
</a>
<a href=":1500" class="app-item" data-name="Maps">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<path d="M3 7l6-3 6 3 6-3v13l-6 3-6-3-6 3z"/>
<line x1="9" y1="4" x2="9" y2="17"/>
<line x1="15" y1="7" x2="15" y2="20"/>
</svg>
</div>
<div class="app-name">Maps</div>
</a>
<a href=":1600" class="app-item" data-name="Meshtastic">
<div class="app-icon-wrapper">
<svg class="app-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="second-action">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
<line x1="12" y1="0" x2="12" y2="24" />
<line x1="4" y1="11" x2="12" y2="21" />
<line x1="20" y1="11" x2="12" y2="21" />
<line x1="1" y1="22" x2="23" y2="22" />
</svg>
</div>
</div>
<div class="app-name">Meshtastic</div>
</a>
<a href=":1700" class="app-item" data-name="Messages">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
<div class="second-action">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
<line x1="12" y1="0" x2="12" y2="24" />
<line x1="4" y1="11" x2="12" y2="21" />
<line x1="20" y1="11" x2="12" y2="21" />
<line x1="1" y1="22" x2="23" y2="22" />
</svg>
</div>
</div>
<div class="app-name">Messages</div>
</a>
<a href=":1800" class="app-item" data-name="Pentesting">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</div>
<div class="app-name">Pentesting</div>
</a>
<a href=":1900" class="app-item" data-name="Software Defined Radio">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<g transform="translate(0,-2)">
<circle cx="12" cy="12" r="1.5"/>
<path d="M16.24 7.76a6 6 0 0 1 0 8.49"/>
<path d="M7.76 16.24a6 6 0 0 1 0-8.49"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
<path d="M4.93 19.07a10 10 0 0 1 0-14.14"/>
<line x1="12" y1="10" x2="12" y2="24"/>
</g>
</svg>
<div class="second-action">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
<line x1="12" y1="0" x2="12" y2="24" />
<line x1="4" y1="11" x2="12" y2="21" />
<line x1="20" y1="11" x2="12" y2="21" />
<line x1="1" y1="22" x2="23" y2="22" />
</svg>
</div>
</div>
<div class="app-name">SDR</div>
</a>
<a href=":2000" class="app-item" data-name="Speed Test">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<polygon points="8 1 16 1 14 15 10 15"/>
<circle cx="12" cy="20" r="3"/>
</svg>
</div>
<div class="app-name">Situation Room</div>
</a>
<a href=":2100" class="app-item" data-name="Speed Test">
<div class="app-icon-wrapper">
<svg class="app-icon" viewBox="0 0 24 24">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
</div>
<div class="app-name">Speed Test</div>
</a>
</div>
</div>
<script src="index.js"></script>
</body>
</html>
+368
View File
@@ -0,0 +1,368 @@
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();
});
});
+19
View File
@@ -0,0 +1,19 @@
{
"name": "MRS",
"short_name": "MRS",
"description": "Multipurpose Radio Server",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/favicon.png",
"sizes": "512x512",
"type": "image/png"
}
],
"lang": "en-US",
"scope": "/"
}