import { useEffect, useRef, useState } from 'react' import { api, ApiError, type Integration } from '../lib/api' import { useAuth } from '../lib/AuthContext' import { User, Palette, Plug, Bell, Database, Info, Eye, EyeOff, Check, Download, Upload, Trash2, RotateCcw, Camera, } from 'lucide-react' const navSections = [ { id: 'profile', label: 'Profile', icon: User }, { id: 'appearance', label: 'Appearance', icon: Palette }, { 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 } const integrationTypeDefs: { type: string; name: string; fields: FieldDef[] }[] = [ { type: 'proxmox', name: 'Proxmox', fields: [{ key: 'baseUrl', label: 'Host URL' }, { key: 'apiKey', label: 'API Token', secret: true }] }, { type: 'docker', name: 'Docker', fields: [{ key: 'baseUrl', label: 'Socket / Remote URL' }] }, { type: 'netbird', name: 'NetBird', fields: [{ key: 'apiKey', label: 'API Key', secret: true }] }, { type: 'cloudflare', name: 'Cloudflare', fields: [{ key: 'apiKey', label: 'API Token', secret: true }, { key: 'zoneId', label: 'Zone ID' }] }, { type: 'aws', name: 'AWS', fields: [{ key: 'accessKey', label: 'Access Key' }, { key: 'secretKey', label: 'Secret', secret: true }, { key: 'region', label: 'Region' }] }, { type: 'uptime_kuma', name: 'Uptime Kuma', fields: [{ key: 'baseUrl', label: 'URL' }, { key: 'apiKey', label: 'API Key', secret: true }] }, { type: 'weather', name: 'Weather API', fields: [{ key: 'location', label: 'Location' }, { key: 'units', label: 'Units' }] }, { type: 'remote_desktop', name: 'Remote Desktop', 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 }, ] }, ] 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 }, { key: 'passphrase', label: 'Key Passphrase', secret: true }, { key: 'certificate', label: 'OPKSSH Certificate (id_key-cert.pub)', secret: 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 [newDrafts, setNewDrafts] = useState<{ key: number; values: Record }[]>([]) const nextNewKey = useRef(-1) useEffect(() => { refresh() }, []) function refresh() { api.listIntegrations().then(({ integrations }) => setHosts(integrations.filter((i) => i.type === 'ssh'))) } 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) { const config: Record = {} 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) const { integration } = await api.updateIntegration(host.id, { config, secrets }) setHosts((prev) => (prev ?? []).map((h) => (h.id === integration.id ? integration : h))) setStatusMsg((prev) => ({ ...prev, [host.id]: 'Saved' })) } 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.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 return (
onChange(f.key, e.target.value)} placeholder={f.secret && existing ? '••••••••••••' : 'Not configured'} /> {f.secret && ( )}
) }) } if (!hosts) { return (

Loading SSH hosts…

) } return (
{hosts.map((host) => { const online = host.status === 'connected' const draft = drafts[host.id] ?? {} return (
{host.name}
{statusMsg[host.id] && {statusMsg[host.id]}}
{renderFields(fieldsWithJumpHost(), draft, (k, v) => setDraftField(host.id, k, v), host.id, host, host.id)}
) })} {newDrafts.map((d) => (
New SSH Host
{statusMsg[d.key] && {statusMsg[d.key]}}
{renderFields(fieldsWithJumpHost(), d.values, (k, v) => setNewDraftField(d.key, k, v), d.key, undefined)}
))}
) } function IntegrationsSection() { const [integrations, setIntegrations] = useState(null) const [revealed, setRevealed] = useState>(new Set()) const [drafts, setDrafts] = useState>>({}) const [statusMsg, setStatusMsg] = useState>({}) const [busy, setBusy] = useState>(new Set()) useEffect(() => { api.listIntegrations().then(({ integrations }) => setIntegrations(integrations)) }, []) 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(type: string, value: boolean) { setBusy((prev) => { const next = new Set(prev) if (value) next.add(type) else next.delete(type) return next }) } function setDraftField(type: string, fieldKey: string, value: string) { setDrafts((prev) => ({ ...prev, [type]: { ...prev[type], [fieldKey]: value } })) } async function handleSave(def: (typeof integrationTypeDefs)[number], existing: Integration | undefined) { setBusyFlag(def.type, true) setStatusMsg((prev) => ({ ...prev, [def.type]: '' })) try { const draft = drafts[def.type] ?? {} const config: Record = {} const secrets: Record = {} for (const f of def.fields) { const value = draft[f.key] if (value === undefined) continue if (f.secret) secrets[f.key] = value else config[f.key] = value } let integration: Integration if (existing) { ;({ integration } = await api.updateIntegration(existing.id, { config, secrets })) } else { ;({ integration } = await api.createIntegration({ type: def.type, name: def.name, config, secrets })) } setIntegrations((prev) => { const others = (prev ?? []).filter((i) => i.id !== integration.id) return [...others, integration] }) setStatusMsg((prev) => ({ ...prev, [def.type]: 'Saved' })) } catch (err) { setStatusMsg((prev) => ({ ...prev, [def.type]: err instanceof ApiError ? err.message : 'Save failed' })) } finally { setBusyFlag(def.type, false) } } async function handleTest(def: (typeof integrationTypeDefs)[number], existing: Integration | undefined) { if (!existing) { setStatusMsg((prev) => ({ ...prev, [def.type]: 'Save the integration before testing' })) return } setBusyFlag(def.type, true) try { const result = await api.testIntegration(existing.id) setStatusMsg((prev) => ({ ...prev, [def.type]: result.message })) const { integrations } = await api.listIntegrations() setIntegrations(integrations) } catch (err) { setStatusMsg((prev) => ({ ...prev, [def.type]: err instanceof ApiError ? err.message : 'Test failed' })) } finally { setBusyFlag(def.type, false) } } if (!integrations) { return (

Loading integrations…

) } return (

SSH Hosts

Other Integrations

{integrationTypeDefs.map((def) => { const existing = integrations.find((i) => i.type === def.type) const online = existing?.status === 'connected' const draft = drafts[def.type] ?? {} return (
{def.name}
{statusMsg[def.type] && ( {statusMsg[def.type]} )}
{def.fields.map((f) => { const key = `${def.type}-${f.key}` const isRevealed = revealed.has(key) const savedValue = f.secret ? '' : existing?.config[f.key] ?? '' const value = draft[f.key] ?? savedValue return (
setDraftField(def.type, f.key, e.target.value)} placeholder={f.secret && existing ? '••••••••••••' : 'Not configured'} /> {f.secret && ( )}
) })}
) })}
) } function NotificationsSection() { const [enabled, setEnabled] = useState(true) const [email, setEmail] = useState(true) const [push, setPush] = useState(false) const [sound, setSound] = useState(true) return (

Notifications

Enable Notifications setEnabled((v) => !v)} />
Email Notifications setEmail((v) => !v)} />
Browser Push setPush((v) => !v)} />
Sound setSound((v) => !v)} />
{sound && ( )}
) } function DataBackupSection() { const [busy, setBusy] = useState(false) const [message, setMessage] = useState(null) const [error, setError] = useState(null) const importRef = useRef(null) async function handleExport() { setBusy(true) setError(null) setMessage(null) try { const data = await api.exportData() const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `archnest-backup-${new Date().toISOString().slice(0, 10)}.json` a.click() URL.revokeObjectURL(url) setMessage(`Exported ${data.integrations.length} integrations, ${data.bookmarks.length} bookmarks, ${data.tunnels.length} tunnels.`) } catch (err) { setError(err instanceof Error ? err.message : 'Export failed') } finally { setBusy(false) } } async function handleImportFile(file: File) { setBusy(true) setError(null) setMessage(null) try { const text = await file.text() const parsed = JSON.parse(text) const result = await api.importData(parsed) const c = result.imported setMessage(`Imported ${c.integrations} integrations, ${c.bookmarkCategories} categories, ${c.bookmarks} bookmarks, ${c.tunnels} tunnels.`) } catch (err) { setError(err instanceof Error ? err.message : 'Import failed — is this a valid ArchNest backup file?') } finally { setBusy(false) } } return (

Data & Backup

Export a portable backup of all integrations (including their credentials), bookmarks, and tunnels as a single JSON file, or import one into this instance. Imports are additive — existing data is kept and the backup's items are added alongside it. The backup contains plaintext credentials, so store it securely.

Export all data (JSON) Export
Import from backup (JSON) importRef.current?.click()} disabled={busy}> Import { const file = e.target.files?.[0] if (file) handleImportFile(file) e.target.value = '' }} />
{message &&

{message}

} {error &&

{error}

}
) } function AboutSection() { const rows: [string, string][] = [ ['App', 'ArchNest Dashboard v1.0.0'], ['Author', 'Samuel James'], ['Repo', 'github.com/SamuelSJames/archnest'], ['Stack', 'React 19, Vite, TypeScript'], ['License', 'MIT'], ] return (

About

{rows.map(([label, value]) => (
{label} {value}
))}
) } const sectionComponents: Record React.ReactElement> = { profile: ProfileSection, appearance: AppearanceSection, integrations: IntegrationsSection, notifications: NotificationsSection, data: DataBackupSection, about: AboutSection, } export default function Settings() { const [active, setActive] = useState('profile') const ActiveSection = sectionComponents[active] return (
{/* Settings nav */}
{navSections.map((s) => { const Icon = s.icon const isActive = active === s.id return ( ) })}
{/* Content */}
) }