import { useEffect, useState } from 'react'; import { api, Machine, SSHKey, TestConnectionResponse } from '../api/client'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Label } from '@/components/ui/Label'; import { Select, SelectValue, SelectTrigger, SelectContent, SelectItem } from '@/components/ui/Select'; import { Badge } from '@/components/ui/Badge'; import { Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, } from '@/components/ui/Modal'; import { PageHeader } from '@/components/ui/PageHeader'; import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, } from '@/components/ui/Table'; import { EmptyState } from '@/components/ui/EmptyState'; import { CopyButton } from '@/components/ui/CopyButton'; import { Card } from '@/components/ui/Card'; import { Pencil, Trash2, Plus, Server, Zap, Cable } from 'lucide-react'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { subscribeMachineStatus } from '@/lib/sse'; type MachineForm = { id: number | undefined; name: string; host: string; port: number; ssh_user: string; ssh_key_id: number | null; mac_address: string; wol_enabled: boolean; broadcast_addr: string; wake_timeout_seconds: number; wake_check_interval_seconds: number; }; const defaultForm: MachineForm = { id: undefined, name: '', host: '', port: 22, ssh_user: 'root', ssh_key_id: null, mac_address: '', wol_enabled: false, broadcast_addr: '', wake_timeout_seconds: 180, wake_check_interval_seconds: 5, }; export default function Machines() { const [machines, setMachines] = useState([]); const [sshKeys, setSSHKeys] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [deleteId, setDeleteId] = useState(null); const [form, setForm] = useState(defaultForm); const [loading, setLoading] = useState(false); const [probing, setProbing] = useState(false); const [connModal, setConnModal] = useState<{ machine: Machine | null; result: TestConnectionResponse | null; loading: boolean }>({ machine: null, result: null, loading: false }); useEffect(() => { load(); setProbing(true); api('/api/machines/refresh', { method: 'POST' }).catch(() => {}); const timer = setTimeout(() => setProbing(false), 5000); return () => clearTimeout(timer); }, []); useEffect(() => { const unsub = subscribeMachineStatus((evt) => { setMachines((prev) => prev.map((m) => m.id === evt.machine_id ? { ...m, status: evt.status } : m ) ); }); return unsub; }, []); async function load() { try { const [ms, ks] = await Promise.all([ api('/api/machines'), api('/api/ssh-keys'), ]); setMachines(ms); setSSHKeys(ks); } catch {} } function openCreate() { setForm(defaultForm); setModalOpen(true); } function openEdit(m: Machine) { setForm({ id: m.id, name: m.name, host: m.host, port: m.port, ssh_user: m.ssh_user, ssh_key_id: m.ssh_key_id, mac_address: m.mac_address || '', wol_enabled: m.wol_enabled, broadcast_addr: m.broadcast_addr || '', wake_timeout_seconds: m.wake_timeout_seconds || 180, wake_check_interval_seconds: m.wake_check_interval_seconds || 5, }); setModalOpen(true); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!form.name.trim() || !form.host.trim()) { toast.error('Name and host are required'); return; } if ( form.mac_address && !/^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/.test(form.mac_address) ) { toast.error('Invalid MAC address format (AA:BB:CC:DD:EE:FF)'); return; } setLoading(true); try { const payload: Record = { id: form.id || null, name: form.name, host: form.host, port: Number(form.port), ssh_user: form.ssh_user, ssh_key_id: form.ssh_key_id, mac_address: form.mac_address || null, wol_enabled: Boolean(form.wol_enabled), broadcast_addr: form.broadcast_addr || null, wake_timeout_seconds: Number(form.wake_timeout_seconds), wake_check_interval_seconds: Number(form.wake_check_interval_seconds), }; await api(form.id ? `/api/machines/${form.id}` : '/api/machines', { method: form.id ? 'PUT' : 'POST', body: payload, }); setModalOpen(false); toast.success(form.id ? 'Machine updated' : 'Machine created'); load(); } catch (e: unknown) { toast.error((e as Error).message); } finally { setLoading(false); } } async function handleDelete() { if (deleteId === null) return; try { await api(`/api/machines/${deleteId}`, { method: 'DELETE' }); toast.success('Machine deleted'); setDeleteId(null); load(); } catch (e: unknown) { toast.error((e as Error).message); } } async function handleTestWol(m: Machine) { try { await api<{ ok: boolean; sent: number }>(`/api/machines/${m.id}/test-wol`, { method: 'POST' }); toast.success(`Magic packet sent (${m.mac_address})`); } catch (e: unknown) { toast.error(`Wake failed: ${(e as Error).message}`); } } async function handleTestConnection(m: Machine) { setConnModal({ machine: m, result: null, loading: true }); try { const result = await api(`/api/machines/${m.id}/test-connection`, { method: 'POST' }); setConnModal({ machine: m, result, loading: false }); } catch (e: unknown) { setConnModal({ machine: m, result: { success: false, error: (e as Error).message }, loading: false }); } } async function handleApproveFingerprint() { if (!connModal.machine || !connModal.result?.fingerprint) return; try { const updated = await api(`/api/machines/${connModal.machine.id}/approve-fingerprint`, { method: 'POST', body: { fingerprint: connModal.result.fingerprint }, }); setMachines(prev => prev.map(m => m.id === updated.id ? updated : m)); toast.success('Fingerprint approved'); setConnModal({ machine: null, result: null, loading: false }); } catch (e: unknown) { toast.error(`Approve failed: ${(e as Error).message}`); } } function closeConnModal() { setConnModal({ machine: null, result: null, loading: false }); } function keyLabel(id: number | null) { if (!id) return 'Server Key'; const k = sshKeys.find(k => k.id === id); return k ? k.label : `Key #${id}`; } return (
Add Machine } />
{machines.length === 0 ? ( } title="No machines" description="Add a remote machine to start syncing data" action={ } /> ) : ( <> {probing && (
Checking machine status...
)} Name Host SSH Key WoL WoL Timeout Status Actions {machines.map(m => ( {m.name} {m.host}:{m.port} {keyLabel(m.ssh_key_id)} {m.wol_enabled ? ( ) : ( No )} {m.wol_enabled ? ( {m.wake_timeout_seconds}s ) : ( )}
{m.wol_enabled && ( )}
))}
)}
{form.id ? 'Edit Machine' : 'Add Machine'} {form.id ? 'Update the configuration for this machine' : 'Configure a new remote machine for syncing'}
setForm({ ...form, name: e.target.value })} />
setForm({ ...form, host: e.target.value })} />
setForm({ ...form, port: Number(e.target.value) }) } />
setForm({ ...form, ssh_user: e.target.value }) } />
setForm({ ...form, mac_address: e.target.value }) } />

