mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-06-03 22:31:18 +00:00
ca80924e38
- Updated `config.ini.example` to include a new option for additional hashtag channels to decode in the packet stream. - Modified `BotDataViewer` to retrieve and display additional decode-only channels from the configuration. - Improved packet handling in `message_handler.py` to capture full packet data for web viewer integration. - Enhanced the web viewer's JavaScript to support detailed packet analysis and display, including color-coded hex breakdowns and improved user interface elements. - Added new styles and scripts to the web viewer templates for better visual representation of packet data and improved user experience.
656 lines
22 KiB
HTML
656 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>
|
|
|
|
<!-- 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 */
|
|
body {
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.footer {
|
|
background-color: var(--footer-bg);
|
|
padding: 20px 0;
|
|
margin-top: 50px;
|
|
border-top: 1px solid var(--border-color);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.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="/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">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="footer">
|
|
<div class="container">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<p class="text-muted mb-0">
|
|
<i class="fas fa-satellite-dish"></i> MeshCore Bot Data Viewer v2.0
|
|
</p>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<p class="text-muted mb-0">
|
|
Last updated: <span id="last-update">Loading...</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- 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>
|