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,8 +9,7 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { CheckCircle2, XCircle, Loader2, Activity, GripHorizontal } from "lucide-react"
|
import { CheckCircle2, XCircle, Loader2, Activity, GripHorizontal } from "lucide-react"
|
||||||
import { TerminalPanel } from "./terminal-panel"
|
import { TerminalPanel } from "./terminal-panel"
|
||||||
const API_PORT =
|
import { API_PORT } from "@/lib/api-config"
|
||||||
typeof window !== "undefined" && process.env.NEXT_PUBLIC_API_PORT ? process.env.NEXT_PUBLIC_API_PORT : "8008"
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
|
|
||||||
interface WebInteraction {
|
interface WebInteraction {
|
||||||
@@ -59,28 +58,6 @@ export function ScriptTerminalModal({
|
|||||||
const startYRef = useRef(0)
|
const startYRef = useRef(0)
|
||||||
const startHeightRef = useRef(80)
|
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(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setIsComplete(false)
|
setIsComplete(false)
|
||||||
@@ -212,6 +189,20 @@ export function ScriptTerminalModal({
|
|||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.send(JSON.stringify({ type: "resize" }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize)
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open}>
|
<Dialog open={open}>
|
||||||
@@ -239,7 +230,7 @@ export function ScriptTerminalModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden relative" ref={terminalRef}>
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<TerminalPanel
|
<TerminalPanel
|
||||||
websocketUrl={wsUrl}
|
websocketUrl={wsUrl}
|
||||||
initMessage={{
|
initMessage={{
|
||||||
@@ -255,7 +246,6 @@ export function ScriptTerminalModal({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isScriptModal={true}
|
isScriptModal={true}
|
||||||
onResizeNeeded={handleTerminalResize}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isWaitingNextInteraction && !currentInteraction && (
|
{isWaitingNextInteraction && !currentInteraction && (
|
||||||
|
|||||||
@@ -2,27 +2,14 @@
|
|||||||
|
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
const API_PORT =
|
import { API_PORT } from "../lib/api-config"
|
||||||
typeof window !== "undefined" && process.env.NEXT_PUBLIC_API_PORT ? process.env.NEXT_PUBLIC_API_PORT : "8008"
|
import { fetchApi } from "@/lib/api-config" // Cambiando import para usar fetchApi directamente
|
||||||
import {
|
import { X, Search, Send, Lightbulb, Terminal, Plus, GripHorizontal } from "lucide-react"
|
||||||
Activity,
|
|
||||||
Trash2,
|
|
||||||
X,
|
|
||||||
Search,
|
|
||||||
Send,
|
|
||||||
Lightbulb,
|
|
||||||
Terminal,
|
|
||||||
Plus,
|
|
||||||
AlignJustify,
|
|
||||||
Grid2X2,
|
|
||||||
GripHorizontal,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||||
import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare CheatSheetResult here
|
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 = {
|
type TerminalPanelProps = {
|
||||||
websocketUrl?: string
|
websocketUrl?: string
|
||||||
@@ -32,7 +19,6 @@ type TerminalPanelProps = {
|
|||||||
onWebSocketCreated?: (ws: WebSocket) => void
|
onWebSocketCreated?: (ws: WebSocket) => void
|
||||||
onTerminalOutput?: () => void
|
onTerminalOutput?: () => void
|
||||||
isScriptModal?: boolean
|
isScriptModal?: boolean
|
||||||
onResizeNeeded?: () => void // Added prop for notifying when resize is needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TerminalInstance {
|
interface TerminalInstance {
|
||||||
@@ -147,7 +133,6 @@ export function TerminalPanel({
|
|||||||
onWebSocketCreated,
|
onWebSocketCreated,
|
||||||
onTerminalOutput,
|
onTerminalOutput,
|
||||||
isScriptModal = false,
|
isScriptModal = false,
|
||||||
onResizeNeeded,
|
|
||||||
}: TerminalPanelProps) {
|
}: TerminalPanelProps) {
|
||||||
const [terminals, setTerminals] = useState<TerminalInstance[]>([])
|
const [terminals, setTerminals] = useState<TerminalInstance[]>([])
|
||||||
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
||||||
@@ -163,6 +148,7 @@ export function TerminalPanel({
|
|||||||
const [useOnline, setUseOnline] = useState(true)
|
const [useOnline, setUseOnline] = useState(true)
|
||||||
|
|
||||||
const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
|
const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
|
||||||
|
const panelRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDeviceType = () => {
|
const updateDeviceType = () => {
|
||||||
@@ -244,17 +230,11 @@ export function TerminalPanel({
|
|||||||
|
|
||||||
const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}`
|
const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}`
|
||||||
|
|
||||||
const response = await fetch(searchEndpoint, {
|
const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
signal: AbortSignal.timeout(10000),
|
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) {
|
if (!data.success || !data.examples || data.examples.length === 0) {
|
||||||
throw new Error("No examples found")
|
throw new Error("No examples found")
|
||||||
}
|
}
|
||||||
@@ -333,6 +313,11 @@ export function TerminalPanel({
|
|||||||
delete containerRefs.current[id]
|
delete containerRefs.current[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCloseTab = (e: React.MouseEvent, id: string) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
closeTerminal(id)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
terminals.forEach((terminal) => {
|
terminals.forEach((terminal) => {
|
||||||
const container = containerRefs.current[terminal.id]
|
const container = containerRefs.current[terminal.id]
|
||||||
@@ -370,10 +355,14 @@ export function TerminalPanel({
|
|||||||
}, [terminalHeight, layout, terminals, isMobile])
|
}, [terminalHeight, layout, terminals, isMobile])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onResizeNeeded && terminals.length > 0) {
|
if (!isScriptModal) return
|
||||||
const terminal = terminals[0]
|
|
||||||
if (terminal.term && terminal.fitAddon && terminal.isConnected) {
|
const mainContainer = containerRefs.current["main"]
|
||||||
const triggerResize = () => {
|
if (!mainContainer) return
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
terminals.forEach((terminal) => {
|
||||||
|
if (terminal.term && terminal.fitAddon && terminal.isConnected) {
|
||||||
try {
|
try {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
terminal.fitAddon?.fit()
|
terminal.fitAddon?.fit()
|
||||||
@@ -388,28 +377,20 @@ export function TerminalPanel({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 50)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[Terminal] resize failed:", err)
|
// Silently handle resize errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Llamar automáticamente cuando sea necesario
|
resizeObserver.observe(mainContainer)
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
triggerResize()
|
|
||||||
})
|
|
||||||
|
|
||||||
const terminalElement = document.querySelector(".xterm")
|
return () => {
|
||||||
if (terminalElement) {
|
resizeObserver.disconnect()
|
||||||
resizeObserver.observe(terminalElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [terminals, onResizeNeeded])
|
}, [terminals, isScriptModal])
|
||||||
|
|
||||||
const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => {
|
const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => {
|
||||||
const [TerminalClass, FitAddonClass] = await Promise.all([
|
const [TerminalClass, FitAddonClass] = await Promise.all([
|
||||||
@@ -655,99 +636,61 @@ export function TerminalPanel({
|
|||||||
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col ${isScriptModal ? "h-full" : "h-screen"}`}>
|
<div
|
||||||
|
ref={panelRef}
|
||||||
|
className="flex flex-col bg-background"
|
||||||
|
style={isScriptModal ? { height: "100%" } : { height: `${terminalHeight}px` }}
|
||||||
|
>
|
||||||
{!isScriptModal && (
|
{!isScriptModal && (
|
||||||
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800">
|
<div className="border-b border-border flex-none relative">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between px-2 pt-1">
|
||||||
<Activity className="h-5 w-5 text-blue-500" />
|
<div className="flex gap-1 overflow-x-auto scrollbar-hide">
|
||||||
<div
|
{terminals.map((terminal) => (
|
||||||
className={`w-2 h-2 rounded-full ${activeTerminal?.isConnected ? "bg-green-500" : "bg-red-500"}`}
|
<button
|
||||||
title={activeTerminal?.isConnected ? "Connected" : "Disconnected"}
|
key={terminal.id}
|
||||||
></div>
|
onClick={() => setActiveTerminalId(terminal.id)}
|
||||||
<span className="text-xs text-zinc-500">{terminals.length} / 4 terminals</span>
|
className={`px-3 py-1 text-sm rounded-t-md transition-colors whitespace-nowrap ${
|
||||||
</div>
|
terminal.id === activeTerminalId
|
||||||
|
? "bg-background text-foreground font-medium"
|
||||||
<div className="flex gap-2">
|
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||||
{!isMobile && terminals.length > 1 && (
|
}`}
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={() => setLayout("single")}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className={`h-8 px-2 ${layout === "single" ? "bg-blue-500/20 border-blue-500" : ""}`}
|
|
||||||
title="Vista apilada (filas)"
|
|
||||||
>
|
>
|
||||||
<AlignJustify className="h-4 w-4" />
|
{terminal.title}
|
||||||
</Button>
|
{terminals.length > 1 && (
|
||||||
<Button
|
<span onClick={(e) => handleCloseTab(e, terminal.id)} className="ml-2 hover:text-destructive">
|
||||||
onClick={() => setLayout("grid")}
|
×
|
||||||
variant="outline"
|
</span>
|
||||||
size="sm"
|
)}
|
||||||
className={`h-8 px-2 ${layout === "grid" ? "bg-blue-500/20 border-blue-500" : ""}`}
|
</button>
|
||||||
title="Vista cuadrícula 2x2"
|
))}
|
||||||
>
|
</div>
|
||||||
<Grid2X2 className="h-4 w-4" />
|
<button
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
onClick={addNewTerminal}
|
onClick={addNewTerminal}
|
||||||
variant="outline"
|
className="text-muted-foreground hover:text-foreground transition-colors p-1"
|
||||||
size="sm"
|
title="New Terminal"
|
||||||
disabled={terminals.length >= 4}
|
|
||||||
className="h-8 gap-2 bg-green-600 hover:bg-green-700 border-green-500 text-white disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">New</span>
|
</button>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => setSearchModalOpen(true)}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={!activeTerminal?.isConnected}
|
|
||||||
className="h-8 gap-2 bg-blue-600 hover:bg-blue-700 border-blue-500 text-white disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Search className="h-4 w-4" />
|
|
||||||
<span className="hidden sm:inline">Search</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleClear}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={!activeTerminal?.isConnected}
|
|
||||||
className="h-8 gap-2 bg-yellow-600 hover:bg-yellow-700 border-yellow-500 text-white disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
<span className="hidden sm:inline">Clear</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleClose}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 gap-2 bg-red-600 hover:bg-red-700 border-red-500 text-white"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="hidden sm:inline">Close</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Terminal Tabs */}
|
{/* Terminal Tabs */}
|
||||||
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar">
|
{!isScriptModal && (
|
||||||
{terminals.map((terminal) => (
|
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar">
|
||||||
<button
|
{terminals.map((terminal) => (
|
||||||
key={terminal.id}
|
<button
|
||||||
onClick={() => setActiveTerminalId(terminal.id)}
|
key={terminal.id}
|
||||||
className={`px-3 py-1 text-xs rounded-t transition-colors ${
|
onClick={() => setActiveTerminalId(terminal.id)}
|
||||||
terminal.id === activeTerminalId ? "bg-zinc-800 text-white" : "text-zinc-500 hover:text-white"
|
className={`px-3 py-1 text-xs rounded-t transition-colors ${
|
||||||
}`}
|
terminal.id === activeTerminalId ? "bg-zinc-800 text-white" : "text-zinc-500 hover:text-white"
|
||||||
style={isScriptModal ? { display: "none" } : undefined}
|
}`}
|
||||||
>
|
>
|
||||||
{terminal.title}
|
{terminal.title}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isScriptModal && (
|
{isScriptModal && (
|
||||||
<div className="sr-only" data-connection-status={activeTerminal?.isConnected ? "connected" : "disconnected"}>
|
<div className="sr-only" data-connection-status={activeTerminal?.isConnected ? "connected" : "disconnected"}>
|
||||||
@@ -755,108 +698,89 @@ export function TerminalPanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ResizablePanelGroup direction="horizontal" className="flex-1">
|
<div
|
||||||
<ResizablePanel defaultSize={100} className="flex flex-col">
|
data-terminal-container
|
||||||
<div className="flex items-center border-b bg-background">
|
ref={(el) => {
|
||||||
{!isScriptModal && (
|
containerRefs.current["main"] = el
|
||||||
<button
|
}}
|
||||||
onClick={() => setActiveTerminalId(terminals[0].id)}
|
className={`overflow-hidden flex flex-col ${isMobile ? "flex-1 h-[60vh]" : "overflow-hidden"} w-full max-w-full`}
|
||||||
className={`px-3 py-1.5 text-sm ${
|
style={
|
||||||
activeTerminalId === terminals[0].id
|
isScriptModal
|
||||||
? "border-b-2 border-blue-500 text-blue-500"
|
? { height: "100%", flexShrink: 0 }
|
||||||
: "text-muted-foreground"
|
: !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" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Terminal {1}
|
<div className="flex-shrink-0 flex items-center justify-between px-2 py-1 bg-zinc-900/95 border-b border-zinc-800">
|
||||||
</button>
|
<button
|
||||||
)}
|
onClick={() => setActiveTerminalId(terminal.id)}
|
||||||
</div>
|
className={`text-xs font-medium ${
|
||||||
|
activeTerminalId === terminal.id ? "text-blue-400" : "text-zinc-500"
|
||||||
<div
|
} ${isScriptModal ? "hidden" : ""}`}
|
||||||
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
|
{terminal.title}
|
||||||
ref={(el) => (containerRefs.current[terminal.id] = el)}
|
</button>
|
||||||
className="w-full h-full flex-1 bg-black overflow-hidden"
|
{terminals.length > 1 && (
|
||||||
/>
|
<button onClick={() => closeTerminal(terminal.id)} className="hover:bg-zinc-700 rounded p-0.5">
|
||||||
</TabsContent>
|
<X className="h-3 w-3" />
|
||||||
))}
|
</button>
|
||||||
</Tabs>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<div className={`${getLayoutClass()} h-full gap-0.5 bg-zinc-800 p-0.5 w-full overflow-hidden`}>
|
<div
|
||||||
{terminals.map((terminal) => (
|
ref={(el) => (containerRefs.current[terminal.id] = el)}
|
||||||
<div
|
onClick={() => setActiveTerminalId(terminal.id)}
|
||||||
key={terminal.id}
|
className="flex-1 w-full max-w-full bg-black overflow-hidden cursor-pointer"
|
||||||
className={`relative bg-zinc-900 overflow-hidden flex flex-col min-h-0 w-full ${
|
data-terminal-container
|
||||||
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>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
)}
|
||||||
</ResizablePanelGroup>
|
</div>
|
||||||
|
|
||||||
{!isScriptModal && (isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && (
|
{!isScriptModal && (isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -19,21 +19,28 @@ export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008"
|
|||||||
*/
|
*/
|
||||||
export function getApiBaseUrl(): string {
|
export function getApiBaseUrl(): string {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
|
console.log("[v0] getApiBaseUrl: Running on server (SSR)")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const { protocol, hostname, port } = window.location
|
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
|
// 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
|
// In this case, use relative URLs so the proxy handles routing
|
||||||
const isStandardPort = port === "" || port === "80" || port === "443"
|
const isStandardPort = port === "" || port === "80" || port === "443"
|
||||||
|
|
||||||
|
console.log("[v0] getApiBaseUrl - isStandardPort:", isStandardPort)
|
||||||
|
|
||||||
if (isStandardPort) {
|
if (isStandardPort) {
|
||||||
// Behind a proxy - use relative URL
|
// Behind a proxy - use relative URL
|
||||||
|
console.log("[v0] getApiBaseUrl: Detected proxy access, using relative URLs")
|
||||||
return ""
|
return ""
|
||||||
} else {
|
} else {
|
||||||
// Direct access - use explicit API port
|
// Direct access - use explicit API port
|
||||||
const baseUrl = `${protocol}//${hostname}:${API_PORT}`
|
const baseUrl = `${protocol}//${hostname}:${API_PORT}`
|
||||||
|
console.log("[v0] getApiBaseUrl: Direct access detected, using:", baseUrl)
|
||||||
return baseUrl
|
return baseUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +69,12 @@ export function getAuthToken(): string | null {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return localStorage.getItem("proxmenux-auth-token")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,6 +96,9 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
headers["Authorization"] = `Bearer ${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 {
|
try {
|
||||||
@@ -93,8 +108,11 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("[v0] fetchApi:", endpoint, "- Response status:", response.status)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
|
console.error("[v0] fetchApi: 401 UNAUTHORIZED -", endpoint, "- Token present:", !!token)
|
||||||
throw new Error(`Unauthorized: ${endpoint}`)
|
throw new Error(`Unauthorized: ${endpoint}`)
|
||||||
}
|
}
|
||||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
||||||
@@ -102,7 +120,7 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("API fetch error for", endpoint, ":", error)
|
console.error("[v0] fetchApi error for", endpoint, ":", error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user