mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-12-15 00:26:23 +00:00
Update AppImage
This commit is contained in:
@@ -273,7 +273,7 @@ export function ProxmoxDashboard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<div className="min-h-screen bg-background">
|
||||
<OnboardingCarousel />
|
||||
<ReleaseNotesModal open={showReleaseNotes} onClose={() => setShowReleaseNotes(false)} />
|
||||
|
||||
@@ -610,8 +610,8 @@ export function ProxmoxDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6 flex-1 flex flex-col min-h-0">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0">
|
||||
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4 md:space-y-6">
|
||||
<TabsContent value="overview" className="space-y-4 md:space-y-6 mt-0">
|
||||
<SystemOverview key={`overview-${componentKey}`} />
|
||||
</TabsContent>
|
||||
@@ -632,11 +632,7 @@ export function ProxmoxDashboard() {
|
||||
<Hardware key={`hardware-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="terminal"
|
||||
className="flex flex-col overflow-hidden mt-0 w-full"
|
||||
style={{ height: "calc(100vh - 300px)" }}
|
||||
>
|
||||
<TabsContent value="terminal" className="mt-0">
|
||||
<TerminalPanel key={`terminal-${componentKey}`} />
|
||||
</TabsContent>
|
||||
|
||||
@@ -648,9 +644,8 @@ export function ProxmoxDashboard() {
|
||||
<Settings />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<footer className="container mx-auto px-4 md:px-6 py-4 border-t border-border text-center text-xs md:text-sm text-muted-foreground flex-shrink-0">
|
||||
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
|
||||
<p className="font-medium mb-2">ProxMenux Monitor v1.0.1</p>
|
||||
<p>
|
||||
<a
|
||||
@@ -663,6 +658,7 @@ export function ProxmoxDashboard() {
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<HealthStatusModal open={showHealthModal} onOpenChange={setShowHealthModal} getApiUrl={getApiUrl} />
|
||||
</div>
|
||||
|
||||
@@ -288,6 +288,8 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
cursorBlink: true,
|
||||
scrollback: 2000,
|
||||
disableStdin: false,
|
||||
cols: isMobile ? 40 : layout === "grid" ? 60 : 120,
|
||||
rows: isMobile ? 20 : layout === "grid" ? 15 : 30,
|
||||
theme: {
|
||||
background: "#000000",
|
||||
foreground: "#ffffff",
|
||||
@@ -316,47 +318,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
term.loadAddon(fitAddon)
|
||||
|
||||
term.open(container)
|
||||
|
||||
const performResize = () => {
|
||||
// Ensure xterm viewport has no extra padding
|
||||
const xtermViewport = container.querySelector(".xterm-viewport") as HTMLElement
|
||||
const xtermScreen = container.querySelector(".xterm-screen") as HTMLElement
|
||||
if (xtermViewport) xtermViewport.style.padding = "0"
|
||||
if (xtermScreen) xtermScreen.style.padding = "0"
|
||||
|
||||
// Get actual container dimensions
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
console.log(`[v0] Container dimensions: ${containerRect.width}x${containerRect.height}`)
|
||||
|
||||
// Only resize if container has valid dimensions
|
||||
if (containerRect.width > 0 && containerRect.height > 0) {
|
||||
fitAddon.fit()
|
||||
const cols = term.cols
|
||||
const rows = term.rows
|
||||
console.log(`[v0] Terminal fitted to: ${cols}x${rows}`)
|
||||
|
||||
// Send resize to backend via HTTP
|
||||
const apiUrl = getApiUrl()
|
||||
fetch(`${apiUrl}/api/terminal/${terminal.id}/resize`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ cols, rows }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(`[v0] Backend PTY resized to: ${data.cols}x${data.rows}`)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`[v0] Error resizing backend PTY:`, err)
|
||||
})
|
||||
} else {
|
||||
console.log(`[v0] Container not ready yet, dimensions: ${containerRect.width}x${containerRect.height}`)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => performResize(), 150)
|
||||
setTimeout(() => performResize(), 400)
|
||||
setTimeout(() => performResize(), 800)
|
||||
|
||||
const wsUrl = websocketUrl || getWebSocketUrl()
|
||||
const ws = new WebSocket(wsUrl)
|
||||
@@ -364,9 +326,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
ws.onopen = () => {
|
||||
setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws } : t)))
|
||||
term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m")
|
||||
|
||||
setTimeout(() => performResize(), 250)
|
||||
setTimeout(() => performResize(), 600)
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
@@ -392,7 +351,7 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
|
||||
const handleResize = () => {
|
||||
try {
|
||||
performResize()
|
||||
fitAddon.fit()
|
||||
} catch {
|
||||
// Ignore resize errors
|
||||
}
|
||||
@@ -496,18 +455,8 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx>{`
|
||||
:global(.xterm .xterm-viewport) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
:global(.xterm .xterm-screen) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="h-full w-full flex flex-col bg-zinc-900 rounded-md overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800 flex-shrink-0">
|
||||
<div className="flex flex-col h-full bg-zinc-950 rounded-md overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-blue-500" />
|
||||
<div
|
||||
@@ -588,57 +537,57 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden w-full">
|
||||
<div className="flex-1 min-h-0">
|
||||
{isMobile ? (
|
||||
<Tabs value={activeTerminalId} onValueChange={setActiveTerminalId} className="h-full w-full flex flex-col">
|
||||
<TabsList className="bg-zinc-900 border-b border-zinc-800 rounded-none justify-start overflow-x-auto flex-shrink-0">
|
||||
<Tabs value={activeTerminalId} onValueChange={setActiveTerminalId} className="h-full flex flex-col">
|
||||
<TabsList className="w-full justify-start bg-zinc-900 rounded-none border-b border-zinc-800">
|
||||
{terminals.map((terminal) => (
|
||||
<TabsTrigger
|
||||
key={terminal.id}
|
||||
value={terminal.id}
|
||||
className="data-[state=active]:bg-blue-500/20 data-[state=active]:border-b-2 data-[state=active]:border-blue-500 rounded-none"
|
||||
>
|
||||
<TabsTrigger key={terminal.id} value={terminal.id} className="relative">
|
||||
{terminal.title}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{terminals.map((terminal) => (
|
||||
<TabsContent
|
||||
key={terminal.id}
|
||||
value={terminal.id}
|
||||
className="flex-1 m-0 data-[state=active]:flex data-[state=inactive]:hidden"
|
||||
>
|
||||
<div ref={setContainerRef(terminal.id)} className="w-full h-full bg-black" />
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className={`${getLayoutClass()} gap-2 h-full w-full p-2`}>
|
||||
{terminals.map((terminal) => (
|
||||
<div
|
||||
key={terminal.id}
|
||||
className={`relative bg-black rounded flex flex-col overflow-hidden border ${
|
||||
terminal.id === activeTerminalId ? "border-blue-500" : "border-zinc-700"
|
||||
}`}
|
||||
onClick={() => setActiveTerminalId(terminal.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between px-2 py-1 bg-zinc-800 border-b border-zinc-700">
|
||||
<span className="text-xs font-medium text-white">{terminal.title}</span>
|
||||
{terminals.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0"
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
closeTerminal(terminal.id)
|
||||
}}
|
||||
className="ml-2 hover:bg-zinc-700 rounded p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{terminals.map((terminal) => (
|
||||
<TabsContent key={terminal.id} value={terminal.id} className="flex-1 m-0 p-0">
|
||||
<div
|
||||
ref={setContainerRef(terminal.id)}
|
||||
className="w-full h-full bg-black"
|
||||
style={{ height: "calc(100vh - 24rem)" }}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className={`${getLayoutClass()} h-full gap-0.5 bg-zinc-800 p-0.5`}>
|
||||
{terminals.map((terminal) => (
|
||||
<div key={terminal.id} className="relative bg-zinc-900 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-2 py-1 bg-zinc-900/95 border-b border-zinc-800">
|
||||
<button
|
||||
onClick={() => setActiveTerminalId(terminal.id)}
|
||||
className={`text-xs font-medium ${
|
||||
activeTerminalId === terminal.id ? "text-blue-400" : "text-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{terminal.title}
|
||||
</button>
|
||||
{terminals.length > 1 && (
|
||||
<button onClick={() => closeTerminal(terminal.id)} className="hover:bg-zinc-700 rounded p-0.5">
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div ref={setContainerRef(terminal.id)} className="flex-1 w-full bg-black" />
|
||||
<div ref={setContainerRef(terminal.id)} className="w-full h-full bg-black pt-7" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -850,6 +799,5 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,79 @@ import requests
|
||||
terminal_bp = Blueprint('terminal', __name__)
|
||||
sock = Sock()
|
||||
|
||||
# Active terminal sessions - now stores session by session_id string
|
||||
sessions = {}
|
||||
# Active terminal sessions
|
||||
active_sessions = {}
|
||||
|
||||
@terminal_bp.route('/api/terminal/health', methods=['GET'])
|
||||
def terminal_health():
|
||||
"""Health check for terminal service"""
|
||||
return {'success': True, 'active_sessions': len(active_sessions)}
|
||||
|
||||
@terminal_bp.route('/api/terminal/search-command', methods=['GET'])
|
||||
def search_command():
|
||||
"""Proxy endpoint for cheat.sh API to avoid CORS issues"""
|
||||
query = request.args.get('q', '')
|
||||
|
||||
if not query or len(query) < 2:
|
||||
return jsonify({'error': 'Query too short'}), 400
|
||||
|
||||
try:
|
||||
url = f'https://cht.sh/{query.replace(" ", "+")}?QT'
|
||||
headers = {
|
||||
'User-Agent': 'curl/7.68.0'
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
content = response.text
|
||||
examples = []
|
||||
current_description = []
|
||||
|
||||
for line in content.split('\n'):
|
||||
stripped = line.strip()
|
||||
|
||||
# Ignorar líneas vacías
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
# Si es un comentario
|
||||
if stripped.startswith('#'):
|
||||
# Acumular descripciones
|
||||
current_description.append(stripped[1:].strip())
|
||||
# Si no es comentario, es un comando
|
||||
elif stripped and not stripped.startswith('http'):
|
||||
# Unir las descripciones acumuladas
|
||||
description = ' '.join(current_description) if current_description else ''
|
||||
|
||||
examples.append({
|
||||
'description': description,
|
||||
'command': stripped
|
||||
})
|
||||
|
||||
# Resetear descripciones para el siguiente comando
|
||||
current_description = []
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'examples': examples
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'API returned status {response.status_code}'
|
||||
}), response.status_code
|
||||
|
||||
except requests.Timeout:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Request timeout'
|
||||
}), 504
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
def set_winsize(fd, rows, cols):
|
||||
"""Set terminal window size"""
|
||||
@@ -50,33 +121,6 @@ def read_and_forward_output(master_fd, ws):
|
||||
print(f"Error reading from PTY: {e}")
|
||||
break
|
||||
|
||||
@terminal_bp.route('/api/terminal/health', methods=['GET'])
|
||||
def terminal_health():
|
||||
"""Health check for terminal service"""
|
||||
return {'success': True, 'active_sessions': len(sessions)}
|
||||
|
||||
@terminal_bp.route('/api/terminal/<session_id>/resize', methods=['POST'])
|
||||
def resize_terminal(session_id):
|
||||
"""Resize the PTY for a given terminal session."""
|
||||
if session_id not in sessions:
|
||||
return jsonify({'error': 'Session not found'}), 404
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
cols = int(data.get('cols', 120))
|
||||
rows = int(data.get('rows', 30))
|
||||
|
||||
# Resize the PTY to match the frontend terminal dimensions
|
||||
master_fd = sessions[session_id]['master_fd']
|
||||
set_winsize(master_fd, rows, cols)
|
||||
|
||||
print(f"[v0] Terminal {session_id} resized to {cols}x{rows}")
|
||||
|
||||
return jsonify({'status': 'success', 'cols': cols, 'rows': rows})
|
||||
except Exception as e:
|
||||
print(f"Error resizing terminal {session_id}: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@sock.route('/ws/terminal')
|
||||
def terminal_websocket(ws):
|
||||
"""WebSocket endpoint for terminal sessions"""
|
||||
@@ -95,8 +139,8 @@ def terminal_websocket(ws):
|
||||
env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ')
|
||||
)
|
||||
|
||||
session_id = str(int(time.time() * 1000))
|
||||
sessions[session_id] = {
|
||||
session_id = id(ws)
|
||||
active_sessions[session_id] = {
|
||||
'process': shell_process,
|
||||
'master_fd': master_fd
|
||||
}
|
||||
@@ -105,10 +149,8 @@ def terminal_websocket(ws):
|
||||
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
|
||||
set_winsize(master_fd, 30, 80)
|
||||
|
||||
# Send session_id to frontend
|
||||
ws.send(f"\x1b]0;SESSION_ID:{session_id}\x07")
|
||||
# Set initial terminal size
|
||||
set_winsize(master_fd, 30, 120)
|
||||
|
||||
# Start thread to read PTY output and forward to WebSocket
|
||||
output_thread = threading.Thread(
|
||||
@@ -127,17 +169,14 @@ def terminal_websocket(ws):
|
||||
# Client closed connection
|
||||
break
|
||||
|
||||
# Handle terminal resize (optional)
|
||||
if data.startswith('\x1b[8;'):
|
||||
try:
|
||||
# Parse: \x1b[8;{rows};{cols}t
|
||||
parts = data[4:-1].split(';')
|
||||
if len(parts) >= 2:
|
||||
rows, cols = int(parts[0]), int(parts[1])
|
||||
set_winsize(master_fd, rows, cols)
|
||||
print(f"[v0] Terminal resized via WebSocket to {rows}x{cols}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Error parsing resize: {e}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Send input to bash
|
||||
@@ -174,8 +213,8 @@ def terminal_websocket(ws):
|
||||
except:
|
||||
pass
|
||||
|
||||
if session_id in sessions:
|
||||
del sessions[session_id]
|
||||
if session_id in active_sessions:
|
||||
del active_sessions[session_id]
|
||||
|
||||
def init_terminal_routes(app):
|
||||
"""Initialize terminal routes with Flask app"""
|
||||
|
||||
Reference in New Issue
Block a user