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