// 🤠 Heady DNS page — React + shadcn rebuild. import { Globe, Network, Plus, RotateCcw, Save, Server, Trash2, } from 'lucide-react'; import * as React from 'react'; import { AppShell } from '@/components/shell/AppShell'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; export type DnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'TXT' | 'MX'; export interface DnsRecord { name: string; type: DnsRecordType; value: string; } export interface DnsConfig { prefixes: string[]; magicDns: boolean; baseDomain: string; nameservers: string[]; splitDns: Record; searchDomains: string[]; overrideDns: boolean; extraRecords: DnsRecord[]; } export interface DnsPageProps { initial: DnsConfig; canEdit?: boolean; } export function DnsPage(props: DnsPageProps) { return ( {() => } ); } function DnsBody({ initial, canEdit = true }: DnsPageProps) { const [config, setConfig] = React.useState(initial); const [dirty, setDirty] = React.useState(false); const update = (key: K, value: DnsConfig[K]) => { setConfig((c) => ({ ...c, [key]: value })); setDirty(true); }; const reset = () => { setConfig(initial); setDirty(false); }; const save = () => { // TODO: PATCH /api/dns once the backend lands. console.log('Would save DNS config', config); setDirty(false); }; return (

DNS

Magic DNS, nameservers, search domains, and custom records for your tailnet.

{/* Magic DNS toggle row */}

Magic DNS

Auto-resolve devices by name on{' '} {config.baseDomain || ''} .

update('magicDns', v)} disabled={!canEdit} />
{/* Base domain + override toggle */} Base domain Suffix appended to every Magic DNS hostname.
update('baseDomain', e.target.value)} placeholder="heady.local" disabled={!canEdit} />

Override local DNS

Force clients to use these nameservers.

update('overrideDns', v)} disabled={!canEdit} />
{/* Tailnet prefixes (read-only) */} Tailnet prefixes Allocated by Headscale. Read-only. {config.prefixes.map((p) => ( {p} ))} {/* String list cards */}
} values={config.nameservers} placeholder="1.1.1.1" onChange={(v) => update('nameservers', v)} disabled={!canEdit} /> } values={config.searchDomains} placeholder="company.com" onChange={(v) => update('searchDomains', v)} disabled={!canEdit} />
{/* Split DNS */} update('splitDns', v)} disabled={!canEdit} /> {/* Extra records */} update('extraRecords', v)} disabled={!canEdit} />
); } /* ───── String list editor (nameservers, search domains) ───── */ function StringListCard({ title, description, icon, values, placeholder, onChange, disabled, }: { title: string; description: string; icon: React.ReactNode; values: string[]; placeholder: string; onChange: (v: string[]) => void; disabled?: boolean; }) { const [draft, setDraft] = React.useState(''); const add = () => { const v = draft.trim(); if (!v || values.includes(v)) return; onChange([...values, v]); setDraft(''); }; const remove = (i: number) => { onChange(values.filter((_, idx) => idx !== i)); }; return ( {icon}{title} {description} {values.length === 0 && (

No entries yet.

)}
    {values.map((v, i) => (
  • {v}
  • ))}
setDraft(e.target.value)} placeholder={placeholder} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); add(); } }} disabled={disabled} />
); } /* ───── Split DNS ───── */ function SplitDnsCard({ rules, onChange, disabled, }: { rules: Record; onChange: (v: Record) => void; disabled?: boolean; }) { const [domain, setDomain] = React.useState(''); const [servers, setServers] = React.useState(''); const add = () => { const d = domain.trim(); const s = servers .split(',') .map((x) => x.trim()) .filter(Boolean); if (!d || s.length === 0) return; onChange({ ...rules, [d]: s }); setDomain(''); setServers(''); }; const remove = (key: string) => { const next = { ...rules }; delete next[key]; onChange(next); }; const entries = Object.entries(rules); return ( Split DNS Per-domain resolvers. Queries for these domains skip the global list. {entries.length === 0 ? (

No split DNS rules.

) : (
    {entries.map(([d, ips]) => (
  • {d}

    {ips.join(', ')}

  • ))}
)}
setDomain(e.target.value)} placeholder="corp.example.com" disabled={disabled} /> setServers(e.target.value)} placeholder="10.0.0.1, 10.0.0.2" disabled={disabled} />
); } /* ───── Extra records ───── */ function ExtraRecordsCard({ records, onChange, disabled, }: { records: DnsRecord[]; onChange: (v: DnsRecord[]) => void; disabled?: boolean; }) { const [name, setName] = React.useState(''); const [type, setType] = React.useState('A'); const [value, setValue] = React.useState(''); const add = () => { if (!name.trim() || !value.trim()) return; onChange([...records, { name: name.trim(), type, value: value.trim() }]); setName(''); setValue(''); }; const remove = (i: number) => onChange(records.filter((_, idx) => idx !== i)); return ( Custom records Extra A/AAAA/CNAME/TXT/MX records served alongside Magic DNS. {records.length === 0 ? (

No custom records.

) : (
{records.map((r, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: stable position ))}
Name Type Value
{r.name} {r.type} {r.value}
)}
setName(e.target.value)} placeholder="api.heady.local" disabled={disabled} /> setValue(e.target.value)} placeholder="100.64.0.10" disabled={disabled} />
); }