Update AppImage

This commit is contained in:
MacRimi
2025-12-06 20:27:00 +01:00
parent d30c836d04
commit 05a2eca9a7
3 changed files with 185 additions and 253 deletions

View File

@@ -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 && (

View File

@@ -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]
const mainContainer = containerRefs.current["main"]
if (!mainContainer) return
const resizeObserver = new ResizeObserver(() => {
terminals.forEach((terminal) => {
if (terminal.term && terminal.fitAddon && terminal.isConnected) { if (terminal.term && terminal.fitAddon && terminal.isConnected) {
const triggerResize = () => {
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
const resizeObserver = new ResizeObserver(() => {
triggerResize()
}) })
const terminalElement = document.querySelector(".xterm") resizeObserver.observe(mainContainer)
if (terminalElement) {
resizeObserver.observe(terminalElement)
}
return () => { return () => {
resizeObserver.disconnect() resizeObserver.disconnect()
} }
} }, [terminals, isScriptModal])
}
}, [terminals, onResizeNeeded])
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,85 +636,47 @@ 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"}`}>
{!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">
<Activity className="h-5 w-5 text-blue-500" />
<div <div
className={`w-2 h-2 rounded-full ${activeTerminal?.isConnected ? "bg-green-500" : "bg-red-500"}`} ref={panelRef}
title={activeTerminal?.isConnected ? "Connected" : "Disconnected"} className="flex flex-col bg-background"
></div> style={isScriptModal ? { height: "100%" } : { height: `${terminalHeight}px` }}
<span className="text-xs text-zinc-500">{terminals.length} / 4 terminals</span>
</div>
<div className="flex gap-2">
{!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" /> {!isScriptModal && (
</Button> <div className="border-b border-border flex-none relative">
<Button <div className="flex items-center justify-between px-2 pt-1">
onClick={() => setLayout("grid")} <div className="flex gap-1 overflow-x-auto scrollbar-hide">
variant="outline" {terminals.map((terminal) => (
size="sm" <button
className={`h-8 px-2 ${layout === "grid" ? "bg-blue-500/20 border-blue-500" : ""}`} key={terminal.id}
title="Vista cuadrícula 2x2" onClick={() => setActiveTerminalId(terminal.id)}
className={`px-3 py-1 text-sm rounded-t-md transition-colors whitespace-nowrap ${
terminal.id === activeTerminalId
? "bg-background text-foreground font-medium"
: "bg-muted text-muted-foreground hover:bg-muted/80"
}`}
> >
<Grid2X2 className="h-4 w-4" /> {terminal.title}
</Button> {terminals.length > 1 && (
</> <span onClick={(e) => handleCloseTab(e, terminal.id)} className="ml-2 hover:text-destructive">
×
</span>
)} )}
<Button </button>
))}
</div>
<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 */}
{!isScriptModal && (
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar"> <div className="flex items-center gap-2 overflow-x-auto no-scrollbar">
{terminals.map((terminal) => ( {terminals.map((terminal) => (
<button <button
@@ -742,12 +685,12 @@ export function TerminalPanel({
className={`px-3 py-1 text-xs rounded-t transition-colors ${ 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" 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,23 +698,6 @@ export function TerminalPanel({
</div> </div>
)} )}
<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"
}`}
>
Terminal {1}
</button>
)}
</div>
<div <div
data-terminal-container data-terminal-container
ref={(el) => { ref={(el) => {
@@ -855,8 +781,6 @@ export function TerminalPanel({
</div> </div>
)} )}
</div> </div>
</ResizablePanel>
</ResizablePanelGroup>
{!isScriptModal && (isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && ( {!isScriptModal && (isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && (
<div <div

View File

@@ -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
} }
} }