import { useState, useEffect } from 'react' import { useParams, useNavigate } from 'react-router-dom' import toast from 'react-hot-toast' import client from '../api/client' import LicenseStatus from '../components/LicenseStatus' import ConfirmModal from '../components/ConfirmModal' function fmtDt(dt) { if (!dt) return '—' return new Date(dt).toLocaleString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } function fmtDate(dt) { if (!dt) return '' return new Date(dt).toISOString().slice(0, 10) } export default function SiteDetailPage() { const { siteId } = useParams() const navigate = useNavigate() const [site, setSite] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [modal, setModal] = useState(null) // 'lock' | 'unlock' | 'delete' | 'license' const [lockReason, setLockReason] = useState('') const [newExpiry, setNewExpiry] = useState('') const [acting, setActing] = useState(false) useEffect(() => { async function load() { try { const { data } = await client.get(`/api/sites/${siteId}`) setSite(data) setNewExpiry(fmtDate(data.license_expires_at)) } catch { setError('Site not found') } finally { setLoading(false) } } load() }, [siteId]) async function doLock() { if (!lockReason.trim()) return setActing(true) try { const { data } = await client.post(`/api/sites/${siteId}/lock`, { reason: lockReason.trim() }) setSite(data) setModal(null) setLockReason('') toast.success('Site locked') } catch (e) { toast.error(e.response?.data?.detail || 'Failed to lock site') } finally { setActing(false) } } async function doUnlock() { setActing(true) try { const { data } = await client.post(`/api/sites/${siteId}/unlock`) setSite(data) setModal(null) toast.success('Site unlocked') } catch (e) { toast.error(e.response?.data?.detail || 'Failed to unlock site') } finally { setActing(false) } } async function doExtendLicense() { if (!newExpiry) return setActing(true) try { const { data } = await client.put(`/api/sites/${siteId}`, { license_expires_at: new Date(newExpiry).toISOString(), }) setSite(data) setModal(null) toast.success('License updated') } catch (e) { toast.error(e.response?.data?.detail || 'Failed to update license') } finally { setActing(false) } } async function doDelete() { setActing(true) try { await client.delete(`/api/sites/${siteId}`) toast.success('Site deregistered') navigate('/sites', { replace: true }) } catch (e) { toast.error(e.response?.data?.detail || 'Failed to delete site') setActing(false) } } if (loading) return
Loading…
if (error || !site) return
{error || 'Not found'}
const expires = new Date(site.license_expires_at) const isExpired = expires < new Date() return (
{/* Header */}

{site.name}

{site.owner_name} · {site.contact_email}

{/* Site info */}

Site Info

Site ID {site.site_id}
Registered {fmtDt(site.created_at)}
Contact {site.contact_email}
{/* License */}

License

Expires {fmtDt(site.license_expires_at)} {isExpired && ' (EXPIRED)'}
{/* Heartbeat */}

Heartbeat

Last seen {fmtDt(site.last_seen_at)}
Last IP {site.last_seen_ip || '—'}
{/* Lock status */} {site.is_locked && (

Locked

{site.lock_reason || 'No reason given'}

)} {/* Actions */}

Actions

{site.is_locked ? ( ) : ( )}
{/* Modals */} {modal === 'lock' && ( { setModal(null); setLockReason('') }} onConfirm={doLock} >