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' }] }, ] 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() { return (

Data & Backup

Export Bookmarks (JSON) Export
Import Bookmarks (JSON) Import
Export Settings Export
Clear Cache Clear
Reset to Defaults Reset
) } 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 */}
) }