import { useEffect, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { api, ApiError, type Integration, type AuthSession, type LoginEvent, type ManagedUser, type MeshStatus } from '../lib/api' import { useAuth } from '../lib/AuthContext' import { User, Palette, Plug, Bell, Database, Info, Eye, EyeOff, Check, Download, Upload, Trash2, Camera, ChevronDown, ChevronRight, Shield, Monitor, LogOut, Users, UserPlus, Network, } from 'lucide-react' const navSections = [ { id: 'profile', label: 'Profile', icon: User }, { id: 'appearance', label: 'Appearance', icon: Palette }, { id: 'security', label: 'Security', icon: Shield }, { id: 'users', label: 'Users', icon: Users, adminOnly: true }, { id: 'mesh', label: 'Mesh', icon: Network, adminOnly: true }, { id: 'integrations', label: 'Integrations', icon: Plug }, { id: 'notifications', label: 'Notifications', icon: Bell }, { id: 'data', label: 'Data & Backup', icon: Database }, { id: 'about', label: 'About', icon: Info }, ] const accentColors = [ { name: 'Gold', color: '#C8A434' }, { name: 'Teal', color: '#2DD4BF' }, { name: 'Purple', color: '#A855F7' }, { name: 'Blue', color: '#3B82F6' }, { name: 'Green', color: '#2ECC71' }, { name: 'Red', color: '#E74C3C' }, ] type FieldDef = { key: string; label: string; secret?: boolean; hint?: string; placeholder?: string; file?: boolean } const integrationTypeDefs: { type: string; name: string; multiInstance?: boolean; fields: FieldDef[] }[] = [ { type: 'proxmox', name: 'Proxmox', multiInstance: true, fields: [ { key: 'baseUrl', label: 'Host URL', hint: 'e.g. https://192.168.1.10:8006', placeholder: 'https://192.168.1.10:8006' }, { key: 'apiKey', label: 'API Token', secret: true, hint: 'Must be the FULL token string from Datacenter → Permissions → API Tokens, in the form USER@REALM!TOKENID=SECRET — not just the secret.', placeholder: 'root@pam!archnest=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', }, ] }, { type: 'docker', name: 'Docker', multiInstance: true, fields: [ { key: 'baseUrl', label: 'Socket / Remote URL', hint: 'Unix socket path or remote daemon URL, e.g. unix:///var/run/docker.sock or tcp://host:2375', placeholder: 'unix:///var/run/docker.sock' }, ] }, { type: 'netbird', name: 'NetBird', fields: [ { key: 'apiKey', label: 'API Key', secret: true, hint: 'Personal access token from NetBird dashboard → Settings → Access Tokens.' }, ] }, { type: 'cloudflare', name: 'Cloudflare', fields: [ { key: 'apiKey', label: 'API Token', secret: true, hint: 'A scoped API token (not your Global API Key) from My Profile → API Tokens.' }, { key: 'zoneId', label: 'Zone ID', hint: 'Found on the domain overview page in the Cloudflare dashboard.' }, ] }, { type: 'aws', name: 'AWS', multiInstance: true, fields: [ { key: 'accessKey', label: 'Access Key ID', hint: 'IAM user access key, e.g. AKIAIOSFODNN7EXAMPLE' }, { key: 'secretKey', label: 'Secret Access Key', secret: true, hint: 'IAM user secret key — paired with the Access Key ID above.' }, { key: 'region', label: 'Region', hint: 'e.g. us-east-1', placeholder: 'us-east-1' }, ] }, { type: 'uptime_kuma', name: 'Uptime Kuma', fields: [ { key: 'baseUrl', label: 'URL', placeholder: 'https://uptime.example.com' }, { key: 'username', label: 'Username', secret: true }, { key: 'password', label: 'Password', secret: true }, ] }, { type: 'weather', name: 'Weather API', fields: [{ key: 'location', label: 'Location' }, { key: 'units', label: 'Units' }] }, { type: 'remote_desktop', name: 'Remote Desktop', multiInstance: true, fields: [ { key: 'protocol', label: 'Protocol (rdp / vnc / telnet)' }, { key: 'hostname', label: 'Hostname' }, { key: 'port', label: 'Port' }, { key: 'username', label: 'Username' }, { key: 'domain', label: 'Domain (RDP only)' }, { key: 'password', label: 'Password', secret: true }, { key: 'security', label: 'Security Mode (RDP only — any / nla / tls / rdp)', placeholder: 'any', hint: '"Server refused connection (wrong security type?)" usually means the target enforces NLA — try setting this to "nla".' }, ] }, ] const sshFields: FieldDef[] = [ { key: 'host', label: 'Host / IP' }, { key: 'port', label: 'Port (default 22)' }, { key: 'username', label: 'Username' }, { key: 'password', label: 'Password', secret: true }, { key: 'privateKey', label: 'Private Key (PEM)', secret: true, file: true }, { key: 'passphrase', label: 'Key Passphrase', secret: true }, { key: 'certificate', label: 'OPKSSH Certificate (id_key-cert.pub)', secret: true, file: true }, ] const cardBase: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.92)', border: '1px solid rgba(200, 164, 52, 0.08)', borderRadius: '12px', padding: '22px', position: 'relative', } const sectionTitle: React.CSSProperties = { fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px', } const labelStyle: React.CSSProperties = { fontSize: '11px', color: '#7A7D85', marginBottom: '6px', display: 'block', } const inputStyle: React.CSSProperties = { width: '100%', height: '34px', borderRadius: '8px', border: '1px solid rgba(200,164,52,0.12)', backgroundColor: 'rgba(255,255,255,0.03)', color: '#E8E6E0', fontSize: '12px', padding: '0 12px', outline: 'none', } function Toggle({ on, onClick }: { on: boolean; onClick: () => void }) { return ( ) } function GoldButton({ children, danger, onClick, disabled }: { children: React.ReactNode; danger?: boolean; onClick?: () => void; disabled?: boolean }) { return ( ) } function ProfileSection() { const { user, setUser } = useAuth() const fileInputRef = useRef(null) const [displayName, setDisplayName] = useState(user?.display_name ?? '') const [email, setEmail] = useState(user?.email ?? '') const [avatar, setAvatar] = useState(user?.avatar_data_url ?? null) const [saving, setSaving] = useState(false) const [savedMsg, setSavedMsg] = useState('') useEffect(() => { setDisplayName(user?.display_name ?? '') setEmail(user?.email ?? '') setAvatar(user?.avatar_data_url ?? null) }, [user]) const initials = (displayName || user?.username || '?').slice(0, 2).toUpperCase() function handleAvatarChange(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = () => setAvatar(reader.result as string) reader.readAsDataURL(file) } async function handleSave() { setSaving(true) setSavedMsg('') try { const { user: updated } = await api.updateMe({ displayName, email, avatarDataUrl: avatar }) setUser(updated) setSavedMsg('Saved') } catch (err) { setSavedMsg(err instanceof ApiError ? err.message : 'Failed to save') } finally { setSaving(false) } } return (

Profile

fileInputRef.current?.click()} className="relative rounded-full border-2 flex items-center justify-center font-bold cursor-pointer group" style={{ width: '64px', height: '64px', borderColor: '#C8A434', color: '#C8A434', fontSize: '20px', backgroundColor: 'rgba(200,164,52,0.08)', backgroundImage: avatar ? `url(${avatar})` : undefined, backgroundSize: 'cover', backgroundPosition: 'center', overflow: 'hidden', }} title="Upload photo" > {!avatar && initials}
{displayName || user?.username}
{email || 'No email set'}
setDisplayName(e.target.value)} />
setEmail(e.target.value)} />
{saving ? 'Saving…' : 'Save Changes'} {savedMsg && {savedMsg}}
) } function AppearanceSection() { const [theme, setTheme] = useState<'dark' | 'light'>('dark') const [accent, setAccent] = useState('Gold') const [fontSize, setFontSize] = useState(13) const [radius, setRadius] = useState(12) const [sidebarExpanded, setSidebarExpanded] = useState(true) const [animations, setAnimations] = useState(true) return (

Appearance

Theme
{(['dark', 'light'] as const).map((t) => ( ))}
Accent Color
{accentColors.map((a) => ( ))}
Font Size {fontSize}px
setFontSize(Number(e.target.value))} className="w-full" style={{ accentColor: '#C8A434' }} />
Card Border Radius {radius}px
setRadius(Number(e.target.value))} className="w-full" style={{ accentColor: '#C8A434' }} />
Sidebar Expanded by Default setSidebarExpanded((v) => !v)} />
Animations setAnimations((v) => !v)} />
) } function SshHostsSection() { const [hosts, setHosts] = useState(null) const [revealed, setRevealed] = useState>(new Set()) const [drafts, setDrafts] = useState>>({}) const [statusMsg, setStatusMsg] = useState>({}) const [busy, setBusy] = useState>(new Set()) const [collapsed, setCollapsed] = useState>(new Set()) const [newDrafts, setNewDrafts] = useState<{ key: number; values: Record }[]>([]) const nextNewKey = useRef(-1) const fileInputRefs = useRef>({}) const collapseInitialized = useRef(false) function toggleCollapsed(id: number) { setCollapsed((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } useEffect(() => { refresh() }, []) function refresh() { api.listIntegrations().then(({ integrations }) => { const sshHosts = integrations.filter((i) => i.type === 'ssh') setHosts(sshHosts) if (!collapseInitialized.current) { collapseInitialized.current = true setCollapsed(new Set(sshHosts.filter((h) => h.secretKeys.length > 0).map((h) => h.id))) } }) } function toggleReveal(key: string) { setRevealed((prev) => { const next = new Set(prev) if (next.has(key)) next.delete(key) else next.add(key) return next }) } function setBusyFlag(id: number, value: boolean) { setBusy((prev) => { const next = new Set(prev) if (value) next.add(id) else next.delete(id) return next }) } function addNewHost() { const key = nextNewKey.current-- setNewDrafts((prev) => [...prev, { key, values: {} }]) } function setNewDraftField(key: number, fieldKey: string, value: string) { setNewDrafts((prev) => prev.map((d) => (d.key === key ? { ...d, values: { ...d.values, [fieldKey]: value } } : d))) } function removeNewDraft(key: number) { setNewDrafts((prev) => prev.filter((d) => d.key !== key)) } function setDraftField(id: number, fieldKey: string, value: string) { setDrafts((prev) => ({ ...prev, [id]: { ...prev[id], [fieldKey]: value } })) } const fieldsWithJumpHost = (): FieldDef[] => [ ...sshFields, { key: 'jumpHostIntegrationId', label: 'Jump Host (optional)' }, { key: 'sessionLogging', label: 'Record session to disk' }, ] function buildPayload(fields: FieldDef[], values: Record, existing?: Integration) { const config: Record = { ...(existing?.config ?? {}) } const secrets: Record = {} for (const f of fields) { const value = values[f.key] if (value === undefined) continue if (f.secret) secrets[f.key] = value else config[f.key] = value } return { config, secrets } } async function handleSaveExisting(host: Integration) { setBusyFlag(host.id, true) setStatusMsg((prev) => ({ ...prev, [host.id]: '' })) try { const draft = drafts[host.id] ?? {} const { config, secrets } = buildPayload(fieldsWithJumpHost(), draft, host) const name = draft.__name?.trim() const { integration } = await api.updateIntegration(host.id, { ...(name ? { name } : {}), config, secrets }) setHosts((prev) => (prev ?? []).map((h) => (h.id === integration.id ? integration : h))) setStatusMsg((prev) => ({ ...prev, [host.id]: 'Saved' })) setCollapsed((prev) => new Set(prev).add(host.id)) } catch (err) { setStatusMsg((prev) => ({ ...prev, [host.id]: err instanceof ApiError ? err.message : 'Save failed' })) } finally { setBusyFlag(host.id, false) } } async function handleSaveNew(key: number, values: Record) { setBusyFlag(key, true) try { const { config, secrets } = buildPayload(fieldsWithJumpHost(), values) const name = values.__name?.trim() || (values.host ? `SSH: ${values.host}` : 'SSH Host') await api.createIntegration({ type: 'ssh', name, config, secrets }) removeNewDraft(key) refresh() } catch (err) { setStatusMsg((prev) => ({ ...prev, [key]: err instanceof ApiError ? err.message : 'Save failed' })) } finally { setBusyFlag(key, false) } } async function handleTest(host: Integration) { setBusyFlag(host.id, true) try { const result = await api.testIntegration(host.id) setStatusMsg((prev) => ({ ...prev, [host.id]: result.message })) refresh() } catch (err) { setStatusMsg((prev) => ({ ...prev, [host.id]: err instanceof ApiError ? err.message : 'Test failed' })) } finally { setBusyFlag(host.id, false) } } async function handleDelete(host: Integration) { setBusyFlag(host.id, true) try { await api.deleteIntegration(host.id) refresh() } catch (err) { setStatusMsg((prev) => ({ ...prev, [host.id]: err instanceof ApiError ? err.message : 'Delete failed' })) setBusyFlag(host.id, false) } } function renderFields( fields: FieldDef[], values: Record, onChange: (fieldKey: string, value: string) => void, idForReveal: number, existing: Integration | undefined, excludeHostId?: number, ) { return fields.map((f) => { const key = `${idForReveal}-${f.key}` if (f.key === 'jumpHostIntegrationId') { const options = (hosts ?? []).filter((h) => h.id !== excludeHostId) const savedValue = existing?.config[f.key] ?? '' const value = values[f.key] ?? savedValue return (
) } if (f.key === 'sessionLogging') { const savedValue = existing?.config[f.key] === 'true' const value = values[f.key] !== undefined ? values[f.key] === 'true' : savedValue return (
) } const isRevealed = revealed.has(key) const savedValue = f.secret ? '' : existing?.config[f.key] ?? '' const value = values[f.key] ?? savedValue if (f.file) { const isSaved = existing?.secretKeys?.includes(f.key) return (