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'
|
Fix favicon, dark select dropdowns, add brand bookmark icons and Help page
- Replace mislabeled Vite-logo favicon.svg with proper ArchNest mark
extracted from the logo, generated at 32/48/256px PNGs
- Force native <select>/<option> elements to render with the dark theme
(color-scheme + explicit colors) so options are readable
- Auto-detect real brand/service icons for bookmarks (AWS, Proxmox,
Azure, Docker, etc.) via the dashboard-icons CDN, with manual
override and graceful fallback to lucide icons
- Add a Help page with a guided tour of every page, linked from the
sidebar, top-bar search, and the user dropdown menu
2026-06-19 21:13:32 +00:00
|
|
|
import Help from './pages/Help'
|
2026-06-18 19:13:27 +00:00
|
|
|
import Login from './pages/Login'
|
|
|
|
|
import Enrollment from './pages/Enrollment'
|
Add mesh prerequisite gate (#33)
* Add mesh prerequisite gate (NetBird verification before app config)
Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.
- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird
* Make mesh verification universal (CIDR check, not NetBird-specific)
Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.
* Add reachability fallback for routed meshes (VPC peering, etc.)
A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 17:30:46 -04:00
|
|
|
import MeshGate from './pages/MeshGate'
|
2026-06-18 19:13:27 +00:00
|
|
|
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 />
|
Add mesh prerequisite gate (#33)
* Add mesh prerequisite gate (NetBird verification before app config)
Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.
- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird
* Make mesh verification universal (CIDR check, not NetBird-specific)
Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.
* Add reachability fallback for routed meshes (VPC peering, etc.)
A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 17:30:46 -04:00
|
|
|
if (status === 'needs-mesh') return <MeshGate />
|
2026-06-18 19:13:27 +00:00
|
|
|
|
|
|
|
|
return <Dashboard />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Dashboard() {
|
Add mesh prerequisite gate (#33)
* Add mesh prerequisite gate (NetBird verification before app config)
Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.
- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird
* Make mesh verification universal (CIDR check, not NetBird-specific)
Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.
* Add reachability fallback for routed meshes (VPC peering, etc.)
A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 17:30:46 -04:00
|
|
|
const { user, meshStatus } = useAuth()
|
|
|
|
|
const showMeshNotice = !!meshStatus && meshStatus.required && !meshStatus.verified && !meshStatus.overridden && user?.role !== 'admin'
|
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>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-06-19 21:34:04 +00:00
|
|
|
<div className="relative" style={{ zIndex: 20 }}>
|
2026-06-18 16:50:06 +00:00
|
|
|
<TopBar />
|
|
|
|
|
</div>
|
2026-06-18 08:14:00 -04:00
|
|
|
|
Add mesh prerequisite gate (#33)
* Add mesh prerequisite gate (NetBird verification before app config)
Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.
- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird
* Make mesh verification universal (CIDR check, not NetBird-specific)
Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.
* Add reachability fallback for routed meshes (VPC peering, etc.)
A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 17:30:46 -04:00
|
|
|
{showMeshNotice && (
|
|
|
|
|
<div
|
|
|
|
|
className="relative flex items-center justify-center"
|
|
|
|
|
style={{
|
|
|
|
|
zIndex: 20,
|
|
|
|
|
padding: '6px 16px',
|
|
|
|
|
backgroundColor: 'rgba(230,126,34,0.1)',
|
|
|
|
|
borderBottom: '1px solid rgba(230,126,34,0.2)',
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
color: '#E67E22',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Mesh setup is still in progress — an admin needs to finish verifying the network. Some features may be limited.
|
|
|
|
|
</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 />} />
|
Fix favicon, dark select dropdowns, add brand bookmark icons and Help page
- Replace mislabeled Vite-logo favicon.svg with proper ArchNest mark
extracted from the logo, generated at 32/48/256px PNGs
- Force native <select>/<option> elements to render with the dark theme
(color-scheme + explicit colors) so options are readable
- Auto-detect real brand/service icons for bookmarks (AWS, Proxmox,
Azure, Docker, etc.) via the dashboard-icons CDN, with manual
override and graceful fallback to lucide icons
- Add a Help page with a guided tour of every page, linked from the
sidebar, top-bar search, and the user dropdown menu
2026-06-19 21:13:32 +00:00
|
|
|
<Route path="/help" element={<Help />} />
|
2026-06-18 16:15:34 +00:00
|
|
|
</Routes>
|
2026-06-18 08:14:00 -04:00
|
|
|
</section>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App
|