2026-07-29 13:47:45 +00:00
|
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
|
|
import { Sidebar } from '@/components/layout/Sidebar'
|
|
|
|
|
|
import { Dashboard } from '@/components/layout/Dashboard'
|
|
|
|
|
|
import { ConnectionDialog } from '@/components/connections/ConnectionDialog'
|
|
|
|
|
|
import { VMList } from '@/components/vms/VMList'
|
|
|
|
|
|
import { VMDetail } from '@/components/vms/VMDetail'
|
2026-08-09 23:01:51 +00:00
|
|
|
|
import { ContainerList } from '@/components/vms/ContainerList'
|
|
|
|
|
|
import { NodesPage } from '@/components/nodes/NodesPage'
|
2026-07-29 13:47:45 +00:00
|
|
|
|
import { TaskList } from '@/components/tasks/TaskList'
|
2026-08-10 14:33:06 +00:00
|
|
|
|
import { TaskStatusBar } from '@/components/tasks/TaskStatusBar'
|
2026-07-29 13:47:45 +00:00
|
|
|
|
import { BackupList } from '@/components/backups/BackupList'
|
|
|
|
|
|
import { CommandPalette } from '@/components/command/CommandPalette'
|
|
|
|
|
|
import { StorageOverview } from '@/components/storage/StorageOverview'
|
|
|
|
|
|
import { StorageDetail } from '@/components/storage/StorageDetail'
|
2026-08-13 01:02:33 +00:00
|
|
|
|
import { PbsOverview } from '@/components/pbs/PbsOverview'
|
|
|
|
|
|
import { PbsDatastores } from '@/components/pbs/PbsDatastores'
|
|
|
|
|
|
import { PbsDatastoreDetail } from '@/components/pbs/PbsDatastoreDetail'
|
2026-07-29 13:47:45 +00:00
|
|
|
|
import { SettingsPage } from '@/components/settings/SettingsPage'
|
|
|
|
|
|
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
2026-08-09 23:01:51 +00:00
|
|
|
|
import { ToastProvider, useToast } from '@/components/ui/toast'
|
2026-08-25 13:08:21 +00:00
|
|
|
|
import { DotMatrixText } from '@/components/ui/dot-matrix'
|
2026-08-09 23:01:51 +00:00
|
|
|
|
import { Server, AlertTriangle } from 'lucide-react'
|
2026-07-29 13:47:45 +00:00
|
|
|
|
import { useConnectionStore } from '@/stores/connectionStore'
|
|
|
|
|
|
import { useUIStore } from '@/stores/uiStore'
|
|
|
|
|
|
import { useWebSocket } from '@/hooks/useWebSocket'
|
2026-08-10 14:33:06 +00:00
|
|
|
|
import { isTauri, loadConnections, connectToServer, getConnectionStatus, getWebSocketURL, updateTrayMenu } from '@/lib/tauri'
|
2026-08-22 16:54:02 +00:00
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
2026-07-29 13:47:45 +00:00
|
|
|
|
import type { ProxmoxVM } from '@/types/proxmox'
|
|
|
|
|
|
|
|
|
|
|
|
const queryClient = new QueryClient({
|
|
|
|
|
|
defaultOptions: {
|
|
|
|
|
|
queries: {
|
|
|
|
|
|
retry: 1,
|
|
|
|
|
|
refetchOnWindowFocus: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
type View =
|
|
|
|
|
|
| { type: 'dashboard' }
|
|
|
|
|
|
| { type: 'vms' }
|
|
|
|
|
|
| { type: 'vm-detail'; vm: ProxmoxVM }
|
2026-08-09 23:01:51 +00:00
|
|
|
|
| { type: 'nodes' }
|
|
|
|
|
|
| { type: 'node-detail'; nodeName: string }
|
|
|
|
|
|
| { type: 'containers' }
|
2026-07-29 13:47:45 +00:00
|
|
|
|
| { type: 'tasks' }
|
|
|
|
|
|
| { type: 'backups' }
|
|
|
|
|
|
| { type: 'storage' }
|
2026-07-29 19:35:37 +00:00
|
|
|
|
| { type: 'storage-detail'; storage: string; node: string }
|
2026-08-13 01:02:33 +00:00
|
|
|
|
| { type: 'pbs-overview' }
|
|
|
|
|
|
| { type: 'pbs-datastores' }
|
|
|
|
|
|
| { type: 'pbs-datastore-detail'; store: string }
|
2026-07-29 13:47:45 +00:00
|
|
|
|
| { type: 'settings' }
|
|
|
|
|
|
|
|
|
|
|
|
function AppContent() {
|
|
|
|
|
|
const activeConnectionId = useConnectionStore((s) => s.activeConnectionId)
|
2026-08-09 23:01:51 +00:00
|
|
|
|
const connections = useConnectionStore((s) => s.connections)
|
|
|
|
|
|
const hydrate = useConnectionStore((s) => s.hydrate)
|
|
|
|
|
|
const setActiveConnection = useConnectionStore((s) => s.setActiveConnection)
|
|
|
|
|
|
const setConnectionStatus = useConnectionStore((s) => s.setConnectionStatus)
|
|
|
|
|
|
const updateConnection = useConnectionStore((s) => s.updateConnection)
|
|
|
|
|
|
const removeConnection = useConnectionStore((s) => s.removeConnection)
|
|
|
|
|
|
const setAuthStatus = useConnectionStore((s) => s.setAuthStatus)
|
|
|
|
|
|
const { addToast } = useToast()
|
2026-07-29 13:47:45 +00:00
|
|
|
|
const [connectionDialogOpen, setConnectionDialogOpen] = useState(false)
|
|
|
|
|
|
const [view, setView] = useState<View>({ type: 'dashboard' })
|
|
|
|
|
|
const commandPaletteOpen = useUIStore((s) => s.commandPaletteOpen)
|
|
|
|
|
|
const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen)
|
2026-08-09 23:01:51 +00:00
|
|
|
|
// In browser mock mode there is nothing to load, so the app is ready
|
|
|
|
|
|
// immediately. In Tauri mode the persisted connections load on mount.
|
|
|
|
|
|
const [connectionsLoaded, setConnectionsLoaded] = useState(!isTauri())
|
2026-07-29 13:47:45 +00:00
|
|
|
|
|
2026-08-22 16:54:02 +00:00
|
|
|
|
// Startup: load persisted connections and auto-reconnect the active one.
|
|
|
|
|
|
// React StrictMode mounts this effect twice in development (setup, cleanup,
|
|
|
|
|
|
// setup): the first boot is cancelled by the cleanup, and the second setup
|
|
|
|
|
|
// runs the load again and completes it. Guarding against a double run with
|
|
|
|
|
|
// a ref would cancel the first boot and suppress the second, so the
|
|
|
|
|
|
// persisted connections would never hydrate.
|
2026-07-29 18:27:27 +00:00
|
|
|
|
useEffect(() => {
|
2026-08-09 23:01:51 +00:00
|
|
|
|
if (!isTauri()) return
|
|
|
|
|
|
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
const boot = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { connections: loaded, activeConnectionId } = await loadConnections()
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
hydrate(loaded, activeConnectionId)
|
|
|
|
|
|
|
|
|
|
|
|
if (activeConnectionId && loaded.some((c) => c.id === activeConnectionId)) {
|
|
|
|
|
|
setActiveConnection(activeConnectionId)
|
|
|
|
|
|
setConnectionStatus(activeConnectionId, 'connecting')
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await connectToServer(activeConnectionId)
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
if (result.mergedInto && result.mergedInto !== activeConnectionId) {
|
|
|
|
|
|
// This connection belongs to a cluster we already have; the
|
|
|
|
|
|
// backend folded it into the surviving connection.
|
|
|
|
|
|
removeConnection(activeConnectionId)
|
|
|
|
|
|
setActiveConnection(result.mergedInto)
|
|
|
|
|
|
setConnectionStatus(result.mergedInto, result.status)
|
|
|
|
|
|
setAuthStatus('authenticated')
|
|
|
|
|
|
addToast('Already connected to this cluster — added as a failover endpoint.', 'success')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setConnectionStatus(result.connectionId, result.status)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setConnectionStatus(activeConnectionId, 'failed')
|
|
|
|
|
|
console.error('[App] Auto-reconnect failed:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('[App] Failed to load connections:', err)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!cancelled) setConnectionsLoaded(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
boot()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [hydrate, setActiveConnection, setConnectionStatus, setAuthStatus, removeConnection, addToast])
|
|
|
|
|
|
|
|
|
|
|
|
// Keep the system tray menu in sync with the connection list (no-op in browser mode)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
updateTrayMenu(
|
|
|
|
|
|
connections.map((c) => ({ id: c.id, name: c.name, status: c.status })),
|
|
|
|
|
|
)
|
|
|
|
|
|
}, [connections])
|
|
|
|
|
|
|
2026-08-10 14:33:06 +00:00
|
|
|
|
// Tray connection clicks switch the active connection (mirrored to the
|
|
|
|
|
|
// backend by the store so it persists across launches).
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!isTauri()) return
|
|
|
|
|
|
let unlisten: (() => void) | undefined
|
|
|
|
|
|
import('@tauri-apps/api/event')
|
|
|
|
|
|
.then(async ({ listen }) => {
|
|
|
|
|
|
unlisten = await listen<string>('tray-connection-click', (event) => {
|
|
|
|
|
|
setActiveConnection(event.payload)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
unlisten?.()
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [setActiveConnection])
|
|
|
|
|
|
|
2026-08-09 23:01:51 +00:00
|
|
|
|
// Poll the backend connection status while a connection is active so the
|
|
|
|
|
|
// sidebar node list and failover state stay in sync with the cluster.
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!isTauri() || !activeConnectionId) return
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
|
|
|
|
|
|
const poll = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const info = await getConnectionStatus(activeConnectionId)
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
setConnectionStatus(activeConnectionId, info.status)
|
|
|
|
|
|
updateConnection(activeConnectionId, {
|
|
|
|
|
|
nodes: info.nodes,
|
|
|
|
|
|
currentEndpointUrl: info.currentEndpointUrl,
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Transient failures are ignored; the next tick keeps polling.
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
poll()
|
|
|
|
|
|
const interval = setInterval(poll, 10_000)
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
clearInterval(interval)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [activeConnectionId, setConnectionStatus, updateConnection])
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-open login dialog only when there are genuinely zero connections
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (connectionsLoaded && connections.length === 0 && !connectionDialogOpen) {
|
2026-07-29 18:27:27 +00:00
|
|
|
|
setConnectionDialogOpen(true)
|
|
|
|
|
|
}
|
2026-08-09 23:01:51 +00:00
|
|
|
|
}, [connectionsLoaded, connections.length, connectionDialogOpen])
|
2026-07-29 18:27:27 +00:00
|
|
|
|
|
2026-07-29 13:47:45 +00:00
|
|
|
|
// WebSocket integration – connects when a connection is active
|
2026-08-10 14:33:06 +00:00
|
|
|
|
const { connect: connectRelay, disconnect: disconnectRelay } = useWebSocket(activeConnectionId)
|
|
|
|
|
|
|
|
|
|
|
|
// Start the backend event relay once a connection is connected, so task and
|
|
|
|
|
|
// node events invalidate queries in near real-time. Polling remains the
|
|
|
|
|
|
// fallback when the relay is not connected.
|
|
|
|
|
|
const activeStatus = useMemo(
|
|
|
|
|
|
() => connections.find((c) => c.id === activeConnectionId)?.status,
|
|
|
|
|
|
[connections, activeConnectionId],
|
|
|
|
|
|
)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!isTauri() || !activeConnectionId) return
|
|
|
|
|
|
if (activeStatus !== 'connected' && activeStatus !== 'failover') {
|
|
|
|
|
|
disconnectRelay().catch(() => {})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
const startRelay = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const origin = await getWebSocketURL(activeConnectionId, '')
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
await connectRelay(`${origin}/api2/json/events`)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('[App] Failed to start event relay:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
startRelay()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [activeConnectionId, activeStatus, connectRelay, disconnectRelay])
|
2026-07-29 13:47:45 +00:00
|
|
|
|
|
|
|
|
|
|
// Global keyboard shortcut: Cmd/Ctrl+K to open command palette
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
setCommandPaletteOpen(!commandPaletteOpen)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
window.addEventListener('keydown', handleGlobalKeyDown)
|
|
|
|
|
|
return () => window.removeEventListener('keydown', handleGlobalKeyDown)
|
|
|
|
|
|
}, [commandPaletteOpen, setCommandPaletteOpen])
|
|
|
|
|
|
|
|
|
|
|
|
const handleNavigate = (newView: View) => {
|
|
|
|
|
|
setView(newView)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renderMainContent = () => {
|
|
|
|
|
|
if (view.type === 'settings') {
|
|
|
|
|
|
return <SettingsPage />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!activeConnectionId) {
|
|
|
|
|
|
return (
|
2026-08-25 13:08:21 +00:00
|
|
|
|
<div className="flex h-full items-center justify-center p-6 dot-grid">
|
|
|
|
|
|
<div className="flex flex-col items-center text-center rounded-lg border border-dotted border-border bg-card p-8 shadow-card">
|
|
|
|
|
|
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-sm border border-dotted border-border bg-muted/40">
|
2026-08-09 23:01:51 +00:00
|
|
|
|
<Server className="h-5 w-5 text-primary" />
|
|
|
|
|
|
</div>
|
2026-08-25 13:08:21 +00:00
|
|
|
|
<DotMatrixText text="NO CONNECTION" size="sm" className="text-foreground" />
|
|
|
|
|
|
<p className="mt-2 max-w-sm text-sm text-muted-foreground">
|
2026-07-29 18:27:27 +00:00
|
|
|
|
Connect to a Proxmox server to get started
|
2026-07-29 13:47:45 +00:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (view.type) {
|
|
|
|
|
|
case 'dashboard':
|
2026-07-29 19:35:37 +00:00
|
|
|
|
return <Dashboard connectionId={activeConnectionId} onNavigate={(viewName) => {
|
|
|
|
|
|
switch (viewName) {
|
|
|
|
|
|
case 'dashboard': handleNavigate({ type: 'dashboard' }); break
|
|
|
|
|
|
case 'vms': handleNavigate({ type: 'vms' }); break
|
2026-08-10 14:33:06 +00:00
|
|
|
|
case 'nodes': handleNavigate({ type: 'nodes' }); break
|
2026-07-29 19:35:37 +00:00
|
|
|
|
case 'tasks': handleNavigate({ type: 'tasks' }); break
|
|
|
|
|
|
case 'backups': handleNavigate({ type: 'backups' }); break
|
|
|
|
|
|
case 'storage': handleNavigate({ type: 'storage' }); break
|
|
|
|
|
|
default: break
|
|
|
|
|
|
}
|
|
|
|
|
|
}} />
|
2026-07-29 13:47:45 +00:00
|
|
|
|
case 'vms':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<VMList
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onVMClick={(vm) => handleNavigate({ type: 'vm-detail', vm })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'vm-detail':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<VMDetail
|
|
|
|
|
|
vm={view.vm}
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onBack={() => handleNavigate({ type: 'vms' })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-08-09 23:01:51 +00:00
|
|
|
|
case 'nodes':
|
2026-08-10 14:33:06 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<NodesPage
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onVMClick={(vm) => handleNavigate({ type: 'vm-detail', vm })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-08-09 23:01:51 +00:00
|
|
|
|
case 'node-detail':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<NodesPage
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
initialNodeName={view.nodeName}
|
2026-08-10 14:33:06 +00:00
|
|
|
|
onVMClick={(vm) => handleNavigate({ type: 'vm-detail', vm })}
|
2026-08-09 23:01:51 +00:00
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'containers':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ContainerList
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onContainerClick={(vm) => handleNavigate({ type: 'vm-detail', vm })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-07-29 13:47:45 +00:00
|
|
|
|
case 'tasks':
|
|
|
|
|
|
return <TaskList connectionId={activeConnectionId} />
|
|
|
|
|
|
case 'backups':
|
|
|
|
|
|
return <BackupList connectionId={activeConnectionId} />
|
|
|
|
|
|
case 'storage':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<StorageOverview
|
|
|
|
|
|
connectionId={activeConnectionId}
|
2026-07-29 19:35:37 +00:00
|
|
|
|
onStorageClick={(storage, node) =>
|
|
|
|
|
|
handleNavigate({ type: 'storage-detail', storage, node })
|
2026-07-29 13:47:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'storage-detail':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<StorageDetail
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
storage={view.storage}
|
2026-07-29 19:35:37 +00:00
|
|
|
|
node={view.node}
|
2026-07-29 13:47:45 +00:00
|
|
|
|
onBack={() => handleNavigate({ type: 'storage' })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-08-13 01:02:33 +00:00
|
|
|
|
case 'pbs-overview':
|
|
|
|
|
|
return <PbsOverview connectionId={activeConnectionId} />
|
|
|
|
|
|
case 'pbs-datastores':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PbsDatastores
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onDatastoreClick={(store) => handleNavigate({ type: 'pbs-datastore-detail', store })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'pbs-datastore-detail':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PbsDatastoreDetail
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
store={view.store}
|
|
|
|
|
|
onBack={() => handleNavigate({ type: 'pbs-datastores' })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-07-29 19:35:37 +00:00
|
|
|
|
default:
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
|
<p className="text-muted-foreground">Unknown view: {(view as { type: string }).type}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
2026-07-29 13:47:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 23:01:51 +00:00
|
|
|
|
const activeConnection = connections.find((c) => c.id === activeConnectionId)
|
|
|
|
|
|
|
2026-08-13 01:02:33 +00:00
|
|
|
|
// When the active connection switches to a PBS server, leave any VE-only
|
|
|
|
|
|
// view behind and land on the PBS overview. Shared views (tasks, settings)
|
|
|
|
|
|
// and PBS views are left untouched.
|
|
|
|
|
|
const activeServerType = activeConnection?.serverType ?? 'pve'
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (activeServerType !== 'pbs') return
|
|
|
|
|
|
const veOnlyViews = new Set([
|
|
|
|
|
|
'dashboard',
|
|
|
|
|
|
'vms',
|
|
|
|
|
|
'vm-detail',
|
|
|
|
|
|
'nodes',
|
|
|
|
|
|
'node-detail',
|
|
|
|
|
|
'containers',
|
|
|
|
|
|
'backups',
|
|
|
|
|
|
'storage',
|
|
|
|
|
|
'storage-detail',
|
|
|
|
|
|
])
|
|
|
|
|
|
if (veOnlyViews.has(view.type)) {
|
|
|
|
|
|
setView({ type: 'pbs-overview' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [activeConnectionId, activeServerType, view.type])
|
|
|
|
|
|
|
2026-07-29 13:47:45 +00:00
|
|
|
|
return (
|
2026-08-25 13:08:21 +00:00
|
|
|
|
<div className="flex h-screen flex-col bg-background dot-grid">
|
2026-08-09 23:01:51 +00:00
|
|
|
|
{activeConnection?.status === 'failover' && activeConnection.currentEndpointUrl && (
|
2026-08-25 13:08:21 +00:00
|
|
|
|
<div className="flex items-center gap-2 border-b border-dotted border-warning/30 bg-warning/10 px-4 py-1.5 dot-grid">
|
2026-08-09 23:01:51 +00:00
|
|
|
|
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-warning" />
|
2026-08-25 13:08:21 +00:00
|
|
|
|
<DotMatrixText text="FALLBACK NODE" size="xs" className="text-warning" />
|
2026-08-09 23:01:51 +00:00
|
|
|
|
<p className="truncate font-mono text-[11px] tabular-nums text-warning/80">
|
|
|
|
|
|
{activeConnection.currentEndpointUrl}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="flex min-h-0 flex-1">
|
|
|
|
|
|
<Sidebar
|
|
|
|
|
|
onAddConnection={() => setConnectionDialogOpen(true)}
|
|
|
|
|
|
activeView={view.type}
|
|
|
|
|
|
onNavigate={(v) => handleNavigate(v as View)}
|
|
|
|
|
|
/>
|
2026-08-25 13:08:21 +00:00
|
|
|
|
<main className="flex min-h-0 flex-1 flex-col bg-background/60">
|
2026-08-10 14:33:06 +00:00
|
|
|
|
{activeConnectionId && (
|
2026-08-25 13:08:21 +00:00
|
|
|
|
<div className="shrink-0 border-b border-dotted border-border px-3 py-1.5 dot-grid">
|
2026-08-10 14:33:06 +00:00
|
|
|
|
<TaskStatusBar
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onClick={() => handleNavigate({ type: 'tasks' })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="min-h-0 flex-1 overflow-hidden">
|
|
|
|
|
|
{renderMainContent()}
|
|
|
|
|
|
</div>
|
2026-08-09 23:01:51 +00:00
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
2026-07-29 13:47:45 +00:00
|
|
|
|
<ConnectionDialog
|
|
|
|
|
|
open={connectionDialogOpen}
|
|
|
|
|
|
onOpenChange={setConnectionDialogOpen}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<CommandPalette
|
|
|
|
|
|
open={commandPaletteOpen}
|
|
|
|
|
|
onOpenChange={setCommandPaletteOpen}
|
|
|
|
|
|
onNavigate={handleNavigate}
|
|
|
|
|
|
onAddConnection={() => setConnectionDialogOpen(true)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<QueryClientProvider client={queryClient}>
|
|
|
|
|
|
<ToastProvider>
|
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
|
<AppContent />
|
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
|
</ToastProvider>
|
|
|
|
|
|
</QueryClientProvider>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default App
|