Required for Wake-on-LAN

setForm({ ...form, wol_enabled: e.target.checked }) } className="h-4 w-4 rounded border-border accent-accent" />
{form.wol_enabled && (

Wake-on-LAN Settings

setForm({ ...form, broadcast_addr: e.target.value }) } />

Leave blank to use 255.255.255.255. Set to your subnet broadcast address if the server has multiple interfaces.

setForm({ ...form, wake_timeout_seconds: Number(e.target.value) }) } />

HP Microservers typically need ~180s.

setForm({ ...form, wake_check_interval_seconds: Number(e.target.value) }) } />

How often to poll SSH readiness.

)}
!v && setDeleteId(null)}> Delete Machine Are you sure you want to delete this machine? This action cannot be undone. !v && closeConnModal()}> SSH Connection Test {connModal.machine?.name} ({connModal.machine?.host}:{connModal.machine?.port}) {connModal.loading && (
)} {!connModal.loading && connModal.result && (
{connModal.result.success ? (

Connection successful

{connModal.result.output}
) : (

Connection failed

{connModal.result.error}

)} {connModal.result.fingerprint && (

Host Key Fingerprint

{connModal.result.fingerprint}
{connModal.machine && !connModal.machine.fingerprint_confirmed && (
Approve to trust this fingerprint
)} {connModal.machine && connModal.machine.fingerprint_confirmed && (
)}
)}
)} {!connModal.loading && connModal.result && connModal.result.fingerprint && connModal.machine && !connModal.machine.fingerprint_confirmed && ( )}
); } function StatusBadge({ status }: { status: string }) { const variant = status === 'online' ? 'success' : status === 'offline' ? 'neutral' : 'info'; return ; }