docs(reference): import Dune: Awakening server-manager references
All checks were successful
CI / backend-types (push) Successful in 10s
CI / frontend-build (push) Successful in 15s
CI / agent-tests (push) Successful in 39s
CI / integration (push) Successful in 22s

Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.

- icehunter/    dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
                closest analog to our agent's Dune docker control plane (compose
                lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/  Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
                daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
                Hyper-V self-host path + game-config schema

See docs/reference-repos/README.md for the full index + how we use each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 21:08:05 -04:00
parent 0715492ddf
commit 651a35d4be
1334 changed files with 238971 additions and 0 deletions

View File

@@ -0,0 +1,706 @@
import type React from 'react'
import { memo, useState, useCallback, useEffect, useRef, type ReactNode } from 'react'
import { Show, SignInButton, UserButton, useAuth } from '@clerk/react'
import { Button, Chip, Modal, Spinner, Tabs, Toast, ToggleButton, ToggleButtonGroup, toast } from '@heroui/react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useStatus } from './hooks/useStatus'
import { BackendUnreachable } from './components/BackendUnreachable'
import { SettingsConfigForm } from './components/SettingsConfigForm'
import { LanguageSelector } from './components/LanguageSelector'
import { ThemeSelector } from './components/ThemeSelector'
import { HelpMenu } from './components/HelpMenu'
import { BattlegroupTab } from './tabs/BattlegroupTab'
import { LiveMapTab } from './tabs/LiveMapTab'
import { PlayersTab } from './tabs/PlayersTab'
import { DatabaseTab } from './tabs/DatabaseTab'
import { LogsTab } from './tabs/LogsTab'
import { BlueprintsTab } from './tabs/BlueprintsTab'
import { BasesTab } from './tabs/BasesTab'
import { GuildsTab } from './tabs/GuildsTab'
import { LandsraadTab } from './tabs/LandsraadTab'
import { StorageTab } from './tabs/StorageTab'
import { ServerSettingsTab } from './tabs/ServerSettingsTab'
import { DirectorTab } from './tabs/DirectorTab'
import { MarketTab } from './tabs/MarketTab'
import { WelcomePackageTab } from './tabs/WelcomePackageTab'
import { Icon, SideNav } from './dune-ui'
import { api } from './api/client'
import type { UpdateCheckResult } from './api/client'
const TAB_IDS = [
'battlegroup',
'players',
'database',
'logs',
'blueprints',
'bases',
'guilds',
'landsraad',
'storage',
'livemap',
'server',
'director',
'market',
'welcome',
] as const
type TabId = (typeof TAB_IDS)[number]
const DEFAULT_TAB: TabId = 'battlegroup'
function currentTabFromPath(pathname: string): TabId {
const seg = pathname.replace(/^\//, '').split('/')[0]
return (TAB_IDS as readonly string[]).includes(seg) ? (seg as TabId) : DEFAULT_TAB
}
type DbSection = 'backups' | 'tables' | 'describe' | 'sample' | 'search' | 'sql'
type WelcomeSection = 'config' | 'packages' | 'grants'
type LayoutMode = 'sidenav' | 'topnav'
// Memoized at module level so identity is stable — prevents all inactive tabs from
// re-rendering whenever AppCore re-renders (e.g. router location change, useStatus poll).
const MBattlegroupTab = memo(BattlegroupTab)
const MLiveMapTab = memo(LiveMapTab)
const MPlayersTab = memo(PlayersTab)
const MDatabaseTab = memo(DatabaseTab)
const MLogsTab = memo(LogsTab)
const MBlueprintsTab = memo(BlueprintsTab)
const MBasesTab = memo(BasesTab)
const MGuildsTab = memo(GuildsTab)
const MLandsraadTab = memo(LandsraadTab)
const MStorageTab = memo(StorageTab)
const MServerSettingsTab = memo(ServerSettingsTab)
const MDirectorTab = memo(DirectorTab)
const MMarketTab = memo(MarketTab)
const MWelcomePackageTab = memo(WelcomePackageTab)
const hasClerk = !!import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
interface AppCoreProps {
isSignedIn: boolean
}
interface TabPaneProps {
active: boolean
children: ReactNode
}
interface ConnectionBadgeProps {
label: string
connected: boolean
}
function AppWithAuth() {
const { isSignedIn } = useAuth()
return <AppCore isSignedIn={!!isSignedIn} />
}
export const App: React.FC = () => {
return hasClerk ? <AppWithAuth /> : <AppCore isSignedIn={true} />
}
const AppCore: React.FC<AppCoreProps> = ({ isSignedIn }) => {
const { status, state: connState } = useStatus()
const location = useLocation()
const navigate = useNavigate()
const { t, i18n } = useTranslation()
const [reconnecting, setReconnecting] = useState(false)
const DB_SECTIONS: { key: string, label: string, depth: number }[] = [
{ key: 'db:backups', label: `╰─ ${t('database.sections.backups')}`, depth: 1 },
{ key: 'db:tables', label: `╰─ ${t('database.sections.tables')}`, depth: 1 },
{ key: 'db:describe', label: `╰─ ${t('database.sections.describe')}`, depth: 1 },
{ key: 'db:sample', label: `╰─ ${t('database.sections.sample')}`, depth: 1 },
{ key: 'db:search', label: `╰─ ${t('database.sections.search')}`, depth: 1 },
{ key: 'db:sql', label: `╰─ ${t('database.sections.sql')}`, depth: 1 },
]
const WELCOME_SECTIONS: { key: string, label: string, depth: number }[] = [
{ key: 'welcome:config', label: `╰─ ${t('welcome.sections.config')}`, depth: 1 },
{ key: 'welcome:packages', label: `╰─ ${t('welcome.sections.packages')}`, depth: 1 },
{ key: 'welcome:grants', label: `╰─ ${t('welcome.sections.grants')}`, depth: 1 },
]
// Re-establish backend connections (DB + control plane) without a service
// restart — used by the header Reconnect button when the DB shows disconnected
// (e.g. dune-admin came up before the database was ready).
const handleReconnect = async () => {
setReconnecting(true)
try {
const s = await api.reconnect()
if (s.db_connected) toast.success(t('app.reconnected'))
else toast.danger(t('app.reconnectFailed', { error: 'database still unreachable' }))
}
catch (e) {
toast.danger(t('app.reconnectFailed', { error: e instanceof Error ? e.message : String(e) }))
}
finally {
setReconnecting(false)
}
}
// Left-sidebar navigation, grouped to mirror the product's structure
// (operator tooling today; a Player Portal group lands here later).
const NAV_GROUPS: { title: string, items: { key: TabId, label: string }[] }[] = [
{
title: t('nav.groups.operations'),
items: [
{ key: 'battlegroup' as TabId, label: t('nav.battlegroup') },
{ key: 'logs' as TabId, label: t('nav.logs') },
{ key: 'database' as TabId, label: t('nav.database') },
{ key: 'server' as TabId, label: t('nav.server') },
{ key: 'director' as TabId, label: t('nav.director') },
],
},
{
title: t('nav.groups.playerWorld'),
items: [
{ key: 'players' as TabId, label: t('nav.players') },
{ key: 'livemap' as TabId, label: t('nav.liveMap') },
{ key: 'storage' as TabId, label: t('nav.storage') },
{ key: 'bases' as TabId, label: t('nav.bases') },
{ key: 'guilds' as TabId, label: t('nav.guilds') },
{ key: 'landsraad' as TabId, label: t('nav.landsraad') },
{ key: 'blueprints' as TabId, label: t('nav.blueprints') },
],
},
{
title: t('nav.groups.economy'),
items: [
{ key: 'market' as TabId, label: t('nav.market') },
{ key: 'welcome' as TabId, label: t('nav.welcome') },
],
},
]
const [layoutMode, setLayoutMode] = useState<LayoutMode>(
() => (localStorage.getItem('dune_admin_layout') === 'topnav' ? 'topnav' : 'sidenav'),
)
const setLayout = useCallback((m: LayoutMode) => {
localStorage.setItem('dune_admin_layout', m)
setLayoutMode(m)
}, [])
const [dbSection, setDbSection] = useState<DbSection>('backups')
const [welcomeSection, setWelcomeSection] = useState<WelcomeSection>('config')
const [showBackendConfig, setShowBackendConfig] = useState(false)
const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null)
const [showUpdateModal, setShowUpdateModal] = useState(false)
const [updateChecking, setUpdateChecking] = useState(false)
const [updateApplying, setUpdateApplying] = useState(false)
const [formSaving, setFormSaving] = useState(false)
const formSaveRef = useRef<(() => Promise<void>) | null>(null)
useEffect(() => {
const seg = location.pathname.replace(/^\//, '').split('/')[0]
if (!seg || !(TAB_IDS as readonly string[]).includes(seg)) {
navigate(`/${DEFAULT_TAB}`, { replace: true })
}
}, [location.pathname, navigate])
const currentTab = currentTabFromPath(location.pathname)
// Tracks which tabs have been visited at least once — they get mounted and stay
// mounted (TabPane keeps them hidden), preserving in-tab state and the isActive
// auto-refresh contract. Unvisited tabs never mount, avoiding the startup query storm.
const [mounted, setMounted] = useState<Set<TabId>>(() => new Set<TabId>([currentTab]))
useEffect(() => {
setMounted((prev) => { // eslint-disable-line react-hooks/set-state-in-effect
if (prev.has(currentTab)) return prev
const next = new Set(prev)
next.add(currentTab)
return next
})
}, [currentTab])
// Check for a newer release via the backend (it knows this build's version and
// returns the release-notes URL) — drives the clickable header update widget (#129).
useEffect(() => {
api.update.check().then(setUpdateInfo).catch(() => {})
}, [])
const checkUpdate = async () => {
setUpdateChecking(true)
try {
setUpdateInfo(await api.update.check())
}
catch {
// silently ignore — user can retry
}
finally {
setUpdateChecking(false)
}
}
const applyUpdate = async (force = false) => {
setUpdateApplying(true)
try {
const result = await api.update.apply(force)
if (result.updated) {
toast.success(force ? t('app.reinstalled', { version: result.version ?? 'latest' }) : t('app.updated', { version: result.version ?? 'latest' }))
setUpdateInfo(null)
setTimeout(() => {
window.location.reload()
}, 1500)
}
else {
toast.info(result.message)
}
}
catch (e) {
toast.danger(t('app.updateFailed', { message: e instanceof Error ? e.message : String(e) }))
}
finally {
setUpdateApplying(false)
}
}
const renderTab = (id: TabId, node: ReactNode) => (
<TabPane active={currentTab === id}>
{mounted.has(id) ? node : null}
</TabPane>
)
// #165: when the SPA never reached the backend, show an informative setup
// screen instead of an empty, non-working dashboard.
if (connState === 'error') {
return <BackendUnreachable onRetry={() => window.location.reload()} />
}
return (
// Keyed on the active language so switching language remounts the content
// subtree once. The module-level memo() tabs stay mounted and otherwise keep
// stale-language text on a language change (their props don't change), until
// an unrelated local state update forces them to re-render (#123).
<div key={i18n.language} className="h-screen flex flex-col overflow-hidden bg-background">
<Toast.Provider />
{/* Header */}
<header
className="flex items-center justify-between px-6 py-3 border-b border-border bg-surface shrink-0"
style={{ background: 'linear-gradient(180deg, var(--surface-secondary) 0%, var(--surface) 100%)' }}
>
<div className="flex items-center gap-3">
<Button
variant="ghost"
className="text-xl font-bold uppercase tracking-[0.2em] text-accent px-0 h-auto min-w-0 hover:opacity-80"
onPress={() => navigate(`/${DEFAULT_TAB}`)}
aria-label={t('app.goHome')}
>
{t('app.title')}
</Button>
{status?.control && status.control !== 'none' && <span className="text-xs text-muted">{status.control}</span>}
{status?.ssh_host && <span className="text-xs text-muted">{status.ssh_host}</span>}
{status?.db_host && status.control !== 'kubectl' && (
<span className="text-xs text-muted">{status.db_host}</span>
)}
{status?.version && (
<Button
variant="ghost"
className="text-xs text-muted hover:text-foreground px-0 h-auto min-w-0"
onPress={() => setShowBackendConfig(true)}
aria-label={t('app.openSettings')}
>
v
{status.version}
</Button>
)}
{updateInfo?.needs_update && (
<button
type="button"
onClick={() => setShowUpdateModal(true)}
aria-label={t('app.updateAvailable')}
className="cursor-pointer border-0 bg-transparent p-0"
>
<Chip size="sm" color="warning" variant="soft">
{' '}
{updateInfo.latest.replace(/^v/, '')}
</Chip>
</button>
)}
</div>
<div className="flex items-center gap-3">
{status?.executor === 'ssh' && <ConnectionBadge label="SSH" connected={status.ssh_connected} />}
<ConnectionBadge label="DB" connected={status?.db_connected ?? false} />
{status && !status.db_connected && (
<Button
size="sm"
variant="outline"
isDisabled={reconnecting}
onPress={handleReconnect}
>
{reconnecting ? t('app.reconnecting') : t('app.reconnect')}
</Button>
)}
{status?.pod_ns && (
<span className="text-xs text-muted">
ns:
{status.pod_ns}
</span>
)}
<HelpMenu status={status} />
<ThemeSelector />
<LanguageSelector />
<ToggleButtonGroup
selectionMode="single"
disallowEmptySelection
selectedKeys={[layoutMode]}
onSelectionChange={(keys) => {
const next = [...keys][0]
if (next === 'sidenav' || next === 'topnav') setLayout(next)
}}
>
<ToggleButton id="sidenav" isIconOnly aria-label={t('app.switchToSidenav')}>
<Icon name="layout-panel-left" />
</ToggleButton>
<ToggleButton id="topnav" isIconOnly aria-label={t('app.switchToTopnav')}>
<Icon name="layout-panel-top" />
</ToggleButton>
</ToggleButtonGroup>
<Button
size="sm"
variant="outline"
aria-label={t('app.configureBackend')}
onPress={() => setShowBackendConfig((v) => !v)}
className={showBackendConfig ? 'text-accent border-accent' : ''}
>
<Icon name="settings" />
{' '}
{t('app.settings')}
</Button>
{hasClerk && (
<>
<Show when="signed-out">
<SignInButton>
<Button size="sm" variant="outline">
{t('app.signIn')}
</Button>
</SignInButton>
</Show>
<Show when="signed-in">
<UserButton />
</Show>
</>
)}
</div>
</header>
{/* Settings modal — structure mirrors BotControlPanel */}
<Modal>
<Modal.Backdrop isOpen={showBackendConfig} onOpenChange={(v) => !v && setShowBackendConfig(false)}>
<Modal.Container size="cover" scroll="outside">
<Modal.Dialog className="h-[92vh] flex flex-col">
<Modal.CloseTrigger />
<Modal.Header>
<div className="flex items-baseline gap-6 flex-wrap">
<Modal.Heading className="text-accent">{t('app.settings')}</Modal.Heading>
{status && (
<div className="flex items-center gap-4 text-xs text-muted">
{status.version && (
<span className="font-mono">
v
{status.version}
</span>
)}
{status.control && status.control !== 'none' && <span>{status.control}</span>}
{status.commit && status.commit !== 'unknown' && (
<span className="font-mono opacity-60">{status.commit}</span>
)}
</div>
)}
</div>
</Modal.Header>
{/* Body scrolls; form fills it with its own internal tab scroll */}
<Modal.Body className="flex flex-col overflow-y-auto flex-1 min-h-0 pr-1">
{showBackendConfig && (
<SettingsConfigForm saveRef={formSaveRef} onSavingChange={setFormSaving} />
)}
</Modal.Body>
<Modal.Footer className="flex items-center gap-2">
{/* Left: update controls — fixed positions so buttons don't shift */}
<Button
size="sm"
variant="ghost"
onPress={checkUpdate}
isDisabled={updateChecking || updateApplying}
>
{updateChecking
? (
<>
<Spinner size="sm" color="current" />
{' '}
{t('common.checking')}
</>
)
: t('app.checkUpdates')}
</Button>
{updateInfo && !updateInfo.needs_update && (
<Button
size="sm"
variant="ghost"
onPress={() => applyUpdate(true)}
isDisabled={updateApplying}
>
{updateApplying ? <Spinner size="sm" color="current" /> : t('app.reinstall')}
</Button>
)}
{updateInfo?.needs_update && (
<Button size="sm" onPress={() => applyUpdate()} isDisabled={updateApplying}>
{updateApplying
? <Spinner size="sm" color="current" />
: (
<span className="font-mono text-xs">
v
{updateInfo.current}
{' → '}
v
{updateInfo.latest.replace(/^v/, '')}
</span>
)}
</Button>
)}
{/* Spacer */}
<span className="flex-1" />
{/* Right: save + close */}
<span className="text-xs text-muted">{t('app.changesNote')}</span>
<Button
size="sm"
onPress={() => formSaveRef.current?.()}
isDisabled={formSaving}
>
{formSaving
? (
<>
<Spinner size="sm" color="current" />
{' '}
{t('common.saving')}
</>
)
: (
<>
<Icon name="save" />
{' '}
{t('app.saveApply')}
</>
)}
</Button>
<Button
size="sm"
variant="tertiary"
onPress={() => setShowBackendConfig(false)}
>
{t('common.close')}
</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
{/* Update-available prompt — opened from the header release widget (#129).
Reuses the backend update check for the release-notes link + Continue/Cancel. */}
<Modal>
<Modal.Backdrop isOpen={showUpdateModal} onOpenChange={(v) => !v && setShowUpdateModal(false)}>
<Modal.Container size="sm">
<Modal.Dialog>
<Modal.CloseTrigger />
<Modal.Header>
<Modal.Heading className="text-accent">{t('app.updateAvailable')}</Modal.Heading>
</Modal.Header>
<Modal.Body className="flex flex-col gap-3">
<p className="text-sm text-muted">
{t('app.updateAvailableBody', {
current: updateInfo?.current ?? '',
latest: updateInfo?.latest?.replace(/^v/, '') ?? '',
})}
</p>
{updateInfo?.release_url && (
<a
href={updateInfo.release_url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-sm text-accent hover:opacity-80"
>
<Icon name="external-link" />
{' '}
{t('app.viewReleaseNotes')}
</a>
)}
</Modal.Body>
<Modal.Footer className="flex items-center justify-end gap-2">
<Button
size="sm"
variant="tertiary"
onPress={() => setShowUpdateModal(false)}
>
{t('common.cancel')}
</Button>
<Button
size="sm"
onPress={() => {
setShowUpdateModal(false)
void applyUpdate()
}}
isDisabled={updateApplying}
>
{updateApplying ? <Spinner size="sm" color="current" /> : t('app.updateNow')}
</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
{/* Body: layout-conditional rendering.
In sidenav mode: grouped left sidebar + content.
In topnav mode: horizontal nav bar + full-width content.
All tabs stay mounted (inactive hidden) so per-tab state and isActive
auto-refresh behavior persist. */}
{(() => {
const databaseNode = layoutMode === 'topnav'
? <MDatabaseTab section={dbSection} showSubnav onSectionChange={setDbSection} />
: <MDatabaseTab section={dbSection} />
const welcomeNode = layoutMode === 'topnav'
? <MWelcomePackageTab section={welcomeSection} showSubnav onSectionChange={setWelcomeSection} />
: <MWelcomePackageTab section={welcomeSection} />
if (layoutMode === 'sidenav') {
return (
<div className="flex-1 flex gap-3 p-3 overflow-hidden min-h-0">
<nav className="w-60 shrink-0 flex flex-col gap-2 overflow-y-auto">
{/* Operations: rendered separately so Database can expand DB sub-items inline */}
<SideNav
width="w-full"
title={NAV_GROUPS[0].title}
items={[
...NAV_GROUPS[0].items.slice(0, 3),
...(currentTab === 'database' ? DB_SECTIONS : []),
...NAV_GROUPS[0].items.slice(3),
] as { key: string, label: string, depth?: number }[]}
active={currentTab === 'database' ? `db:${dbSection}` : currentTab}
onSelect={(k: string) => {
if (k.startsWith('db:')) {
setDbSection(k.slice(3) as DbSection)
if (currentTab !== 'database') navigate('/database')
}
else {
navigate(`/${k}`)
}
}}
/>
{/* Player World: unchanged */}
<SideNav
key={NAV_GROUPS[1].title}
width="w-full"
title={NAV_GROUPS[1].title}
items={NAV_GROUPS[1].items}
active={currentTab}
onSelect={(k) => navigate(`/${k}`)}
/>
{/* Economy: expand Welcome sub-items inline */}
<SideNav
width="w-full"
title={NAV_GROUPS[2].title}
items={[
...NAV_GROUPS[2].items,
...(currentTab === 'welcome' ? WELCOME_SECTIONS : []),
] as { key: string, label: string, depth?: number }[]}
active={currentTab === 'welcome' ? `welcome:${welcomeSection}` : currentTab}
onSelect={(k: string) => {
if (k.startsWith('welcome:')) {
setWelcomeSection(k.slice(8) as WelcomeSection)
if (currentTab !== 'welcome') navigate('/welcome')
}
else {
navigate(`/${k}`)
}
}}
/>
</nav>
<main className="flex-1 flex flex-col overflow-hidden min-h-0">
{renderTab('battlegroup', <MBattlegroupTab isActive={currentTab === 'battlegroup'} />)}
{renderTab('players', <MPlayersTab isActive={currentTab === 'players'} />)}
{renderTab('database', databaseNode)}
{renderTab('logs', <MLogsTab control={status?.control} />)}
{renderTab('blueprints', <MBlueprintsTab isSignedIn={isSignedIn} />)}
{renderTab('bases', <MBasesTab isSignedIn={isSignedIn} />)}
{renderTab('guilds', <MGuildsTab isSignedIn={isSignedIn} />)}
{renderTab('landsraad', <MLandsraadTab />)}
{renderTab('storage', <MStorageTab />)}
{renderTab('livemap', <MLiveMapTab isActive={currentTab === 'livemap'} />)}
{renderTab('server', <MServerSettingsTab />)}
{renderTab('director', <MDirectorTab />)}
{renderTab('market', <MMarketTab />)}
{renderTab('welcome', welcomeNode)}
</main>
</div>
)
}
// topnav mode: horizontal nav bar + full-width content area
return (
<>
<Tabs
selectedKey={currentTab}
onSelectionChange={(k) => navigate(`/${String(k)}`)}
className="shrink-0 border-b border-border bg-surface"
>
<Tabs.ListContainer className="px-3 py-2 overflow-x-auto">
<Tabs.List aria-label={t('app.title')}>
{NAV_GROUPS.flatMap((g) => g.items).map((item) => (
<Tabs.Tab key={item.key} id={item.key}>
{item.label}
<Tabs.Indicator />
</Tabs.Tab>
))}
</Tabs.List>
</Tabs.ListContainer>
</Tabs>
<div className="flex-1 flex flex-col p-3 overflow-hidden min-h-0">
<main className="flex-1 flex flex-col overflow-hidden min-h-0">
{renderTab('battlegroup', <MBattlegroupTab isActive={currentTab === 'battlegroup'} />)}
{renderTab('players', <MPlayersTab isActive={currentTab === 'players'} />)}
{renderTab('database', databaseNode)}
{renderTab('logs', <MLogsTab control={status?.control} />)}
{renderTab('blueprints', <MBlueprintsTab isSignedIn={isSignedIn} />)}
{renderTab('bases', <MBasesTab isSignedIn={isSignedIn} />)}
{renderTab('guilds', <MGuildsTab isSignedIn={isSignedIn} />)}
{renderTab('landsraad', <MLandsraadTab />)}
{renderTab('storage', <MStorageTab />)}
{renderTab('livemap', <MLiveMapTab isActive={currentTab === 'livemap'} />)}
{renderTab('server', <MServerSettingsTab />)}
{renderTab('director', <MDirectorTab />)}
{renderTab('market', <MMarketTab />)}
{renderTab('welcome', welcomeNode)}
</main>
</div>
</>
)
})()}
</div>
)
}
function TabPane({ active, children }: TabPaneProps) {
return (
<div className={`flex-1 min-h-0 ${active ? 'flex flex-col dune-tab-active' : 'hidden'}`}>
{children}
</div>
)
}
function ConnectionBadge({ label, connected }: ConnectionBadgeProps) {
return (
<div className="flex items-center gap-1.5 text-xs">
<div className={`w-2 h-2 rounded-full ${connected ? 'bg-success' : 'bg-muted/40'}`} />
<span className={connected ? 'text-foreground' : 'text-muted'}>{label}</span>
</div>
)
}