Files
meshcore-bot/modules/web_viewer/templates/realtime.html
2025-12-06 12:35:58 -08:00

429 lines
16 KiB
HTML

{% extends "base.html" %}
{% block title %}Real-time Monitoring - MeshCore Bot Data Viewer{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-broadcast-tower"></i> Real-time Monitoring
<small class="text-muted">Live command and packet stream monitoring</small>
</h1>
</div>
</div>
<!-- Stream Controls -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6><i class="fas fa-comments"></i> Command Stream</h6>
<div>
<span class="badge bg-success" id="command-status">Connected</span>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="clearCommands()">Clear</button>
</div>
</div>
<div class="card-body">
<div id="command-stream" class="stream-container">
<div class="text-muted text-center py-3">
<i class="fas fa-hourglass-half"></i> Waiting for command data...
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6><i class="fas fa-route"></i> Packet Stream</h6>
<div>
<span class="badge bg-success" id="packet-status">Connected</span>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="clearPackets()">Clear</button>
</div>
</div>
<div class="card-body">
<div id="packet-stream" class="stream-container">
<div class="text-muted text-center py-3">
<i class="fas fa-hourglass-half"></i> Waiting for packet data...
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Initialize Socket.IO connection
const socket = io();
// Connection status
let commandConnected = false;
let packetConnected = false;
// Load recent commands on page load
function loadRecentCommands() {
fetch('/api/recent_commands')
.then(response => response.json())
.then(data => {
if (data.commands && data.commands.length > 0) {
console.log(`Loading ${data.commands.length} recent commands`);
// Clear the waiting message
const container = document.getElementById('command-stream');
const waiting = container.querySelector('.text-muted.text-center');
if (waiting) {
waiting.remove();
}
// Add each command (in reverse order since they come newest first)
data.commands.reverse().forEach(commandData => {
addCommandEntry(commandData);
});
updateStatus('command-status', 'Loaded Recent', 'info');
}
})
.catch(error => {
console.error('Error loading recent commands:', error);
});
}
// Socket event handlers
socket.on('connect', function() {
console.log('Connected to server');
socket.emit('subscribe_commands');
socket.emit('subscribe_packets');
// Load recent commands when connected
loadRecentCommands();
// Start ping interval to keep connection alive
startPingInterval();
});
socket.on('disconnect', function() {
console.log('Disconnected from server');
updateStatus('command-status', 'Disconnected', 'danger');
updateStatus('packet-status', 'Disconnected', 'danger');
// Stop ping interval on disconnect
stopPingInterval();
});
socket.on('force_disconnect', function(data) {
console.log('Server requested disconnect:', data.reason);
updateStatus('command-status', 'Disconnected by server', 'warning');
updateStatus('packet-status', 'Disconnected by server', 'warning');
// Disconnect the socket
socket.disconnect();
});
socket.on('error', function(data) {
console.error('Socket error:', data.message);
updateStatus('command-status', 'Error', 'danger');
updateStatus('packet-status', 'Error', 'danger');
});
socket.on('status', function(data) {
console.log('Status:', data.message);
});
socket.on('command_data', function(data) {
addCommandEntry(data);
});
socket.on('packet_data', function(data) {
addPacketEntry(data);
});
// Ping mechanism to keep connection alive
let pingInterval = null;
function startPingInterval() {
if (pingInterval) clearInterval(pingInterval);
pingInterval = setInterval(function() {
if (socket.connected) {
socket.emit('ping');
}
}, 25000); // Ping every 25 seconds (less than 30s timeout)
}
function stopPingInterval() {
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
}
socket.on('pong', function(data) {
console.log('Pong received from server');
// Connection is alive, update status
updateStatus('command-status', 'Connected', 'success');
updateStatus('packet-status', 'Connected', 'success');
});
// Helper functions
function updateStatus(elementId, text, type) {
const element = document.getElementById(elementId);
element.textContent = text;
element.className = `badge bg-${type}`;
}
function addCommandEntry(data) {
const container = document.getElementById('command-stream');
// Remove waiting message if present
const waiting = container.querySelector('.text-muted.text-center');
if (waiting) {
waiting.remove();
}
// Create conversation-style entry
const entry = document.createElement('div');
entry.className = 'conversation-entry mb-3';
// User command message
const userMessage = document.createElement('div');
userMessage.className = 'd-flex justify-content-start mb-2';
userMessage.innerHTML = `
<div class="user-message p-2 rounded" style="max-width: 80%;">
<div class="d-flex align-items-center mb-1">
<strong class="text-primary me-2">${data.user || 'Unknown'}</strong>
<small class="text-muted me-2">${data.channel || 'Unknown'}</small>
<small class="text-muted ms-auto">${data.timestamp ? new Date(data.timestamp * 1000).toLocaleTimeString() : new Date().toLocaleTimeString()}</small>
</div>
<div class="fw-bold">${data.user_input || `/${data.command || 'Unknown Command'}`}</div>
</div>
`;
// Bot response message
const botMessage = document.createElement('div');
botMessage.className = 'd-flex justify-content-end mb-2';
const responseText = data.response || 'No response';
const responseClass = data.success ? 'success' : 'danger';
const botMessageClass = data.success ? 'bot-message-success' : 'bot-message-danger';
botMessage.innerHTML = `
<div class="bot-message p-2 rounded ${botMessageClass}" style="max-width: 80%;">
<div class="d-flex align-items-center mb-1">
<strong class="text-success me-2">🤖 Bot</strong>
<span class="badge bg-${responseClass} small me-2">${data.success ? 'Success' : 'Failed'}</span>
<small class="text-muted ms-auto">${data.timestamp ? new Date(data.timestamp * 1000).toLocaleTimeString() : new Date().toLocaleTimeString()}</small>
</div>
<div class="response-text">${responseText}</div>
</div>
`;
entry.appendChild(userMessage);
entry.appendChild(botMessage);
container.insertBefore(entry, container.firstChild);
// Limit to 25 conversation pairs (50 total messages)
while (container.children.length > 25) {
container.removeChild(container.lastChild);
}
updateStatus('command-status', 'Active', 'success');
}
function addPacketEntry(data) {
const container = document.getElementById('packet-stream');
// Remove waiting message if present
const waiting = container.querySelector('.text-muted.text-center');
if (waiting) {
waiting.remove();
}
// Format timestamp
const timestamp = data.datetime ? new Date(data.datetime).toLocaleTimeString() : new Date().toLocaleTimeString();
// Format path display as comma-separated list
const pathDisplay = data.path ? data.path.join(',') : 'No path';
// Format header with resolved components (human-readable with names and numbers)
let headerInfo = '';
if (data.header) {
const routeName = data.route_type_name || 'Unknown';
const routeValue = data.route_type !== undefined ? `0x${data.route_type.toString(16).padStart(2, '0')}` : 'Unknown';
const payloadName = data.payload_type_name || 'Unknown';
const payloadValue = data.payload_type !== undefined ? `0x${data.payload_type.toString(16).padStart(2, '0')}` : 'Unknown';
const version = data.payload_version !== undefined ? `v${data.payload_version}` : 'v?';
headerInfo = `<div class="small text-muted">Header: ${data.header} (${routeName} (${routeValue}) | ${payloadName} (${payloadValue}) | ${version})</div>`;
}
// Format transport info (only show if present)
const transportInfo = data.has_transport && data.transport_codes ?
`<div class="small text-warning">Transport: ${data.transport_codes.hex || 'Unknown'}</div>` : '';
// Format payload info (only show if present)
const payloadInfo = data.payload_bytes > 0 ?
`<div class="small text-muted">Payload: ${data.payload_bytes} bytes</div>` : '';
// Format advert info (only show if this is an ADVERT packet with advert data)
const advertInfo = data.advert_name && data.payload_type === 'ADVERT' ?
`<div class="small text-success"><strong>Advertised:</strong> ${data.advert_name} (${data.advert_mode || 'Unknown'})</div>` : '';
const entry = document.createElement('div');
entry.className = 'stream-entry mb-2 p-3 border rounded';
entry.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
<div class="fw-bold text-info me-2">${data.route_type || 'Unknown Route'}</div>
<span class="badge bg-secondary">${data.payload_type || 'Unknown'}</span>
${data.payload_version !== undefined ? `<span class="badge bg-light text-dark ms-1">v${data.payload_version}</span>` : ''}
</div>
<div class="small text-muted mb-1">
<strong>Path:</strong> ${pathDisplay}
${data.path_len > 0 ? ` (${data.path_len} bytes)` : ''}
</div>
${advertInfo}
${headerInfo}
${transportInfo}
${payloadInfo}
</div>
<div class="text-end">
<div class="small text-muted mb-1">${timestamp}</div>
<div class="d-flex flex-column align-items-end">
<span class="badge bg-info mb-1">${data.hops || 0} hops</span>
${data.has_transport ? '<span class="badge bg-warning small">Transport</span>' : ''}
</div>
</div>
</div>
`;
container.insertBefore(entry, container.firstChild);
// Limit to 50 entries
while (container.children.length > 50) {
container.removeChild(container.lastChild);
}
updateStatus('packet-status', 'Active', 'success');
}
function clearCommands() {
document.getElementById('command-stream').innerHTML = `
<div class="text-muted text-center py-3">
<i class="fas fa-hourglass-half"></i> Waiting for command data...
</div>
`;
}
function clearPackets() {
document.getElementById('packet-stream').innerHTML = `
<div class="text-muted text-center py-3">
<i class="fas fa-hourglass-half"></i> Waiting for packet data...
</div>
`;
}
</script>
<style>
.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; }
.stream-container {
height: calc(100vh - 300px); /* Use full viewport height minus header/nav space */
min-height: 400px; /* Ensure minimum usable height */
overflow-y: auto;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
padding: 0.5rem;
}
.stream-entry {
background-color: var(--card-bg);
transition: all 0.3s ease;
border-left: 3px solid #007bff;
color: var(--text-color);
}
.stream-entry:hover {
box-shadow: 0 2px 8px var(--shadow-color);
transform: translateY(-1px);
}
.stream-entry:first-child {
border-left: 4px solid #28a745;
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.2);
}
.badge {
font-size: 0.75em;
}
.text-warning {
color: #fd7e14 !important;
}
.conversation-entry {
border-left: 3px solid #007bff;
padding-left: 10px;
margin-left: 5px;
}
.user-message {
background-color: #e3f2fd;
border: 1px solid #bbdefb;
box-shadow: 0 1px 3px var(--shadow-color);
color: var(--text-color);
}
[data-theme="dark"] .user-message {
background-color: #1a237e;
border-color: #283593;
color: #e3f2fd;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.bot-message-success {
background-color: #e8f5e8;
border: 1px solid #c8e6c9;
box-shadow: 0 1px 3px var(--shadow-color);
color: var(--text-color);
}
[data-theme="dark"] .bot-message-success {
background-color: #1b5e20;
border-color: #2e7d32;
color: #c8e6c9;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.bot-message-danger {
background-color: #ffebee;
border: 1px solid #ffcdd2;
box-shadow: 0 1px 3px var(--shadow-color);
color: var(--text-color);
}
[data-theme="dark"] .bot-message-danger {
background-color: #4a148c;
border-color: #6a1b9a;
color: #f8bbd0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.response-text {
white-space: pre-wrap;
word-break: break-word;
}
</style>
{% endblock %}