diff --git a/app/routes/__pycache__/api.cpython-313.pyc b/app/routes/__pycache__/api.cpython-313.pyc index 98194e4..7412ec5 100644 Binary files a/app/routes/__pycache__/api.cpython-313.pyc and b/app/routes/__pycache__/api.cpython-313.pyc differ diff --git a/app/routes/api.py b/app/routes/api.py index 1e819a1..7332934 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -325,4 +325,41 @@ def get_subnet_servers(subnet_id): 'id': server.id, 'hostname': server.hostname, 'ip_address': server.ip_address - } for server in servers]) \ No newline at end of file + } for server in servers]) + +@bp.route('/server//ports', methods=['GET']) +@login_required +def get_server_ports(server_id): + """Get all used ports for a server""" + server = Server.query.get_or_404(server_id) + + # Get all ports associated with this server + ports = Port.query.filter_by(server_id=server_id).all() + used_ports = [port.number for port in ports] + + return jsonify({ + 'server_id': server_id, + 'used_ports': used_ports + }) + +@bp.route('/server//free-port', methods=['GET']) +@login_required +def get_free_port(server_id): + """Find a free port for a server""" + server = Server.query.get_or_404(server_id) + + # Get all ports associated with this server + used_ports = [port.number for port in Port.query.filter_by(server_id=server_id).all()] + + # Find the first free port (starting from 8000) + for port_number in range(8000, 9000): + if port_number not in used_ports: + return jsonify({ + 'success': True, + 'port': port_number + }) + + return jsonify({ + 'success': False, + 'error': 'No free ports available in the range 8000-9000' + }) \ No newline at end of file diff --git a/app/static/js/ports.js b/app/static/js/ports.js new file mode 100644 index 0000000..7d1a885 --- /dev/null +++ b/app/static/js/ports.js @@ -0,0 +1,181 @@ +document.addEventListener('DOMContentLoaded', function () { + // Initialize the compact port display + initPortDisplay(); + + // Add event listener for copy buttons + document.addEventListener('click', function (e) { + if (e.target.classList.contains('copy-port')) { + const port = e.target.dataset.port; + copyToClipboard(port); + + // Visual feedback + const originalText = e.target.innerHTML; + e.target.innerHTML = ''; + setTimeout(() => { + e.target.innerHTML = originalText; + }, 1000); + } + }); + + // Get free port button + const getFreePortBtn = document.getElementById('get-free-port'); + if (getFreePortBtn) { + getFreePortBtn.addEventListener('click', function () { + fetch(`/api/server/${serverId}/free-port`) + .then(response => response.json()) + .then(data => { + if (data.success) { + copyToClipboard(data.port); + showNotification('success', `Free port ${data.port} copied to clipboard`); + } else { + showNotification('error', data.error || 'Error finding free port'); + } + }) + .catch(error => { + console.error('Error:', error); + showNotification('error', 'Failed to get free port'); + }); + }); + } +}); + +function initPortDisplay() { + const portRangesContainer = document.getElementById('port-ranges'); + const usedPortsList = document.getElementById('used-ports-list'); + + if (!portRangesContainer || !usedPortsList) return; + + // Get port usage data from server + fetch(`/api/server/${serverId}/ports`) + .then(response => response.json()) + .then(data => { + // Process the data and organize into ranges + const usedPorts = data.used_ports || []; + const ranges = generatePortRanges(usedPorts); + + // Render port ranges visualization + renderPortRanges(ranges, portRangesContainer); + + // Render list of used ports with copy buttons + renderUsedPorts(usedPorts, usedPortsList); + }) + .catch(error => { + console.error('Error fetching port data:', error); + portRangesContainer.innerHTML = '
Failed to load port data
'; + }); +} + +function generatePortRanges(usedPorts) { + // Create a simplified representation of port ranges + const ranges = []; + const portMax = 9100; + const segmentSize = 100; + + for (let i = 0; i < portMax; i += segmentSize) { + const start = i; + const end = Math.min(i + segmentSize - 1, portMax); + + // Count used ports in this range + const usedCount = usedPorts.filter(port => port >= start && port <= end).length; + const percentageUsed = (usedCount / segmentSize) * 100; + + ranges.push({ + start, + end, + percentageUsed, + isEmpty: usedCount === 0, + isFull: usedCount === segmentSize + }); + } + + return ranges; +} + +function renderPortRanges(ranges, container) { + let html = ''; + + ranges.forEach(range => { + const width = (range.end - range.start + 1) / 91; // Calculate relative width + const cssClass = range.percentageUsed > 0 ? 'port-range-used' : 'port-range-free'; + + html += `
`; + }); + + container.innerHTML = html; +} + +function renderUsedPorts(usedPorts, container) { + if (usedPorts.length === 0) { + container.innerHTML = '
No ports in use
'; + return; + } + + let html = ''; + usedPorts.sort((a, b) => a - b).forEach(port => { + html += ` +
+ ${port} + +
+ `; + }); + + container.innerHTML = html; +} + +function copyToClipboard(text) { + // Use modern clipboard API with fallback + if (navigator.clipboard) { + navigator.clipboard.writeText(text) + .catch(err => { + console.error('Failed to copy: ', err); + fallbackCopyToClipboard(text); + }); + } else { + fallbackCopyToClipboard(text); + } +} + +function fallbackCopyToClipboard(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + } catch (err) { + console.error('Fallback copy failed:', err); + } + + document.body.removeChild(textArea); +} + +function showNotification(type, message) { + const notificationArea = document.getElementById('notification-area'); + if (!notificationArea) return; + + const notification = document.createElement('div'); + notification.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show notification`; + notification.innerHTML = ` + ${message} + + `; + + notificationArea.appendChild(notification); + + // Auto dismiss after 5 seconds + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => { + notificationArea.removeChild(notification); + }, 300); + }, 5000); +} \ No newline at end of file diff --git a/app/templates/dashboard/app_view.html b/app/templates/dashboard/app_view.html index ed7b910..8363d72 100644 --- a/app/templates/dashboard/app_view.html +++ b/app/templates/dashboard/app_view.html @@ -120,7 +120,7 @@
{% if app.documentation %} - {{ app.documentation|markdown|safe }} + {{ app.documentation|markdown }} {% else %}
@@ -147,7 +147,7 @@