'use client' import { useState } from 'react' import { toast } from 'sonner' import { DoorClosed, Plus } from 'lucide-react' import { PageHeader, PageTitle } from '@/components/ui/page-title' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' import { useConfirm } from '@/components/ui/confirm' import { createDoorConnection, updateDoorConnection, deleteDoorConnection, testDoorConnection, } from '@/lib/actions' type Conn = { id: string name: string adapter: string base_url: string pinned_host: string auth_mode: string auth_param: string | null secret_ref: string | null verbs: Record | null allow_member_self_entry: boolean is_enabled: boolean } type LogRow = { id: string; action: string; success: boolean; detail: string | null; occurred_at: string } const input = 'w-full bg-background border border-border text-foreground font-sans text-sm rounded px-3 py-2 focus:outline-none focus:border-primary' function emptyDraft() { return { name: '', adapter: 'native_heatsync', base_url: '', pinned_host: '', secret_ref: '', allow_member_self_entry: false, grant: '', revoke: '', open: '', status: '', } } export function DoorManageClient({ initial, secrets, log, }: { initial: unknown[] secrets: { id: string; title: string }[] log: unknown[] }) { const confirm = useConfirm() const [conns, setConns] = useState(initial as Conn[]) const [showNew, setShowNew] = useState(false) const [d, setD] = useState(emptyDraft()) const [busy, setBusy] = useState(false) const rows = log as LogRow[] function buildVerbs(dr: typeof d) { if (dr.adapter === 'native_heatsync') return {} const v: Record = {} if (dr.grant.trim()) v.grant = dr.grant.trim() if (dr.revoke.trim()) v.revoke = dr.revoke.trim() if (dr.open.trim()) v.open = dr.open.trim() if (dr.status.trim()) v.status = dr.status.trim() return v } async function onCreate(e: React.FormEvent) { e.preventDefault() if (!d.name.trim() || !d.base_url.trim() || !d.pinned_host.trim()) { return toast.error('Name, base URL and pinned host are required') } setBusy(true) const res = await createDoorConnection({ name: d.name.trim(), adapter: d.adapter, base_url: d.base_url.trim(), pinned_host: d.pinned_host.trim(), auth_mode: d.secret_ref ? 'query' : 'none', auth_param: d.adapter === 'native_heatsync' ? 'e' : null, secret_ref: d.secret_ref || null, verbs: buildVerbs(d), allow_member_self_entry: d.allow_member_self_entry, }) setBusy(false) if ('error' in res && res.error) return toast.error(res.error) const id = (res as { data: { id: string } }).data.id setConns(prev => [ { id, name: d.name.trim(), adapter: d.adapter, base_url: d.base_url.trim(), pinned_host: d.pinned_host.trim(), auth_mode: d.secret_ref ? 'query' : 'none', auth_param: d.adapter === 'native_heatsync' ? 'e' : null, secret_ref: d.secret_ref || null, verbs: buildVerbs(d), allow_member_self_entry: d.allow_member_self_entry, is_enabled: true, }, ...prev, ]) setD(emptyDraft()) setShowNew(false) toast.success('Connection created') } async function onToggleEnabled(c: Conn) { const res = await updateDoorConnection({ connectionId: c.id, is_enabled: !c.is_enabled }) if ('error' in res && res.error) return toast.error(res.error) setConns(prev => prev.map(x => (x.id === c.id ? { ...x, is_enabled: !x.is_enabled } : x))) } async function onToggleSelfEntry(c: Conn) { const next = !c.allow_member_self_entry if (next) { const ok = await confirm({ title: 'Allow member self-entry?', description: 'Any active member will be able to trigger this door themselves. This is an elevated physical-security risk. Continue?', confirmText: 'Enable self-entry', destructive: true, }) if (!ok) return } const res = await updateDoorConnection({ connectionId: c.id, allow_member_self_entry: next }) if ('error' in res && res.error) return toast.error(res.error) setConns(prev => prev.map(x => (x.id === c.id ? { ...x, allow_member_self_entry: next } : x))) } async function onTest(c: Conn) { setBusy(true) const res = await testDoorConnection({ connectionId: c.id }) setBusy(false) if ('error' in res && res.error) return toast.error(`Test failed: ${res.error}`) toast.success(`Reachable (HTTP ${(res as { data: { status: number } }).data.status})`) } async function onDelete(c: Conn) { const ok = await confirm({ title: 'Delete connection', description: `"${c.name}" will be removed. Existing access-log entries are kept.`, confirmText: 'Delete', destructive: true, }) if (!ok) return const res = await deleteDoorConnection({ connectionId: c.id }) if ('error' in res && res.error) return toast.error(res.error) setConns(prev => prev.filter(x => x.id !== c.id)) toast.success('Deleted') } return ( <> Door access

A connection targets a door controller on your LAN. The shared password is read from your encrypted Secrets vault, never stored here. The server only ever calls the exact pinned host, never follows redirects, and records every action in the access log with secrets redacted.

{showNew && (
setD({ ...d, name: e.target.value })} />
setD({ ...d, base_url: e.target.value })} /> setD({ ...d, pinned_host: e.target.value })} />
{d.adapter === 'generic_http' && (
setD({ ...d, grant: e.target.value })} /> setD({ ...d, revoke: e.target.value })} /> setD({ ...d, open: e.target.value })} /> setD({ ...d, status: e.target.value })} />
)}
)} {conns.length === 0 ? ( No door connections Add a controller connection to manage physical access. ) : (
{conns.map(c => (
{c.name} {c.adapter === 'native_heatsync' ? 'HeatSync' : 'Generic'} {!c.is_enabled && Disabled} {c.allow_member_self_entry && self-entry on}

{c.base_url} · pinned {c.pinned_host} · auth {c.secret_ref ? 'secret' : 'none'}

))}
)}

Access log

{rows.length === 0 ? (

No door activity recorded yet.

) : (
    {rows.map(r => (
  • {r.success ? 'ok' : 'fail'} {' · '}{r.action}{r.detail ? ` · ${r.detail}` : ''} {new Date(r.occurred_at).toLocaleString()}
  • ))}
)}
) }