Radar search: track taken domains, Watchlist: radar style cards, fixed health check
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-12 22:08:07 +01:00
parent 7b0b6a3669
commit e737de6ff5
2 changed files with 241 additions and 268 deletions

View File

@ -295,17 +295,29 @@ export default function RadarPage() {
</span> </span>
</div> </div>
{/* Available Actions */} {/* Registrar Info */}
{searchResult.is_available && ( {!searchResult.is_available && searchResult.registrar && (
<div className="flex gap-3"> <p className="text-xs text-white/30 mb-4">
<button Registered with {searchResult.registrar}
onClick={handleAddToWatchlist} </p>
disabled={addingToWatchlist} )}
className="flex-1 py-3 border border-white/20 text-white/80 text-sm font-medium hover:bg-white/5 transition-colors flex items-center justify-center gap-2"
> {/* Actions - Always show */}
{addingToWatchlist ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />} <div className="flex gap-3">
Add to Watchlist <button
</button> onClick={handleAddToWatchlist}
disabled={addingToWatchlist}
className={clsx(
"py-3 text-sm font-medium transition-colors flex items-center justify-center gap-2",
searchResult.is_available
? "flex-1 border border-white/20 text-white/80 hover:bg-white/5"
: "flex-1 border border-accent/30 text-accent hover:bg-accent/10"
)}
>
{addingToWatchlist ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
{searchResult.is_available ? 'Add to Watchlist' : 'Track for Availability'}
</button>
{searchResult.is_available && (
<a <a
href={`https://www.namecheap.com/domains/registration/results/?domain=${searchResult.domain}`} href={`https://www.namecheap.com/domains/registration/results/?domain=${searchResult.domain}`}
target="_blank" target="_blank"
@ -313,15 +325,8 @@ export default function RadarPage() {
> >
Register Now <ArrowRight className="w-4 h-4" /> Register Now <ArrowRight className="w-4 h-4" />
</a> </a>
</div> )}
)} </div>
{/* Taken Info */}
{!searchResult.is_available && searchResult.registrar && (
<p className="text-xs text-white/30 mt-2">
Registered with {searchResult.registrar}
</p>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -15,19 +15,14 @@ import {
Eye, Eye,
X, X,
Activity, Activity,
AlertTriangle,
ArrowRight, ArrowRight,
CheckCircle2, CheckCircle2,
XCircle, XCircle,
Target, Target,
Globe,
ExternalLink, ExternalLink,
Calendar,
Shield,
Crosshair Crosshair
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link'
// ============================================================================ // ============================================================================
// HELPERS // HELPERS
@ -46,26 +41,12 @@ function formatExpiryDate(expirationDate: string | null): string {
return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
} }
function getTimeAgo(date: string | null): string { const healthConfig: Record<HealthStatus, { label: string; color: string; bgClass: string }> = {
if (!date) return 'Never' healthy: { label: 'Healthy', color: 'text-accent', bgClass: 'bg-accent/10' },
const now = new Date() weakening: { label: 'Weak', color: 'text-amber-400', bgClass: 'bg-amber-500/10' },
const past = new Date(date) parked: { label: 'Parked', color: 'text-blue-400', bgClass: 'bg-blue-500/10' },
const diffMs = now.getTime() - past.getTime() critical: { label: 'Critical', color: 'text-rose-400', bgClass: 'bg-rose-500/10' },
const diffMins = Math.floor(diffMs / 60000) unknown: { label: 'Unknown', color: 'text-white/40', bgClass: 'bg-white/5' },
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return 'Just now'
if (diffMins < 60) return `${diffMins}m ago`
if (diffHours < 24) return `${diffHours}h ago`
return `${diffDays}d ago`
}
const healthConfig: Record<HealthStatus, { label: string; color: string; bg: string }> = {
healthy: { label: 'Healthy', color: 'text-accent', bg: 'bg-accent/10 border-accent/20' },
weakening: { label: 'Weak', color: 'text-amber-400', bg: 'bg-amber-500/10 border-amber-500/20' },
parked: { label: 'Parked', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/20' },
critical: { label: 'Critical', color: 'text-rose-400', bg: 'bg-rose-500/10 border-rose-500/20' },
unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5 border-white/10' },
} }
// ============================================================================ // ============================================================================
@ -121,10 +102,10 @@ export default function WatchlistPage() {
setAdding(true) setAdding(true)
try { try {
await addDomain(newDomain.trim()) await addDomain(newDomain.trim())
showToast(`Target locked: ${newDomain.trim()}`, 'success') showToast(`Added ${newDomain.trim()} to watchlist`, 'success')
setNewDomain('') setNewDomain('')
} catch (err: any) { } catch (err: any) {
showToast(err.message || 'Failed', 'error') showToast(err.message || 'Failed to add domain', 'error')
} finally { } finally {
setAdding(false) setAdding(false)
} }
@ -134,18 +115,18 @@ export default function WatchlistPage() {
setRefreshingId(id) setRefreshingId(id)
try { try {
await refreshDomain(id) await refreshDomain(id)
showToast('Intel updated', 'success') showToast('Domain refreshed', 'success')
} catch { showToast('Update failed', 'error') } } catch { showToast('Refresh failed', 'error') }
finally { setRefreshingId(null) } finally { setRefreshingId(null) }
}, [refreshDomain, showToast]) }, [refreshDomain, showToast])
const handleDelete = useCallback(async (id: number, name: string) => { const handleDelete = useCallback(async (id: number, name: string) => {
if (!confirm(`Drop target: ${name}?`)) return if (!confirm(`Remove ${name} from watchlist?`)) return
setDeletingId(id) setDeletingId(id)
try { try {
await deleteDomain(id) await deleteDomain(id)
showToast('Target dropped', 'success') showToast('Domain removed', 'success')
} catch { showToast('Failed', 'error') } } catch { showToast('Failed to remove', 'error') }
finally { setDeletingId(null) } finally { setDeletingId(null) }
}, [deleteDomain, showToast]) }, [deleteDomain, showToast])
@ -154,8 +135,8 @@ export default function WatchlistPage() {
try { try {
await api.updateDomainNotify(id, !current) await api.updateDomainNotify(id, !current)
updateDomain(id, { notify_on_available: !current }) updateDomain(id, { notify_on_available: !current })
showToast(!current ? 'Alerts armed' : 'Alerts disarmed', 'success') showToast(!current ? 'Notifications enabled' : 'Notifications disabled', 'success')
} catch { showToast('Failed', 'error') } } catch { showToast('Failed to update', 'error') }
finally { setTogglingNotifyId(null) } finally { setTogglingNotifyId(null) }
}, [updateDomain, showToast]) }, [updateDomain, showToast])
@ -165,11 +146,13 @@ export default function WatchlistPage() {
try { try {
const report = await api.getDomainHealth(id, { refresh: true }) const report = await api.getDomainHealth(id, { refresh: true })
setHealthReports(prev => ({ ...prev, [id]: report })) setHealthReports(prev => ({ ...prev, [id]: report }))
} catch {} } catch (err) {
console.error('Health check failed:', err)
}
finally { setLoadingHealth(prev => ({ ...prev, [id]: false })) } finally { setLoadingHealth(prev => ({ ...prev, [id]: false })) }
}, [loadingHealth]) }, [loadingHealth])
// Load health // Load health on mount
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
if (!domains?.length) return if (!domains?.length) return
@ -190,25 +173,21 @@ export default function WatchlistPage() {
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HEADER - Compact */} {/* HEADER */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="pt-6 lg:pt-8 pb-6"> <section className="pt-6 lg:pt-8 pb-6">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6"> <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
{/* Left */}
<div className="space-y-3"> <div className="space-y-3">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<Target className="w-4 h-4 text-accent" /> <Target className="w-4 h-4 text-accent" />
<span className="text-[10px] font-mono tracking-wide text-accent">Domain Surveillance</span> <span className="text-[10px] font-mono tracking-wide text-accent">Domain Surveillance</span>
</div> </div>
<h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]"> <h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
<span className="text-white">Watchlist</span> <span className="text-white">Watchlist</span>
<span className="text-white/30 ml-3">{stats.total}</span> <span className="text-white/30 ml-3">{stats.total}</span>
</h1> </h1>
</div> </div>
{/* Right: Stats */}
<div className="flex gap-6 lg:gap-8"> <div className="flex gap-6 lg:gap-8">
<div className="text-right"> <div className="text-right">
<div className="text-xl font-display text-accent">{stats.available}</div> <div className="text-xl font-display text-accent">{stats.available}</div>
@ -223,28 +202,41 @@ export default function WatchlistPage() {
</section> </section>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* ADD DOMAIN */} {/* ADD DOMAIN - Radar Style */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="pb-6"> <section className="pb-6">
<form onSubmit={handleAdd} className="relative max-w-xl"> <div className="relative max-w-xl">
<div className="flex items-center bg-[#050505] border border-white/10 focus-within:border-accent/40 transition-colors"> <div className="absolute -inset-2 bg-gradient-to-r from-accent/5 via-transparent to-accent/5 blur-2xl opacity-30" />
<input
type="text" <div className="relative bg-[#0A0A0A] border border-white/10 overflow-hidden">
value={newDomain} <div className="flex items-center justify-between px-4 py-2 border-b border-white/[0.06] bg-black/40">
onChange={(e) => setNewDomain(e.target.value)} <span className="text-[10px] font-mono text-white/40 flex items-center gap-2">
placeholder="Add domain to watch..." <Plus className="w-3 h-3 text-accent" />
className="flex-1 bg-transparent px-4 py-3 text-sm text-white placeholder:text-white/25 outline-none" Add Domain
/> </span>
<button </div>
type="submit"
disabled={adding || !newDomain.trim()} <form onSubmit={handleAdd} className="p-4">
className="h-full px-5 py-3 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors disabled:opacity-30 flex items-center gap-2" <div className="flex items-center gap-3">
> <input
{adding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />} type="text"
<span className="hidden sm:inline">Add</span> value={newDomain}
</button> onChange={(e) => setNewDomain(e.target.value)}
placeholder="example.com"
className="flex-1 bg-black/30 border border-white/10 px-4 py-3 text-white placeholder:text-white/20 outline-none focus:border-accent/40 transition-colors"
/>
<button
type="submit"
disabled={adding || !newDomain.trim()}
className="px-6 py-3 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors disabled:opacity-30 flex items-center gap-2"
>
{adding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
Add
</button>
</div>
</form>
</div> </div>
</form> </div>
</section> </section>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
@ -274,30 +266,19 @@ export default function WatchlistPage() {
</section> </section>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* TABLE */} {/* DOMAIN LIST - Radar Style Cards */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="py-6"> <section className="py-6">
{!filteredDomains.length ? ( {!filteredDomains.length ? (
<div className="text-center py-16"> <div className="text-center py-16">
<div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 rounded-lg flex items-center justify-center mb-4"> <div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4">
<Eye className="w-6 h-6 text-white/20" /> <Eye className="w-6 h-6 text-white/20" />
</div> </div>
<p className="text-white/40 text-sm">No domains in your watchlist</p> <p className="text-white/40 text-sm">No domains in your watchlist</p>
<p className="text-white/25 text-xs mt-1">Add a domain above to start monitoring</p> <p className="text-white/25 text-xs mt-1">Add a domain above to start monitoring</p>
</div> </div>
) : ( ) : (
<div className="space-y-px"> <div className="space-y-3">
{/* Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_100px] gap-4 px-4 py-2 text-xs text-white/40 border-b border-white/[0.06]">
<div>Domain</div>
<div>Status</div>
<div>Health</div>
<div>Expires</div>
<div>Alert</div>
<div className="text-right">Actions</div>
</div>
{/* Rows */}
{filteredDomains.map((domain) => { {filteredDomains.map((domain) => {
const health = healthReports[domain.id] const health = healthReports[domain.id]
const healthStatus = health?.status || 'unknown' const healthStatus = health?.status || 'unknown'
@ -307,125 +288,105 @@ export default function WatchlistPage() {
return ( return (
<div <div
key={domain.id} key={domain.id}
className="group bg-white/[0.01] hover:bg-white/[0.03] border border-white/[0.05] hover:border-white/[0.08] transition-all" className="relative bg-[#0A0A0A] border border-white/10 overflow-hidden group hover:border-white/20 transition-colors"
> >
{/* Mobile */} {/* Card Header */}
<div className="lg:hidden p-4"> <div className="flex items-center justify-between px-4 py-2 border-b border-white/[0.06] bg-black/40">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-3">
<div className={clsx(
"w-2 h-2 rounded-full",
domain.is_available ? "bg-accent shadow-[0_0_8px_rgba(16,185,129,0.8)]" : "bg-white/20"
)} />
<span className="text-sm text-white font-medium">{domain.name}</span>
</div>
<span className={clsx(
"text-xs px-2 py-0.5",
domain.is_available ? "text-accent bg-accent/10" : "text-white/40 bg-white/5"
)}>
{domain.is_available ? 'Available' : 'Taken'}
</span>
</div>
<div className="flex items-center justify-between text-xs text-white/40">
<span>{formatExpiryDate(domain.expiration_date)}</span>
<div className="flex gap-2">
<button onClick={() => handleRefresh(domain.id)} className="p-1.5 hover:bg-white/5 rounded">
<RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
</button>
<button onClick={() => handleDelete(domain.id, domain.name)} className="p-1.5 hover:bg-rose-500/10 hover:text-rose-400 rounded">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Desktop */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_100px] gap-4 items-center px-4 py-3">
{/* Domain */}
<div className="flex items-center gap-3 min-w-0">
<div className={clsx( <div className={clsx(
"w-2 h-2 rounded-full shrink-0", "w-2 h-2 rounded-full",
domain.is_available ? "bg-accent shadow-[0_0_6px_rgba(16,185,129,0.8)]" : "bg-white/20" domain.is_available ? "bg-accent shadow-[0_0_8px_rgba(16,185,129,0.8)]" : "bg-white/20"
)} /> )} />
<span className="text-sm text-white font-medium truncate">{domain.name}</span> <span className="text-sm font-medium text-white">{domain.name}</span>
<a href={`https://${domain.name}`} target="_blank" className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity"> <a href={`https://${domain.name}`} target="_blank" className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity">
<ExternalLink className="w-3.5 h-3.5" /> <ExternalLink className="w-3.5 h-3.5 text-white/50" />
</a> </a>
</div> </div>
<span className={clsx(
{/* Status */} "text-xs px-2 py-0.5",
<div> domain.is_available ? "text-accent bg-accent/10" : "text-white/40 bg-white/5"
<span className={clsx( )}>
"text-xs px-2 py-0.5", {domain.is_available ? 'Available' : 'Taken'}
domain.is_available ? "text-accent bg-accent/10" : "text-white/40 bg-white/5" </span>
)}> </div>
{domain.is_available ? 'Available' : 'Taken'}
</span> {/* Card Body */}
</div> <div className="p-4">
<div className="flex items-center justify-between">
{/* Health */} {/* Left: Info */}
<button <div className="flex items-center gap-6">
onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }} {/* Health */}
className="flex items-center gap-1.5 hover:opacity-80 transition-opacity" <button
> onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }}
{loadingHealth[domain.id] ? ( className="flex items-center gap-2 hover:opacity-80 transition-opacity"
<Loader2 className="w-3.5 h-3.5 animate-spin text-white/30" /> >
) : ( {loadingHealth[domain.id] ? (
<> <Loader2 className="w-4 h-4 animate-spin text-white/30" />
<Activity className={clsx("w-3.5 h-3.5", config.color)} /> ) : (
<span className={clsx("text-xs", config.color)}>{config.label}</span> <>
</> <Activity className={clsx("w-4 h-4", config.color)} />
)} <span className={clsx("text-xs", config.color)}>{config.label}</span>
</button> </>
)}
{/* Expires */} </button>
<div className="text-xs text-white/50">
{days !== null && days <= 30 && days > 0 ? ( {/* Expiry */}
<span className="text-amber-400 font-medium">{days} days</span> <div className="text-xs text-white/50">
) : ( {days !== null && days <= 30 && days > 0 ? (
formatExpiryDate(domain.expiration_date) <span className="text-amber-400 font-medium">{days} days left</span>
)} ) : domain.expiration_date ? (
</div> <span>Expires {formatExpiryDate(domain.expiration_date)}</span>
) : (
{/* Alert */} <span className="text-white/30">No expiry data</span>
<button )}
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)} </div>
disabled={togglingNotifyId === domain.id} </div>
className={clsx(
"w-6 h-6 flex items-center justify-center transition-colors", {/* Right: Actions */}
domain.notify_on_available ? "text-accent" : "text-white/20 hover:text-white/40" <div className="flex items-center gap-2">
)} {/* Notify Toggle */}
> <button
{togglingNotifyId === domain.id ? ( onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
<Loader2 className="w-3.5 h-3.5 animate-spin" /> disabled={togglingNotifyId === domain.id}
) : domain.notify_on_available ? ( className={clsx(
<Bell className="w-3.5 h-3.5" /> "w-8 h-8 flex items-center justify-center rounded hover:bg-white/5 transition-all",
) : ( domain.notify_on_available ? "text-accent" : "text-white/25 hover:text-white/50"
<BellOff className="w-3.5 h-3.5" /> )}
)} title={domain.notify_on_available ? 'Notifications on' : 'Notifications off'}
</button> >
{togglingNotifyId === domain.id ? (
{/* Actions */} <Loader2 className="w-4 h-4 animate-spin" />
<div className="flex items-center justify-end gap-1"> ) : domain.notify_on_available ? (
<button <Bell className="w-4 h-4" />
onClick={() => handleRefresh(domain.id)} ) : (
disabled={refreshingId === domain.id} <BellOff className="w-4 h-4" />
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white hover:bg-white/5 transition-all" )}
> </button>
<RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin")} />
</button> {/* Refresh */}
<button <button
onClick={() => handleDelete(domain.id, domain.name)} onClick={() => handleRefresh(domain.id)}
disabled={deletingId === domain.id} disabled={refreshingId === domain.id}
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-rose-400 hover:bg-rose-500/10 transition-all" className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white hover:bg-white/5 rounded transition-all"
> title="Refresh"
{deletingId === domain.id ? ( >
<Loader2 className="w-3.5 h-3.5 animate-spin" /> <RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
) : ( </button>
<Trash2 className="w-3.5 h-3.5" />
)} {/* Delete */}
</button> <button
onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-rose-400 hover:bg-rose-500/10 rounded transition-all"
title="Remove"
>
{deletingId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -436,79 +397,86 @@ export default function WatchlistPage() {
</section> </section>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HEALTH MODAL */} {/* HEALTH MODAL - Radar Style */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{selectedDomainData && ( {selectedDomainData && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick={() => setSelectedDomain(null)}> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick={() => setSelectedDomain(null)}>
<div className="w-full max-w-md bg-[#0A0A0A] border border-white/15 p-1.5" onClick={(e) => e.stopPropagation()}> <div className="relative w-full max-w-md" onClick={(e) => e.stopPropagation()}>
{/* Corner Decorations */} <div className="absolute -inset-4 bg-gradient-to-tr from-accent/10 via-transparent to-accent/5 blur-3xl opacity-30" />
<div className="absolute -top-px -left-px w-4 h-4 border-t border-l border-accent/60" />
<div className="absolute -top-px -right-px w-4 h-4 border-t border-r border-accent/60" />
<div className="absolute -bottom-px -left-px w-4 h-4 border-b border-l border-accent/60" />
<div className="absolute -bottom-px -right-px w-4 h-4 border-b border-r border-accent/60" />
<div className="bg-[#050505] p-6 relative"> <div className="relative bg-[#0A0A0A] border border-white/10 overflow-hidden">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between px-5 py-3 border-b border-white/[0.06] bg-black/40">
<div className="flex items-center gap-3"> <span className="text-[10px] font-mono text-white/40 flex items-center gap-2">
<Activity className="w-4 h-4 text-accent" /> <Activity className="w-3 h-3 text-accent" />
<span className="text-sm font-medium text-white">Health Report</span> Health Report
</div> </span>
<button onClick={() => setSelectedDomain(null)} className="text-white/30 hover:text-white p-1"> <button onClick={() => setSelectedDomain(null)} className="text-white/30 hover:text-white p-1">
<X className="w-5 h-5" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Domain */} <div className="p-5">
<div className="mb-6"> {/* Domain */}
<h3 className="text-lg font-medium text-white">{selectedDomainData.name}</h3> <div className="mb-5">
<div className="flex items-center gap-2 mt-2"> <h3 className="text-lg font-medium text-white">{selectedDomainData.name}</h3>
<span className={clsx( <div className="flex items-center gap-2 mt-2">
"text-xs px-2.5 py-1", <span className={clsx(
healthConfig[selectedHealth?.status || 'unknown'].bg, "text-xs px-2.5 py-1",
healthConfig[selectedHealth?.status || 'unknown'].color healthConfig[selectedHealth?.status || 'unknown'].bgClass,
)}> healthConfig[selectedHealth?.status || 'unknown'].color
{healthConfig[selectedHealth?.status || 'unknown'].label} )}>
</span> {healthConfig[selectedHealth?.status || 'unknown'].label}
</span>
{selectedHealth?.score !== undefined && (
<span className="text-xs text-white/40">Score: {selectedHealth.score}/100</span>
)}
</div>
</div> </div>
</div>
{/* Checks */}
{/* Checks */} {selectedHealth ? (
{selectedHealth && ( <div className="space-y-1 mb-5">
<div className="space-y-1"> {[
{[ { label: 'DNS Resolution', value: selectedHealth.dns?.has_a ?? selectedHealth.dns?.has_ns },
{ label: 'DNS Resolution', value: selectedHealth.dns?.has_a }, { label: 'HTTP Reachable', value: selectedHealth.http?.is_reachable },
{ label: 'HTTP Reachable', value: selectedHealth.http?.is_reachable }, { label: 'SSL Certificate', value: selectedHealth.ssl?.has_certificate },
{ label: 'SSL Certificate', value: selectedHealth.ssl?.has_certificate }, { label: 'Not Parked', value: !(selectedHealth.dns?.is_parked || selectedHealth.http?.is_parked) },
{ label: 'Not Parked', value: !selectedHealth.dns?.is_parked && !selectedHealth.http?.is_parked }, ].map((check) => (
].map((check) => ( <div key={check.label} className="flex items-center justify-between py-2.5 border-b border-white/[0.05]">
<div key={check.label} className="flex items-center justify-between py-2.5 border-b border-white/[0.05]"> <span className="text-sm text-white/60">{check.label}</span>
<span className="text-sm text-white/60">{check.label}</span> {check.value ? (
{check.value ? ( <CheckCircle2 className="w-5 h-5 text-accent" />
<CheckCircle2 className="w-5 h-5 text-accent" /> ) : check.value === false ? (
) : ( <XCircle className="w-5 h-5 text-rose-400" />
<XCircle className="w-5 h-5 text-rose-400" /> ) : (
)} <span className="text-xs text-white/30">Unknown</span>
</div> )}
))} </div>
</div> ))}
)} </div>
{/* Refresh */}
<button
onClick={() => handleHealthCheck(selectedDomainData.id)}
disabled={loadingHealth[selectedDomainData.id]}
className="w-full mt-6 py-3 bg-white/5 border border-white/10 text-white/70 text-sm font-medium hover:bg-white/10 hover:text-white transition-all flex items-center justify-center gap-2"
>
{loadingHealth[selectedDomainData.id] ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : ( ) : (
<> <div className="py-6 text-center text-white/30 text-sm">
<RefreshCw className="w-4 h-4" /> No health data available yet
Refresh Health Check </div>
</>
)} )}
</button>
{/* Refresh Button */}
<button
onClick={() => handleHealthCheck(selectedDomainData.id)}
disabled={loadingHealth[selectedDomainData.id]}
className="w-full py-3 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors flex items-center justify-center gap-2"
>
{loadingHealth[selectedDomainData.id] ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<>
<RefreshCw className="w-4 h-4" />
Run Health Check
</>
)}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>