Files
pyMC_Repeater/repeater/templates/neighbors.html
Lloyd 97256eb132 Initial commit: PyMC Repeater Daemon
This commit sets up the initial project structure for the PyMC Repeater Daemon.
It includes base configuration files, dependency definitions, and scaffolding
for the main daemon service responsible for handling PyMC repeating operations.
2025-10-24 23:13:48 +01:00

396 lines
14 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>pyMC Repeater - Neighbors</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="layout">
<!-- Navigation Component -->
<!-- NAVIGATION_PLACEHOLDER -->
<!-- Main Content -->
<main class="content">
<header>
<h1>Neighbor Repeaters</h1>
<div class="header-info">
<span>Tracking: <strong id="neighbor-count">0</strong> repeaters</span>
<span>Updated: <strong id="update-time">{{ last_updated }}</strong></span>
</div>
</header>
<!-- Neighbors Table -->
<div class="table-card">
<h2>Discovered Repeaters</h2>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Node Name</th>
<th>Public Key</th>
<th>Contact Type</th>
<th>Location</th>
<th>RSSI</th>
<th>SNR</th>
<th>Last Seen</th>
<th>First Seen</th>
<th>Advert Count</th>
</tr>
</thead>
<tbody id="neighbors-table">
<tr>
<td colspan="9" class="empty-message">
No repeaters discovered yet - waiting for adverts...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
<script>
let updateInterval;
// Handle Send Advert button
function sendAdvert() {
const btn = document.getElementById('send-advert-btn');
if (!btn) return;
const icon = btn.querySelector('.icon');
const iconHTML = icon ? icon.outerHTML : '';
btn.disabled = true;
btn.innerHTML = iconHTML + 'Sending...';
fetch('/api/send_advert', {
method: 'POST'
})
.then(r => r.json())
.then(data => {
if (data.success) {
btn.innerHTML = iconHTML + 'Sent!';
setTimeout(() => {
btn.innerHTML = iconHTML + 'Send Advert';
btn.disabled = false;
}, 2000);
} else {
btn.innerHTML = iconHTML + 'Error';
console.error('Failed to send advert:', data.error);
setTimeout(() => {
btn.innerHTML = iconHTML + 'Send Advert';
btn.disabled = false;
}, 2000);
}
})
.catch(e => {
console.error('Error sending advert:', e);
btn.innerHTML = iconHTML + 'Error';
setTimeout(() => {
btn.innerHTML = iconHTML + 'Send Advert';
btn.disabled = false;
}, 2000);
});
}
function updateNeighbors() {
fetch('/api/stats')
.then(r => r.json())
.then(data => {
const neighbors = data.neighbors || {};
const neighborCount = Object.keys(neighbors).length;
document.getElementById('neighbor-count').textContent = neighborCount;
document.getElementById('update-time').textContent = new Date().toLocaleTimeString();
updateNeighborsTable(neighbors);
})
.catch(e => console.error('Error fetching neighbors:', e));
}
function updateNeighborsTable(neighbors) {
const tbody = document.getElementById('neighbors-table');
if (!neighbors || Object.keys(neighbors).length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="9" class="empty-message">
No repeaters discovered yet - waiting for adverts...
</td>
</tr>
`;
return;
}
// Sort by last_seen (most recent first)
const sortedNeighbors = Object.entries(neighbors).sort((a, b) => {
return b[1].last_seen - a[1].last_seen;
});
tbody.innerHTML = sortedNeighbors.map(([pubkey, neighbor]) => {
const name = neighbor.node_name || 'Unknown';
// Format pubkey properly - it's a 64-char hex string
const pubkeyShort = pubkey.length >= 16
? `&lt;${pubkey.substring(0, 8)}...${pubkey.substring(pubkey.length - 8)}&gt;`
: `&lt;${pubkey}&gt;`;
const contactType = neighbor.contact_type || 'Repeater';
const location = neighbor.latitude && neighbor.longitude && (neighbor.latitude !== 0.0 || neighbor.longitude !== 0.0)
? `${neighbor.latitude.toFixed(6)}, ${neighbor.longitude.toFixed(6)}`
: 'N/A';
const rssi = neighbor.rssi || 'N/A';
const snr = neighbor.snr !== undefined ? neighbor.snr.toFixed(1) + ' dB' : 'N/A';
const lastSeen = new Date(neighbor.last_seen * 1000).toLocaleString();
const firstSeen = new Date(neighbor.first_seen * 1000).toLocaleString();
const advertCount = neighbor.advert_count || 0;
// Color code RSSI
let rssiClass = 'rssi-poor';
if (rssi !== 'N/A') {
if (rssi > -80) rssiClass = 'rssi-excellent';
else if (rssi > -90) rssiClass = 'rssi-good';
else if (rssi > -100) rssiClass = 'rssi-fair';
}
return `
<tr>
<td data-label="Node Name"><strong>${name}</strong></td>
<td data-label="Public Key"><code class="pubkey">${pubkeyShort}</code></td>
<td data-label="Contact Type"><span class="contact-type-badge">${contactType}</span></td>
<td data-label="Location">${location}</td>
<td data-label="RSSI"><span class="${rssiClass}">${rssi}</span></td>
<td data-label="SNR">${snr}</td>
<td data-label="Last Seen">${lastSeen}</td>
<td data-label="First Seen">${firstSeen}</td>
<td data-label="Advert Count">${advertCount}</td>
</tr>
`;
}).join('');
}
// Initialize on load
document.addEventListener('DOMContentLoaded', () => {
updateNeighbors();
// Auto-update every 10 seconds
updateInterval = setInterval(updateNeighbors, 10000);
// Attach send advert button handler
const sendAdvertBtn = document.getElementById('send-advert-btn');
if (sendAdvertBtn) {
sendAdvertBtn.addEventListener('click', sendAdvert);
}
});
</script>
<style>
.pubkey {
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #4ec9b0;
}
.contact-type-badge {
display: inline-block;
padding: 4px 8px;
background-color: rgba(59, 130, 246, 0.2);
border: 1px solid rgba(59, 130, 246, 0.4);
border-radius: 4px;
font-size: 0.85em;
color: #60a5fa;
font-weight: 500;
}
.rssi-excellent {
color: #4ade80;
font-weight: 500;
}
.rssi-good {
color: #4ec9b0;
font-weight: 500;
}
.rssi-fair {
color: #dcdcaa;
font-weight: 500;
}
.rssi-poor {
color: #f48771;
font-weight: 500;
}
/* Mobile responsive table styling */
@media (max-width: 768px) {
.data-table {
display: block;
width: 100%;
}
.data-table thead {
display: none;
}
.data-table tbody {
display: block;
width: 100%;
}
.data-table tbody tr {
display: block;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--spacing-md);
padding: var(--spacing-md);
overflow: hidden;
}
.data-table tbody tr:hover {
background: var(--color-bg-tertiary);
border-color: var(--color-accent-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.data-table td {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
margin-right: var(--spacing-lg);
padding: 0;
border: none;
text-align: left;
font-size: 0.75rem;
flex-wrap: wrap;
}
.data-table td::before {
content: attr(data-label);
font-weight: var(--font-weight-bold);
color: var(--color-text-tertiary);
text-transform: uppercase;
font-size: 0.6rem;
letter-spacing: 0.5px;
min-width: fit-content;
padding: 2px 6px;
background: var(--color-bg-tertiary);
border-radius: 4px;
}
/* Node Name and Public Key get full width */
.data-table td:nth-child(1),
.data-table td:nth-child(2) {
display: block;
width: 100%;
margin-right: 0;
margin-bottom: var(--spacing-md);
}
.data-table td:nth-child(1)::before { content: "Node Name"; }
.data-table td:nth-child(2)::before { content: "Public Key"; }
.data-table td:nth-child(3)::before { content: "Contact Type"; }
.data-table td:nth-child(4)::before { content: "Location"; }
.data-table td:nth-child(5)::before { content: "RSSI"; }
.data-table td:nth-child(6)::before { content: "SNR"; }
.data-table td:nth-child(7)::before { content: "Last Seen"; }
.data-table td:nth-child(8)::before { content: "First Seen"; }
.data-table td:nth-child(9)::before { content: "Advert Count"; }
/* Location and timestamps wrap to next line */
.data-table td:nth-child(4),
.data-table td:nth-child(7),
.data-table td:nth-child(8) {
display: block;
width: 100%;
margin-right: 0;
margin-bottom: var(--spacing-sm);
}
}
@media (max-width: 600px) {
.data-table tbody tr {
padding: var(--spacing-md);
}
.data-table td {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-xs);
margin-right: var(--spacing-md);
font-size: 0.7rem;
}
.data-table td::before {
font-size: 0.55rem;
padding: 1px 4px;
}
/* Full width items */
.data-table td:nth-child(1),
.data-table td:nth-child(2),
.data-table td:nth-child(4),
.data-table td:nth-child(7),
.data-table td:nth-child(8) {
display: block;
width: 100%;
margin-right: 0;
margin-bottom: var(--spacing-sm);
}
.pubkey {
font-size: 0.7rem;
word-break: break-all;
}
}
@media (max-width: 480px) {
.data-table tbody tr {
padding: var(--spacing-sm);
}
.data-table td {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-xs);
margin-right: var(--spacing-sm);
font-size: 0.65rem;
}
.data-table td::before {
font-size: 0.5rem;
padding: 1px 3px;
}
/* Full width items */
.data-table td:nth-child(1),
.data-table td:nth-child(2),
.data-table td:nth-child(4),
.data-table td:nth-child(7),
.data-table td:nth-child(8) {
display: block;
width: 100%;
margin-right: 0;
margin-bottom: var(--spacing-xs);
}
.pubkey {
font-size: 0.6rem;
word-break: break-all;
white-space: normal;
}
.contact-type-badge {
font-size: 0.7rem;
padding: 2px 4px;
}
}
</style>
</body>
</html>