mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-12-14 16:16:21 +00:00
Update AppImage
This commit is contained in:
@@ -9,7 +9,8 @@ import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { CheckCircle2, XCircle, Loader2, Activity, GripHorizontal } from "lucide-react"
|
||||
import { TerminalPanel } from "./terminal-panel"
|
||||
import { API_PORT } from "@/lib/api-config"
|
||||
const API_PORT =
|
||||
typeof window !== "undefined" && process.env.NEXT_PUBLIC_API_PORT ? process.env.NEXT_PUBLIC_API_PORT : "8008"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
|
||||
interface WebInteraction {
|
||||
@@ -58,6 +59,28 @@ export function ScriptTerminalModal({
|
||||
const startYRef = useRef(0)
|
||||
const startHeightRef = useRef(80)
|
||||
|
||||
const terminalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminalRef.current) return
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
// Notificar a la terminal que necesita redimensionarse
|
||||
const event = new CustomEvent("terminal-resize-needed")
|
||||
window.dispatchEvent(event)
|
||||
})
|
||||
|
||||
resizeObserver.observe(terminalRef.current)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleTerminalResize = () => {
|
||||
// Este callback será usado por TerminalPanel para saber cuándo redimensionar
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setIsComplete(false)
|
||||
@@ -216,7 +239,7 @@ export function ScriptTerminalModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className="flex-1 overflow-hidden relative" ref={terminalRef}>
|
||||
<TerminalPanel
|
||||
websocketUrl={wsUrl}
|
||||
initMessage={{
|
||||
@@ -232,6 +255,7 @@ export function ScriptTerminalModal({
|
||||
}
|
||||
}}
|
||||
isScriptModal={true}
|
||||
onResizeNeeded={handleTerminalResize}
|
||||
/>
|
||||
|
||||
{isWaitingNextInteraction && !currentInteraction && (
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import type React from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { API_PORT } from "../lib/api-config"
|
||||
import { fetchApi } from "@/lib/api-config" // Cambiando import para usar fetchApi directamente
|
||||
const API_PORT =
|
||||
typeof window !== "undefined" && process.env.NEXT_PUBLIC_API_PORT ? process.env.NEXT_PUBLIC_API_PORT : "8008"
|
||||
import {
|
||||
Activity,
|
||||
Trash2,
|
||||
@@ -22,6 +22,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||
import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare CheatSheetResult here
|
||||
import { ResizablePanelGroup, ResizablePanel } from "@/components/ui/resizable-panel-group" // Added import for ResizablePanelGroup and ResizablePanel
|
||||
|
||||
type TerminalPanelProps = {
|
||||
websocketUrl?: string
|
||||
@@ -31,6 +32,7 @@ type TerminalPanelProps = {
|
||||
onWebSocketCreated?: (ws: WebSocket) => void
|
||||
onTerminalOutput?: () => void
|
||||
isScriptModal?: boolean
|
||||
onResizeNeeded?: () => void // Added prop for notifying when resize is needed
|
||||
}
|
||||
|
||||
interface TerminalInstance {
|
||||
@@ -145,6 +147,7 @@ export function TerminalPanel({
|
||||
onWebSocketCreated,
|
||||
onTerminalOutput,
|
||||
isScriptModal = false,
|
||||
onResizeNeeded,
|
||||
}: TerminalPanelProps) {
|
||||
const [terminals, setTerminals] = useState<TerminalInstance[]>([])
|
||||
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
||||
@@ -241,11 +244,17 @@ export function TerminalPanel({
|
||||
|
||||
const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}`
|
||||
|
||||
const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, {
|
||||
const response = await fetch(searchEndpoint, {
|
||||
method: "GET",
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok")
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success || !data.examples || data.examples.length === 0) {
|
||||
throw new Error("No examples found")
|
||||
}
|
||||
@@ -360,6 +369,48 @@ export function TerminalPanel({
|
||||
})
|
||||
}, [terminalHeight, layout, terminals, isMobile])
|
||||
|
||||
useEffect(() => {
|
||||
if (onResizeNeeded && terminals.length > 0) {
|
||||
const terminal = terminals[0]
|
||||
if (terminal.term && terminal.fitAddon && terminal.isConnected) {
|
||||
const triggerResize = () => {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
terminal.fitAddon?.fit()
|
||||
if (terminal.ws?.readyState === WebSocket.OPEN) {
|
||||
const cols = terminal.term?.cols || 80
|
||||
const rows = terminal.term?.rows || 24
|
||||
terminal.ws.send(
|
||||
JSON.stringify({
|
||||
type: "resize",
|
||||
cols,
|
||||
rows,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}, 100)
|
||||
} catch (err) {
|
||||
console.warn("[Terminal] resize failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Llamar automáticamente cuando sea necesario
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
triggerResize()
|
||||
})
|
||||
|
||||
const terminalElement = document.querySelector(".xterm")
|
||||
if (terminalElement) {
|
||||
resizeObserver.observe(terminalElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [terminals, onResizeNeeded])
|
||||
|
||||
const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => {
|
||||
const [TerminalClass, FitAddonClass] = await Promise.all([
|
||||
import("xterm").then((mod) => mod.Terminal),
|
||||
@@ -604,7 +655,7 @@ export function TerminalPanel({
|
||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-zinc-950 rounded-md overflow-hidden">
|
||||
<div className={`flex flex-col ${isScriptModal ? "h-full" : "h-screen"}`}>
|
||||
{!isScriptModal && (
|
||||
<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">
|
||||
@@ -704,89 +755,108 @@ export function TerminalPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
data-terminal-container
|
||||
ref={(el) => {
|
||||
containerRefs.current["main"] = el
|
||||
}}
|
||||
className={`overflow-hidden flex flex-col ${isMobile ? "flex-1 h-[60vh]" : "overflow-hidden"} w-full max-w-full`}
|
||||
style={
|
||||
isScriptModal
|
||||
? { height: "100%", flexShrink: 0 }
|
||||
: !isMobile || isTablet
|
||||
? { height: `${terminalHeight}px`, flexShrink: 0 }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isMobile ? (
|
||||
<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 overflow-x-auto">
|
||||
{terminals.map((terminal) => (
|
||||
<TabsTrigger key={terminal.id} value={terminal.id} className="relative">
|
||||
{terminal.title}
|
||||
{terminals.length > 1 && (
|
||||
<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>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{terminals.map((terminal) => (
|
||||
<TabsContent
|
||||
key={terminal.id}
|
||||
value={terminal.id}
|
||||
forceMount
|
||||
className={`flex-1 h-full mt-0 ${activeTerminalId === terminal.id ? "block" : "hidden"}`}
|
||||
>
|
||||
<div
|
||||
ref={(el) => (containerRefs.current[terminal.id] = el)}
|
||||
className="w-full h-full flex-1 bg-black overflow-hidden"
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className={`${getLayoutClass()} h-full gap-0.5 bg-zinc-800 p-0.5 w-full overflow-hidden`}>
|
||||
{terminals.map((terminal) => (
|
||||
<div
|
||||
key={terminal.id}
|
||||
className={`relative bg-zinc-900 overflow-hidden flex flex-col min-h-0 w-full ${
|
||||
terminals.length > 1 && activeTerminalId === terminal.id ? "ring-2 ring-blue-500" : ""
|
||||
<ResizablePanelGroup direction="horizontal" className="flex-1">
|
||||
<ResizablePanel defaultSize={100} className="flex flex-col">
|
||||
<div className="flex items-center border-b bg-background">
|
||||
{!isScriptModal && (
|
||||
<button
|
||||
onClick={() => setActiveTerminalId(terminals[0].id)}
|
||||
className={`px-3 py-1.5 text-sm ${
|
||||
activeTerminalId === terminals[0].id
|
||||
? "border-b-2 border-blue-500 text-blue-500"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<div className="flex-shrink-0 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"
|
||||
} ${isScriptModal ? "hidden" : ""}`}
|
||||
>
|
||||
{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={(el) => (containerRefs.current[terminal.id] = el)}
|
||||
onClick={() => setActiveTerminalId(terminal.id)}
|
||||
className="flex-1 w-full max-w-full bg-black overflow-hidden cursor-pointer"
|
||||
data-terminal-container
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
Terminal {1}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-terminal-container
|
||||
ref={(el) => {
|
||||
containerRefs.current["main"] = el
|
||||
}}
|
||||
className={`overflow-hidden flex flex-col ${isMobile ? "flex-1 h-[60vh]" : "overflow-hidden"} w-full max-w-full`}
|
||||
style={
|
||||
isScriptModal
|
||||
? { height: "100%", flexShrink: 0 }
|
||||
: !isMobile || isTablet
|
||||
? { height: `${terminalHeight}px`, flexShrink: 0 }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isMobile ? (
|
||||
<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 overflow-x-auto">
|
||||
{terminals.map((terminal) => (
|
||||
<TabsTrigger key={terminal.id} value={terminal.id} className="relative">
|
||||
{terminal.title}
|
||||
{terminals.length > 1 && (
|
||||
<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>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{terminals.map((terminal) => (
|
||||
<TabsContent
|
||||
key={terminal.id}
|
||||
value={terminal.id}
|
||||
forceMount
|
||||
className={`flex-1 h-full mt-0 ${activeTerminalId === terminal.id ? "block" : "hidden"}`}
|
||||
>
|
||||
<div
|
||||
ref={(el) => (containerRefs.current[terminal.id] = el)}
|
||||
className="w-full h-full flex-1 bg-black overflow-hidden"
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className={`${getLayoutClass()} h-full gap-0.5 bg-zinc-800 p-0.5 w-full overflow-hidden`}>
|
||||
{terminals.map((terminal) => (
|
||||
<div
|
||||
key={terminal.id}
|
||||
className={`relative bg-zinc-900 overflow-hidden flex flex-col min-h-0 w-full ${
|
||||
terminals.length > 1 && activeTerminalId === terminal.id ? "ring-2 ring-blue-500" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex-shrink-0 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"
|
||||
} ${isScriptModal ? "hidden" : ""}`}
|
||||
>
|
||||
{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={(el) => (containerRefs.current[terminal.id] = el)}
|
||||
onClick={() => setActiveTerminalId(terminal.id)}
|
||||
className="flex-1 w-full max-w-full bg-black overflow-hidden cursor-pointer"
|
||||
data-terminal-container
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
{!isScriptModal && (isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && (
|
||||
<div
|
||||
|
||||
@@ -19,28 +19,21 @@ export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008"
|
||||
*/
|
||||
export function getApiBaseUrl(): string {
|
||||
if (typeof window === "undefined") {
|
||||
console.log("[v0] getApiBaseUrl: Running on server (SSR)")
|
||||
return ""
|
||||
}
|
||||
|
||||
const { protocol, hostname, port } = window.location
|
||||
|
||||
console.log("[v0] getApiBaseUrl - protocol:", protocol, "hostname:", hostname, "port:", port)
|
||||
|
||||
// If accessing via standard ports (80/443) or no port, assume we're behind a proxy
|
||||
// In this case, use relative URLs so the proxy handles routing
|
||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
||||
|
||||
console.log("[v0] getApiBaseUrl - isStandardPort:", isStandardPort)
|
||||
|
||||
if (isStandardPort) {
|
||||
// Behind a proxy - use relative URL
|
||||
console.log("[v0] getApiBaseUrl: Detected proxy access, using relative URLs")
|
||||
return ""
|
||||
} else {
|
||||
// Direct access - use explicit API port
|
||||
const baseUrl = `${protocol}//${hostname}:${API_PORT}`
|
||||
console.log("[v0] getApiBaseUrl: Direct access detected, using:", baseUrl)
|
||||
return baseUrl
|
||||
}
|
||||
}
|
||||
@@ -69,12 +62,7 @@ export function getAuthToken(): string | null {
|
||||
if (typeof window === "undefined") {
|
||||
return null
|
||||
}
|
||||
const token = localStorage.getItem("proxmenux-auth-token")
|
||||
console.log(
|
||||
"[v0] getAuthToken called:",
|
||||
token ? `Token found (length: ${token.length})` : "No token found in localStorage",
|
||||
)
|
||||
return token
|
||||
return localStorage.getItem("proxmenux-auth-token")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,9 +84,6 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
||||
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`
|
||||
console.log("[v0] fetchApi:", endpoint, "- Authorization header ADDED")
|
||||
} else {
|
||||
console.log("[v0] fetchApi:", endpoint, "- NO TOKEN - Request will fail if endpoint is protected")
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -108,11 +93,8 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
||||
cache: "no-store",
|
||||
})
|
||||
|
||||
console.log("[v0] fetchApi:", endpoint, "- Response status:", response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
console.error("[v0] fetchApi: 401 UNAUTHORIZED -", endpoint, "- Token present:", !!token)
|
||||
throw new Error(`Unauthorized: ${endpoint}`)
|
||||
}
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||
@@ -120,7 +102,7 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
||||
|
||||
return response.json()
|
||||
} catch (error) {
|
||||
console.error("[v0] fetchApi error for", endpoint, ":", error)
|
||||
console.error("API fetch error for", endpoint, ":", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user