Added new codebook page
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
|||||||
.idea
|
.idea
|
||||||
node_modules
|
client/node_modules
|
||||||
|
|||||||
1170
client/package-lock.json
generated
Normal file
2
code/package.json → client/package.json
Executable file → Normal file
@@ -11,6 +11,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Dashboard",
|
"description": "Dashboard",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dotenv": "^17.4.1",
|
||||||
|
"express": "^5.2.1",
|
||||||
"node-gpsd": "^0.3.4"
|
"node-gpsd": "^0.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
284
client/public/code-book-print.html
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Code Book</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
margin: 0.5in;
|
||||||
|
}
|
||||||
|
.page-break {
|
||||||
|
page-break-before: always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 14pt;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid #000;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 12pt;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-value {
|
||||||
|
font-weight: bold;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 0.15rem 0.25rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #ddd;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-column {
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-pair {
|
||||||
|
margin: 0.1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tables {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table-wrapper {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table-wrapper h3 {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table-wrapper table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-list {
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-item {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-cipher {
|
||||||
|
word-break: break-all;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Page 1: Key & ASCII -->
|
||||||
|
<div>
|
||||||
|
<div class="key-section">
|
||||||
|
<h2>Encryption Key</h2>
|
||||||
|
<div class="key-value" id="print-key"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>ASCII Reference Table</h2>
|
||||||
|
<div id="ascii-print"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page 2+: OTP Pairs -->
|
||||||
|
<div class="page-break">
|
||||||
|
<h1>ONE-TIME PASSWORDS (HOTP)</h1>
|
||||||
|
<div class="otp-grid" id="otp-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page 3+: Auth Tables -->
|
||||||
|
<div class="page-break">
|
||||||
|
<h1>AUTHENTICATION TABLES</h1>
|
||||||
|
<div class="auth-tables" id="auth-tables"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page 4+: Pad Keys -->
|
||||||
|
<div class="page-break">
|
||||||
|
<h1>ONE-TIME PAD</h1>
|
||||||
|
<div class="pad-list" id="pad-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
||||||
|
<script src="encryption.js"></script>
|
||||||
|
<script>
|
||||||
|
function getKey() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get('key') || localStorage.getItem('encryption_key') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderASCIITable() {
|
||||||
|
let html = '<table class="ascii-table">';
|
||||||
|
html += '<tr><th>Dec</th><th>Hex</th><th>Char</th><th>Dec</th><th>Hex</th><th>Char</th><th>Dec</th><th>Hex</th><th>Char</th><th>Dec</th><th>Hex</th><th>Char</th></tr>';
|
||||||
|
|
||||||
|
for (let row = 0; row < 32; row++) {
|
||||||
|
html += '<tr>';
|
||||||
|
for (let col = 0; col < 4; col++) {
|
||||||
|
const code = row + (col * 32);
|
||||||
|
if (code < 128) {
|
||||||
|
const hex = code.toString(16).toUpperCase().padStart(2, '0');
|
||||||
|
let char = String.fromCharCode(code);
|
||||||
|
|
||||||
|
if (code < 32) {
|
||||||
|
const controlChars = ['NUL','SOH','STX','ETX','EOT','ENQ','ACK','BEL','BS','TAB','LF','VT','FF','CR','SO','SI',
|
||||||
|
'DLE','DC1','DC2','DC3','DC4','NAK','SYN','ETB','CAN','EM','SUB','ESC','FS','GS','RS','US'];
|
||||||
|
char = controlChars[code];
|
||||||
|
} else if (code === 32) {
|
||||||
|
char = 'SP';
|
||||||
|
} else if (code === 127) {
|
||||||
|
char = 'DEL';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<td>${code}</td><td>${hex}</td><td><b>${char}</b></td>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</table>';
|
||||||
|
document.getElementById('ascii-print').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOTPPairs(key) {
|
||||||
|
const columns = [[], [], [], [], []];
|
||||||
|
|
||||||
|
for (let i = 0; i < 250; i++) {
|
||||||
|
const nonce = i.toString().padStart(3, '0');
|
||||||
|
const code = generateHOTP(key, nonce);
|
||||||
|
const pair = `${nonce} ${code}`;
|
||||||
|
columns[Math.floor(i / 50)].push(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (let col of columns) {
|
||||||
|
html += '<div class="otp-column">';
|
||||||
|
for (let pair of col) {
|
||||||
|
html += `<div class="otp-pair">${pair}</div>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('otp-grid').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAuthTables(key) {
|
||||||
|
let html = '';
|
||||||
|
const rows = 'NOPQRSTUVWXYZ'.split('');
|
||||||
|
const cols = 'ABCDEFGHIJKLM'.split('');
|
||||||
|
|
||||||
|
for (let tableIndex = 0; tableIndex < 26; tableIndex++) {
|
||||||
|
const table = generateAuthTable(key, tableIndex);
|
||||||
|
|
||||||
|
html += '<div class="auth-table-wrapper">';
|
||||||
|
html += `<h3>${PHONETIC[tableIndex]}</h3>`;
|
||||||
|
html += '<table>';
|
||||||
|
html += '<tr><th></th>';
|
||||||
|
for (let col of cols) {
|
||||||
|
html += `<th>${col}</th>`;
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
html += `<tr><th>${rows[i]}</th>`;
|
||||||
|
for (let j = 0; j < cols.length; j++) {
|
||||||
|
html += `<td>${table[i][j]}</td>`;
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
html += '</table>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('auth-tables').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPadKeys(key) {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const padKey = (i * 1000).toString().padStart(5, '0');
|
||||||
|
const cipher = generatePadDisplay(key, padKey);
|
||||||
|
|
||||||
|
html += '<div class="pad-item">';
|
||||||
|
html += `<div><b>Key: ${padKey}</b></div>`;
|
||||||
|
html += `<div class="pad-cipher">${cipher}</div>`;
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('pad-list').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
const key = getKey();
|
||||||
|
if (!key) {
|
||||||
|
alert('No encryption key found! Redirecting to main page...');
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
document.getElementById('print-key').textContent = key;
|
||||||
|
renderASCIITable();
|
||||||
|
renderOTPPairs(key);
|
||||||
|
renderAuthTables(key);
|
||||||
|
renderPadKeys(key);
|
||||||
|
|
||||||
|
// Auto-print after everything loads
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
669
client/public/code-book.html
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Code Book</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-section img {
|
||||||
|
height: 3rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 0.75rem 2.5rem 0.75rem 0.75rem;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23fff' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.75rem center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option {
|
||||||
|
background: #1e293b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.save {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border-color: rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.save:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-description {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotp-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotp-row button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotp-row input,
|
||||||
|
.hotp-code {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotp-code {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border-color: rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table th,
|
||||||
|
.auth-table td {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table th {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table td.highlight {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-table th.highlight {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-table {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-table table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-table th,
|
||||||
|
.ascii-table td {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 0.4rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-table th {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-list {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-item {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper label {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-box {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo-section">
|
||||||
|
<img src="/favicon.png" alt="Logo">
|
||||||
|
<div class="logo">ENCRYPTION TOOLS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="key-section">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<label>Encryption Key</label>
|
||||||
|
<div class="key-input-group">
|
||||||
|
<input type="text" id="master-key" placeholder="Enter encryption key">
|
||||||
|
<button onclick="generateNewKey()" class="danger">Random</button>
|
||||||
|
<button onclick="save()" class="save">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HOTP Section -->
|
||||||
|
<div class="tool-section">
|
||||||
|
<div class="tool-title">One Time Password (HOTP)</div>
|
||||||
|
<details style="margin: 1rem 0; color: #888;">
|
||||||
|
<summary>Description</summary>
|
||||||
|
|
||||||
|
<div style="padding-top: 0.5rem;">
|
||||||
|
<p>Use a 3-digit challenge to generate & verify a corresponding 3-letter response.</p>
|
||||||
|
<p>Provides higher entropy and security than authentication tables while being easier to use.</p>
|
||||||
|
|
||||||
|
<div style="margin-top: 0.75rem;">
|
||||||
|
<strong>Pre-Authenticate:</strong>
|
||||||
|
<div class="example-box">
|
||||||
|
<p><strong>A->B:</strong> AUTH 123 ABC</p>
|
||||||
|
<p><strong>B->A:</strong> ACK. AUTH 456 DEF</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 0.75rem;">
|
||||||
|
<strong>Challenge-Response:</strong>
|
||||||
|
<div class="example-box">
|
||||||
|
<p><strong>A->B:</strong> CONFIRM <span style="text-decoration: underline;">123</span></p>
|
||||||
|
<p><strong>B->A:</strong> ACK <span style="text-decoration: underline;">ABC</span>. CONFIRM <span style="text-decoration: underline;">456</span></p>
|
||||||
|
<p><strong>A->B:</strong> ACK <span style="text-decoration: underline;">DEF</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div class="hotp-row">
|
||||||
|
<button onclick="randomHOTPNonce()">Random</button>
|
||||||
|
<input type="number" id="hotp-nonce" placeholder="000" min="0" max="999" oninput="updateHOTP()">
|
||||||
|
<div class="hotp-code" id="hotp-display">---</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth Table Section -->
|
||||||
|
<div class="tool-section">
|
||||||
|
<div class="tool-title">Authentication Tables</div>
|
||||||
|
<details style="margin: 1rem 0; color: #888;">
|
||||||
|
<summary>Description</summary>
|
||||||
|
|
||||||
|
<div style="padding-top: 0.5rem;">
|
||||||
|
<p>Challenge-response tables for authentication.</p>
|
||||||
|
|
||||||
|
<div style="margin-top: 0.75rem;">
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<div class="example-box">
|
||||||
|
<p><strong>A->B:</strong> CONFIRM <span style="text-decoration: underline;">CEY</span> <span style="font-style: italic">(Table: Charlie, Column: E, Row: Y)</span></p>
|
||||||
|
<p><strong>B->A:</strong> ACK <span style="text-decoration: underline;">7</span>. CONFIRM <span style="text-decoration: underline;">AIT</span> <span style="font-style: italic">(Table: Alpha, Column: I, Row: T)</span></p>
|
||||||
|
<p><strong>A->B:</strong> ACK <span style="text-decoration: underline;">T</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div>
|
||||||
|
<label>Auth Table</label>
|
||||||
|
<select id="table-select" onchange="renderAuthTable()"></select>
|
||||||
|
</div>
|
||||||
|
<div class="auth-table" id="auth-table-display"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message Encoder Section -->
|
||||||
|
<div class="tool-section">
|
||||||
|
<div class="tool-title">One-Time Pad</div>
|
||||||
|
<details style="margin: 1rem 0; color: #888;">
|
||||||
|
<summary>Description</summary>
|
||||||
|
|
||||||
|
<div style="padding-top: 0.5rem;">
|
||||||
|
<p>Use a 5-digit key to generate a corresponding fixed (paper friendly) or streamed (more secure) cipher.</p>
|
||||||
|
<p>Cipher values are added to the ASCII encoded message to create encrypted message.</p>
|
||||||
|
|
||||||
|
<div style="margin-top: 0.75rem;">
|
||||||
|
<strong>Encoding:</strong>
|
||||||
|
<div class="example-box">
|
||||||
|
Message: "HI"<br>
|
||||||
|
Pad Key: 12345<br>
|
||||||
|
Cipher: 42 85 28 71 14 ...<br><br>
|
||||||
|
|
||||||
|
H = ASCII 72 → 72 + 42 = 114 → Hex 72<br>
|
||||||
|
I = ASCII 73 → 73 + 85 = 158 → Hex 9E<br><br>
|
||||||
|
|
||||||
|
Encoded: 729E
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 0.75rem;">
|
||||||
|
<strong>Decoding:</strong>
|
||||||
|
<div class="example-box">
|
||||||
|
Encoded: 729E<br>
|
||||||
|
Pad Key: 12345<br>
|
||||||
|
Cipher: 42 85 28 71 14 ...<br><br>
|
||||||
|
|
||||||
|
72 (Hex) = 114 → 114 - 42 = 72 → ASCII 'H'<br>
|
||||||
|
9E (Hex) = 158 → 158 - 85 = 73 → ASCII 'I'<br><br>
|
||||||
|
|
||||||
|
Decoded: "HI"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<div class="key-input-group">
|
||||||
|
<button onclick="randomPadKey()">Random</button>
|
||||||
|
<input type="text" id="pad-key" placeholder="00000" maxlength="5" oninput="updatePad()" style="max-width: 100px; text-align: center;">
|
||||||
|
<div class="checkbox-wrapper">
|
||||||
|
<input type="checkbox" id="stream-cipher" checked>
|
||||||
|
<label for="stream-cipher">Stream Cipher</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pad-list" id="pad-display"></div>
|
||||||
|
<hr>
|
||||||
|
<div style="margin-top: 1.5rem;">
|
||||||
|
<textarea id="message-input" placeholder="Input"></textarea>
|
||||||
|
<div class="button-group">
|
||||||
|
<button style="flex: 1 0 0;" onclick="handleEncode()">Encode</button>
|
||||||
|
<button style="flex: 1 0 0;" onclick="handleDecode()">Decode</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="message-output" placeholder="Output" readonly style="margin-top: 1.5rem;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ASCII Table Section -->
|
||||||
|
<div class="tool-section">
|
||||||
|
<div class="tool-title">ASCII Reference Table</div>
|
||||||
|
<div class="ascii-table" id="ascii-table-display"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
|
||||||
|
<script src="encryption.js"></script>
|
||||||
|
<script>
|
||||||
|
function save() {
|
||||||
|
window.open('/code-book-print.html', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMasterKey() {
|
||||||
|
const key = localStorage.getItem('encryption_key');
|
||||||
|
if (key) {
|
||||||
|
document.getElementById('master-key').value = key;
|
||||||
|
initializeTools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMasterKey() {
|
||||||
|
const key = document.getElementById('master-key').value;
|
||||||
|
if (key) {
|
||||||
|
localStorage.setItem('encryption_key', key);
|
||||||
|
initializeTools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateNewKey() {
|
||||||
|
if (confirm('⚠️ This will replace your current encryption key. Its recommended you back it up before you continue.')) {
|
||||||
|
const key = CryptoJS.lib.WordArray.random(32).toString(CryptoJS.enc.Hex);
|
||||||
|
document.getElementById('master-key').value = key;
|
||||||
|
saveMasterKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHOTP() {
|
||||||
|
const key = document.getElementById('master-key').value;
|
||||||
|
const nonce = document.getElementById('hotp-nonce').value.padStart(3, '0');
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
const code = generateHOTP(key, nonce);
|
||||||
|
document.getElementById('hotp-display').textContent = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomHOTPNonce() {
|
||||||
|
const nonce = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
|
||||||
|
document.getElementById('hotp-nonce').value = nonce;
|
||||||
|
updateHOTP();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAuthTable() {
|
||||||
|
const key = document.getElementById('master-key').value;
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
const tableIndex = parseInt(document.getElementById('table-select').value);
|
||||||
|
const table = generateAuthTable(key, tableIndex);
|
||||||
|
const rows = 'NOPQRSTUVWXYZ'.split('');
|
||||||
|
const cols = 'ABCDEFGHIJKLM'.split('');
|
||||||
|
|
||||||
|
let html = '<table>';
|
||||||
|
html += '<tr><th></th>';
|
||||||
|
for (let col of cols) {
|
||||||
|
html += `<th class="col-header" data-col="${col}">${col}</th>`;
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
html += `<tr><th class="row-header" data-row="${rows[i]}">${rows[i]}</th>`;
|
||||||
|
for (let j = 0; j < cols.length; j++) {
|
||||||
|
html += `<td data-col="${cols[j]}" data-row="${rows[i]}">${table[i][j]}</td>`;
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
html += '</table>';
|
||||||
|
|
||||||
|
document.getElementById('auth-table-display').innerHTML = html;
|
||||||
|
|
||||||
|
// Add hover listeners
|
||||||
|
const authTable = document.querySelector('#auth-table-display table');
|
||||||
|
const cells = authTable.querySelectorAll('td');
|
||||||
|
|
||||||
|
cells.forEach(cell => {
|
||||||
|
cell.addEventListener('mouseenter', function() {
|
||||||
|
const col = this.getAttribute('data-col');
|
||||||
|
const row = this.getAttribute('data-row');
|
||||||
|
|
||||||
|
authTable.querySelectorAll(`[data-col="${col}"]`).forEach(el => el.classList.add('highlight'));
|
||||||
|
authTable.querySelectorAll(`[data-row="${row}"]`).forEach(el => el.classList.add('highlight'));
|
||||||
|
});
|
||||||
|
|
||||||
|
cell.addEventListener('mouseleave', function() {
|
||||||
|
authTable.querySelectorAll('.highlight').forEach(el => el.classList.remove('highlight'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateTableSelect() {
|
||||||
|
const select = document.getElementById('table-select');
|
||||||
|
select.innerHTML = '';
|
||||||
|
for (let i = 0; i < 26; i++) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = i;
|
||||||
|
option.textContent = PHONETIC[i];
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePad() {
|
||||||
|
const key = document.getElementById('master-key').value;
|
||||||
|
const padKey = document.getElementById('pad-key').value.padStart(5, '0');
|
||||||
|
if (!key) return;
|
||||||
|
const pad = generatePadDisplay(key, padKey);
|
||||||
|
document.getElementById('pad-display').innerHTML = `<div class="pad-item"><strong>Cipher:</strong> ${pad}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomPadKey() {
|
||||||
|
const key = Math.floor(Math.random() * 100000).toString().padStart(5, '0');
|
||||||
|
document.getElementById('pad-key').value = key;
|
||||||
|
updatePad();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEncode() {
|
||||||
|
const message = document.getElementById('message-input').value;
|
||||||
|
const key = document.getElementById('master-key').value;
|
||||||
|
const padKey = document.getElementById('pad-key').value.padStart(5, '0');
|
||||||
|
const useStream = document.getElementById('stream-cipher').checked;
|
||||||
|
if (!key || !message) return;
|
||||||
|
|
||||||
|
const encoded = encodeMessage(message, key, padKey, useStream);
|
||||||
|
document.getElementById('message-output').value = encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDecode() {
|
||||||
|
const encoded = document.getElementById('message-input').value.replace(/\s/g, '');
|
||||||
|
const key = document.getElementById('master-key').value;
|
||||||
|
const padKey = document.getElementById('pad-key').value.padStart(5, '0');
|
||||||
|
const useStream = document.getElementById('stream-cipher').checked;
|
||||||
|
if (!key || !encoded) return;
|
||||||
|
|
||||||
|
const decoded = decodeMessage(encoded, key, padKey, useStream);
|
||||||
|
document.getElementById('message-output').value = decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderASCIITable() {
|
||||||
|
const cols = 16;
|
||||||
|
const rows = 8;
|
||||||
|
|
||||||
|
let html = '<table>';
|
||||||
|
html += '<tr><th>Dec</th><th>Hex</th><th>Char</th><th>Dec</th><th>Hex</th><th>Char</th><th>Dec</th><th>Hex</th><th>Char</th><th>Dec</th><th>Hex</th><th>Char</th></tr>';
|
||||||
|
|
||||||
|
for (let row = 0; row < 32; row++) {
|
||||||
|
html += '<tr>';
|
||||||
|
for (let col = 0; col < 4; col++) {
|
||||||
|
const code = row + (col * 32);
|
||||||
|
if (code < 128) {
|
||||||
|
const hex = code.toString(16).toUpperCase().padStart(2, '0');
|
||||||
|
let char = String.fromCharCode(code);
|
||||||
|
|
||||||
|
// Handle non-printable characters
|
||||||
|
if (code < 32) {
|
||||||
|
const controlChars = ['NUL','SOH','STX','ETX','EOT','ENQ','ACK','BEL','BS','TAB','LF','VT','FF','CR','SO','SI',
|
||||||
|
'DLE','DC1','DC2','DC3','DC4','NAK','SYN','ETB','CAN','EM','SUB','ESC','FS','GS','RS','US'];
|
||||||
|
char = controlChars[code];
|
||||||
|
} else if (code === 32) {
|
||||||
|
char = 'SP';
|
||||||
|
} else if (code === 127) {
|
||||||
|
char = 'DEL';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<td>${code}</td><td>${hex}</td><td style="font-weight: bolder">${char}</td>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</table>';
|
||||||
|
document.getElementById('ascii-table-display').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeTools() {
|
||||||
|
updateHOTP();
|
||||||
|
renderAuthTable();
|
||||||
|
if (document.getElementById('pad-key').value) {
|
||||||
|
updatePad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
document.getElementById('master-key').addEventListener('input', saveMasterKey);
|
||||||
|
|
||||||
|
// Initialize on load
|
||||||
|
loadMasterKey();
|
||||||
|
populateTableSelect();
|
||||||
|
renderASCIITable();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
code/public/downloads/ATAK_5.4.0.16.apk → client/public/downloads/ATAK_5.4.0.16.apk
Executable file → Normal file
0
code/public/downloads/Element.apk → client/public/downloads/Element.apk
Executable file → Normal file
0
code/public/downloads/Meshtastic_2.7.1.apk → client/public/downloads/Meshtastic_2.7.1.apk
Executable file → Normal file
0
code/public/downloads/Remote Desktop 8_8.1.82.445.apk → client/public/downloads/Remote Desktop 8_8.1.82.445.apk
Executable file → Normal file
0
code/public/downloads/SDRangel_7.21.3.1.apk → client/public/downloads/SDRangel_7.21.3.1.apk
Executable file → Normal file
113
client/public/encryption.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// encryption.js
|
||||||
|
|
||||||
|
// Phonetic alphabet for table names
|
||||||
|
const PHONETIC = ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel',
|
||||||
|
'India', 'Juliet', 'Kilo', 'Lima', 'Mike', 'November', 'Oscar', 'Papa',
|
||||||
|
'Quebec', 'Romeo', 'Sierra', 'Tango', 'Uniform', 'Victor', 'Whiskey',
|
||||||
|
'Xray', 'Yankee', 'Zulu'];
|
||||||
|
|
||||||
|
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
|
// Seeded RNG using SHA256
|
||||||
|
class SeededRandom {
|
||||||
|
constructor(seed) {
|
||||||
|
this.seed = seed;
|
||||||
|
this.counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
const hash = CryptoJS.SHA256(this.seed + ':' + this.counter);
|
||||||
|
this.counter++;
|
||||||
|
const hex = hash.toString(CryptoJS.enc.Hex);
|
||||||
|
return parseInt(hex.substring(0, 8), 16) / 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextInt(min, max) {
|
||||||
|
return Math.floor(this.next() * (max - min)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
choice(arr) {
|
||||||
|
return arr[this.nextInt(0, arr.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HOTP Generation - 3 digit nonce -> 3 letter code
|
||||||
|
function generateHOTP(secret, nonce) {
|
||||||
|
const rng = new SeededRandom(secret + ':hotp:' + nonce);
|
||||||
|
let code = '';
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
code += rng.choice(LETTERS.split(''));
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth Tables
|
||||||
|
function generateAuthTable(seed, tableIndex) {
|
||||||
|
const rng = new SeededRandom(seed + ':table:' + tableIndex);
|
||||||
|
const rows = 'NOPQRSTUVWXYZ'.split('');
|
||||||
|
const cols = 'ABCDEFGHIJKLM'.split('');
|
||||||
|
|
||||||
|
const table = [];
|
||||||
|
for (let row of rows) {
|
||||||
|
const rowData = [];
|
||||||
|
for (let col of cols) {
|
||||||
|
rowData.push(rng.choice(CHARS.split('')));
|
||||||
|
}
|
||||||
|
table.push(rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-Time Pad (for encode/decode)
|
||||||
|
function generatePad(seed, padKey, length = 60) {
|
||||||
|
const rng = new SeededRandom(seed + ':pad:' + padKey);
|
||||||
|
const pad = [];
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
pad.push(rng.nextInt(0, 256));
|
||||||
|
}
|
||||||
|
return pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePadDisplay(seed, padKey) {
|
||||||
|
const rng = new SeededRandom(seed + ':pad:' + padKey);
|
||||||
|
const groups = [];
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const num = rng.nextInt(0, 100000).toString().padStart(5, '0');
|
||||||
|
groups.push(num);
|
||||||
|
}
|
||||||
|
return groups.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeMessage(message, key, padKey, useStream = true) {
|
||||||
|
const messageBytes = new TextEncoder().encode(message);
|
||||||
|
const padLength = useStream ? messageBytes.length : 60;
|
||||||
|
const pad = generatePad(key, padKey, padLength);
|
||||||
|
|
||||||
|
const encoded = [];
|
||||||
|
for (let i = 0; i < messageBytes.length; i++) {
|
||||||
|
const padByte = pad[i % pad.length];
|
||||||
|
encoded.push(messageBytes[i] ^ padByte);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(encoded).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeMessage(encoded, key, padKey, useStream = true) {
|
||||||
|
const bytes = [];
|
||||||
|
for (let i = 0; i < encoded.length; i += 2) {
|
||||||
|
bytes.push(parseInt(encoded.substr(i, 2), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
const padLength = useStream ? bytes.length : 60;
|
||||||
|
const pad = generatePad(key, padKey, padLength);
|
||||||
|
|
||||||
|
const decoded = [];
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
const padByte = pad[i % pad.length];
|
||||||
|
decoded.push(bytes[i] ^ padByte);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextDecoder().decode(new Uint8Array(decoded));
|
||||||
|
}
|
||||||
0
code/public/favicon.png → client/public/favicon.png
Executable file → Normal file
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
1
client/public/icons/cog.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
client/public/icons/download.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M352 96C352 78.3 337.7 64 320 64C302.3 64 288 78.3 288 96L288 306.7L246.6 265.3C234.1 252.8 213.8 252.8 201.3 265.3C188.8 277.8 188.8 298.1 201.3 310.6L297.3 406.6C309.8 419.1 330.1 419.1 342.6 406.6L438.6 310.6C451.1 298.1 451.1 277.8 438.6 265.3C426.1 252.8 405.8 252.8 393.3 265.3L352 306.7L352 96zM160 384C124.7 384 96 412.7 96 448L96 480C96 515.3 124.7 544 160 544L480 544C515.3 544 544 515.3 544 480L544 448C544 412.7 515.3 384 480 384L433.1 384L376.5 440.6C345.3 471.8 294.6 471.8 263.4 440.6L206.9 384L160 384zM464 440C477.3 440 488 450.7 488 464C488 477.3 477.3 488 464 488C450.7 488 440 477.3 440 464C440 450.7 450.7 440 464 440z"/></svg>
|
||||||
|
After Width: | Height: | Size: 872 B |
1
client/public/icons/guage.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg class="app-icon" style="fill: #fff;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320zM352 160C352 142.3 337.7 128 320 128C302.3 128 288 142.3 288 160C288 177.7 302.3 192 320 192C337.7 192 352 177.7 352 160zM320 480C355.3 480 384 451.3 384 416C384 399.8 378 384.9 368 373.7L437.5 234.8C443.4 222.9 438.6 208.5 426.8 202.6C415 196.7 400.5 201.5 394.6 213.3L325.1 352.2C323.4 352.1 321.7 352 320 352C284.7 352 256 380.7 256 416C256 451.3 284.7 480 320 480zM240 208C240 190.3 225.7 176 208 176C190.3 176 176 190.3 176 208C176 225.7 190.3 240 208 240C225.7 240 240 225.7 240 208zM160 352C177.7 352 192 337.7 192 320C192 302.3 177.7 288 160 288C142.3 288 128 302.3 128 320C128 337.7 142.3 352 160 352zM512 320C512 302.3 497.7 288 480 288C462.3 288 448 302.3 448 320C448 337.7 462.3 352 480 352C497.7 352 512 337.7 512 320z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
client/public/icons/open-sight.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
81
code/public/index.html → client/public/index.html
Executable file → Normal file
@@ -342,6 +342,9 @@
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.second-action:hover {
|
.second-action:hover {
|
||||||
@@ -353,7 +356,7 @@
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.second-action svg {
|
.second-action img {
|
||||||
stroke: white;
|
stroke: white;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
@@ -784,17 +787,25 @@
|
|||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="second-action">
|
<div class="second-action">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
<img class="app-icon" src="/icons/download.svg" alt="Download" />
|
||||||
<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>
|
</div>
|
||||||
<div class="app-name">ATAK</div>
|
<div class="app-name">ATAK</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="/code-book.html" class="app-item" data-name="Code Book">
|
||||||
|
<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 class="second-action">
|
||||||
|
<img class="app-icon" src="/icons/download.svg" alt="Download" onclick="saveCodeBook(event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-name">Code Book</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href=":1200" class="app-item" data-name="Remote Desktop">
|
<a href=":1200" class="app-item" data-name="Remote Desktop">
|
||||||
<div class="app-icon-wrapper">
|
<div class="app-icon-wrapper">
|
||||||
<svg class="app-icon" viewBox="0 0 24 24">
|
<svg class="app-icon" viewBox="0 0 24 24">
|
||||||
@@ -803,12 +814,7 @@
|
|||||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="second-action">
|
<div class="second-action">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
<img class="app-icon" src="/icons/download.svg" alt="Download" />
|
||||||
<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>
|
</div>
|
||||||
<div class="app-name">Desktop</div>
|
<div class="app-name">Desktop</div>
|
||||||
@@ -830,17 +836,7 @@
|
|||||||
<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"/>
|
<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>
|
</svg>
|
||||||
<div class="second-action" onclick="openKiwixm(event)">
|
<div class="second-action" onclick="openKiwixm(event)">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
<img class="app-icon" src="/icons/cog.svg" alt="Download" />
|
||||||
<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>
|
</div>
|
||||||
<div class="app-name">Library</div>
|
<div class="app-name">Library</div>
|
||||||
@@ -865,12 +861,7 @@
|
|||||||
<line x1="16" y1="6" x2="24" y2="18"/>
|
<line x1="16" y1="6" x2="24" y2="18"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="second-action">
|
<div class="second-action">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
<img class="app-icon" src="/icons/download.svg" alt="Download" />
|
||||||
<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>
|
</div>
|
||||||
<div class="app-name">Meshtastic</div>
|
<div class="app-name">Meshtastic</div>
|
||||||
@@ -882,12 +873,7 @@
|
|||||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
<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>
|
</svg>
|
||||||
<div class="second-action">
|
<div class="second-action">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
<img class="app-icon" src="/icons/download.svg" alt="Download" />
|
||||||
<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>
|
</div>
|
||||||
<div class="app-name">Messages</div>
|
<div class="app-name">Messages</div>
|
||||||
@@ -895,10 +881,7 @@
|
|||||||
|
|
||||||
<a href=":1800" class="app-item" data-name="Pentesting">
|
<a href=":1800" class="app-item" data-name="Pentesting">
|
||||||
<div class="app-icon-wrapper">
|
<div class="app-icon-wrapper">
|
||||||
<svg class="app-icon" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M267 48C230.6 48 209.2 106.3 198.7 160L168 160C154.7 160 144 170.7 144 184C144 197.3 154.7 208 168 208L192 208L192 240C192 257 195.3 273.2 201.3 288L192 288L192 288L171.5 288C156.3 288 144 300.3 144 315.5C144 318.5 144.5 321.4 145.4 324.2L174.3 410.8C136.2 443.6 112 492.1 112 546.3C112 562.7 125.3 576 141.7 576L498.3 576C514.7 576 528 562.7 528 546.3C528 492.1 503.8 443.6 465.7 410.9L494.6 324.3C495.5 321.5 496 318.6 496 315.6C496 300.4 483.7 288.1 468.5 288.1L448 288.1L448 288.1L438.7 288.1C444.7 273.3 448 257.1 448 240.1L448 208.1L472 208.1C485.3 208.1 496 197.4 496 184.1C496 170.8 485.3 160.1 472 160.1L441.3 160.1C430.9 106.4 409.4 48.1 373 48.1C363.4 48.1 354 52 345.5 56.3C337.3 60.4 327.1 64.1 320 64.1C312.9 64.1 302.7 60.4 294.5 56.3C286 51.9 276.6 48 267 48zM360.7 532.4L335.9 461.5L363.8 429C366.5 425.8 368 421.8 368 417.6C368 407.9 360.2 400.1 350.5 400.1L289.5 400.1C279.8 400.1 272 407.9 272 417.6C272 421.8 273.5 425.8 276.2 429L304.1 461.5L279.3 532.4L222.3 352L258 352C276.4 362.2 297.5 368 320 368C342.5 368 363.6 362.2 382 352L417.7 352L360.7 532.4zM320 320C285.3 320 255.8 297.9 244.7 267C250.4 270.2 257 272 264 272L276.4 272C292.9 272 307.5 261.4 312.7 245.8C315 238.8 324.9 238.8 327.2 245.8C332.4 261.4 347.1 272 363.5 272L375.9 272C382.9 272 389.5 270.2 395.2 267C384.1 297.9 354.6 320 319.9 320z"/></svg>
|
||||||
<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>
|
||||||
<div class="app-name">Pentesting</div>
|
<div class="app-name">Pentesting</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -916,12 +899,7 @@
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="second-action">
|
<div class="second-action">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4">
|
<img class="app-icon" src="/icons/download.svg" alt="Download" />
|
||||||
<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>
|
</div>
|
||||||
<div class="app-name">SDR</div>
|
<div class="app-name">SDR</div>
|
||||||
@@ -929,19 +907,14 @@
|
|||||||
|
|
||||||
<a href=":2000" class="app-item" data-name="Speed Test">
|
<a href=":2000" class="app-item" data-name="Speed Test">
|
||||||
<div class="app-icon-wrapper">
|
<div class="app-icon-wrapper">
|
||||||
<svg class="app-icon" viewBox="0 0 24 24">
|
<img src="/icons/open-sight.png" alt="OpenSight" style="height: 40px; width: auto;" />
|
||||||
<polygon points="8 1 16 1 14 15 10 15"/>
|
|
||||||
<circle cx="12" cy="20" r="3"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="app-name">Situation Room</div>
|
<div class="app-name">Intelligence</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href=":2100" class="app-item" data-name="Speed Test">
|
<a href=":2100" class="app-item" data-name="Speed Test">
|
||||||
<div class="app-icon-wrapper">
|
<div class="app-icon-wrapper">
|
||||||
<svg class="app-icon" viewBox="0 0 24 24">
|
<img class="app-icon" src="/icons/guage.svg" />
|
||||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="app-name">Speed Test</div>
|
<div class="app-name">Speed Test</div>
|
||||||
</a>
|
</a>
|
||||||
8
code/public/index.js → client/public/index.js
Executable file → Normal file
@@ -349,10 +349,16 @@ class Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveCodeBook() {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.href = '/code-book-print.html';
|
||||||
|
}
|
||||||
|
|
||||||
function openKiwixm(event) {
|
function openKiwixm(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
window.location.href = window.location.origin.replace(/:\d+/g, '') + ':1300/admin';
|
window.location.href = window.location.origin.replace(/:\d+/g, '') + ':1400/admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize dashboard when DOM is ready
|
// Initialize dashboard when DOM is ready
|
||||||
0
public/manifest.json → client/public/manifest.json
Executable file → Normal file
0
code/src/main.mjs → client/src/main.mjs
Executable file → Normal file
0
code/src/services/environment.mjs → client/src/services/environment.mjs
Executable file → Normal file
0
src/services/status.mjs → client/src/services/status.mjs
Executable file → Normal file
370
code/package-lock.json
generated
@@ -1,370 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "dashboard",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "dashboard",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"node-gpsd": "^0.3.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^3.1.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/anymatch": {
|
|
||||||
"version": "3.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
|
||||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"normalize-path": "^3.0.0",
|
|
||||||
"picomatch": "^2.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/binary-extensions": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
|
||||||
"version": "1.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/braces": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"fill-range": "^7.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"anymatch": "~3.1.2",
|
|
||||||
"braces": "~3.0.2",
|
|
||||||
"glob-parent": "~5.1.2",
|
|
||||||
"is-binary-path": "~2.1.0",
|
|
||||||
"is-glob": "~4.0.1",
|
|
||||||
"normalize-path": "~3.0.0",
|
|
||||||
"readdirp": "~3.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8.10.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/concat-map": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fill-range": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
|
||||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"to-regex-range": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob-parent": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-glob": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-flag": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ignore-by-default": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/is-binary-path": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"binary-extensions": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-glob": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-extglob": "^2.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-number": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimatch": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/node-gpsd": {
|
|
||||||
"version": "0.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-gpsd/-/node-gpsd-0.3.4.tgz",
|
|
||||||
"integrity": "sha512-sI9hPfHiaWDmhjE1oJZnhMo7UF2vQVGl3qk0K4HbN1L8BkS0I+rd6V687eHOj/6ervXiHrhwXzYYsWdXxGy0Qg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=v0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nodemon": {
|
|
||||||
"version": "3.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
|
||||||
"integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"chokidar": "^3.5.2",
|
|
||||||
"debug": "^4",
|
|
||||||
"ignore-by-default": "^1.0.1",
|
|
||||||
"minimatch": "^3.1.2",
|
|
||||||
"pstree.remy": "^1.1.8",
|
|
||||||
"semver": "^7.5.3",
|
|
||||||
"simple-update-notifier": "^2.0.0",
|
|
||||||
"supports-color": "^5.5.0",
|
|
||||||
"touch": "^3.1.0",
|
|
||||||
"undefsafe": "^2.0.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"nodemon": "bin/nodemon.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/nodemon"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/normalize-path": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picomatch": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pstree.remy": {
|
|
||||||
"version": "1.1.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
|
||||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/readdirp": {
|
|
||||||
"version": "3.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"picomatch": "^2.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
|
||||||
"version": "7.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/simple-update-notifier": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"semver": "^7.5.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/supports-color": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/to-regex-range": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-number": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/touch": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"nodetouch": "bin/nodetouch.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undefsafe": {
|
|
||||||
"version": "2.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
|
||||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"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": "/"
|
|
||||||
}
|
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
|
|
||||||
import express from 'express';
|
|
||||||
import { exec } from 'child_process';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import { readFileSync, existsSync } from 'fs';
|
|
||||||
import pkg from 'node-gpsd';
|
|
||||||
const { GPS } = pkg;
|
|
||||||
import {environment} from './environment.mjs';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
// Status cache with TTL
|
|
||||||
let statusCache = null;
|
|
||||||
let lastUpdate = 0;
|
|
||||||
const CACHE_TTL = 15 * 1000; // 15 seconds
|
|
||||||
|
|
||||||
// GPS client setup
|
|
||||||
let gpsClient = null;
|
|
||||||
let gpsData = {
|
|
||||||
fix: 'No Fix',
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
altitude: null,
|
|
||||||
satellites: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize GPS connection
|
|
||||||
async function initGPS() {
|
|
||||||
try {
|
|
||||||
gpsClient = new GPS({ hostname: 'localhost', port: 2947 });
|
|
||||||
|
|
||||||
gpsClient.on('TPV', (data) => {
|
|
||||||
gpsData.latitude = data.lat || null;
|
|
||||||
gpsData.longitude = data.lon || null;
|
|
||||||
gpsData.altitude = data.alt || null;
|
|
||||||
gpsData.fix = data.mode === 3 ? '3D Fix' : data.mode === 2 ? '2D Fix' : 'No Fix';
|
|
||||||
});
|
|
||||||
|
|
||||||
gpsClient.on('SKY', (data) => {
|
|
||||||
gpsData.satellites = (data.satellites || []).length;
|
|
||||||
});
|
|
||||||
|
|
||||||
gpsClient.on('error', (err) => {
|
|
||||||
console.warn('GPS error:', err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
await gpsClient.watch();
|
|
||||||
console.log('GPS client initialized');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('GPS not available:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLI-based system information gathering
|
|
||||||
async function getSystemInfo() {
|
|
||||||
const info = {
|
|
||||||
temperature: 0,
|
|
||||||
load: 0,
|
|
||||||
cpu: 0,
|
|
||||||
memory: 0,
|
|
||||||
storage: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// CPU temperature (Raspberry Pi specific)
|
|
||||||
if (existsSync('/sys/class/thermal/thermal_zone0/temp')) {
|
|
||||||
const tempData = readFileSync('/sys/class/thermal/thermal_zone0/temp', 'utf8');
|
|
||||||
info.temperature = Math.round(parseInt(tempData.trim()) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load average
|
|
||||||
const { stdout: loadAvg } = await execAsync("cat /proc/loadavg | awk '{print $1}'");
|
|
||||||
info.load = parseFloat(loadAvg.trim()) || 0;
|
|
||||||
|
|
||||||
// CPU usage
|
|
||||||
const { stdout: cpuUsage } = await execAsync("grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$3+$4+$5)} END {print usage}'");
|
|
||||||
info.cpu = parseFloat(cpuUsage.trim()) || 0;
|
|
||||||
|
|
||||||
// Memory usage percentage
|
|
||||||
const { stdout: memUsage } = await execAsync("free | awk 'NR==2{printf \"%.1f\", $3*100/$2}'");
|
|
||||||
info.memory = parseFloat(memUsage.trim()) || 0;
|
|
||||||
|
|
||||||
// Disk usage percentage for root filesystem
|
|
||||||
const { stdout: diskUsage } = await execAsync("df / | awk 'NR==2{printf \"%.1f\", $5}' | sed 's/%//'");
|
|
||||||
info.storage = parseFloat(diskUsage.trim()) || 0;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('System info error:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getGPSInfo() {
|
|
||||||
const gpsInfo = {
|
|
||||||
satellites: 0,
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
altitude: null
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if gpsd is running
|
|
||||||
await execAsync('systemctl is-active gpsd');
|
|
||||||
|
|
||||||
// Try to get GPS data using gpspipe
|
|
||||||
const { stdout: gpsRaw } = await execAsync('timeout 3s gpspipe -w -n 5 | grep -E \'"class":"TPV"\' | head -1', { timeout: 4000 });
|
|
||||||
|
|
||||||
if (gpsRaw.trim()) {
|
|
||||||
const gpsJson = JSON.parse(gpsRaw.trim());
|
|
||||||
gpsInfo.latitude = gpsJson.lat || null;
|
|
||||||
gpsInfo.longitude = gpsJson.lon || null;
|
|
||||||
gpsInfo.altitude = gpsJson.alt || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get satellite count
|
|
||||||
try {
|
|
||||||
const { stdout: skyRaw } = await execAsync('timeout 2s gpspipe -w -n 3 | grep -E \'"class":"SKY"\' | head -1', { timeout: 3000 });
|
|
||||||
if (skyRaw.trim()) {
|
|
||||||
const skyJson = JSON.parse(skyRaw.trim());
|
|
||||||
gpsInfo.satellites = (skyJson.satellites || []).length;
|
|
||||||
}
|
|
||||||
} catch (skyError) { }
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('GPS info error:', error.message);
|
|
||||||
// Use cached GPS data from node-gpsd if available
|
|
||||||
if (gpsData.latitude !== null) {
|
|
||||||
return gpsData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gpsInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNetworkInfo() {
|
|
||||||
const networkInfo = {
|
|
||||||
wifi: {
|
|
||||||
mode: 'client',
|
|
||||||
ssid: 'Unknown'
|
|
||||||
},
|
|
||||||
ethernet: {
|
|
||||||
connected: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if hostapd is running (AP mode)
|
|
||||||
try {
|
|
||||||
await execAsync('systemctl is-active hostapd');
|
|
||||||
networkInfo.wifi.mode = 'ap';
|
|
||||||
|
|
||||||
// Get AP SSID from hostapd config or use hostname
|
|
||||||
try {
|
|
||||||
const { stdout: hostname } = await execAsync('hostname');
|
|
||||||
networkInfo.wifi.ssid = process.env.AP_SSID || hostname.trim();
|
|
||||||
} catch {
|
|
||||||
networkInfo.wifi.ssid = 'MRS-AP';
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not in AP mode, check if connected to WiFi
|
|
||||||
try {
|
|
||||||
// First, try iwconfig directly since it's more reliable for ESSID
|
|
||||||
const { stdout: iwconfig } = await execAsync("iwconfig wlan0 2>/dev/null");
|
|
||||||
const essidMatch = iwconfig.match(/ESSID:"([^"]+)"/);
|
|
||||||
|
|
||||||
if (essidMatch && essidMatch[1] && essidMatch[1] !== 'off/any') {
|
|
||||||
networkInfo.wifi.mode = 'client';
|
|
||||||
networkInfo.wifi.ssid = essidMatch[1];
|
|
||||||
} else {
|
|
||||||
// Fallback to nmcli if iwconfig doesn't work
|
|
||||||
try {
|
|
||||||
const { stdout: activeWifi } = await execAsync("nmcli -t -f NAME,TYPE,DEVICE connection show --active | grep -E '(wifi|802-11-wireless)' | cut -d':' -f1 | head -1");
|
|
||||||
if (activeWifi.trim()) {
|
|
||||||
networkInfo.wifi.mode = 'client';
|
|
||||||
networkInfo.wifi.ssid = activeWifi.trim();
|
|
||||||
} else {
|
|
||||||
// Try getting SSID from currently connected interface
|
|
||||||
const { stdout: connectedSSID } = await execAsync("nmcli -t -f active,ssid dev wifi | grep '^yes' | cut -d':' -f2 | head -1");
|
|
||||||
if (connectedSSID.trim()) {
|
|
||||||
networkInfo.wifi.mode = 'client';
|
|
||||||
networkInfo.wifi.ssid = connectedSSID.trim();
|
|
||||||
} else {
|
|
||||||
throw new Error('No active WiFi connection found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Final fallback: check if wlan0 interface is up and get info via ip
|
|
||||||
const { stdout: wlanStatus } = await execAsync("ip addr show wlan0 2>/dev/null | grep 'inet ' | wc -l");
|
|
||||||
if (parseInt(wlanStatus.trim()) > 0) {
|
|
||||||
// Interface has IP but we can't determine SSID
|
|
||||||
networkInfo.wifi.mode = 'client';
|
|
||||||
networkInfo.wifi.ssid = 'Connected (Unknown SSID)';
|
|
||||||
} else {
|
|
||||||
networkInfo.wifi.mode = 'disconnected';
|
|
||||||
networkInfo.wifi.ssid = 'Not Connected';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('WiFi detection error:', error.message);
|
|
||||||
networkInfo.wifi.mode = 'disconnected';
|
|
||||||
networkInfo.wifi.ssid = 'Not Connected';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Ethernet status
|
|
||||||
try {
|
|
||||||
const { stdout: ethStatus } = await execAsync("ip link show eth0 | grep 'state UP'");
|
|
||||||
networkInfo.ethernet.connected = ethStatus.includes('state UP');
|
|
||||||
} catch {
|
|
||||||
networkInfo.ethernet.connected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Network info error:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return networkInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
let loraInfo = null;
|
|
||||||
async function getLoRaInfo() {
|
|
||||||
if(loraInfo) return loraInfo;
|
|
||||||
|
|
||||||
loraInfo = {
|
|
||||||
connected: false,
|
|
||||||
onlineNodes: 0,
|
|
||||||
totalNodes: 0,
|
|
||||||
channelUtilization: 0,
|
|
||||||
airtimeUtilization: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Getting LoRa/Meshtastic status...');
|
|
||||||
|
|
||||||
// Get node information first
|
|
||||||
const { stdout: nodeList } = await execAsync('timeout 10s meshtastic --nodes 2>/dev/null', { timeout: 15000 });
|
|
||||||
|
|
||||||
if (nodeList && nodeList.trim()) {
|
|
||||||
loraInfo.connected = true;
|
|
||||||
|
|
||||||
// Parse node list - look for actual node entries
|
|
||||||
const lines = nodeList.split('\n');
|
|
||||||
let totalNodes = 0;
|
|
||||||
let onlineNodes = 0;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
|
|
||||||
// Skip header lines and separators
|
|
||||||
if (trimmedLine.includes('│') &&
|
|
||||||
!trimmedLine.includes('User') &&
|
|
||||||
!trimmedLine.includes('───') &&
|
|
||||||
!trimmedLine.includes('Node') &&
|
|
||||||
trimmedLine.length > 10) {
|
|
||||||
|
|
||||||
totalNodes++;
|
|
||||||
|
|
||||||
// Check if node is online (has recent lastHeard or SNR data)
|
|
||||||
if (trimmedLine.includes('SNR') ||
|
|
||||||
trimmedLine.match(/\d+[smh]/) || // Time indicators like 5m, 2h, 30s
|
|
||||||
trimmedLine.includes('now')) {
|
|
||||||
onlineNodes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loraInfo.totalNodes = totalNodes;
|
|
||||||
loraInfo.onlineNodes = onlineNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get channel and airtime utilization from --info
|
|
||||||
try {
|
|
||||||
const { stdout: meshInfo } = await execAsync('timeout 10s meshtastic --info 2>/dev/null', { timeout: 15000 });
|
|
||||||
|
|
||||||
if (meshInfo && meshInfo.trim()) {
|
|
||||||
const lines = meshInfo.split('\n');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
|
|
||||||
// Look for channel utilization (various formats)
|
|
||||||
if (trimmedLine.includes('channelUtilization') || trimmedLine.includes('Channel utilization')) {
|
|
||||||
const channelMatch = trimmedLine.match(/([\d.]+)%?/) || trimmedLine.match(/:\s*([\d.]+)/);
|
|
||||||
if (channelMatch) {
|
|
||||||
loraInfo.channelUtilization = parseFloat(channelMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for airtime utilization (various formats)
|
|
||||||
if (trimmedLine.includes('airUtilTx') || trimmedLine.includes('Air time') || trimmedLine.includes('Airtime')) {
|
|
||||||
const airtimeMatch = trimmedLine.match(/([\d.]+)%?/) || trimmedLine.match(/:\s*([\d.]+)/);
|
|
||||||
if (airtimeMatch) {
|
|
||||||
loraInfo.airtimeUtilization = parseFloat(airtimeMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (infoError) {
|
|
||||||
console.warn('Failed to get mesh info:', infoError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('LoRa info error:', error.message);
|
|
||||||
loraInfo.connected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => loraInfo = null, 60000)
|
|
||||||
return loraInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main status generation function - matches frontend expectations
|
|
||||||
async function generateStatus() {
|
|
||||||
try {
|
|
||||||
console.log('Generating fresh status...');
|
|
||||||
|
|
||||||
const [systemInfo, gpsInfo, networkInfo, loraInfo] = await Promise.all([
|
|
||||||
getSystemInfo(),
|
|
||||||
getGPSInfo(),
|
|
||||||
getNetworkInfo(),
|
|
||||||
getLoRaInfo()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Format exactly as the frontend expects
|
|
||||||
const status = {
|
|
||||||
system: {
|
|
||||||
temperature: systemInfo.temperature,
|
|
||||||
load: systemInfo.load,
|
|
||||||
cpu: systemInfo.cpu,
|
|
||||||
memory: systemInfo.memory,
|
|
||||||
storage: systemInfo.storage
|
|
||||||
},
|
|
||||||
gps: {
|
|
||||||
fix: gpsInfo.fix,
|
|
||||||
satellites: gpsInfo.satellites,
|
|
||||||
latitude: gpsInfo.latitude,
|
|
||||||
longitude: gpsInfo.longitude,
|
|
||||||
altitude: gpsInfo.altitude
|
|
||||||
},
|
|
||||||
network: {
|
|
||||||
wifi: networkInfo.wifi,
|
|
||||||
ethernet: networkInfo.ethernet
|
|
||||||
},
|
|
||||||
lora: loraInfo,
|
|
||||||
time: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
return status;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Status generation error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/hostname', (req, res) => res.json({ hostname: environment.hostname }));
|
|
||||||
|
|
||||||
// Main status endpoint that matches frontend expectations
|
|
||||||
router.get('/status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Return cached status if within TTL
|
|
||||||
if (statusCache && (now - lastUpdate) < CACHE_TTL) {
|
|
||||||
console.log('Returning cached status');
|
|
||||||
return res.json(statusCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate fresh status
|
|
||||||
statusCache = await generateStatus();
|
|
||||||
lastUpdate = now;
|
|
||||||
|
|
||||||
console.log('Status updated successfully');
|
|
||||||
res.json(statusCache);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Status API error:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Failed to get system status',
|
|
||||||
message: error.message,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Individual debug endpoints
|
|
||||||
router.get('/system', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const systemInfo = await getSystemInfo();
|
|
||||||
res.json(systemInfo);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/gps', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const gpsInfo = await getGPSInfo();
|
|
||||||
res.json(gpsInfo);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/network', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const networkInfo = await getNetworkInfo();
|
|
||||||
res.json(networkInfo);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add individual LoRa endpoint
|
|
||||||
router.get('/lora', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const loraInfo = await getLoRaInfo();
|
|
||||||
res.json(loraInfo);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize GPS on startup
|
|
||||||
initGPS().catch(err => console.warn('GPS initialization failed:', err.message));
|
|
||||||
|
|
||||||
export { router as statusRouter };
|
|
||||||
|
Before Width: | Height: | Size: 32 KiB |
@@ -1,939 +0,0 @@
|
|||||||
<!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;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
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;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-status-wrapper {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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</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="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=":1300" 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=":1400" 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=":1500" 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=":1600" 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=":1700" 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=":1800" 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>
|
|
||||||
<div class="app-name">Remote Desktop</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">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="app-name">SDR</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">
|
|
||||||
<circle cx="12" cy="12" r="10"/>
|
|
||||||
<polyline points="12 6 12 12 16 14"/>
|
|
||||||
</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
public/index.js
@@ -1,368 +0,0 @@
|
|||||||
|
|
||||||
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;
|
|
||||||
} 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
51
src/main.mjs
@@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
import express from "express";
|
|
||||||
import { join } from 'path';
|
|
||||||
import {environment} from './services/environment.mjs';
|
|
||||||
import { statusRouter } from './services/status.mjs';
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(express.json());
|
|
||||||
app.use((err, req, res, next) => {
|
|
||||||
console.error('Middleware error:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
console.log(join(environment.root, '../public'));
|
|
||||||
app.use(express.static(join(environment.root, '../public')));
|
|
||||||
|
|
||||||
app.use('/api', statusRouter);
|
|
||||||
|
|
||||||
// Error handler
|
|
||||||
app.use((err, req, res, next) => {
|
|
||||||
console.error('Unhandled error:', err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Startup
|
|
||||||
const server = app.listen(environment.port, () => {
|
|
||||||
console.log(`Dashboard running on http://localhost:${environment.port}`);
|
|
||||||
}).on('error', (err) => {
|
|
||||||
console.error('Server startup error:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Shutdown
|
|
||||||
const gracefulShutdown = (signal) => {
|
|
||||||
console.log(`Received ${signal}, shutting down gracefully`);
|
|
||||||
server.close((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error during server shutdown:', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log('Server closed');
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
||||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
||||||
})();
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import dotenv from 'dotenv';
|
|
||||||
import * as fs from 'node:fs';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
export const environment = {
|
|
||||||
config: process.env.CONFIG || 'config.json',
|
|
||||||
hostname: process.env.HOSTNAME || execSync('hostname', { encoding: "utf-8" }) || 'localhost',
|
|
||||||
gpsd: process.env.GPSD || 2947,
|
|
||||||
port: process.env.PORT || 3000,
|
|
||||||
root: dirname(dirname(fileURLToPath(import.meta.url))),
|
|
||||||
settings: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!environment.config.startsWith('/')) environment.config = `${environment.root}/${environment.config}`;
|
|
||||||
|
|
||||||
export function loadSettings() {
|
|
||||||
if(!fs.existsSync(environment.config)) fs.writeFileSync(environment.config, '', { flag: 'a' });
|
|
||||||
const value = fs.readFileSync(environment.config, 'utf-8');
|
|
||||||
try { return JSON.parse(value); }
|
|
||||||
catch (e) {
|
|
||||||
return {
|
|
||||||
lora: {
|
|
||||||
mode: 'MESH',
|
|
||||||
telemetry: true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveSettings() {
|
|
||||||
fs.writeFileSync(environment.config, JSON.stringify(environment.settings, null, 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSettings();
|
|
||||||