Files
mc-webui/app/static/js/console.js
MarekWo c376ecff30 fix: Proxy console WebSocket through main app for HTTPS compatibility
Browser blocks mixed content (HTTPS page -> HTTP WebSocket on port 5001).
Solution: Route WebSocket through main Flask app which goes through
existing HTTPS reverse proxy.

- Add Flask-SocketIO to main mc-webui app
- WebSocket handler proxies commands to bridge via HTTP
- Remove port 5001 external exposure (no longer needed)
- Remove duplicate title from console header

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 13:18:56 +01:00

273 lines
7.2 KiB
JavaScript

/**
* mc-webui Console - Chat-style meshcli interface
*
* Provides interactive command console for meshcli via WebSocket.
* Commands are sent to meshcore-bridge and responses are displayed
* in a chat-like format.
*/
let socket = null;
let isConnected = false;
let commandHistory = [];
let historyIndex = -1;
let pendingCommandDiv = null;
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
console.log('Console page initialized');
connectWebSocket();
setupInputHandlers();
});
/**
* Connect to WebSocket server (proxied through main Flask app)
*/
function connectWebSocket() {
updateStatus('connecting');
// Connect to same origin - WebSocket is proxied through main Flask app
const wsUrl = window.location.origin;
console.log('Connecting to WebSocket:', wsUrl);
try {
socket = io(wsUrl + '/console', {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000
});
// Connection events
socket.on('connect', () => {
console.log('WebSocket connected');
isConnected = true;
updateStatus('connected');
enableInput(true);
addMessage('Connected to meshcli', 'system');
});
socket.on('disconnect', (reason) => {
console.log('WebSocket disconnected:', reason);
isConnected = false;
updateStatus('disconnected');
enableInput(false);
addMessage('Disconnected from meshcli', 'error');
// Clear pending command indicator
if (pendingCommandDiv) {
pendingCommandDiv.classList.remove('pending');
pendingCommandDiv = null;
}
});
socket.on('connect_error', (error) => {
console.error('WebSocket connection error:', error);
updateStatus('disconnected');
});
// Console events from server
socket.on('console_status', (data) => {
console.log('Console status:', data);
if (data.message) {
addMessage(data.message, 'system');
}
});
socket.on('command_response', (data) => {
console.log('Command response:', data);
// Clear pending indicator
if (pendingCommandDiv) {
pendingCommandDiv.classList.remove('pending');
pendingCommandDiv = null;
}
// Display response
if (data.success) {
const output = data.output || '(no output)';
addMessage(output, 'response');
} else {
addMessage(`Error: ${data.error}`, 'error');
}
scrollToBottom();
});
} catch (error) {
console.error('Failed to create WebSocket connection:', error);
updateStatus('disconnected');
addMessage('Failed to connect: ' + error.message, 'error');
}
}
/**
* Setup input form handlers
*/
function setupInputHandlers() {
const form = document.getElementById('consoleForm');
const input = document.getElementById('commandInput');
// Form submit
form.addEventListener('submit', (e) => {
e.preventDefault();
sendCommand();
});
// Command history navigation with arrow keys
input.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
navigateHistory(-1);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
navigateHistory(1);
}
});
}
/**
* Send command to meshcli
*/
function sendCommand() {
const input = document.getElementById('commandInput');
const command = input.value.trim();
if (!command || !isConnected) {
return;
}
// Add to history (avoid duplicates at end)
if (commandHistory.length === 0 || commandHistory[commandHistory.length - 1] !== command) {
commandHistory.push(command);
// Limit history size
if (commandHistory.length > 100) {
commandHistory.shift();
}
}
historyIndex = commandHistory.length;
// Show command in chat with pending indicator
pendingCommandDiv = addMessage(command, 'command pending');
// Send to server
socket.emit('send_command', { command: command });
// Clear input
input.value = '';
scrollToBottom();
}
/**
* Navigate command history
* @param {number} direction -1 for older, 1 for newer
*/
function navigateHistory(direction) {
const input = document.getElementById('commandInput');
if (commandHistory.length === 0) {
return;
}
historyIndex += direction;
// Clamp to valid range
if (historyIndex < 0) {
historyIndex = 0;
}
if (historyIndex >= commandHistory.length) {
historyIndex = commandHistory.length;
input.value = '';
return;
}
input.value = commandHistory[historyIndex];
// Move cursor to end
setTimeout(() => {
input.selectionStart = input.selectionEnd = input.value.length;
}, 0);
}
/**
* Add message to console display
* @param {string} text Message text
* @param {string} type Message type: 'command', 'response', 'error', 'system'
* @returns {HTMLElement} The created message div
*/
function addMessage(text, type) {
const container = document.getElementById('consoleMessages');
const div = document.createElement('div');
div.className = `console-message ${type}`;
div.textContent = text;
container.appendChild(div);
return div;
}
/**
* Scroll messages container to bottom
*/
function scrollToBottom() {
const container = document.getElementById('consoleMessages');
// Use setTimeout to ensure DOM is updated
setTimeout(() => {
container.scrollTop = container.scrollHeight;
}, 10);
}
/**
* Update connection status indicator
* @param {string} status 'connected', 'disconnected', or 'connecting'
*/
function updateStatus(status) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
if (!dot || !text) return;
dot.className = `status-dot ${status}`;
switch (status) {
case 'connected':
text.textContent = 'Connected';
text.className = 'text-success';
break;
case 'disconnected':
text.textContent = 'Disconnected';
text.className = 'text-danger';
break;
case 'connecting':
text.textContent = 'Connecting...';
text.className = 'text-warning';
break;
}
}
/**
* Enable or disable input controls
* @param {boolean} enabled
*/
function enableInput(enabled) {
const input = document.getElementById('commandInput');
const btn = document.getElementById('sendBtn');
if (input) {
input.disabled = !enabled;
if (enabled) {
input.focus();
}
}
if (btn) {
btn.disabled = !enabled;
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (socket) {
socket.disconnect();
}
});