feat(site): implement subdomain discovery and enhanced monitoring dashboard

This commit introduces a comprehensive subdomain discovery system and significantly upgrades the monitoring and domain management user interfaces.

Key changes include:
- **Subdomain Discovery**: Added a new service in the hub that performs advanced subdomain discovery using DNS brute forcing, Certificate Transparency (CT) log searches, pattern enumeration, and HTTP probing.
- **Enhanced Domain Management**:
    - Added API endpoints for retrieving, discovering, and deleting subdomains.
    - Implemented a new `SubdomainList` component in the UI to manage discovered subdomains.
    - Improved WHOIS lookup robustness by supporting a wider range of registry field variations.
- **Advanced Monitoring UI**:
    - Introduced `GroupedMonitorsTable` to organize monitors by root domain and their respective subdomains.
    - Added visual uptime timelines (heartbeat dots) and response time statistics (Avg, Min, Max, P95, P99) to the monitor detail view.
    - Implemented "Uptime Pills" for high-visibility status indicators in the monitors table.
- **Status Page Management**: Replaced the static status pages table with a full `StatusPageManager` capable of managing status pages and incidents.
- **Refactoring & Cleanup**:
    - Cleaned up `.gitignore` and removed unused reference submodules.
    - Improved domain extraction and grouping logic in the frontend.
    - Enhanced the `SystemsTable` with better sorting and layout.
This commit is contained in:
Tomas Dvorak
2026-05-05 16:14:45 +02:00
parent 21657abe38
commit 7ea9a069f9
22 changed files with 248666 additions and 179 deletions
@@ -75,6 +75,140 @@ import { cn } from "@/lib/utils"
type HeartbeatRow = Heartbeat & { timestamp?: string }
// Uptime Bar Component - Visual timeline of recent checks
function UptimeBarVisualization({ heartbeats }: { heartbeats?: HeartbeatRow[] }) {
const recent = useMemo(() => {
if (!heartbeats?.length) return []
return heartbeats.slice(0, 30).reverse()
}, [heartbeats])
if (!recent.length) {
return (
<div className="flex gap-0.5 h-8 items-center">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="flex-1 h-6 rounded-sm bg-muted/50" />
))}
</div>
)
}
return (
<div className="space-y-2">
<div className="flex gap-0.5 h-8">
{recent.map((hb, i) => (
<div
key={i}
className={cn(
"flex-1 rounded-sm transition-all hover:opacity-80 cursor-pointer",
hb.status === "up" ? "bg-green-500" :
hb.status === "down" ? "bg-red-500" :
hb.status === "paused" ? "bg-gray-400" : "bg-yellow-500"
)}
title={`${hb.status} • ${formatPing(hb.ping)} • ${formatDate(hb.time || hb.timestamp || "")}`}
/>
))}
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{recent.length} recent checks</span>
<span>
<span className="inline-flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-green-500" />
{recent.filter(h => h.status === "up").length} up
</span>
<span className="inline-flex items-center gap-1 ml-3">
<span className="w-2 h-2 rounded-full bg-red-500" />
{recent.filter(h => h.status === "down").length} down
</span>
</span>
</div>
</div>
)
}
// Response time statistics component
function ResponseTimeStats({ heartbeats }: { heartbeats?: HeartbeatRow[] }) {
const stats = useMemo(() => {
if (!heartbeats?.length) return null
const pings = heartbeats.filter(h => h.ping && h.ping > 0).map(h => h.ping)
if (!pings.length) return null
const sorted = [...pings].sort((a, b) => a - b)
const avg = Math.round(pings.reduce((a, b) => a + b, 0) / pings.length)
const min = sorted[0]
const max = sorted[sorted.length - 1]
const p95 = sorted[Math.floor(sorted.length * 0.95)]
const p99 = sorted[Math.floor(sorted.length * 0.99)]
return { avg, min, max, p95, p99, count: pings.length }
}, [heartbeats])
if (!stats) return null
return (
<div className="grid grid-cols-5 gap-2 text-center">
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">Avg</div>
<div className="text-lg font-semibold">{formatPing(stats.avg)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">Min</div>
<div className="text-lg font-semibold text-green-600">{formatPing(stats.min)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">Max</div>
<div className="text-lg font-semibold text-red-600">{formatPing(stats.max)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">P95</div>
<div className="text-lg font-semibold">{formatPing(stats.p95)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">P99</div>
<div className="text-lg font-semibold">{formatPing(stats.p99)}</div>
</div>
</div>
)
}
// Core Web Vitals placeholder component
function CoreWebVitalsCard({ url }: { url?: string }) {
if (!url) return null
return (
<Card>
<CardHeader>
<CardTitle>Core Web Vitals</CardTitle>
<CardDescription>Lighthouse performance metrics (coming soon)</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-4 bg-muted/30 rounded-lg">
<div className="text-sm text-muted-foreground mb-1">LCP</div>
<div className="text-2xl font-bold text-yellow-500">-</div>
<div className="text-xs text-muted-foreground mt-1">Largest Contentful Paint</div>
</div>
<div className="text-center p-4 bg-muted/30 rounded-lg">
<div className="text-sm text-muted-foreground mb-1">FID</div>
<div className="text-2xl font-bold text-green-500">-</div>
<div className="text-xs text-muted-foreground mt-1">First Input Delay</div>
</div>
<div className="text-center p-4 bg-muted/30 rounded-lg">
<div className="text-sm text-muted-foreground mb-1">CLS</div>
<div className="text-2xl font-bold text-green-500">-</div>
<div className="text-xs text-muted-foreground mt-1">Cumulative Layout Shift</div>
</div>
</div>
<div className="mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
<div className="flex items-center gap-2 text-sm text-blue-600">
<Activity className="h-4 w-4" />
<span>Core Web Vitals monitoring requires additional configuration</span>
</div>
</div>
</CardContent>
</Card>
)
}
// Status badge component
function StatusBadge({ status }: { status: string }) {
const configs = {
@@ -421,6 +555,31 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
/>
</div>
{/* Uptime Bar & Response Stats */}
<div className="grid sm:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Recent Uptime</CardTitle>
<CardDescription>Visual timeline of the last 30 checks</CardDescription>
</CardHeader>
<CardContent>
<UptimeBarVisualization heartbeats={heartbeats} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Response Time Statistics</CardTitle>
<CardDescription>Distribution of response times</CardDescription>
</CardHeader>
<CardContent>
<ResponseTimeStats heartbeats={heartbeats} />
</CardContent>
</Card>
</div>
{/* Core Web Vitals */}
<CoreWebVitalsCard url={monitor.url} />
{/* Combined Uptime & Response Chart */}
<Card>
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">