mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-14 20:36:59 +00:00
update beta ProxMenux 1.2.1.1-beta
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import type React from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { API_PORT, fetchApi } from "@/lib/api-config" // Unificando importaciones de api-config en una sola línea con alias @/
|
||||
import { getTicketedWsUrl } from "@/lib/terminal-ws"
|
||||
import {
|
||||
Activity,
|
||||
Trash2,
|
||||
@@ -16,7 +17,10 @@ import {
|
||||
Grid2X2,
|
||||
GripHorizontal,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
Clipboard,
|
||||
} from "lucide-react"
|
||||
import { copyTerminalSelection, pasteFromClipboard } from "@/lib/terminal-clipboard"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -156,6 +160,9 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
const [useOnline, setUseOnline] = useState(true)
|
||||
|
||||
const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
|
||||
// Per-terminal reconnect attempt count + last-fired timestamp for the
|
||||
// exponential backoff in the visibilitychange handler.
|
||||
const reconnectAttemptsRef = useRef<{ [key: string]: { attempts: number; lastAt: number } }>({})
|
||||
|
||||
useEffect(() => {
|
||||
const updateDeviceType = () => {
|
||||
@@ -184,21 +191,35 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
// Handle page visibility change for automatic reconnection when user returns
|
||||
// This is especially important for mobile/tablet devices (iPad) where switching apps
|
||||
// puts the browser tab in background and may close WebSocket connections
|
||||
//
|
||||
// Per-terminal exponential backoff (2s, 4s, 8s, ..., capped at 60s) so a
|
||||
// server-side outage doesn't get hammered every time the user switches
|
||||
// tabs. `reconnectAttemptsRef` survives re-renders and tracks attempts +
|
||||
// last-fired timestamps. The success path in `reconnectTerminal.onopen`
|
||||
// resets the counter back to 0.
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// When page becomes visible again, check all terminal connections
|
||||
terminals.forEach((terminal) => {
|
||||
if (terminal.ws && terminal.ws.readyState !== WebSocket.OPEN && terminal.term) {
|
||||
// Terminal is disconnected, attempt to reconnect
|
||||
reconnectTerminal(terminal.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (document.visibilityState !== 'visible') return
|
||||
const now = Date.now()
|
||||
terminals.forEach((terminal) => {
|
||||
if (!(terminal.ws && terminal.ws.readyState !== WebSocket.OPEN && terminal.term)) {
|
||||
return
|
||||
}
|
||||
const state = reconnectAttemptsRef.current[terminal.id] || { attempts: 0, lastAt: 0 }
|
||||
const backoffMs = Math.min(60000, 2000 * Math.pow(2, state.attempts))
|
||||
if (now - state.lastAt < backoffMs) {
|
||||
return
|
||||
}
|
||||
reconnectAttemptsRef.current[terminal.id] = {
|
||||
attempts: state.attempts + 1,
|
||||
lastAt: now,
|
||||
}
|
||||
reconnectTerminal(terminal.id)
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
@@ -269,7 +290,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
throw new Error("No examples found")
|
||||
}
|
||||
|
||||
console.log("[v0] Received parsed examples from server:", data.examples.length)
|
||||
|
||||
const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({
|
||||
command: example.command,
|
||||
@@ -280,7 +300,6 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
setUseOnline(true)
|
||||
setSearchResults(formattedResults)
|
||||
} catch (error) {
|
||||
console.log("[v0] Error fetching from cheat.sh proxy, using offline commands:", error)
|
||||
const filtered = proxmoxCommands.filter(
|
||||
(item) =>
|
||||
item.cmd.toLowerCase().includes(query.toLowerCase()) ||
|
||||
@@ -314,11 +333,14 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
|
||||
// Show reconnecting message
|
||||
terminal.term.writeln('\r\n\x1b[33m[INFO] Reconnecting...\x1b[0m')
|
||||
|
||||
|
||||
const wsUrl = websocketUrl || getWebSocketUrl()
|
||||
const ws = new WebSocket(wsUrl)
|
||||
// Append the single-use auth ticket so the backend handshake can validate.
|
||||
const ws = new WebSocket(await getTicketedWsUrl(wsUrl))
|
||||
|
||||
ws.onopen = () => {
|
||||
// Successful connect — reset backoff state for this terminal.
|
||||
reconnectAttemptsRef.current[terminalId] = { attempts: 0, lastAt: 0 }
|
||||
// Clear any existing ping interval
|
||||
if (terminal.pingInterval) {
|
||||
clearInterval(terminal.pingInterval)
|
||||
@@ -479,11 +501,22 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
import("xterm/css/xterm.css"),
|
||||
]).then(([Terminal, FitAddon]) => [Terminal, FitAddon])
|
||||
|
||||
// After the (potentially slow) dynamic import, verify the container
|
||||
// is still the one we were given. If the user removed the terminal
|
||||
// tab while xterm was loading, the original `container` element is
|
||||
// detached and `containerRefs.current[terminal.id]` is gone — bail
|
||||
// out to avoid attaching to a stale DOM node + opening an orphan
|
||||
// WebSocket. Audit Tier 6 — `import("xterm")` sin cancelación.
|
||||
if (containerRefs.current[terminal.id] !== container) return
|
||||
|
||||
const fontSize = window.innerWidth < 768 ? 12 : 16
|
||||
|
||||
const term = new TerminalClass({
|
||||
rendererType: "dom",
|
||||
fontFamily: '"Courier", "Courier New", "Liberation Mono", "DejaVu Sans Mono", monospace',
|
||||
// Issue #182: prepend common Nerd Font families so users who already
|
||||
// have one installed see Starship/atuin/ble.sh icons render. Falls
|
||||
// back to Courier if no NF is present.
|
||||
fontFamily: '"MesloLGS NF", "FiraCode Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "Symbols Nerd Font", "Courier", "Courier New", "Liberation Mono", "DejaVu Sans Mono", monospace',
|
||||
fontSize: fontSize,
|
||||
lineHeight: 1,
|
||||
cursorBlink: true,
|
||||
@@ -524,12 +557,13 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
|
||||
fitAddon.fit()
|
||||
|
||||
const wsUrl = websocketUrl || getWebSocketUrl()
|
||||
|
||||
|
||||
// Connection with timeout for VPN/mobile (15 seconds)
|
||||
const connectionTimeout = 15000
|
||||
let connectionTimedOut = false
|
||||
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
// Single-use auth ticket appended as ?ticket=... — see lib/terminal-ws.ts.
|
||||
const ws = new WebSocket(await getTicketedWsUrl(wsUrl))
|
||||
|
||||
// Set connection timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -724,12 +758,35 @@ const handleClose = () => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
|
||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||
if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) {
|
||||
activeTerminal.ws.send(seq)
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile clipboard helpers — desktop users have ctrl/cmd shortcuts via xterm,
|
||||
// but on touch devices xterm's selection / clipboard isn't reachable from the
|
||||
// OS clipboard manager so we expose explicit Copy / Paste buttons.
|
||||
const handleCopy = async (e?: React.MouseEvent | React.TouchEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||
await copyTerminalSelection(activeTerminal?.term)
|
||||
}
|
||||
|
||||
const handlePaste = async (e?: React.MouseEvent | React.TouchEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||
if (!activeTerminal?.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return
|
||||
const ws = activeTerminal.ws
|
||||
await pasteFromClipboard((text) => ws.send(text))
|
||||
}
|
||||
|
||||
const getLayoutClass = () => {
|
||||
const count = terminals.length
|
||||
@@ -1015,7 +1072,7 @@ const handleClose = () => {
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Control Sequences</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={() => sendSequence("\x03")}>
|
||||
@@ -1030,6 +1087,16 @@ const handleClose = () => {
|
||||
<span className="font-mono text-xs mr-2">Ctrl+R</span>
|
||||
<span className="text-muted-foreground text-xs">Search history</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Clipboard</DropdownMenuLabel>
|
||||
<DropdownMenuItem onSelect={() => { void handleCopy() }}>
|
||||
<Copy className="h-3.5 w-3.5 mr-2" />
|
||||
<span className="text-xs">Copy selection</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => { void handlePaste() }}>
|
||||
<Clipboard className="h-3.5 w-3.5 mr-2" />
|
||||
<span className="text-xs">Paste</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user