2026-06-18 08:14:00 -04:00
|
|
|
import { useState } from 'react'
|
2026-06-18 16:50:06 +00:00
|
|
|
import { Routes, Route, useLocation } from 'react-router-dom'
|
2026-06-18 08:14:00 -04:00
|
|
|
import Sidebar from './components/Sidebar'
|
|
|
|
|
import TopBar from './components/TopBar'
|
2026-06-18 16:15:34 +00:00
|
|
|
import Glance from './pages/Glance'
|
|
|
|
|
import Infrastructure from './pages/Infrastructure'
|
2026-06-18 18:10:16 +00:00
|
|
|
import BookNest from './pages/BookNest'
|
Add Phase 1a: core SSH terminal (Termix migration)
Implements the minimal-viable terminal described in TERMIX_MIGRATION.md
Phase 1a: a real interactive SSH session in the browser over a
WebSocket, using xterm.js on the frontend and ssh2 on the backend.
Reuses ArchNest's existing SSH integrations (host/port/username/
password/privateKey/passphrase) instead of introducing a second,
duplicate host-management system the way Termix has one.
Backend: new /api/terminal WebSocket route (registered via
@fastify/websocket) handling connect/input/resize/disconnect messages,
authenticated via a JWT passed as a query param (browsers can't set
custom headers on the WS handshake). Extracted the integration secret
loader out of routes/integrations.ts into db/secrets.ts so the new
terminal route can reuse it without duplicating the decrypt logic.
Frontend: new Terminal.tsx page listing configured SSH hosts and
rendering an xterm.js terminal wired to the WebSocket; wired into
App.tsx at /terminal. vite.config.ts's dev proxy now forwards
WebSocket upgrades (ws: true) so this works under `npm run dev`.
Verified end-to-end against a real (test) ssh2-based SSH server:
connect, shell banner, keystroke echo, and prompt redraw all worked
correctly over the actual WebSocket protocol.
Deliberately deferred to Phase 1b/1c per the migration doc: jump-host
chaining, tab/split-pane UI, terminal theme/font settings, OPKSSH cert
auth, tmux session monitor, session recording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-19 10:52:04 +00:00
|
|
|
import Terminal from './pages/Terminal'
|
2026-06-19 11:40:59 +00:00
|
|
|
import Tunnels from './pages/Tunnels'
|
2026-06-19 11:56:04 +00:00
|
|
|
import Files from './pages/Files'
|
2026-06-19 12:28:30 +00:00
|
|
|
import Containers from './pages/Containers'
|
2026-06-19 15:25:10 +00:00
|
|
|
import RemoteDesktop from './pages/RemoteDesktop'
|
2026-06-19 15:38:30 +00:00
|
|
|
import HostMetrics from './pages/HostMetrics'
|
2026-06-18 18:44:26 +00:00
|
|
|
import Settings from './pages/Settings'
|
2026-06-18 19:13:27 +00:00
|
|
|
import Login from './pages/Login'
|
|
|
|
|
import Enrollment from './pages/Enrollment'
|
|
|
|
|
import { useAuth } from './lib/AuthContext'
|
2026-06-18 08:14:00 -04:00
|
|
|
|
|
|
|
|
function App() {
|
2026-06-18 19:13:27 +00:00
|
|
|
const { status } = useAuth()
|
|
|
|
|
|
|
|
|
|
if (status === 'loading') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-screen w-screen items-center justify-center bg-page">
|
|
|
|
|
<p style={{ color: '#7A7D85', fontSize: '13px' }}>Loading…</p>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (status === 'needs-setup' || status === 'enrolling') return <Enrollment />
|
|
|
|
|
if (status === 'logged-out') return <Login />
|
|
|
|
|
|
|
|
|
|
return <Dashboard />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Dashboard() {
|
2026-06-18 08:14:00 -04:00
|
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
2026-06-18 14:45:00 +00:00
|
|
|
const sidebarWidth = sidebarCollapsed ? 64 : 200
|
2026-06-18 16:50:06 +00:00
|
|
|
const location = useLocation()
|
2026-06-18 18:13:26 +00:00
|
|
|
const showHero = location.pathname === '/infrastructure' || location.pathname === '/booknest'
|
2026-06-18 18:27:37 +00:00
|
|
|
const heroPaddingTop = location.pathname === '/booknest' ? '70px' : '72px'
|
2026-06-18 18:25:19 +00:00
|
|
|
const heroObjectPosition = location.pathname === '/booknest' ? '54% 8%' : 'center 5%'
|
|
|
|
|
const topBarHeight = location.pathname === '/booknest' ? 72 : 56
|
2026-06-18 08:14:00 -04:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen w-screen overflow-hidden bg-page">
|
|
|
|
|
<Sidebar
|
|
|
|
|
collapsed={sidebarCollapsed}
|
|
|
|
|
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<main
|
2026-06-18 16:50:06 +00:00
|
|
|
className="relative h-screen overflow-hidden"
|
2026-06-18 08:14:00 -04:00
|
|
|
style={{ marginLeft: `${sidebarWidth}px`, width: `calc(100vw - ${sidebarWidth}px)` }}
|
|
|
|
|
>
|
2026-06-18 16:50:06 +00:00
|
|
|
{showHero && (
|
|
|
|
|
<div className="pointer-events-none absolute left-0 right-0 top-0" style={{ height: '300px', zIndex: 0 }}>
|
|
|
|
|
<img
|
|
|
|
|
src="/archnest-hero-banner.png"
|
|
|
|
|
alt=""
|
|
|
|
|
className="absolute inset-0 h-full w-full"
|
|
|
|
|
style={{
|
|
|
|
|
objectFit: 'cover',
|
2026-06-18 18:25:19 +00:00
|
|
|
objectPosition: heroObjectPosition,
|
2026-06-18 16:50:06 +00:00
|
|
|
maskImage: 'linear-gradient(to bottom, black 0%, black 55%, transparent 100%)',
|
|
|
|
|
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 55%, transparent 100%)',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
className="absolute inset-0"
|
|
|
|
|
style={{
|
|
|
|
|
background: 'radial-gradient(ellipse 70% 100% at center, transparent 40%, var(--color-page) 100%)',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="relative" style={{ zIndex: 1 }}>
|
|
|
|
|
<TopBar />
|
|
|
|
|
</div>
|
2026-06-18 08:14:00 -04:00
|
|
|
|
|
|
|
|
<section
|
2026-06-18 16:50:06 +00:00
|
|
|
className="relative flex w-full flex-col overflow-hidden"
|
2026-06-18 18:25:19 +00:00
|
|
|
style={{ height: `calc(100vh - ${topBarHeight}px)`, scrollbarWidth: 'none', padding: showHero ? `${heroPaddingTop} 24px 24px 24px` : '16px 24px 24px 24px', gap: '20px', zIndex: 1 }}
|
2026-06-18 08:14:00 -04:00
|
|
|
>
|
2026-06-18 16:15:34 +00:00
|
|
|
<Routes>
|
|
|
|
|
<Route path="/" element={<Glance />} />
|
|
|
|
|
<Route path="/infrastructure" element={<Infrastructure />} />
|
2026-06-18 18:10:16 +00:00
|
|
|
<Route path="/booknest" element={<BookNest />} />
|
Add Phase 1a: core SSH terminal (Termix migration)
Implements the minimal-viable terminal described in TERMIX_MIGRATION.md
Phase 1a: a real interactive SSH session in the browser over a
WebSocket, using xterm.js on the frontend and ssh2 on the backend.
Reuses ArchNest's existing SSH integrations (host/port/username/
password/privateKey/passphrase) instead of introducing a second,
duplicate host-management system the way Termix has one.
Backend: new /api/terminal WebSocket route (registered via
@fastify/websocket) handling connect/input/resize/disconnect messages,
authenticated via a JWT passed as a query param (browsers can't set
custom headers on the WS handshake). Extracted the integration secret
loader out of routes/integrations.ts into db/secrets.ts so the new
terminal route can reuse it without duplicating the decrypt logic.
Frontend: new Terminal.tsx page listing configured SSH hosts and
rendering an xterm.js terminal wired to the WebSocket; wired into
App.tsx at /terminal. vite.config.ts's dev proxy now forwards
WebSocket upgrades (ws: true) so this works under `npm run dev`.
Verified end-to-end against a real (test) ssh2-based SSH server:
connect, shell banner, keystroke echo, and prompt redraw all worked
correctly over the actual WebSocket protocol.
Deliberately deferred to Phase 1b/1c per the migration doc: jump-host
chaining, tab/split-pane UI, terminal theme/font settings, OPKSSH cert
auth, tmux session monitor, session recording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-19 10:52:04 +00:00
|
|
|
<Route path="/terminal" element={<Terminal />} />
|
2026-06-19 11:40:59 +00:00
|
|
|
<Route path="/tunnels" element={<Tunnels />} />
|
2026-06-19 11:56:04 +00:00
|
|
|
<Route path="/files" element={<Files />} />
|
2026-06-19 12:28:30 +00:00
|
|
|
<Route path="/containers" element={<Containers />} />
|
2026-06-19 15:25:10 +00:00
|
|
|
<Route path="/remote-desktop" element={<RemoteDesktop />} />
|
2026-06-19 15:38:30 +00:00
|
|
|
<Route path="/host-metrics" element={<HostMetrics />} />
|
2026-06-18 18:44:26 +00:00
|
|
|
<Route path="/settings" element={<Settings />} />
|
2026-06-18 16:15:34 +00:00
|
|
|
</Routes>
|
2026-06-18 08:14:00 -04:00
|
|
|
</section>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App
|