mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-04-25 16:52:06 +00:00
429 lines
16 KiB
HTML
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 %}
|