Files
meshcore-bot/modules/web_viewer/templates/base.html
agessaman bd2849655c feat: Enhance mesh graph functionality and path validation
- Added a new mesh graph feature for improved path validation, allowing for enhanced routing accuracy.
- Introduced configuration options for recency decay half-life and graph-based validation settings in config.ini.example.
- Updated the PathCommand class to utilize graph-based selection methods, combining graph and geographic scores for better repeater selection.
- Implemented new methods in MessageHandler to update the mesh graph with advertisement paths and trace packet data.
- Created a new database table for mesh connections to support graph-based path validation.
- Enhanced web viewer integration to display mesh graph updates in real-time, improving user interaction and monitoring capabilities.
2026-01-23 20:26:56 -08:00

650 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}MeshCore Bot Data Viewer{% endblock %}</title>
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<!-- Apply theme immediately to prevent flash -->
<script>
(function() {
const storedTheme = localStorage.getItem('theme');
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const theme = storedTheme || systemPreference;
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<!-- Bootstrap 5.1.3 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome for icons -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<!-- Custom styles -->
<style>
/* CSS Variables for Light Mode (Default) */
:root {
--bg-color: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-color: #212529;
--text-muted: #6c757d;
--border-color: #dee2e6;
--card-bg: #ffffff;
--card-header-bg: #f8f9fa;
--footer-bg: #f8f9fa;
--log-entry-bg: #f8f9fa;
--connection-info-bg: #e9ecef;
--shadow-color: rgba(0, 0, 0, 0.1);
}
/* CSS Variables for Dark Mode */
[data-theme="dark"] {
--bg-color: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--text-color: #e9ecef;
--text-muted: #adb5bd;
--border-color: #495057;
--card-bg: #2d2d2d;
--card-header-bg: #3d3d3d;
--footer-bg: #2d2d2d;
--log-entry-bg: #2d2d2d;
--connection-info-bg: #3d3d3d;
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* Apply theme variables */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
display: flex;
flex-direction: column;
}
.navbar-brand {
font-weight: bold;
color: #007bff !important;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-connected { background-color: #28a745; }
.status-disconnected { background-color: #dc3545; }
.status-warning { background-color: #ffc107; }
.log-entry {
font-family: 'Courier New', monospace;
font-size: 0.9em;
margin-bottom: 5px;
padding: 8px;
border-left: 3px solid #007bff;
background-color: var(--log-entry-bg);
border-radius: 0 5px 5px 0;
color: var(--text-color);
}
.command-entry {
border-left-color: #28a745;
}
.packet-entry {
border-left-color: #ffc107;
}
.connection-info {
background-color: var(--connection-info-bg);
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
color: var(--text-color);
}
.card {
box-shadow: 0 2px 4px var(--shadow-color);
border: none;
background-color: var(--card-bg);
color: var(--text-color);
}
.card-header {
background-color: var(--card-header-bg);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
color: var(--text-color);
}
.card-body {
color: var(--text-color);
}
.btn-group .btn {
margin-right: 5px;
}
.table-responsive {
border-radius: 8px;
overflow: hidden;
}
/* Dark mode table styles */
[data-theme="dark"] .table {
color: var(--text-color);
}
[data-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > td {
background-color: var(--bg-tertiary);
}
[data-theme="dark"] .table-hover > tbody > tr:hover > td {
background-color: var(--bg-tertiary);
}
.badge {
font-size: 0.8em;
}
.loading {
text-align: center;
padding: 20px;
color: var(--text-muted);
}
.error {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
[data-theme="dark"] .error {
color: #f5c6cb;
background-color: #721c24;
border-color: #842029;
}
.success {
color: #155724;
background-color: #d4edda;
border: 1px solid #c3e6cb;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
[data-theme="dark"] .success {
color: #d1e7dd;
background-color: #0f5132;
border-color: #146c43;
}
/* Dark mode toggle button */
.dark-mode-toggle {
background: none;
border: none;
color: rgba(255, 255, 255, 0.85);
font-size: 1.2rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
border-radius: 0.375rem;
transition: background-color 0.2s ease, color 0.2s ease;
}
.dark-mode-toggle:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.dark-mode-toggle:focus {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(255, 255, 255, 0.25);
}
/* Dark mode form controls */
[data-theme="dark"] .form-control,
[data-theme="dark"] .form-select {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
color: var(--text-color);
}
[data-theme="dark"] .form-control:focus,
[data-theme="dark"] .form-select:focus {
background-color: var(--bg-tertiary);
border-color: #007bff;
color: var(--text-color);
}
/* Dark mode text muted */
[data-theme="dark"] .text-muted {
color: var(--text-muted) !important;
}
/* Dark mode borders */
[data-theme="dark"] .border {
border-color: var(--border-color) !important;
}
/* Dark mode alerts */
[data-theme="dark"] .alert-info {
background-color: #084298;
border-color: #0a58ca;
color: #cfe2ff;
}
[data-theme="dark"] .alert-warning {
background-color: #664d03;
border-color: #997404;
color: #fffbeb;
}
[data-theme="dark"] .alert-danger {
background-color: #842029;
border-color: #b02a37;
color: #f8d7da;
}
[data-theme="dark"] .alert-success {
background-color: #0f5132;
border-color: #146c43;
color: #d1e7dd;
}
/* Dark mode modal styling */
[data-theme="dark"] .modal-content {
background-color: var(--card-bg);
color: var(--text-color);
border-color: var(--border-color);
}
[data-theme="dark"] .modal-header {
background-color: var(--card-header-bg);
border-bottom-color: var(--border-color);
color: var(--text-color);
}
[data-theme="dark"] .modal-title {
color: var(--text-color);
}
[data-theme="dark"] .modal-body {
background-color: var(--card-bg);
color: var(--text-color);
}
[data-theme="dark"] .modal-footer {
background-color: var(--card-bg);
border-top-color: var(--border-color);
}
[data-theme="dark"] .modal-backdrop {
background-color: rgba(0, 0, 0, 0.7);
}
/* Dark mode code/pre elements in modals */
[data-theme="dark"] .modal pre,
[data-theme="dark"] .modal code {
background-color: var(--bg-tertiary);
color: var(--text-color);
border-color: var(--border-color);
}
[data-theme="dark"] .modal .bg-light {
background-color: var(--bg-tertiary) !important;
color: var(--text-color) !important;
}
/* Dark mode accordion styling */
[data-theme="dark"] .accordion-item {
background-color: var(--card-bg);
border-color: var(--border-color);
}
[data-theme="dark"] .accordion-button {
background-color: var(--card-header-bg);
color: var(--text-color);
}
[data-theme="dark"] .accordion-button:not(.collapsed) {
background-color: var(--bg-tertiary);
color: var(--text-color);
}
[data-theme="dark"] .accordion-button:focus {
box-shadow: 0 0 0 0.25rem rgba(0, 123, 255, 0.25);
}
[data-theme="dark"] .accordion-body {
background-color: var(--card-bg);
color: var(--text-color);
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="fas fa-satellite-dish"></i> MeshCore Bot
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-tachometer-alt"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/realtime">
<i class="fas fa-broadcast-tower"></i> Real-time
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/contacts">
<i class="fas fa-users"></i> Contacts
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/mesh">
<i class="fas fa-project-diagram"></i> Mesh Graph
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/cache">
<i class="fas fa-database"></i> Cache
</a>
</li>
{% if greeter_enabled %}
<li class="nav-item">
<a class="nav-link" href="/greeter">
<i class="fas fa-hand-sparkles"></i> Greeter
</a>
</li>
{% endif %}
{% if feed_manager_enabled %}
<li class="nav-item">
<a class="nav-link" href="/feeds">
<i class="fas fa-rss"></i> Feeds
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="/radio">
<i class="fas fa-broadcast-tower"></i> Radio
</a>
</li>
</ul>
<!-- Dark Mode Toggle and Connection Status -->
<div class="navbar-nav d-flex align-items-center">
<div class="nav-item me-3">
<button class="dark-mode-toggle" id="dark-mode-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
<i class="fas fa-moon" id="dark-mode-icon"></i>
</button>
</div>
<div class="nav-item">
<span class="navbar-text">
<span class="status-indicator" id="connection-status"></span>
<span id="connection-text">Disconnected</span>
</span>
</div>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container-fluid mt-4 flex-grow-1" style="display: flex; flex-direction: column; min-height: 0;">
{% block content %}{% endblock %}
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Socket.IO Client -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
<!-- Moment.js for date formatting -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<!-- Chart.js for graphs -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<!-- SHA-256 for channel key derivation (works in non-HTTPS contexts) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.11.0/sha256.min.js"></script>
<!-- Base JavaScript -->
<script>
// Global connection management
class ModernConnectionManager {
constructor() {
this.socket = null;
this.connected = false;
this.lastActivity = null;
this.pingInterval = null;
this.initializeSocket();
this.startPingInterval();
}
initializeSocket() {
this.socket = io({
transports: ['websocket', 'polling'],
timeout: 5000,
forceNew: true
});
this.setupSocketEvents();
}
setupSocketEvents() {
this.socket.on('connect', () => {
console.log('Connected to server');
this.connected = true;
this.updateConnectionStatus('Connected', 'connected');
this.lastActivity = new Date();
this.updateLastActivity();
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected from server:', reason);
this.connected = false;
this.updateConnectionStatus('Disconnected', 'disconnected');
});
this.socket.on('status', (data) => {
console.log('Server status:', data.message);
this.showNotification(data.message, 'info');
});
this.socket.on('error', (data) => {
console.error('Server error:', data.message);
this.showNotification(`Error: ${data.message}`, 'danger');
});
// Modern ping/pong pattern
this.socket.on('pong', () => {
console.log('Pong received from server');
this.lastActivity = new Date();
this.updateLastActivity();
});
}
startPingInterval() {
this.pingInterval = setInterval(() => {
if (this.socket && this.socket.connected) {
this.socket.emit('ping');
}
}, 20000);
}
stopPingInterval() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}
updateConnectionStatus(text, status) {
const statusElement = document.getElementById('connection-text');
const indicatorElement = document.getElementById('connection-status');
if (statusElement) {
statusElement.textContent = text;
}
if (indicatorElement) {
indicatorElement.className = `status-indicator status-${status}`;
}
}
updateLastActivity() {
if (this.lastActivity) {
const lastUpdateElement = document.getElementById('last-update');
if (lastUpdateElement) {
lastUpdateElement.textContent = moment(this.lastActivity).format('YYYY-MM-DD HH:mm:ss');
}
}
}
showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// Auto-remove after 3 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
}
// Update timestamp function
function updateTimestamp() {
const now = new Date();
const timestamp = now.toISOString().replace('T', ' ').substring(0, 19);
const lastUpdateElement = document.getElementById('last-update');
if (lastUpdateElement) {
lastUpdateElement.textContent = timestamp;
}
}
// Dark Mode Management
class DarkModeManager {
constructor() {
this.theme = this.getStoredTheme() || this.getSystemPreference();
this.applyTheme();
this.setupToggle();
}
getStoredTheme() {
return localStorage.getItem('theme');
}
getSystemPreference() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
setTheme(theme) {
this.theme = theme;
localStorage.setItem('theme', theme);
this.applyTheme();
}
applyTheme() {
const root = document.documentElement;
if (this.theme === 'dark') {
root.setAttribute('data-theme', 'dark');
const icon = document.getElementById('dark-mode-icon');
if (icon) {
icon.classList.remove('fa-moon');
icon.classList.add('fa-sun');
}
} else {
root.setAttribute('data-theme', 'light');
const icon = document.getElementById('dark-mode-icon');
if (icon) {
icon.classList.remove('fa-sun');
icon.classList.add('fa-moon');
}
}
}
toggleTheme() {
const newTheme = this.theme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
}
setupToggle() {
const toggle = document.getElementById('dark-mode-toggle');
if (toggle) {
toggle.addEventListener('click', () => {
this.toggleTheme();
});
}
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
// Only apply system preference if user hasn't set a preference
if (!this.getStoredTheme()) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
}
}
// Initialize connection manager when page loads
document.addEventListener('DOMContentLoaded', () => {
// Initialize dark mode manager first
window.darkModeManager = new DarkModeManager();
window.connectionManager = new ModernConnectionManager();
// Update timestamp immediately and every second
updateTimestamp();
setInterval(updateTimestamp, 1000);
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>