Add host connection status check endpoint and UI indicator
- Implement handle_connection_status API to test host port reachability - Display Host:Up/Down status in the UI
This commit is contained in:
1 parent
1209384cad
commit
e4a3066445
3 files changed
+92
No files matched your search
@@ -134,6 +134,16 @@ body {
|
|||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.host-status {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-status.up { color: #34d399; }
|
||||||
|
.host-status.down { color: #f87171; }
|
||||||
|
.host-status.checking { color: #fbbf24; }
|
||||||
|
|
||||||
.connection-status {
|
.connection-status {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ let maxUploadSize = localStorage.getItem('maxUploadSize') ? parseInt(localStorag
|
|||||||
let apiBaseUrl = window.location.origin;
|
let apiBaseUrl = window.location.origin;
|
||||||
let backendWsPort = null;
|
let backendWsPort = null;
|
||||||
let backendWsHost = null;
|
let backendWsHost = null;
|
||||||
|
let hostStatusTimer = null;
|
||||||
|
|
||||||
// Use the frontend proxy so the browser does not need direct access to the backend port.
|
// Use the frontend proxy so the browser does not need direct access to the backend port.
|
||||||
function getWsUrl() {
|
function getWsUrl() {
|
||||||
@@ -41,6 +42,8 @@ async function loadSavedConnections() {
|
|||||||
if (!conn.terminalBuffer) conn.terminalBuffer = [];
|
if (!conn.terminalBuffer) conn.terminalBuffer = [];
|
||||||
});
|
});
|
||||||
renderConnectionList();
|
renderConnectionList();
|
||||||
|
refreshHostStatuses();
|
||||||
|
startHostStatusPolling();
|
||||||
if (connections.length > 0) {
|
if (connections.length > 0) {
|
||||||
term.writeln(`[Loaded ${connections.length} connections from server]`);
|
term.writeln(`[Loaded ${connections.length} connections from server]`);
|
||||||
}
|
}
|
||||||
@@ -454,6 +457,9 @@ function renderConnectionList() {
|
|||||||
<span class="connection-status ${conn.connected ? 'connected' : 'disconnected'}"></span>
|
<span class="connection-status ${conn.connected ? 'connected' : 'disconnected'}"></span>
|
||||||
${conn.username}@${conn.host}:${conn.port}
|
${conn.username}@${conn.host}:${conn.port}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="host-status ${conn.hostStatus === true ? 'up' : conn.hostStatus === false ? 'down' : 'checking'}">
|
||||||
|
Host: ${conn.hostStatus === true ? 'Up' : conn.hostStatus === false ? 'Down' : 'Checking...'}
|
||||||
|
</div>
|
||||||
<div class="connection-actions" onclick="event.stopPropagation()">
|
<div class="connection-actions" onclick="event.stopPropagation()">
|
||||||
${conn.connected
|
${conn.connected
|
||||||
? `<button class="btn-small btn-disconnect" onclick="disconnectConnection('${conn.id}')">Disconnect</button>`
|
? `<button class="btn-small btn-disconnect" onclick="disconnectConnection('${conn.id}')">Disconnect</button>`
|
||||||
@@ -467,6 +473,40 @@ function renderConnectionList() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshHostStatuses() {
|
||||||
|
if (!authToken || connections.length === 0) return;
|
||||||
|
|
||||||
|
connections.forEach(conn => { conn.hostStatus = null; });
|
||||||
|
renderConnectionList();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBaseUrl}/api/connections/status`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${authToken}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ids: connections.map(conn => conn.id) })
|
||||||
|
});
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
connections.forEach(conn => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data.statuses || {}, conn.id)) {
|
||||||
|
conn.hostStatus = data.statuses[conn.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderConnectionList();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to refresh host statuses:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startHostStatusPolling() {
|
||||||
|
if (hostStatusTimer) clearInterval(hostStatusTimer);
|
||||||
|
hostStatusTimer = setInterval(refreshHostStatuses, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
function selectConnection(id) {
|
function selectConnection(id) {
|
||||||
currentConnectionId = id;
|
currentConnectionId = id;
|
||||||
const conn = getCurrentConnection();
|
const conn = getCurrentConnection();
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import hashlib
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
import stat
|
import stat
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from http.server import BaseHTTPRequestHandler
|
from http.server import BaseHTTPRequestHandler
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -44,6 +46,8 @@ class LoginHandler(BaseHTTPRequestHandler):
|
|||||||
self.handle_verify()
|
self.handle_verify()
|
||||||
elif parsed.path == '/api/connections':
|
elif parsed.path == '/api/connections':
|
||||||
self.handle_save_connection()
|
self.handle_save_connection()
|
||||||
|
elif parsed.path == '/api/connections/status':
|
||||||
|
self.handle_connection_status()
|
||||||
elif parsed.path == '/api/config':
|
elif parsed.path == '/api/config':
|
||||||
self.handle_config()
|
self.handle_config()
|
||||||
elif parsed.path == '/api/sftp/list':
|
elif parsed.path == '/api/sftp/list':
|
||||||
@@ -197,6 +201,44 @@ class LoginHandler(BaseHTTPRequestHandler):
|
|||||||
logger.error(f"Get connections error: {e}")
|
logger.error(f"Get connections error: {e}")
|
||||||
self.send_json_response(500, {'error': 'Internal error'})
|
self.send_json_response(500, {'error': 'Internal error'})
|
||||||
|
|
||||||
|
def handle_connection_status(self):
|
||||||
|
"""Check whether the configured host ports are reachable."""
|
||||||
|
username = self._get_username_from_token()
|
||||||
|
if not username:
|
||||||
|
self.send_json_response(401, {'error': 'Authentication required'})
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_length = int(self.headers.get('Content-Length', 0))
|
||||||
|
body = self.rfile.read(content_length).decode('utf-8')
|
||||||
|
requested_ids = json.loads(body).get('ids', [])
|
||||||
|
if not isinstance(requested_ids, list):
|
||||||
|
self.send_json_response(400, {'error': 'Connection IDs must be a list'})
|
||||||
|
return
|
||||||
|
|
||||||
|
connections = creds_manager.get_connections(username)
|
||||||
|
requested_ids = set(requested_ids)
|
||||||
|
targets = [conn for conn in connections if conn['id'] in requested_ids]
|
||||||
|
|
||||||
|
def check_host(conn):
|
||||||
|
try:
|
||||||
|
with socket.create_connection(
|
||||||
|
(conn['host'], int(conn.get('port', 22))), timeout=2
|
||||||
|
):
|
||||||
|
return conn['id'], True
|
||||||
|
except (OSError, ValueError, TypeError):
|
||||||
|
return conn['id'], False
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
|
statuses = dict(executor.map(check_host, targets))
|
||||||
|
|
||||||
|
self.send_json_response(200, {'statuses': statuses})
|
||||||
|
except (json.JSONDecodeError, TypeError, ValueError):
|
||||||
|
self.send_json_response(400, {'error': 'Invalid request'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Connection status error: {e}")
|
||||||
|
self.send_json_response(500, {'error': 'Internal error'})
|
||||||
|
|
||||||
def handle_config(self):
|
def handle_config(self):
|
||||||
"""Get configuration including max upload size."""
|
"""Get configuration including max upload size."""
|
||||||
username = self._get_username_from_token()
|
username = self._get_username_from_token()
|
||||||
|
|||||||
Reference in new issue
Block a user