소스 검색

feat(dashboard): add availability dashboard with provider/endpoint components

- Add AvailabilityDashboard main component with tab navigation
- Create overview section with GaugeCard for metrics visualization
- Add provider tab with ConfidenceBadge, LaneChart, LatencyChart
- Add endpoint tab with LatencyCurve, ProbeGrid, ProbeTerminal
- Implement shared components: FloatingProbeButton, TimeRangeSelector
- Improve AvailabilitySkeleton with realistic loading states
- Add comprehensive unit tests for all new components

Co-Authored-By: Claude Opus 4.5 <[email protected]>
ding113 3 주 전
부모
커밋
4ae3cc949c
19개의 변경된 파일3441개의 추가작업 그리고 14개의 파일을 삭제
  1. 174 0
      src/app/[locale]/dashboard/availability/_components/availability-dashboard.tsx
  2. 79 10
      src/app/[locale]/dashboard/availability/_components/availability-skeleton.tsx
  3. 316 0
      src/app/[locale]/dashboard/availability/_components/endpoint/endpoint-tab.tsx
  4. 171 0
      src/app/[locale]/dashboard/availability/_components/endpoint/latency-curve.tsx
  5. 165 0
      src/app/[locale]/dashboard/availability/_components/endpoint/probe-grid.tsx
  6. 285 0
      src/app/[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx
  7. 222 0
      src/app/[locale]/dashboard/availability/_components/overview/gauge-card.tsx
  8. 168 0
      src/app/[locale]/dashboard/availability/_components/overview/overview-section.tsx
  9. 98 0
      src/app/[locale]/dashboard/availability/_components/provider/confidence-badge.tsx
  10. 332 0
      src/app/[locale]/dashboard/availability/_components/provider/lane-chart.tsx
  11. 203 0
      src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx
  12. 239 0
      src/app/[locale]/dashboard/availability/_components/provider/provider-tab.tsx
  13. 66 0
      src/app/[locale]/dashboard/availability/_components/shared/floating-probe-button.tsx
  14. 36 0
      src/app/[locale]/dashboard/availability/_components/shared/time-range-selector.tsx
  15. 4 4
      src/app/[locale]/dashboard/availability/page.tsx
  16. 326 0
      tests/unit/dashboard/availability/availability-dashboard.test.tsx
  17. 136 0
      tests/unit/dashboard/availability/confidence-badge.test.tsx
  18. 174 0
      tests/unit/dashboard/availability/gauge-card.test.tsx
  19. 247 0
      tests/unit/dashboard/availability/probe-terminal.test.tsx

+ 174 - 0
src/app/[locale]/dashboard/availability/_components/availability-dashboard.tsx

@@ -0,0 +1,174 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { useCallback, useEffect, useState } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import type { AvailabilityQueryResult } from "@/lib/availability";
+import { cn } from "@/lib/utils";
+import { EndpointTab } from "./endpoint/endpoint-tab";
+import { OverviewSection } from "./overview/overview-section";
+import { ProviderTab } from "./provider/provider-tab";
+import { FloatingProbeButton } from "./shared/floating-probe-button";
+
+export type TimeRangeOption = "15min" | "1h" | "6h" | "24h" | "7d";
+
+// Target number of buckets to fill the heatmap width consistently
+const TARGET_BUCKETS = 60;
+
+const TIME_RANGE_MAP: Record<TimeRangeOption, number> = {
+  "15min": 15 * 60 * 1000,
+  "1h": 60 * 60 * 1000,
+  "6h": 6 * 60 * 60 * 1000,
+  "24h": 24 * 60 * 60 * 1000,
+  "7d": 7 * 24 * 60 * 60 * 1000,
+};
+
+function calculateBucketSize(timeRangeMs: number): number {
+  const bucketSizeMs = timeRangeMs / TARGET_BUCKETS;
+  const bucketSizeMinutes = bucketSizeMs / (60 * 1000);
+  return Math.max(0.25, Math.round(bucketSizeMinutes * 4) / 4);
+}
+
+export function AvailabilityDashboard() {
+  const t = useTranslations("dashboard.availability");
+  const [activeTab, setActiveTab] = useState<"provider" | "endpoint">("provider");
+  const [timeRange, setTimeRange] = useState<TimeRangeOption>("24h");
+  const [data, setData] = useState<AvailabilityQueryResult | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [refreshing, setRefreshing] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+
+  const fetchData = useCallback(async () => {
+    try {
+      setRefreshing(true);
+      const now = new Date();
+      const timeRangeMs = TIME_RANGE_MAP[timeRange];
+      const startTime = new Date(now.getTime() - timeRangeMs);
+      const bucketSizeMinutes = calculateBucketSize(timeRangeMs);
+
+      const params = new URLSearchParams({
+        startTime: startTime.toISOString(),
+        endTime: now.toISOString(),
+        bucketSizeMinutes: bucketSizeMinutes.toString(),
+        maxBuckets: TARGET_BUCKETS.toString(),
+      });
+
+      const res = await fetch(`/api/availability?${params}`);
+      if (!res.ok) {
+        throw new Error(t("states.fetchFailed"));
+      }
+
+      const result: AvailabilityQueryResult = await res.json();
+      setData(result);
+      setError(null);
+    } catch (err) {
+      console.error("Failed to fetch availability data:", err);
+      setError(err instanceof Error ? err.message : t("states.fetchFailed"));
+    } finally {
+      setLoading(false);
+      setRefreshing(false);
+    }
+  }, [timeRange, t]);
+
+  useEffect(() => {
+    fetchData();
+  }, [fetchData]);
+
+  // Auto-refresh: 30s for provider tab, 10s for endpoint tab
+  useEffect(() => {
+    const interval = activeTab === "provider" ? 30000 : 10000;
+    const timer = setInterval(fetchData, interval);
+    return () => clearInterval(timer);
+  }, [activeTab, fetchData]);
+
+  // Calculate overview metrics
+  const providers = data?.providers ?? [];
+  const overviewMetrics = {
+    systemAvailability: data?.systemAvailability ?? 0,
+    avgLatency:
+      providers.length > 0
+        ? providers.reduce((sum, p) => {
+            const latencies = p.timeBuckets
+              .filter((b) => b.avgLatencyMs > 0)
+              .map((b) => b.avgLatencyMs);
+            return (
+              sum +
+              (latencies.length > 0 ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0)
+            );
+          }, 0) /
+          Math.max(1, providers.filter((p) => p.timeBuckets.some((b) => b.avgLatencyMs > 0)).length)
+        : 0,
+    errorRate:
+      providers.length > 0
+        ? providers.reduce((sum, p) => {
+            const total = p.totalRequests;
+            const errors = p.timeBuckets.reduce((s, b) => s + b.redCount, 0);
+            return sum + (total > 0 ? errors / total : 0);
+          }, 0) / providers.length
+        : 0,
+    activeProbes: providers.filter((p) => p.currentStatus !== "unknown").length,
+    totalProbes: providers.length,
+    healthyCount: providers.filter((p) => p.currentStatus === "green").length,
+    unhealthyCount: providers.filter((p) => p.currentStatus === "red").length,
+  };
+
+  return (
+    <div className="space-y-6">
+      {/* Overview Section */}
+      <OverviewSection
+        systemAvailability={overviewMetrics.systemAvailability}
+        avgLatency={overviewMetrics.avgLatency}
+        errorRate={overviewMetrics.errorRate}
+        activeProbes={overviewMetrics.activeProbes}
+        totalProbes={overviewMetrics.totalProbes}
+        loading={loading}
+        refreshing={refreshing}
+      />
+
+      {/* Tabs */}
+      <Tabs
+        value={activeTab}
+        onValueChange={(v) => setActiveTab(v as "provider" | "endpoint")}
+        className="w-full"
+      >
+        <TabsList className="grid w-full max-w-md grid-cols-2 mb-6">
+          <TabsTrigger
+            value="provider"
+            className={cn(
+              "data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
+            )}
+          >
+            {t("tabs.provider")}
+          </TabsTrigger>
+          <TabsTrigger
+            value="endpoint"
+            className={cn(
+              "data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
+            )}
+          >
+            {t("tabs.endpoint")}
+          </TabsTrigger>
+        </TabsList>
+
+        <TabsContent value="provider" className="mt-0">
+          <ProviderTab
+            data={data}
+            loading={loading}
+            refreshing={refreshing}
+            error={error}
+            timeRange={timeRange}
+            onTimeRangeChange={setTimeRange}
+            onRefresh={fetchData}
+          />
+        </TabsContent>
+
+        <TabsContent value="endpoint" className="mt-0">
+          <EndpointTab />
+        </TabsContent>
+      </Tabs>
+
+      {/* Floating Probe Button */}
+      <FloatingProbeButton onProbeComplete={fetchData} />
+    </div>
+  );
+}

+ 79 - 10
src/app/[locale]/dashboard/availability/_components/availability-skeleton.tsx

@@ -1,18 +1,87 @@
-import { ListSkeleton, LoadingState } from "@/components/loading/page-skeletons";
 import { Skeleton } from "@/components/ui/skeleton";
+import { cn } from "@/lib/utils";
 
-export function AvailabilityViewSkeleton() {
+export function AvailabilityDashboardSkeleton() {
   return (
-    <div className="space-y-4">
-      <div className="flex flex-wrap gap-2">
-        <Skeleton className="h-9 w-32" />
-        <Skeleton className="h-9 w-32" />
-        <Skeleton className="h-9 w-24" />
+    <div className="space-y-6">
+      {/* Overview Section - 4 Gauge Cards */}
+      <div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
+        {[...Array(4)].map((_, i) => (
+          <div
+            key={i}
+            className={cn(
+              "rounded-2xl p-4 md:p-6",
+              "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+              "backdrop-blur-lg",
+              "border border-border/50 dark:border-white/[0.08]"
+            )}
+          >
+            <div className="flex flex-col items-center gap-3">
+              <Skeleton className="h-24 w-24 rounded-full" />
+              <Skeleton className="h-4 w-20" />
+              <Skeleton className="h-3 w-16" />
+            </div>
+          </div>
+        ))}
       </div>
-      <div className="rounded-lg border bg-card p-4 space-y-3">
-        <ListSkeleton rows={6} />
+
+      {/* Tabs */}
+      <div className="flex gap-2">
+        <Skeleton className="h-10 w-40 rounded-md" />
+        <Skeleton className="h-10 w-40 rounded-md" />
+      </div>
+
+      {/* Main Content Area */}
+      <div className="space-y-6">
+        {/* Time Range Selector */}
+        <div className="flex items-center justify-between">
+          <div className="flex gap-2">
+            {[...Array(5)].map((_, i) => (
+              <Skeleton key={i} className="h-8 w-16 rounded-md" />
+            ))}
+          </div>
+          <Skeleton className="h-9 w-24 rounded-md" />
+        </div>
+
+        {/* Lane Chart Area */}
+        <div
+          className={cn(
+            "rounded-2xl p-4 md:p-6",
+            "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+            "backdrop-blur-lg",
+            "border border-border/50 dark:border-white/[0.08]"
+          )}
+        >
+          <Skeleton className="h-5 w-32 mb-4" />
+          <div className="space-y-3">
+            {[...Array(5)].map((_, i) => (
+              <div key={i} className="flex items-center gap-4">
+                <Skeleton className="h-8 w-32 shrink-0" />
+                <Skeleton className="h-8 flex-1" />
+                <Skeleton className="h-8 w-20 shrink-0" />
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {/* Latency Chart Area */}
+        <div
+          className={cn(
+            "rounded-2xl p-4 md:p-6",
+            "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+            "backdrop-blur-lg",
+            "border border-border/50 dark:border-white/[0.08]"
+          )}
+        >
+          <Skeleton className="h-5 w-40 mb-4" />
+          <Skeleton className="h-[200px] w-full rounded-lg" />
+        </div>
       </div>
-      <LoadingState />
     </div>
   );
 }
+
+// Keep the old skeleton for backward compatibility if needed elsewhere
+export function AvailabilityViewSkeleton() {
+  return <AvailabilityDashboardSkeleton />;
+}

+ 316 - 0
src/app/[locale]/dashboard/availability/_components/endpoint/endpoint-tab.tsx

@@ -0,0 +1,316 @@
+"use client";
+
+import { Radio, RefreshCw } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useCallback, useEffect, useState } from "react";
+import { toast } from "sonner";
+import {
+  getProviderEndpointProbeLogs,
+  getProviderEndpoints,
+  getProviderVendors,
+  probeProviderEndpoint,
+} from "@/actions/provider-endpoints";
+import { Button } from "@/components/ui/button";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { Skeleton } from "@/components/ui/skeleton";
+import { cn } from "@/lib/utils";
+import type { ProviderEndpoint, ProviderEndpointProbeLog, ProviderVendor } from "@/types/provider";
+import { LatencyCurve } from "./latency-curve";
+import { ProbeGrid } from "./probe-grid";
+import { ProbeTerminal } from "./probe-terminal";
+
+type ProviderType =
+  | "claude"
+  | "claude-auth"
+  | "codex"
+  | "gemini"
+  | "gemini-cli"
+  | "openai-compatible";
+
+const PROVIDER_TYPES: ProviderType[] = [
+  "claude",
+  "claude-auth",
+  "codex",
+  "gemini",
+  "gemini-cli",
+  "openai-compatible",
+];
+
+export function EndpointTab() {
+  const t = useTranslations("dashboard.availability");
+
+  // State
+  const [vendors, setVendors] = useState<ProviderVendor[]>([]);
+  const [selectedVendorId, setSelectedVendorId] = useState<number | null>(null);
+  const [selectedType, setSelectedType] = useState<ProviderType | null>(null);
+  const [endpoints, setEndpoints] = useState<ProviderEndpoint[]>([]);
+  const [selectedEndpoint, setSelectedEndpoint] = useState<ProviderEndpoint | null>(null);
+  const [probeLogs, setProbeLogs] = useState<ProviderEndpointProbeLog[]>([]);
+
+  // Loading states
+  const [loadingVendors, setLoadingVendors] = useState(true);
+  const [loadingEndpoints, setLoadingEndpoints] = useState(false);
+  const [loadingLogs, setLoadingLogs] = useState(false);
+  const [probing, setProbing] = useState(false);
+
+  // Fetch vendors on mount
+  useEffect(() => {
+    const fetchVendors = async () => {
+      try {
+        const vendors = await getProviderVendors();
+        setVendors(vendors);
+        if (vendors.length > 0) {
+          setSelectedVendorId(vendors[0].id);
+        }
+      } catch (error) {
+        console.error("Failed to fetch vendors:", error);
+      } finally {
+        setLoadingVendors(false);
+      }
+    };
+    fetchVendors();
+  }, []);
+
+  // Fetch endpoints when vendor or type changes
+  useEffect(() => {
+    if (!selectedVendorId || !selectedType) {
+      setEndpoints([]);
+      return;
+    }
+
+    const fetchEndpoints = async () => {
+      setLoadingEndpoints(true);
+      try {
+        const endpoints = await getProviderEndpoints({
+          vendorId: selectedVendorId,
+          providerType: selectedType,
+        });
+        setEndpoints(endpoints);
+        if (endpoints.length > 0) {
+          setSelectedEndpoint(endpoints[0]);
+        } else {
+          setSelectedEndpoint(null);
+        }
+      } catch (error) {
+        console.error("Failed to fetch endpoints:", error);
+      } finally {
+        setLoadingEndpoints(false);
+      }
+    };
+    fetchEndpoints();
+  }, [selectedVendorId, selectedType]);
+
+  // Fetch probe logs when endpoint changes
+  const fetchProbeLogs = useCallback(async () => {
+    if (!selectedEndpoint) {
+      setProbeLogs([]);
+      return;
+    }
+
+    setLoadingLogs(true);
+    try {
+      const result = await getProviderEndpointProbeLogs({
+        endpointId: selectedEndpoint.id,
+        limit: 100,
+      });
+      if (result.ok && result.data) {
+        setProbeLogs(result.data.logs);
+      }
+    } catch (error) {
+      console.error("Failed to fetch probe logs:", error);
+    } finally {
+      setLoadingLogs(false);
+    }
+  }, [selectedEndpoint]);
+
+  useEffect(() => {
+    fetchProbeLogs();
+  }, [fetchProbeLogs]);
+
+  // Auto-refresh logs every 10 seconds
+  useEffect(() => {
+    if (!selectedEndpoint) return;
+    const timer = setInterval(fetchProbeLogs, 10000);
+    return () => clearInterval(timer);
+  }, [selectedEndpoint, fetchProbeLogs]);
+
+  // Handle manual probe
+  const handleProbe = async () => {
+    if (!selectedEndpoint) return;
+
+    setProbing(true);
+    try {
+      const result = await probeProviderEndpoint({
+        endpointId: selectedEndpoint.id,
+      });
+      if (result.ok) {
+        toast.success(t("actions.probeSuccess"));
+        // Refresh logs and endpoints
+        fetchProbeLogs();
+        if (selectedVendorId && selectedType) {
+          const endpoints = await getProviderEndpoints({
+            vendorId: selectedVendorId,
+            providerType: selectedType,
+          });
+          setEndpoints(endpoints);
+          // Update selected endpoint with new data
+          const updated = endpoints.find((e) => e.id === selectedEndpoint.id);
+          if (updated) setSelectedEndpoint(updated);
+        }
+      } else {
+        toast.error(result.error || t("actions.probeFailed"));
+      }
+    } catch (error) {
+      console.error("Probe failed:", error);
+      toast.error(t("actions.probeFailed"));
+    } finally {
+      setProbing(false);
+    }
+  };
+
+  if (loadingVendors) {
+    return (
+      <div className="space-y-6">
+        <div className="flex gap-4">
+          <Skeleton className="h-9 w-[200px]" />
+          <Skeleton className="h-9 w-[160px]" />
+        </div>
+        <div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
+          <Skeleton className="h-[300px] rounded-2xl" />
+          <Skeleton className="h-[300px] rounded-2xl" />
+        </div>
+        <Skeleton className="h-[400px] rounded-2xl" />
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-6">
+      {/* Filters */}
+      <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
+        <div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto">
+          {/* Vendor Select */}
+          <Select
+            value={selectedVendorId?.toString() || ""}
+            onValueChange={(v) => {
+              setSelectedVendorId(Number(v));
+              setSelectedType(null);
+              setSelectedEndpoint(null);
+            }}
+          >
+            <SelectTrigger className="w-full sm:w-[200px]">
+              <SelectValue placeholder={t("endpoint.selectVendor")} />
+            </SelectTrigger>
+            <SelectContent>
+              {vendors.map((vendor) => (
+                <SelectItem key={vendor.id} value={vendor.id.toString()}>
+                  {vendor.displayName || vendor.websiteDomain}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+
+          {/* Provider Type Select */}
+          <Select
+            value={selectedType || ""}
+            onValueChange={(v) => {
+              setSelectedType(v as ProviderType);
+              setSelectedEndpoint(null);
+            }}
+            disabled={!selectedVendorId}
+          >
+            <SelectTrigger className="w-full sm:w-[180px]">
+              <SelectValue placeholder={t("endpoint.selectType")} />
+            </SelectTrigger>
+            <SelectContent>
+              {PROVIDER_TYPES.map((type) => (
+                <SelectItem key={type} value={type}>
+                  {type}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+        </div>
+
+        {/* Probe Button */}
+        <Button
+          onClick={handleProbe}
+          disabled={!selectedEndpoint || probing}
+          className="w-full sm:w-auto"
+        >
+          <Radio className={cn("h-4 w-4 mr-2", probing && "animate-pulse")} />
+          {probing ? t("actions.probing") : t("actions.probeNow")}
+        </Button>
+      </div>
+
+      {/* Main Content */}
+      <div className="grid gap-6 grid-cols-1 lg:grid-cols-2">
+        {/* Probe Grid */}
+        <div
+          className={cn(
+            "rounded-2xl p-4 md:p-6",
+            "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+            "backdrop-blur-lg",
+            "border border-border/50 dark:border-white/[0.08]",
+            "shadow-sm"
+          )}
+        >
+          <h3 className="text-sm font-medium text-muted-foreground mb-4">{t("probeGrid.title")}</h3>
+          {loadingEndpoints ? (
+            <div className="grid gap-3 grid-cols-1 sm:grid-cols-2">
+              {[...Array(4)].map((_, i) => (
+                <Skeleton key={i} className="h-24 rounded-xl" />
+              ))}
+            </div>
+          ) : (
+            <ProbeGrid
+              endpoints={endpoints}
+              selectedEndpointId={selectedEndpoint?.id}
+              onEndpointSelect={setSelectedEndpoint}
+            />
+          )}
+        </div>
+
+        {/* Latency Curve */}
+        <div
+          className={cn(
+            "rounded-2xl p-4 md:p-6",
+            "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+            "backdrop-blur-lg",
+            "border border-border/50 dark:border-white/[0.08]",
+            "shadow-sm"
+          )}
+        >
+          {loadingLogs ? (
+            <Skeleton className="h-[250px] w-full" />
+          ) : (
+            <LatencyCurve logs={probeLogs} />
+          )}
+        </div>
+      </div>
+
+      {/* Probe Terminal */}
+      <div
+        className={cn(
+          "rounded-2xl overflow-hidden",
+          "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+          "backdrop-blur-lg",
+          "border border-border/50 dark:border-white/[0.08]",
+          "shadow-sm"
+        )}
+      >
+        {loadingLogs && probeLogs.length === 0 ? (
+          <Skeleton className="h-[400px] w-full" />
+        ) : (
+          <ProbeTerminal logs={probeLogs} />
+        )}
+      </div>
+    </div>
+  );
+}

+ 171 - 0
src/app/[locale]/dashboard/availability/_components/endpoint/latency-curve.tsx

@@ -0,0 +1,171 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { useMemo } from "react";
+import { CartesianGrid, Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
+import {
+  type ChartConfig,
+  ChartContainer,
+  ChartTooltip,
+  ChartTooltipContent,
+} from "@/components/ui/chart";
+import { cn } from "@/lib/utils";
+import type { ProviderEndpointProbeLog } from "@/types/provider";
+
+interface LatencyCurveProps {
+  logs: ProviderEndpointProbeLog[];
+  className?: string;
+}
+
+const chartConfig = {
+  latency: {
+    label: "Latency",
+    color: "hsl(var(--primary))",
+  },
+} satisfies ChartConfig;
+
+export function LatencyCurve({ logs, className }: LatencyCurveProps) {
+  const t = useTranslations("dashboard.availability.latencyCurve");
+
+  // Transform logs to chart data
+  const chartData = useMemo(() => {
+    return logs
+      .filter((log) => log.latencyMs !== null)
+      .map((log) => ({
+        time: log.createdAt,
+        timestamp: new Date(log.createdAt).getTime(),
+        latency: log.latencyMs,
+        ok: log.ok,
+        statusCode: log.statusCode,
+      }))
+      .sort((a, b) => a.timestamp - b.timestamp);
+  }, [logs]);
+
+  if (chartData.length === 0) {
+    return (
+      <div
+        className={cn(
+          "flex items-center justify-center h-[200px] text-muted-foreground",
+          className
+        )}
+      >
+        {t("noData")}
+      </div>
+    );
+  }
+
+  const formatTime = (time: string) => {
+    const date = new Date(time);
+    return date.toLocaleTimeString(undefined, {
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit",
+    });
+  };
+
+  const formatLatency = (value: number) => {
+    if (value < 1000) return `${Math.round(value)}ms`;
+    return `${(value / 1000).toFixed(1)}s`;
+  };
+
+  // Calculate stats
+  const latencies = chartData.map((d) => d.latency).filter((l): l is number => l !== null);
+  const avgLatency =
+    latencies.length > 0 ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0;
+  const minLatency = latencies.length > 0 ? Math.min(...latencies) : 0;
+  const maxLatency = latencies.length > 0 ? Math.max(...latencies) : 0;
+
+  return (
+    <div className={cn("w-full", className)}>
+      <div className="flex items-center justify-between mb-4">
+        <h3 className="text-sm font-medium text-muted-foreground">{t("title")}</h3>
+        <div className="flex items-center gap-4 text-xs text-muted-foreground">
+          <span>
+            {t("avg")}:{" "}
+            <span className="font-mono text-foreground">{formatLatency(avgLatency)}</span>
+          </span>
+          <span>
+            {t("min")}:{" "}
+            <span className="font-mono text-emerald-500">{formatLatency(minLatency)}</span>
+          </span>
+          <span>
+            {t("max")}: <span className="font-mono text-rose-500">{formatLatency(maxLatency)}</span>
+          </span>
+        </div>
+      </div>
+
+      <ChartContainer config={chartConfig} className="h-[200px] w-full">
+        <LineChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
+          <defs>
+            <linearGradient id="latencyGradient" x1="0" y1="0" x2="0" y2="1">
+              <stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
+              <stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
+            </linearGradient>
+          </defs>
+          <CartesianGrid strokeDasharray="3 3" vertical={false} className="stroke-border/30" />
+          <XAxis
+            dataKey="time"
+            tickLine={false}
+            axisLine={false}
+            tickMargin={8}
+            tickFormatter={formatTime}
+            className="text-xs text-muted-foreground"
+          />
+          <YAxis
+            tickLine={false}
+            axisLine={false}
+            tickMargin={8}
+            tickFormatter={formatLatency}
+            className="text-xs text-muted-foreground"
+            width={50}
+          />
+          <ChartTooltip
+            content={({ active, payload, label }) => {
+              if (!active || !payload?.length) return null;
+              const data = payload[0]?.payload;
+              return (
+                <div className="border-border/50 bg-background rounded-lg border px-2.5 py-1.5 text-xs shadow-xl">
+                  <div className="font-medium mb-1">{formatTime(label as string)}</div>
+                  <div className="space-y-1">
+                    <div>{formatLatency(payload[0]?.value as number)}</div>
+                    <div className={cn("text-xs", data?.ok ? "text-emerald-500" : "text-rose-500")}>
+                      {data?.statusCode || (data?.ok ? "OK" : "FAIL")}
+                    </div>
+                  </div>
+                </div>
+              );
+            }}
+          />
+          <Line
+            type="monotone"
+            dataKey="latency"
+            stroke="hsl(var(--primary))"
+            strokeWidth={2}
+            dot={(props) => {
+              const { cx, cy, payload } = props;
+              if (!payload.ok) {
+                return (
+                  <circle
+                    key={`dot-${payload.timestamp}`}
+                    cx={cx}
+                    cy={cy}
+                    r={4}
+                    fill="hsl(var(--destructive))"
+                    stroke="hsl(var(--destructive))"
+                  />
+                );
+              }
+              return null;
+            }}
+            activeDot={{
+              r: 6,
+              fill: "hsl(var(--primary))",
+              stroke: "hsl(var(--background))",
+              strokeWidth: 2,
+            }}
+          />
+        </LineChart>
+      </ChartContainer>
+    </div>
+  );
+}

+ 165 - 0
src/app/[locale]/dashboard/availability/_components/endpoint/probe-grid.tsx

@@ -0,0 +1,165 @@
+"use client";
+
+import { CheckCircle2, HelpCircle, XCircle } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+import type { ProviderEndpoint } from "@/types/provider";
+
+interface ProbeGridProps {
+  endpoints: ProviderEndpoint[];
+  selectedEndpointId?: number | null;
+  onEndpointSelect?: (endpoint: ProviderEndpoint) => void;
+  className?: string;
+}
+
+function getStatusConfig(endpoint: ProviderEndpoint) {
+  if (endpoint.lastProbeOk === null) {
+    return {
+      icon: HelpCircle,
+      color: "text-slate-400",
+      bgColor: "bg-slate-400/10",
+      borderColor: "border-slate-400/30",
+      label: "unknown",
+    };
+  }
+  if (endpoint.lastProbeOk) {
+    return {
+      icon: CheckCircle2,
+      color: "text-emerald-500",
+      bgColor: "bg-emerald-500/10",
+      borderColor: "border-emerald-500/30",
+      label: "healthy",
+    };
+  }
+  return {
+    icon: XCircle,
+    color: "text-rose-500",
+    bgColor: "bg-rose-500/10",
+    borderColor: "border-rose-500/30",
+    label: "unhealthy",
+  };
+}
+
+function formatLatency(ms: number | null): string {
+  if (ms === null) return "-";
+  if (ms < 1000) return `${Math.round(ms)}ms`;
+  return `${(ms / 1000).toFixed(2)}s`;
+}
+
+function formatTime(date: Date | string | null): string {
+  if (!date) return "-";
+  const d = typeof date === "string" ? new Date(date) : date;
+  return d.toLocaleTimeString(undefined, {
+    hour: "2-digit",
+    minute: "2-digit",
+    second: "2-digit",
+  });
+}
+
+export function ProbeGrid({
+  endpoints,
+  selectedEndpointId,
+  onEndpointSelect,
+  className,
+}: ProbeGridProps) {
+  const t = useTranslations("dashboard.availability.probeGrid");
+
+  if (endpoints.length === 0) {
+    return (
+      <div className={cn("text-center text-muted-foreground py-8", className)}>
+        {t("noEndpoints")}
+      </div>
+    );
+  }
+
+  return (
+    <TooltipProvider>
+      <div className={cn("grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", className)}>
+        {endpoints.map((endpoint) => {
+          const status = getStatusConfig(endpoint);
+          const StatusIcon = status.icon;
+          const isSelected = selectedEndpointId === endpoint.id;
+
+          return (
+            <Tooltip key={endpoint.id}>
+              <TooltipTrigger asChild>
+                <button
+                  onClick={() => onEndpointSelect?.(endpoint)}
+                  className={cn(
+                    "relative p-3 rounded-xl text-left transition-all duration-200",
+                    "border",
+                    status.bgColor,
+                    status.borderColor,
+                    "hover:scale-[1.02] hover:shadow-md",
+                    "active:scale-[0.98]",
+                    isSelected && "ring-2 ring-primary ring-offset-2 ring-offset-background"
+                  )}
+                >
+                  <div className="flex items-start justify-between gap-2">
+                    <div className="flex-1 min-w-0">
+                      <div className="flex items-center gap-2">
+                        <StatusIcon className={cn("h-4 w-4 shrink-0", status.color)} />
+                        <span className="text-sm font-medium truncate">
+                          {endpoint.label || new URL(endpoint.url).hostname}
+                        </span>
+                      </div>
+                      <p className="text-xs text-muted-foreground truncate mt-1">{endpoint.url}</p>
+                    </div>
+                    {endpoint.lastProbeLatencyMs !== null && (
+                      <span
+                        className={cn(
+                          "text-xs font-mono px-1.5 py-0.5 rounded",
+                          endpoint.lastProbeLatencyMs < 200
+                            ? "bg-emerald-500/10 text-emerald-500"
+                            : endpoint.lastProbeLatencyMs < 500
+                              ? "bg-amber-500/10 text-amber-500"
+                              : "bg-rose-500/10 text-rose-500"
+                        )}
+                      >
+                        {formatLatency(endpoint.lastProbeLatencyMs)}
+                      </span>
+                    )}
+                  </div>
+
+                  {/* Last probe time */}
+                  <div className="flex items-center justify-between mt-2 pt-2 border-t border-border/30">
+                    <span className="text-xs text-muted-foreground">{t("lastProbe")}</span>
+                    <span className="text-xs font-mono text-muted-foreground">
+                      {formatTime(endpoint.lastProbedAt)}
+                    </span>
+                  </div>
+
+                  {/* Status code badge */}
+                  {endpoint.lastProbeStatusCode !== null && (
+                    <div
+                      className={cn(
+                        "absolute -top-1 -right-1 text-[10px] font-mono font-bold px-1.5 py-0.5 rounded-full",
+                        endpoint.lastProbeStatusCode >= 200 && endpoint.lastProbeStatusCode < 300
+                          ? "bg-emerald-500 text-white"
+                          : endpoint.lastProbeStatusCode >= 400
+                            ? "bg-rose-500 text-white"
+                            : "bg-amber-500 text-white"
+                      )}
+                    >
+                      {endpoint.lastProbeStatusCode}
+                    </div>
+                  )}
+                </button>
+              </TooltipTrigger>
+              <TooltipContent side="bottom" className="max-w-xs">
+                <div className="text-xs space-y-1">
+                  <p className="font-medium">{endpoint.label || endpoint.url}</p>
+                  <p className="text-muted-foreground">{t(`status.${status.label}`)}</p>
+                  {endpoint.lastProbeErrorMessage && (
+                    <p className="text-rose-400">{endpoint.lastProbeErrorMessage}</p>
+                  )}
+                </div>
+              </TooltipContent>
+            </Tooltip>
+          );
+        })}
+      </div>
+    </TooltipProvider>
+  );
+}

+ 285 - 0
src/app/[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx

@@ -0,0 +1,285 @@
+"use client";
+
+import { AlertCircle, CheckCircle2, Download, Trash2, XCircle } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useEffect, useRef, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import type { ProviderEndpointProbeLog } from "@/types/provider";
+
+interface ProbeTerminalProps {
+  logs: ProviderEndpointProbeLog[];
+  maxLines?: number;
+  autoScroll?: boolean;
+  onLogClick?: (log: ProviderEndpointProbeLog) => void;
+  className?: string;
+}
+
+function formatTime(date: Date | string): string {
+  const d = typeof date === "string" ? new Date(date) : date;
+  return d.toLocaleTimeString(undefined, {
+    hour: "2-digit",
+    minute: "2-digit",
+    second: "2-digit",
+  });
+}
+
+function formatLatency(ms: number | null): string {
+  if (ms === null) return "-";
+  if (ms < 1000) return `${Math.round(ms)}ms`;
+  return `${(ms / 1000).toFixed(2)}s`;
+}
+
+function getLogLevel(log: ProviderEndpointProbeLog): "success" | "error" | "warn" {
+  if (log.ok) return "success";
+  if (log.errorType === "timeout") return "warn";
+  return "error";
+}
+
+const levelConfig = {
+  success: {
+    icon: CheckCircle2,
+    label: "OK",
+    color: "text-emerald-500",
+    bgColor: "bg-emerald-500/5",
+    borderColor: "border-l-emerald-500",
+  },
+  error: {
+    icon: XCircle,
+    label: "FAIL",
+    color: "text-rose-500",
+    bgColor: "bg-rose-500/5",
+    borderColor: "border-l-rose-500",
+  },
+  warn: {
+    icon: AlertCircle,
+    label: "WARN",
+    color: "text-amber-500",
+    bgColor: "bg-amber-500/5",
+    borderColor: "border-l-amber-500",
+  },
+};
+
+export function ProbeTerminal({
+  logs,
+  maxLines = 100,
+  autoScroll = true,
+  onLogClick,
+  className,
+}: ProbeTerminalProps) {
+  const t = useTranslations("dashboard.availability.terminal");
+  const containerRef = useRef<HTMLDivElement>(null);
+  const [userScrolled, setUserScrolled] = useState(false);
+  const [filter, setFilter] = useState("");
+
+  // Auto-scroll to bottom when new logs arrive
+  useEffect(() => {
+    if (autoScroll && !userScrolled && containerRef.current) {
+      containerRef.current.scrollTop = containerRef.current.scrollHeight;
+    }
+  }, [logs, autoScroll, userScrolled]);
+
+  // Detect user scroll
+  const handleScroll = () => {
+    if (!containerRef.current) return;
+    const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
+    const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
+    setUserScrolled(!isAtBottom);
+  };
+
+  // Filter logs
+  const filteredLogs = logs
+    .filter((log) => {
+      if (!filter) return true;
+      const searchLower = filter.toLowerCase();
+      return (
+        log.errorMessage?.toLowerCase().includes(searchLower) ||
+        log.errorType?.toLowerCase().includes(searchLower) ||
+        log.statusCode?.toString().includes(searchLower)
+      );
+    })
+    .slice(-maxLines);
+
+  const handleDownload = () => {
+    const content = filteredLogs
+      .map((log) => {
+        const time = formatTime(log.createdAt);
+        const status = log.ok ? "OK" : "FAIL";
+        const latency = formatLatency(log.latencyMs);
+        const error = log.errorMessage || "";
+        return `[${time}] ${status} ${log.statusCode || "-"} ${latency} ${error}`;
+      })
+      .join("\n");
+
+    const blob = new Blob([content], { type: "text/plain" });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement("a");
+    a.href = url;
+    a.download = `probe-logs-${new Date().toISOString().slice(0, 10)}.txt`;
+    a.click();
+    URL.revokeObjectURL(url);
+  };
+
+  return (
+    <div
+      className={cn(
+        "flex flex-col overflow-hidden rounded-xl",
+        "bg-slate-50 dark:bg-[#0a0a0c]",
+        "border border-slate-200 dark:border-white/[0.06]",
+        className
+      )}
+    >
+      {/* Scanline Overlay - only visible in dark mode */}
+      <div
+        className="absolute inset-0 pointer-events-none z-10 opacity-0 dark:opacity-[0.03]"
+        style={{
+          background:
+            "linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1))",
+          backgroundSize: "100% 4px",
+        }}
+      />
+
+      {/* Header */}
+      <div className="flex items-center justify-between p-3 border-b border-slate-200 dark:border-white/[0.06] bg-slate-100/50 dark:bg-white/[0.02]">
+        <div className="flex items-center gap-2">
+          {/* Traffic Lights */}
+          <div className="flex gap-1.5 mr-2">
+            <div className="h-2.5 w-2.5 rounded-full bg-rose-400/50 dark:bg-rose-500/30" />
+            <div className="h-2.5 w-2.5 rounded-full bg-amber-400/50 dark:bg-amber-500/30" />
+            <div className="h-2.5 w-2.5 rounded-full bg-emerald-400/50 dark:bg-emerald-500/30" />
+          </div>
+          <span className="text-xs font-mono font-medium text-slate-700 dark:text-foreground/80 uppercase tracking-wider">
+            {t("title")}
+          </span>
+          <div className="flex items-center gap-1.5 ml-2">
+            <div className="h-2 w-2 rounded-full bg-rose-500 animate-pulse" />
+            <span className="text-xs text-rose-500 font-mono">{t("live")}</span>
+          </div>
+        </div>
+        <div className="flex items-center gap-1">
+          <Button
+            variant="ghost"
+            size="icon"
+            className="h-7 w-7"
+            onClick={handleDownload}
+            title={t("download")}
+          >
+            <Download className="h-3.5 w-3.5 text-muted-foreground" />
+          </Button>
+        </div>
+      </div>
+
+      {/* Log Content */}
+      <div
+        ref={containerRef}
+        onScroll={handleScroll}
+        className="flex-1 overflow-y-auto p-2 font-mono text-xs md:text-sm space-y-0.5 min-h-[200px] max-h-[400px]"
+      >
+        {filteredLogs.length === 0 ? (
+          <div className="flex items-center justify-center h-full text-muted-foreground">
+            {t("noLogs")}
+          </div>
+        ) : (
+          filteredLogs.map((log) => {
+            const level = getLogLevel(log);
+            const config = levelConfig[level];
+            const Icon = config.icon;
+
+            return (
+              <button
+                key={log.id}
+                onClick={() => onLogClick?.(log)}
+                className={cn(
+                  "flex items-center gap-2 w-full px-2 py-1.5 rounded text-left",
+                  "hover:bg-muted/30 dark:hover:bg-white/[0.03]",
+                  "transition-colors border-l-2",
+                  config.borderColor,
+                  onLogClick && "cursor-pointer"
+                )}
+              >
+                {/* Timestamp */}
+                <span className="text-muted-foreground opacity-60 w-20 shrink-0">
+                  [{formatTime(log.createdAt)}]
+                </span>
+
+                {/* Status */}
+                <span className={cn("w-12 shrink-0 font-bold", config.color)}>{config.label}</span>
+
+                {/* Status Code */}
+                <span
+                  className={cn(
+                    "w-10 shrink-0",
+                    log.statusCode && log.statusCode >= 200 && log.statusCode < 300
+                      ? "text-emerald-500"
+                      : log.statusCode && log.statusCode >= 400
+                        ? "text-rose-500"
+                        : "text-muted-foreground"
+                  )}
+                >
+                  {log.statusCode || "-"}
+                </span>
+
+                {/* Latency */}
+                <span
+                  className={cn(
+                    "w-16 shrink-0 text-right",
+                    log.latencyMs && log.latencyMs < 200
+                      ? "text-emerald-500"
+                      : log.latencyMs && log.latencyMs < 500
+                        ? "text-amber-500"
+                        : "text-rose-500"
+                  )}
+                >
+                  {formatLatency(log.latencyMs)}
+                </span>
+
+                {/* Source badge */}
+                <span
+                  className={cn(
+                    "px-1.5 py-0.5 rounded text-[10px] uppercase shrink-0",
+                    log.source === "manual"
+                      ? "bg-primary/10 text-primary"
+                      : "bg-muted/50 text-muted-foreground"
+                  )}
+                >
+                  {log.source === "manual" ? t("manual") : t("auto")}
+                </span>
+
+                {/* Error message */}
+                {log.errorMessage && (
+                  <span className="text-rose-400 truncate flex-1">{log.errorMessage}</span>
+                )}
+              </button>
+            );
+          })
+        )}
+
+        {/* Loading indicator */}
+        {logs.length > 0 && (
+          <div className="flex items-center gap-2 px-2 py-1 text-muted-foreground animate-pulse">
+            <span className="opacity-50">[{formatTime(new Date())}]</span>
+            <span>...</span>
+          </div>
+        )}
+      </div>
+
+      {/* Filter Input */}
+      <div className="p-2 border-t border-slate-200 dark:border-white/[0.06] bg-slate-100/50 dark:bg-white/[0.02]">
+        <div className="flex items-center gap-2">
+          <span className="text-primary font-mono">&gt;</span>
+          <input
+            type="text"
+            value={filter}
+            onChange={(e) => setFilter(e.target.value)}
+            placeholder={t("filterPlaceholder")}
+            className={cn(
+              "flex-1 bg-transparent border-none text-sm font-mono",
+              "text-foreground placeholder:text-muted-foreground/50",
+              "focus:outline-none focus:ring-0"
+            )}
+          />
+        </div>
+      </div>
+    </div>
+  );
+}

+ 222 - 0
src/app/[locale]/dashboard/availability/_components/overview/gauge-card.tsx

@@ -0,0 +1,222 @@
+"use client";
+
+import type { LucideIcon } from "lucide-react";
+import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
+import { useEffect, useRef, useState } from "react";
+import { cn } from "@/lib/utils";
+
+interface GaugeCardProps {
+  value: number;
+  label: string;
+  icon: LucideIcon;
+  trend?: {
+    value: number;
+    direction: "up" | "down" | "stable";
+  };
+  thresholds?: {
+    warning: number;
+    critical: number;
+  };
+  size?: "sm" | "md" | "lg";
+  formatter?: (value: number) => string;
+  invertColors?: boolean;
+  className?: string;
+}
+
+const sizeConfig = {
+  sm: { gauge: 64, stroke: 4, iconSize: 16, fontSize: "text-lg" },
+  md: { gauge: 80, stroke: 5, iconSize: 20, fontSize: "text-2xl" },
+  lg: { gauge: 96, stroke: 6, iconSize: 24, fontSize: "text-3xl" },
+};
+
+function getGaugeColor(
+  value: number,
+  thresholds: { warning: number; critical: number },
+  invertColors: boolean
+): string {
+  if (invertColors) {
+    // For metrics where lower is better (error rate, latency)
+    if (value <= thresholds.critical) return "text-emerald-500";
+    if (value <= thresholds.warning) return "text-amber-500";
+    return "text-rose-500";
+  }
+  // For metrics where higher is better (availability)
+  if (value >= thresholds.warning) return "text-emerald-500";
+  if (value >= thresholds.critical) return "text-amber-500";
+  return "text-rose-500";
+}
+
+function getTrendIcon(direction: "up" | "down" | "stable") {
+  switch (direction) {
+    case "up":
+      return ArrowUp;
+    case "down":
+      return ArrowDown;
+    default:
+      return ArrowRight;
+  }
+}
+
+function getTrendColor(direction: "up" | "down" | "stable", invertColors: boolean) {
+  if (direction === "stable") return "text-muted-foreground bg-muted/50";
+  if (invertColors) {
+    // For inverted metrics, down is good
+    return direction === "down"
+      ? "text-emerald-500 bg-emerald-500/10"
+      : "text-rose-500 bg-rose-500/10";
+  }
+  // For normal metrics, up is good
+  return direction === "up" ? "text-emerald-500 bg-emerald-500/10" : "text-rose-500 bg-rose-500/10";
+}
+
+export function GaugeCard({
+  value,
+  label,
+  icon: Icon,
+  trend,
+  thresholds = { warning: 80, critical: 50 },
+  size = "md",
+  formatter = (v) => `${v.toFixed(1)}%`,
+  invertColors = false,
+  className,
+}: GaugeCardProps) {
+  const [displayValue, setDisplayValue] = useState(0);
+  const prevValueRef = useRef(0);
+  const config = sizeConfig[size];
+
+  // Animate value changes
+  useEffect(() => {
+    let cancelled = false;
+    const duration = 800;
+    const startValue = prevValueRef.current;
+    const diff = value - startValue;
+    const startTime = Date.now();
+
+    const animate = () => {
+      if (cancelled) return;
+      const elapsed = Date.now() - startTime;
+      const progress = Math.min(elapsed / duration, 1);
+      // Ease out cubic
+      const easeProgress = 1 - (1 - progress) ** 3;
+      const currentValue = startValue + diff * easeProgress;
+
+      setDisplayValue(currentValue);
+
+      if (progress < 1) {
+        requestAnimationFrame(animate);
+      } else {
+        prevValueRef.current = value;
+      }
+    };
+
+    requestAnimationFrame(animate);
+    return () => {
+      cancelled = true;
+    };
+  }, [value]);
+
+  // SVG gauge calculations
+  const radius = (config.gauge - config.stroke) / 2;
+  const circumference = 2 * Math.PI * radius;
+  const normalizedValue = Math.min(Math.max(displayValue, 0), 100);
+  const offset = circumference - (normalizedValue / 100) * circumference;
+  const gaugeColor = getGaugeColor(displayValue, thresholds, invertColors);
+
+  const TrendIcon = trend ? getTrendIcon(trend.direction) : null;
+  const trendColor = trend ? getTrendColor(trend.direction, invertColors) : "";
+
+  return (
+    <div
+      className={cn(
+        // Glass card base
+        "relative overflow-hidden rounded-2xl p-4 md:p-5",
+        "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+        "backdrop-blur-lg",
+        "border border-border/50 dark:border-white/[0.08]",
+        "shadow-sm",
+        // Inner light gradient
+        "before:absolute before:inset-0 before:bg-gradient-to-b before:from-white/[0.02] before:to-transparent before:pointer-events-none before:z-[1]",
+        // Hover effects
+        "transition-all duration-300 ease-out",
+        "hover:border-primary/20 hover:shadow-md",
+        "hover:-translate-y-0.5",
+        "group",
+        className
+      )}
+    >
+      {/* Subtle glow effect */}
+      <div
+        className={cn(
+          "absolute -top-[30%] -right-[15%] w-[120px] h-[120px] rounded-full pointer-events-none z-0",
+          gaugeColor.replace("text-", "bg-").replace("500", "500/10"),
+          "blur-[40px]",
+          "opacity-50 group-hover:opacity-70 transition-opacity duration-500"
+        )}
+      />
+
+      <div className="relative z-10 flex items-center gap-4">
+        {/* Circular Gauge */}
+        <div
+          className="relative flex-shrink-0"
+          style={{ width: config.gauge, height: config.gauge }}
+        >
+          <svg width={config.gauge} height={config.gauge} className="transform -rotate-90">
+            {/* Background circle */}
+            <circle
+              cx={config.gauge / 2}
+              cy={config.gauge / 2}
+              r={radius}
+              stroke="currentColor"
+              strokeWidth={config.stroke}
+              fill="none"
+              className="text-muted/20"
+            />
+            {/* Progress circle with gradient */}
+            <circle
+              cx={config.gauge / 2}
+              cy={config.gauge / 2}
+              r={radius}
+              stroke="currentColor"
+              strokeWidth={config.stroke}
+              fill="none"
+              strokeDasharray={circumference}
+              strokeDashoffset={offset}
+              strokeLinecap="round"
+              className={cn("transition-all duration-700 ease-out", gaugeColor)}
+            />
+          </svg>
+          {/* Center icon */}
+          <div className="absolute inset-0 flex items-center justify-center">
+            <Icon
+              className={cn(gaugeColor, "transition-colors duration-300")}
+              style={{ width: config.iconSize, height: config.iconSize }}
+            />
+          </div>
+        </div>
+
+        {/* Content */}
+        <div className="flex flex-col min-w-0">
+          <p className="text-sm font-medium text-muted-foreground truncate">{label}</p>
+          <h3 className={cn("font-bold tracking-tight text-foreground", config.fontSize)}>
+            {formatter(displayValue)}
+          </h3>
+          {/* Trend indicator */}
+          {trend && TrendIcon && (
+            <div
+              className={cn(
+                "flex items-center gap-1 mt-1 px-1.5 py-0.5 rounded-full w-fit text-xs font-medium",
+                trendColor
+              )}
+            >
+              <TrendIcon className="h-3 w-3" />
+              <span>
+                {trend.value > 0 ? "+" : ""}
+                {trend.value.toFixed(1)}%
+              </span>
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 168 - 0
src/app/[locale]/dashboard/availability/_components/overview/overview-section.tsx

@@ -0,0 +1,168 @@
+"use client";
+
+import { Activity, AlertTriangle, Clock, ShieldCheck } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { Skeleton } from "@/components/ui/skeleton";
+import { cn } from "@/lib/utils";
+import { GaugeCard } from "./gauge-card";
+
+interface OverviewSectionProps {
+  systemAvailability: number;
+  avgLatency: number;
+  errorRate: number;
+  activeProbes: number;
+  totalProbes: number;
+  loading?: boolean;
+  refreshing?: boolean;
+}
+
+export function OverviewSection({
+  systemAvailability,
+  avgLatency,
+  errorRate,
+  activeProbes,
+  totalProbes,
+  loading,
+  refreshing,
+}: OverviewSectionProps) {
+  const t = useTranslations("dashboard.availability.overview");
+
+  if (loading) {
+    return (
+      <div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
+        {[...Array(4)].map((_, i) => (
+          <div
+            key={i}
+            className={cn(
+              "relative overflow-hidden rounded-2xl p-4 md:p-5",
+              "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+              "backdrop-blur-lg",
+              "border border-border/50 dark:border-white/[0.08]"
+            )}
+          >
+            <div className="flex items-center gap-4">
+              <Skeleton className="h-20 w-20 rounded-full" />
+              <div className="space-y-2">
+                <Skeleton className="h-4 w-24" />
+                <Skeleton className="h-8 w-16" />
+              </div>
+            </div>
+          </div>
+        ))}
+      </div>
+    );
+  }
+
+  // Calculate trends (mock for now - would need historical data)
+  const availabilityTrend =
+    systemAvailability > 0.95
+      ? { value: 0.1, direction: "up" as const }
+      : systemAvailability < 0.8
+        ? { value: -2.5, direction: "down" as const }
+        : { value: 0, direction: "stable" as const };
+
+  const latencyTrend =
+    avgLatency < 200
+      ? { value: -5, direction: "down" as const }
+      : avgLatency > 500
+        ? { value: 15, direction: "up" as const }
+        : { value: 0, direction: "stable" as const };
+
+  const errorTrend =
+    errorRate < 0.01
+      ? { value: 0, direction: "stable" as const }
+      : errorRate > 0.05
+        ? { value: 2.3, direction: "up" as const }
+        : { value: -0.5, direction: "down" as const };
+
+  return (
+    <div
+      className={cn(
+        "grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
+        refreshing && "opacity-70 transition-opacity"
+      )}
+    >
+      {/* System Availability */}
+      <GaugeCard
+        value={systemAvailability * 100}
+        label={t("systemAvailability")}
+        icon={ShieldCheck}
+        trend={availabilityTrend}
+        thresholds={{ warning: 95, critical: 80 }}
+        formatter={(v) => `${v.toFixed(2)}%`}
+      />
+
+      {/* Average Latency */}
+      <GaugeCard
+        value={Math.min(avgLatency / 10, 100)} // Normalize to 0-100 (1000ms = 100%)
+        label={t("avgLatency")}
+        icon={Clock}
+        trend={latencyTrend}
+        thresholds={{ warning: 30, critical: 50 }} // 300ms warning, 500ms critical
+        formatter={() =>
+          avgLatency < 1000 ? `${Math.round(avgLatency)}ms` : `${(avgLatency / 1000).toFixed(2)}s`
+        }
+        invertColors
+      />
+
+      {/* Error Rate */}
+      <GaugeCard
+        value={errorRate * 100}
+        label={t("errorRate")}
+        icon={AlertTriangle}
+        trend={errorTrend}
+        thresholds={{ warning: 1, critical: 5 }} // 1% warning, 5% critical
+        formatter={(v) => `${v.toFixed(2)}%`}
+        invertColors
+      />
+
+      {/* Active Probes */}
+      <div
+        className={cn(
+          "relative overflow-hidden rounded-2xl p-4 md:p-5",
+          "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+          "backdrop-blur-lg",
+          "border border-border/50 dark:border-white/[0.08]",
+          "shadow-sm",
+          "before:absolute before:inset-0 before:bg-gradient-to-b before:from-white/[0.02] before:to-transparent before:pointer-events-none before:z-[1]",
+          "transition-all duration-300 ease-out",
+          "hover:border-primary/20 hover:shadow-md",
+          "hover:-translate-y-0.5",
+          "group"
+        )}
+      >
+        {/* Glow effect */}
+        <div className="absolute -top-[30%] -right-[15%] w-[120px] h-[120px] rounded-full pointer-events-none z-0 bg-primary/10 blur-[40px] opacity-50 group-hover:opacity-70 transition-opacity duration-500" />
+
+        <div className="relative z-10">
+          <div className="flex items-start justify-between">
+            <div className="flex flex-col">
+              <p className="text-sm font-medium text-muted-foreground">{t("activeProbes")}</p>
+              <h3 className="text-3xl font-bold tracking-tight text-foreground mt-1">
+                {activeProbes}
+                <span className="text-lg font-normal text-muted-foreground">/{totalProbes}</span>
+              </h3>
+            </div>
+            <div className="p-2 rounded-lg bg-primary/10 dark:bg-primary/15">
+              <Activity className="h-5 w-5 text-primary" />
+            </div>
+          </div>
+
+          {/* Progress bar */}
+          <div className="mt-4">
+            <div className="flex justify-between text-xs text-muted-foreground mb-1">
+              <span>{t("load")}</span>
+              <span>{totalProbes > 0 ? Math.round((activeProbes / totalProbes) * 100) : 0}%</span>
+            </div>
+            <div className="h-1.5 w-full bg-muted/30 rounded-full overflow-hidden">
+              <div
+                className="h-full bg-primary rounded-full transition-all duration-500"
+                style={{ width: `${totalProbes > 0 ? (activeProbes / totalProbes) * 100 : 0}%` }}
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 98 - 0
src/app/[locale]/dashboard/availability/_components/provider/confidence-badge.tsx

@@ -0,0 +1,98 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+
+interface ConfidenceBadgeProps {
+  requestCount: number;
+  thresholds?: {
+    low: number;
+    medium: number;
+    high: number;
+  };
+  className?: string;
+}
+
+type ConfidenceLevel = "low" | "medium" | "high";
+
+function getConfidenceLevel(
+  count: number,
+  thresholds: { low: number; medium: number; high: number }
+): ConfidenceLevel {
+  if (count >= thresholds.high) return "high";
+  if (count >= thresholds.medium) return "medium";
+  return "low";
+}
+
+const confidenceConfig: Record<
+  ConfidenceLevel,
+  { bars: number; color: string; bgColor: string; borderStyle: string }
+> = {
+  low: {
+    bars: 1,
+    color: "bg-slate-400",
+    bgColor: "bg-slate-400/10",
+    borderStyle: "border-dashed border-slate-400/50",
+  },
+  medium: {
+    bars: 2,
+    color: "bg-amber-500",
+    bgColor: "bg-amber-500/10",
+    borderStyle: "border-solid border-amber-500/50",
+  },
+  high: {
+    bars: 3,
+    color: "bg-emerald-500",
+    bgColor: "bg-emerald-500/10",
+    borderStyle: "border-solid border-emerald-500/50",
+  },
+};
+
+export function ConfidenceBadge({
+  requestCount,
+  thresholds = { low: 10, medium: 50, high: 200 },
+  className,
+}: ConfidenceBadgeProps) {
+  const t = useTranslations("dashboard.availability.confidence");
+  const level = getConfidenceLevel(requestCount, thresholds);
+  const config = confidenceConfig[level];
+
+  return (
+    <TooltipProvider>
+      <Tooltip>
+        <TooltipTrigger asChild>
+          <div
+            className={cn(
+              "inline-flex items-center gap-1 px-1.5 py-0.5 rounded border",
+              config.bgColor,
+              config.borderStyle,
+              "cursor-help",
+              className
+            )}
+          >
+            {/* Signal bars */}
+            <div className="flex items-end gap-0.5 h-3">
+              {[1, 2, 3].map((bar) => (
+                <div
+                  key={bar}
+                  className={cn(
+                    "w-1 rounded-sm transition-colors",
+                    bar <= config.bars ? config.color : "bg-muted/30"
+                  )}
+                  style={{ height: `${bar * 4}px` }}
+                />
+              ))}
+            </div>
+          </div>
+        </TooltipTrigger>
+        <TooltipContent side="top" className="max-w-[200px]">
+          <div className="text-xs space-y-1">
+            <p className="font-medium">{t(level)}</p>
+            <p className="text-muted-foreground">{t(`${level}Tooltip`, { count: requestCount })}</p>
+          </div>
+        </TooltipContent>
+      </Tooltip>
+    </TooltipProvider>
+  );
+}

+ 332 - 0
src/app/[locale]/dashboard/availability/_components/provider/lane-chart.tsx

@@ -0,0 +1,332 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { useMemo } from "react";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import type { ProviderAvailabilitySummary, TimeBucketMetrics } from "@/lib/availability";
+import { cn } from "@/lib/utils";
+import { ConfidenceBadge } from "./confidence-badge";
+
+interface LaneChartProps {
+  providers: ProviderAvailabilitySummary[];
+  bucketSizeMinutes: number;
+  startTime: string;
+  endTime: string;
+  onProviderClick?: (providerId: number) => void;
+  className?: string;
+}
+
+// Threshold for switching between dots and bars visualization
+const HIGH_VOLUME_THRESHOLD = 50;
+
+function getAvailabilityColor(score: number, hasData: boolean): string {
+  if (!hasData) return "bg-slate-300/50 dark:bg-slate-600/50";
+  if (score < 0.5) return "bg-rose-500";
+  if (score < 0.8) return "bg-orange-500";
+  if (score < 0.95) return "bg-lime-500";
+  return "bg-emerald-500";
+}
+
+function getStatusColor(status: string): string {
+  switch (status) {
+    case "green":
+      return "text-emerald-500";
+    case "red":
+      return "text-rose-500";
+    default:
+      return "text-slate-400";
+  }
+}
+
+function formatBucketTime(isoString: string, bucketSizeMinutes: number): string {
+  const date = new Date(isoString);
+  if (bucketSizeMinutes >= 1440) {
+    return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
+  }
+  if (bucketSizeMinutes >= 60) {
+    return date.toLocaleString(undefined, {
+      month: "short",
+      day: "numeric",
+      hour: "2-digit",
+      minute: "2-digit",
+    });
+  }
+  if (bucketSizeMinutes < 1) {
+    return date.toLocaleTimeString(undefined, {
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit",
+    });
+  }
+  return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
+}
+
+function formatLatency(ms: number): string {
+  if (ms < 1000) return `${Math.round(ms)}ms`;
+  return `${(ms / 1000).toFixed(2)}s`;
+}
+
+function formatPercentage(value: number): string {
+  return `${(value * 100).toFixed(1)}%`;
+}
+
+export function LaneChart({
+  providers,
+  bucketSizeMinutes,
+  startTime,
+  endTime,
+  onProviderClick,
+  className,
+}: LaneChartProps) {
+  const t = useTranslations("dashboard.availability.laneChart");
+
+  // Generate unified time buckets
+  const unifiedBuckets = useMemo(() => {
+    const start = new Date(startTime);
+    const end = new Date(endTime);
+    const bucketSizeMs = bucketSizeMinutes * 60 * 1000;
+
+    const buckets: string[] = [];
+    let current = new Date(Math.floor(start.getTime() / bucketSizeMs) * bucketSizeMs);
+
+    while (current.getTime() < end.getTime()) {
+      buckets.push(current.toISOString());
+      current = new Date(current.getTime() + bucketSizeMs);
+    }
+
+    return buckets;
+  }, [startTime, endTime, bucketSizeMinutes]);
+
+  // Generate time labels (show ~7 labels)
+  const timeLabels = useMemo(() => {
+    if (unifiedBuckets.length === 0) return [];
+    const step = Math.max(1, Math.floor(unifiedBuckets.length / 6));
+    const labels: { position: number; label: string }[] = [];
+
+    for (let i = 0; i < unifiedBuckets.length; i += step) {
+      labels.push({
+        position: (i / unifiedBuckets.length) * 100,
+        label: formatBucketTime(unifiedBuckets[i], bucketSizeMinutes),
+      });
+    }
+
+    return labels;
+  }, [unifiedBuckets, bucketSizeMinutes]);
+
+  const getBucketData = (
+    provider: ProviderAvailabilitySummary,
+    bucketStart: string
+  ): TimeBucketMetrics | null => {
+    return provider.timeBuckets.find((b) => b.bucketStart === bucketStart) || null;
+  };
+
+  if (providers.length === 0) {
+    return <div className="text-center text-muted-foreground py-12">{t("noData")}</div>;
+  }
+
+  return (
+    <TooltipProvider>
+      <div className={cn("space-y-1", className)}>
+        {/* Time labels header */}
+        <div className="flex items-center gap-4 mb-4">
+          <div className="w-32 md:w-40 shrink-0" />
+          <div className="flex-1 relative h-6">
+            {timeLabels.map((label, i) => (
+              <span
+                key={i}
+                className="absolute text-xs font-mono text-muted-foreground transform -translate-x-1/2"
+                style={{ left: `${label.position}%` }}
+              >
+                {label.label}
+              </span>
+            ))}
+          </div>
+          <div className="w-24 md:w-28 shrink-0" />
+        </div>
+
+        {/* Provider lanes */}
+        {providers.map((provider) => {
+          const isHighVolume = provider.totalRequests >= HIGH_VOLUME_THRESHOLD;
+
+          return (
+            <div
+              key={provider.providerId}
+              className={cn(
+                "flex items-center gap-4 py-2 px-2 rounded-lg",
+                "hover:bg-muted/30 transition-colors",
+                onProviderClick && "cursor-pointer"
+              )}
+              onClick={() => onProviderClick?.(provider.providerId)}
+            >
+              {/* Provider info */}
+              <div className="w-32 md:w-40 shrink-0 flex flex-col gap-1">
+                <div className="flex items-center gap-2">
+                  <span
+                    className={cn(
+                      "w-2 h-2 rounded-full",
+                      provider.currentStatus === "green" && "bg-emerald-500",
+                      provider.currentStatus === "red" && "bg-rose-500",
+                      provider.currentStatus === "unknown" && "bg-slate-400"
+                    )}
+                  />
+                  <span className="font-medium text-sm truncate" title={provider.providerName}>
+                    {provider.providerName}
+                  </span>
+                </div>
+                <div className="flex items-center gap-2 pl-4">
+                  <ConfidenceBadge requestCount={provider.totalRequests} />
+                  <span className="text-xs text-muted-foreground">
+                    {isHighVolume ? t("denseData") : t("sparseData")}
+                  </span>
+                </div>
+              </div>
+
+              {/* Lane visualization */}
+              <div className="flex-1 h-10 bg-muted/20 dark:bg-black/40 rounded relative overflow-hidden border border-border/30">
+                {/* Grid lines */}
+                <div className="absolute inset-0 grid grid-cols-6 divide-x divide-border/20 pointer-events-none" />
+
+                {/* Data visualization */}
+                <div className="absolute inset-0 flex items-center px-1">
+                  {isHighVolume ? (
+                    // High volume: solid bars
+                    <div className="flex items-end gap-px w-full h-8">
+                      {unifiedBuckets.map((bucketStart) => {
+                        const bucket = getBucketData(provider, bucketStart);
+                        const hasData = bucket !== null && bucket.totalRequests > 0;
+                        const score = hasData ? bucket.availabilityScore : 0;
+                        const height = hasData
+                          ? Math.max(20, Math.min(100, bucket.totalRequests / 2))
+                          : 0;
+
+                        return (
+                          <Tooltip key={bucketStart}>
+                            <TooltipTrigger asChild>
+                              <div
+                                className={cn(
+                                  "flex-1 rounded-sm transition-all hover:opacity-80 cursor-crosshair",
+                                  hasData ? getAvailabilityColor(score, hasData) : "bg-transparent"
+                                )}
+                                style={{ height: hasData ? `${height}%` : "2px" }}
+                              />
+                            </TooltipTrigger>
+                            <TooltipContent side="top" className="max-w-xs">
+                              <BucketTooltip
+                                bucketStart={bucketStart}
+                                bucket={bucket}
+                                bucketSizeMinutes={bucketSizeMinutes}
+                              />
+                            </TooltipContent>
+                          </Tooltip>
+                        );
+                      })}
+                    </div>
+                  ) : (
+                    // Low volume: scatter dots
+                    <div className="relative w-full h-full">
+                      {unifiedBuckets.map((bucketStart, index) => {
+                        const bucket = getBucketData(provider, bucketStart);
+                        const hasData = bucket !== null && bucket.totalRequests > 0;
+                        if (!hasData) return null;
+
+                        const score = bucket.availabilityScore;
+                        const size = Math.max(6, Math.min(12, bucket.totalRequests * 2));
+                        const position = (index / unifiedBuckets.length) * 100;
+
+                        return (
+                          <Tooltip key={bucketStart}>
+                            <TooltipTrigger asChild>
+                              <div
+                                className={cn(
+                                  "absolute top-1/2 -translate-y-1/2 rounded-full cursor-pointer",
+                                  "hover:scale-150 transition-transform",
+                                  "shadow-[0_0_8px_rgba(var(--primary),0.5)]",
+                                  getAvailabilityColor(score, true)
+                                )}
+                                style={{
+                                  left: `${position}%`,
+                                  width: size,
+                                  height: size,
+                                }}
+                              />
+                            </TooltipTrigger>
+                            <TooltipContent side="top" className="max-w-xs">
+                              <BucketTooltip
+                                bucketStart={bucketStart}
+                                bucket={bucket}
+                                bucketSizeMinutes={bucketSizeMinutes}
+                              />
+                            </TooltipContent>
+                          </Tooltip>
+                        );
+                      })}
+                      {/* No data indicator */}
+                      {provider.totalRequests === 0 && (
+                        <div className="absolute inset-0 flex items-center justify-center">
+                          <div className="border-t border-dashed border-slate-400/50 w-full" />
+                        </div>
+                      )}
+                    </div>
+                  )}
+                </div>
+              </div>
+
+              {/* Summary stats */}
+              <div className="w-24 md:w-28 shrink-0 text-right">
+                <div
+                  className={cn(
+                    "font-mono text-sm font-medium",
+                    getStatusColor(provider.currentStatus)
+                  )}
+                >
+                  {provider.currentStatus === "unknown"
+                    ? t("noData")
+                    : formatPercentage(provider.currentAvailability)}
+                </div>
+                <div className="text-xs text-muted-foreground">
+                  {provider.totalRequests > 0
+                    ? t("requests", { count: provider.totalRequests.toLocaleString() })
+                    : t("noRequests")}
+                </div>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+    </TooltipProvider>
+  );
+}
+
+function BucketTooltip({
+  bucketStart,
+  bucket,
+  bucketSizeMinutes,
+}: {
+  bucketStart: string;
+  bucket: TimeBucketMetrics | null;
+  bucketSizeMinutes: number;
+}) {
+  const t = useTranslations("dashboard.availability.laneChart");
+  const hasData = bucket !== null && bucket.totalRequests > 0;
+
+  return (
+    <div className="text-sm space-y-1">
+      <div className="font-medium">{formatBucketTime(bucketStart, bucketSizeMinutes)}</div>
+      {hasData && bucket ? (
+        <>
+          <div>{t("requests", { count: bucket.totalRequests })}</div>
+          <div>{t("availability", { value: formatPercentage(bucket.availabilityScore) })}</div>
+          <div>
+            {t("latency")}: {formatLatency(bucket.avgLatencyMs)}
+          </div>
+          <div className="flex gap-2 text-xs">
+            <span className="text-emerald-500">OK: {bucket.greenCount}</span>
+            <span className="text-rose-500">ERR: {bucket.redCount}</span>
+          </div>
+        </>
+      ) : (
+        <div className="text-muted-foreground">{t("noData")}</div>
+      )}
+    </div>
+  );
+}

+ 203 - 0
src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx

@@ -0,0 +1,203 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { useMemo } from "react";
+import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts";
+import {
+  type ChartConfig,
+  ChartContainer,
+  ChartTooltip,
+  ChartTooltipContent,
+} from "@/components/ui/chart";
+import type { ProviderAvailabilitySummary } from "@/lib/availability";
+import { cn } from "@/lib/utils";
+
+interface LatencyChartProps {
+  providers: ProviderAvailabilitySummary[];
+  className?: string;
+}
+
+const chartConfig = {
+  p50: {
+    label: "P50",
+    color: "hsl(var(--chart-2))",
+  },
+  p95: {
+    label: "P95",
+    color: "hsl(var(--chart-4))",
+  },
+  p99: {
+    label: "P99",
+    color: "hsl(var(--chart-1))",
+  },
+} satisfies ChartConfig;
+
+export function LatencyChart({ providers, className }: LatencyChartProps) {
+  const t = useTranslations("dashboard.availability.latencyChart");
+
+  // Aggregate latency data across all providers
+  const chartData = useMemo(() => {
+    // Collect all unique bucket times
+    const bucketMap = new Map<string, { p50: number[]; p95: number[]; p99: number[] }>();
+
+    for (const provider of providers) {
+      for (const bucket of provider.timeBuckets) {
+        if (bucket.totalRequests === 0) continue;
+
+        const existing = bucketMap.get(bucket.bucketStart) || {
+          p50: [],
+          p95: [],
+          p99: [],
+        };
+
+        existing.p50.push(bucket.p50LatencyMs);
+        existing.p95.push(bucket.p95LatencyMs);
+        existing.p99.push(bucket.p99LatencyMs);
+
+        bucketMap.set(bucket.bucketStart, existing);
+      }
+    }
+
+    // Calculate averages and format for chart
+    return Array.from(bucketMap.entries())
+      .map(([time, values]) => ({
+        time,
+        timestamp: new Date(time).getTime(),
+        p50: values.p50.length > 0 ? values.p50.reduce((a, b) => a + b, 0) / values.p50.length : 0,
+        p95: values.p95.length > 0 ? values.p95.reduce((a, b) => a + b, 0) / values.p95.length : 0,
+        p99: values.p99.length > 0 ? values.p99.reduce((a, b) => a + b, 0) / values.p99.length : 0,
+      }))
+      .sort((a, b) => a.timestamp - b.timestamp);
+  }, [providers]);
+
+  if (chartData.length === 0) {
+    return (
+      <div
+        className={cn(
+          "flex items-center justify-center h-[300px] text-muted-foreground",
+          className
+        )}
+      >
+        {t("noData")}
+      </div>
+    );
+  }
+
+  const formatTime = (time: string) => {
+    const date = new Date(time);
+    return date.toLocaleTimeString(undefined, {
+      hour: "2-digit",
+      minute: "2-digit",
+    });
+  };
+
+  const formatLatency = (value: number) => {
+    if (value < 1000) return `${Math.round(value)}ms`;
+    return `${(value / 1000).toFixed(1)}s`;
+  };
+
+  return (
+    <div className={cn("w-full", className)}>
+      <div className="flex items-center justify-between mb-4">
+        <h3 className="text-sm font-medium text-muted-foreground">{t("title")}</h3>
+        <div className="flex items-center gap-4 text-xs">
+          <div className="flex items-center gap-1.5">
+            <div className="w-3 h-0.5 rounded bg-[hsl(var(--chart-2))]" />
+            <span className="text-muted-foreground">{t("p50")}</span>
+          </div>
+          <div className="flex items-center gap-1.5">
+            <div className="w-3 h-0.5 rounded bg-[hsl(var(--chart-4))]" />
+            <span className="text-muted-foreground">{t("p95")}</span>
+          </div>
+          <div className="flex items-center gap-1.5">
+            <div className="w-3 h-0.5 rounded bg-[hsl(var(--chart-1))]" />
+            <span className="text-muted-foreground">{t("p99")}</span>
+          </div>
+        </div>
+      </div>
+
+      <ChartContainer config={chartConfig} className="h-[250px] w-full">
+        <AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
+          <defs>
+            <linearGradient id="fillP50" x1="0" y1="0" x2="0" y2="1">
+              <stop offset="5%" stopColor="hsl(var(--chart-2))" stopOpacity={0.3} />
+              <stop offset="95%" stopColor="hsl(var(--chart-2))" stopOpacity={0} />
+            </linearGradient>
+            <linearGradient id="fillP95" x1="0" y1="0" x2="0" y2="1">
+              <stop offset="5%" stopColor="hsl(var(--chart-4))" stopOpacity={0.3} />
+              <stop offset="95%" stopColor="hsl(var(--chart-4))" stopOpacity={0} />
+            </linearGradient>
+            <linearGradient id="fillP99" x1="0" y1="0" x2="0" y2="1">
+              <stop offset="5%" stopColor="hsl(var(--chart-1))" stopOpacity={0.3} />
+              <stop offset="95%" stopColor="hsl(var(--chart-1))" stopOpacity={0} />
+            </linearGradient>
+          </defs>
+          <CartesianGrid strokeDasharray="3 3" vertical={false} className="stroke-border/30" />
+          <XAxis
+            dataKey="time"
+            tickLine={false}
+            axisLine={false}
+            tickMargin={8}
+            tickFormatter={formatTime}
+            className="text-xs text-muted-foreground"
+          />
+          <YAxis
+            tickLine={false}
+            axisLine={false}
+            tickMargin={8}
+            tickFormatter={formatLatency}
+            className="text-xs text-muted-foreground"
+            width={50}
+          />
+          <ChartTooltip
+            content={({ active, payload, label }) => {
+              if (!active || !payload?.length) return null;
+              return (
+                <div className="border-border/50 bg-background rounded-lg border px-2.5 py-1.5 text-xs shadow-xl">
+                  <div className="font-medium mb-1">{formatTime(label as string)}</div>
+                  <div className="space-y-1">
+                    {payload.map((item) => (
+                      <div key={item.dataKey} className="flex items-center gap-2">
+                        <div
+                          className="w-2 h-2 rounded-full"
+                          style={{ backgroundColor: item.color }}
+                        />
+                        <span className="text-muted-foreground">
+                          {chartConfig[item.dataKey as keyof typeof chartConfig]?.label ||
+                            item.dataKey}
+                          :
+                        </span>
+                        <span className="font-mono">{formatLatency(item.value as number)}</span>
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              );
+            }}
+          />
+          <Area
+            type="monotone"
+            dataKey="p50"
+            stroke="hsl(var(--chart-2))"
+            fill="url(#fillP50)"
+            strokeWidth={2}
+          />
+          <Area
+            type="monotone"
+            dataKey="p95"
+            stroke="hsl(var(--chart-4))"
+            fill="url(#fillP95)"
+            strokeWidth={2}
+          />
+          <Area
+            type="monotone"
+            dataKey="p99"
+            stroke="hsl(var(--chart-1))"
+            fill="url(#fillP99)"
+            strokeWidth={2}
+          />
+        </AreaChart>
+      </ChartContainer>
+    </div>
+  );
+}

+ 239 - 0
src/app/[locale]/dashboard/availability/_components/provider/provider-tab.tsx

@@ -0,0 +1,239 @@
+"use client";
+
+import { RefreshCw } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useMemo, useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { Skeleton } from "@/components/ui/skeleton";
+import type { AvailabilityQueryResult } from "@/lib/availability";
+import { cn } from "@/lib/utils";
+import type { TimeRangeOption } from "../availability-dashboard";
+import { TimeRangeSelector } from "../shared/time-range-selector";
+import { LaneChart } from "./lane-chart";
+import { LatencyChart } from "./latency-chart";
+
+interface ProviderTabProps {
+  data: AvailabilityQueryResult | null;
+  loading: boolean;
+  refreshing: boolean;
+  error: string | null;
+  timeRange: TimeRangeOption;
+  onTimeRangeChange: (value: TimeRangeOption) => void;
+  onRefresh: () => void;
+}
+
+type SortOption = "availability" | "name" | "requests";
+
+export function ProviderTab({
+  data,
+  loading,
+  refreshing,
+  error,
+  timeRange,
+  onTimeRangeChange,
+  onRefresh,
+}: ProviderTabProps) {
+  const t = useTranslations("dashboard.availability");
+  const [sortBy, setSortBy] = useState<SortOption>("availability");
+
+  // Sort providers based on selected option
+  const sortedProviders = useMemo(() => {
+    if (!data?.providers) return [];
+
+    return [...data.providers].sort((a, b) => {
+      switch (sortBy) {
+        case "availability":
+          if (a.currentStatus === "unknown" && b.currentStatus !== "unknown") return 1;
+          if (b.currentStatus === "unknown" && a.currentStatus !== "unknown") return -1;
+          return b.currentAvailability - a.currentAvailability;
+        case "name":
+          return a.providerName.localeCompare(b.providerName);
+        case "requests":
+          return b.totalRequests - a.totalRequests;
+        default:
+          return 0;
+      }
+    });
+  }, [data?.providers, sortBy]);
+
+  if (loading) {
+    return (
+      <div className="space-y-6">
+        {/* Controls skeleton */}
+        <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
+          <div className="flex gap-4">
+            <Skeleton className="h-9 w-[200px]" />
+            <Skeleton className="h-9 w-[140px]" />
+          </div>
+          <Skeleton className="h-9 w-[100px]" />
+        </div>
+
+        {/* Lane chart skeleton */}
+        <div
+          className={cn(
+            "rounded-2xl p-6",
+            "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+            "backdrop-blur-lg",
+            "border border-border/50 dark:border-white/[0.08]"
+          )}
+        >
+          <Skeleton className="h-6 w-48 mb-6" />
+          <div className="space-y-4">
+            {[...Array(5)].map((_, i) => (
+              <div key={i} className="flex items-center gap-4">
+                <Skeleton className="h-10 w-40" />
+                <Skeleton className="h-10 flex-1" />
+                <Skeleton className="h-10 w-24" />
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {/* Latency chart skeleton */}
+        <div
+          className={cn(
+            "rounded-2xl p-6",
+            "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+            "backdrop-blur-lg",
+            "border border-border/50 dark:border-white/[0.08]"
+          )}
+        >
+          <Skeleton className="h-[250px] w-full" />
+        </div>
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <div
+        className={cn(
+          "rounded-2xl p-8 text-center",
+          "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+          "backdrop-blur-lg",
+          "border border-destructive/50"
+        )}
+      >
+        <p className="text-destructive">{error}</p>
+        <Button variant="outline" size="sm" onClick={onRefresh} className="mt-4">
+          <RefreshCw className="h-4 w-4 mr-2" />
+          {t("actions.retry")}
+        </Button>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-6">
+      {/* Controls */}
+      <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
+        <div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full sm:w-auto">
+          <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
+          <Select value={sortBy} onValueChange={(v) => setSortBy(v as SortOption)}>
+            <SelectTrigger className="w-full sm:w-[160px]">
+              <SelectValue placeholder={t("sort.label")} />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="availability">{t("sort.availability")}</SelectItem>
+              <SelectItem value="name">{t("sort.name")}</SelectItem>
+              <SelectItem value="requests">{t("sort.requests")}</SelectItem>
+            </SelectContent>
+          </Select>
+        </div>
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={onRefresh}
+          disabled={refreshing}
+          className="w-full sm:w-auto"
+        >
+          <RefreshCw className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")} />
+          {refreshing ? t("actions.refreshing") : t("actions.refresh")}
+        </Button>
+      </div>
+
+      {/* Lane Chart */}
+      <div
+        className={cn(
+          "rounded-2xl p-4 md:p-6",
+          "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+          "backdrop-blur-lg",
+          "border border-border/50 dark:border-white/[0.08]",
+          "shadow-sm",
+          refreshing && "opacity-70 transition-opacity"
+        )}
+      >
+        <div className="flex items-center justify-between mb-4">
+          <h3 className="text-lg font-semibold">{t("laneChart.title")}</h3>
+          {data && (
+            <span className="text-xs text-muted-foreground">
+              {t("heatmap.bucketSize")}: {data.bucketSizeMinutes} {t("heatmap.minutes")}
+            </span>
+          )}
+        </div>
+        {data && (
+          <LaneChart
+            providers={sortedProviders}
+            bucketSizeMinutes={data.bucketSizeMinutes}
+            startTime={data.startTime}
+            endTime={data.endTime}
+          />
+        )}
+      </div>
+
+      {/* Latency Distribution Chart */}
+      <div
+        className={cn(
+          "rounded-2xl p-4 md:p-6",
+          "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+          "backdrop-blur-lg",
+          "border border-border/50 dark:border-white/[0.08]",
+          "shadow-sm",
+          refreshing && "opacity-70 transition-opacity"
+        )}
+      >
+        {data && <LatencyChart providers={sortedProviders} />}
+      </div>
+
+      {/* Legend */}
+      <div
+        className={cn(
+          "rounded-2xl p-4",
+          "bg-card/60 dark:bg-[rgba(20,20,23,0.5)]",
+          "backdrop-blur-lg",
+          "border border-border/50 dark:border-white/[0.08]"
+        )}
+      >
+        <div className="flex flex-wrap gap-4 md:gap-6 text-sm">
+          <div className="flex items-center gap-2">
+            <div className="w-4 h-4 rounded-sm bg-emerald-500" />
+            <span className="text-muted-foreground">{t("legend.green")}</span>
+          </div>
+          <div className="flex items-center gap-2">
+            <div className="w-4 h-4 rounded-sm bg-lime-500" />
+            <span className="text-muted-foreground">{t("legend.lime")}</span>
+          </div>
+          <div className="flex items-center gap-2">
+            <div className="w-4 h-4 rounded-sm bg-orange-500" />
+            <span className="text-muted-foreground">{t("legend.orange")}</span>
+          </div>
+          <div className="flex items-center gap-2">
+            <div className="w-4 h-4 rounded-sm bg-rose-500" />
+            <span className="text-muted-foreground">{t("legend.red")}</span>
+          </div>
+          <div className="flex items-center gap-2">
+            <div className="w-4 h-4 rounded-sm bg-slate-300 dark:bg-slate-600" />
+            <span className="text-muted-foreground">{t("legend.noData")}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 66 - 0
src/app/[locale]/dashboard/availability/_components/shared/floating-probe-button.tsx

@@ -0,0 +1,66 @@
+"use client";
+
+import { Radio } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useState } from "react";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+
+interface FloatingProbeButtonProps {
+  onProbeComplete?: () => void;
+  className?: string;
+}
+
+export function FloatingProbeButton({ onProbeComplete, className }: FloatingProbeButtonProps) {
+  const t = useTranslations("dashboard.availability.actions");
+  const [isProbing, setIsProbing] = useState(false);
+
+  const handleProbeAll = async () => {
+    if (isProbing) return;
+
+    setIsProbing(true);
+    try {
+      // Trigger global probe via API
+      const res = await fetch("/api/availability/probe-all", { method: "POST" });
+      if (!res.ok) {
+        throw new Error("Probe failed");
+      }
+      toast.success(t("probeSuccess"));
+      onProbeComplete?.();
+    } catch (error) {
+      console.error("Probe all failed:", error);
+      toast.error(t("probeFailed"));
+    } finally {
+      setIsProbing(false);
+    }
+  };
+
+  return (
+    <button
+      onClick={handleProbeAll}
+      disabled={isProbing}
+      className={cn(
+        "fixed bottom-6 right-6 z-50",
+        "flex items-center gap-2 pl-4 pr-5 py-3",
+        "bg-primary hover:bg-primary/90 text-primary-foreground",
+        "rounded-full shadow-lg",
+        "transition-all duration-300 transform",
+        "hover:scale-105 hover:shadow-[0_4px_20px_rgba(var(--primary),0.4)]",
+        "active:scale-95",
+        "disabled:opacity-70 disabled:cursor-not-allowed disabled:hover:scale-100",
+        "group",
+        className
+      )}
+    >
+      <Radio
+        className={cn(
+          "h-5 w-5 transition-transform duration-500",
+          isProbing ? "animate-pulse" : "group-hover:rotate-180"
+        )}
+      />
+      <span className="text-sm font-semibold tracking-wide uppercase">
+        {isProbing ? t("probing") : t("probeAll")}
+      </span>
+    </button>
+  );
+}

+ 36 - 0
src/app/[locale]/dashboard/availability/_components/shared/time-range-selector.tsx

@@ -0,0 +1,36 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { cn } from "@/lib/utils";
+import type { TimeRangeOption } from "../availability-dashboard";
+
+interface TimeRangeSelectorProps {
+  value: TimeRangeOption;
+  onChange: (value: TimeRangeOption) => void;
+  className?: string;
+}
+
+const TIME_RANGE_OPTIONS: TimeRangeOption[] = ["15min", "1h", "6h", "24h", "7d"];
+
+export function TimeRangeSelector({ value, onChange, className }: TimeRangeSelectorProps) {
+  const t = useTranslations("dashboard.availability.timeRange");
+
+  return (
+    <div className={cn("flex gap-1 p-1 rounded-lg bg-muted/50", className)}>
+      {TIME_RANGE_OPTIONS.map((option) => (
+        <button
+          key={option}
+          onClick={() => onChange(option)}
+          className={cn(
+            "px-3 py-1.5 text-xs font-medium rounded-md transition-all duration-200",
+            value === option
+              ? "bg-background text-foreground shadow-sm"
+              : "text-muted-foreground hover:text-foreground hover:bg-background/50"
+          )}
+        >
+          {t(option)}
+        </button>
+      ))}
+    </div>
+  );
+}

+ 4 - 4
src/app/[locale]/dashboard/availability/page.tsx

@@ -5,8 +5,8 @@ import { Section } from "@/components/section";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { getSession } from "@/lib/auth";
-import { AvailabilityViewSkeleton } from "./_components/availability-skeleton";
-import { AvailabilityView } from "./_components/availability-view";
+import { AvailabilityDashboard } from "./_components/availability-dashboard";
+import { AvailabilityDashboardSkeleton } from "./_components/availability-skeleton";
 
 export const dynamic = "force-dynamic";
 
@@ -44,8 +44,8 @@ export default async function AvailabilityPage() {
   return (
     <div className="space-y-6">
       <Section title={t("availability.title")} description={t("availability.description")}>
-        <Suspense fallback={<AvailabilityViewSkeleton />}>
-          <AvailabilityView />
+        <Suspense fallback={<AvailabilityDashboardSkeleton />}>
+          <AvailabilityDashboard />
         </Suspense>
       </Section>
     </div>

+ 326 - 0
tests/unit/dashboard/availability/availability-dashboard.test.tsx

@@ -0,0 +1,326 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import { describe, expect, test } from "vitest";
+
+// Test the time range calculation logic from AvailabilityDashboard
+
+describe("AvailabilityDashboard - time range calculations", () => {
+  type TimeRangeOption = "15min" | "1h" | "6h" | "24h" | "7d";
+
+  const TIME_RANGE_MAP: Record<TimeRangeOption, number> = {
+    "15min": 15 * 60 * 1000,
+    "1h": 60 * 60 * 1000,
+    "6h": 6 * 60 * 60 * 1000,
+    "24h": 24 * 60 * 60 * 1000,
+    "7d": 7 * 24 * 60 * 60 * 1000,
+  };
+
+  const TARGET_BUCKETS = 60;
+
+  function calculateBucketSize(timeRangeMs: number): number {
+    const bucketSizeMs = timeRangeMs / TARGET_BUCKETS;
+    const bucketSizeMinutes = bucketSizeMs / (60 * 1000);
+    return Math.max(0.25, Math.round(bucketSizeMinutes * 4) / 4);
+  }
+
+  describe("TIME_RANGE_MAP values", () => {
+    test("15min should be 15 minutes in milliseconds", () => {
+      expect(TIME_RANGE_MAP["15min"]).toBe(15 * 60 * 1000);
+      expect(TIME_RANGE_MAP["15min"]).toBe(900000);
+    });
+
+    test("1h should be 1 hour in milliseconds", () => {
+      expect(TIME_RANGE_MAP["1h"]).toBe(60 * 60 * 1000);
+      expect(TIME_RANGE_MAP["1h"]).toBe(3600000);
+    });
+
+    test("6h should be 6 hours in milliseconds", () => {
+      expect(TIME_RANGE_MAP["6h"]).toBe(6 * 60 * 60 * 1000);
+      expect(TIME_RANGE_MAP["6h"]).toBe(21600000);
+    });
+
+    test("24h should be 24 hours in milliseconds", () => {
+      expect(TIME_RANGE_MAP["24h"]).toBe(24 * 60 * 60 * 1000);
+      expect(TIME_RANGE_MAP["24h"]).toBe(86400000);
+    });
+
+    test("7d should be 7 days in milliseconds", () => {
+      expect(TIME_RANGE_MAP["7d"]).toBe(7 * 24 * 60 * 60 * 1000);
+      expect(TIME_RANGE_MAP["7d"]).toBe(604800000);
+    });
+  });
+
+  describe("calculateBucketSize", () => {
+    test("should calculate bucket size for 15min range", () => {
+      const bucketSize = calculateBucketSize(TIME_RANGE_MAP["15min"]);
+      // 15min / 60 buckets = 0.25 minutes per bucket
+      expect(bucketSize).toBe(0.25);
+    });
+
+    test("should calculate bucket size for 1h range", () => {
+      const bucketSize = calculateBucketSize(TIME_RANGE_MAP["1h"]);
+      // 60min / 60 buckets = 1 minute per bucket
+      expect(bucketSize).toBe(1);
+    });
+
+    test("should calculate bucket size for 6h range", () => {
+      const bucketSize = calculateBucketSize(TIME_RANGE_MAP["6h"]);
+      // 360min / 60 buckets = 6 minutes per bucket
+      expect(bucketSize).toBe(6);
+    });
+
+    test("should calculate bucket size for 24h range", () => {
+      const bucketSize = calculateBucketSize(TIME_RANGE_MAP["24h"]);
+      // 1440min / 60 buckets = 24 minutes per bucket
+      expect(bucketSize).toBe(24);
+    });
+
+    test("should calculate bucket size for 7d range", () => {
+      const bucketSize = calculateBucketSize(TIME_RANGE_MAP["7d"]);
+      // 10080min / 60 buckets = 168 minutes per bucket
+      expect(bucketSize).toBe(168);
+    });
+
+    test("should enforce minimum bucket size of 0.25 minutes", () => {
+      // Very small time range
+      const bucketSize = calculateBucketSize(1000); // 1 second
+      expect(bucketSize).toBe(0.25);
+    });
+
+    test("should round to nearest 0.25 minutes", () => {
+      // Test rounding behavior
+      const testCases = [
+        { input: 60 * 60 * 1000 * 1.1, expected: 1 }, // ~1.1 min -> 1
+        { input: 60 * 60 * 1000 * 1.3, expected: 1.25 }, // ~1.3 min -> 1.25
+        { input: 60 * 60 * 1000 * 1.6, expected: 1.5 }, // ~1.6 min -> 1.5
+        { input: 60 * 60 * 1000 * 1.9, expected: 2 }, // ~1.9 min -> 2
+      ];
+
+      for (const { input, expected } of testCases) {
+        const result = calculateBucketSize(input);
+        expect(result).toBeCloseTo(expected, 1);
+      }
+    });
+  });
+
+  describe("time range date calculations", () => {
+    test("should calculate correct start time for each range", () => {
+      const now = new Date("2024-01-15T12:00:00Z");
+
+      for (const [range, ms] of Object.entries(TIME_RANGE_MAP)) {
+        const startTime = new Date(now.getTime() - ms);
+        const diff = now.getTime() - startTime.getTime();
+        expect(diff).toBe(ms);
+      }
+    });
+
+    test("15min range should go back 15 minutes", () => {
+      const now = new Date("2024-01-15T12:00:00Z");
+      const startTime = new Date(now.getTime() - TIME_RANGE_MAP["15min"]);
+      expect(startTime.toISOString()).toBe("2024-01-15T11:45:00.000Z");
+    });
+
+    test("24h range should go back 24 hours", () => {
+      const now = new Date("2024-01-15T12:00:00Z");
+      const startTime = new Date(now.getTime() - TIME_RANGE_MAP["24h"]);
+      expect(startTime.toISOString()).toBe("2024-01-14T12:00:00.000Z");
+    });
+
+    test("7d range should go back 7 days", () => {
+      const now = new Date("2024-01-15T12:00:00Z");
+      const startTime = new Date(now.getTime() - TIME_RANGE_MAP["7d"]);
+      expect(startTime.toISOString()).toBe("2024-01-08T12:00:00.000Z");
+    });
+  });
+});
+
+describe("AvailabilityDashboard - overview metrics calculations", () => {
+  interface TimeBucket {
+    avgLatencyMs: number;
+    redCount: number;
+    totalRequests: number;
+  }
+
+  interface Provider {
+    timeBuckets: TimeBucket[];
+    totalRequests: number;
+    currentStatus: "green" | "yellow" | "red" | "unknown";
+  }
+
+  function calculateAvgLatency(providers: Provider[]): number {
+    if (providers.length === 0) return 0;
+
+    const providersWithLatency = providers.filter((p) =>
+      p.timeBuckets.some((b) => b.avgLatencyMs > 0)
+    );
+
+    if (providersWithLatency.length === 0) return 0;
+
+    const totalLatency = providersWithLatency.reduce((sum, p) => {
+      const latencies = p.timeBuckets.filter((b) => b.avgLatencyMs > 0).map((b) => b.avgLatencyMs);
+      if (latencies.length === 0) return sum;
+      return sum + latencies.reduce((a, b) => a + b, 0) / latencies.length;
+    }, 0);
+
+    return totalLatency / providersWithLatency.length;
+  }
+
+  function calculateErrorRate(providers: Provider[]): number {
+    if (providers.length === 0) return 0;
+
+    const totalErrorRate = providers.reduce((sum, p) => {
+      const total = p.totalRequests;
+      const errors = p.timeBuckets.reduce((s, b) => s + b.redCount, 0);
+      return sum + (total > 0 ? errors / total : 0);
+    }, 0);
+
+    return totalErrorRate / providers.length;
+  }
+
+  function countByStatus(providers: Provider[], status: string): number {
+    return providers.filter((p) => p.currentStatus === status).length;
+  }
+
+  describe("calculateAvgLatency", () => {
+    test("should return 0 for empty providers", () => {
+      expect(calculateAvgLatency([])).toBe(0);
+    });
+
+    test("should calculate average latency across providers", () => {
+      const providers: Provider[] = [
+        {
+          timeBuckets: [{ avgLatencyMs: 100, redCount: 0, totalRequests: 10 }],
+          totalRequests: 10,
+          currentStatus: "green",
+        },
+        {
+          timeBuckets: [{ avgLatencyMs: 200, redCount: 0, totalRequests: 10 }],
+          totalRequests: 10,
+          currentStatus: "green",
+        },
+      ];
+      expect(calculateAvgLatency(providers)).toBe(150);
+    });
+
+    test("should ignore providers with no latency data", () => {
+      const providers: Provider[] = [
+        {
+          timeBuckets: [{ avgLatencyMs: 100, redCount: 0, totalRequests: 10 }],
+          totalRequests: 10,
+          currentStatus: "green",
+        },
+        {
+          timeBuckets: [{ avgLatencyMs: 0, redCount: 0, totalRequests: 0 }],
+          totalRequests: 0,
+          currentStatus: "unknown",
+        },
+      ];
+      expect(calculateAvgLatency(providers)).toBe(100);
+    });
+
+    test("should average multiple buckets within a provider", () => {
+      const providers: Provider[] = [
+        {
+          timeBuckets: [
+            { avgLatencyMs: 100, redCount: 0, totalRequests: 10 },
+            { avgLatencyMs: 200, redCount: 0, totalRequests: 10 },
+            { avgLatencyMs: 300, redCount: 0, totalRequests: 10 },
+          ],
+          totalRequests: 30,
+          currentStatus: "green",
+        },
+      ];
+      expect(calculateAvgLatency(providers)).toBe(200);
+    });
+  });
+
+  describe("calculateErrorRate", () => {
+    test("should return 0 for empty providers", () => {
+      expect(calculateErrorRate([])).toBe(0);
+    });
+
+    test("should calculate error rate correctly", () => {
+      const providers: Provider[] = [
+        {
+          timeBuckets: [{ avgLatencyMs: 100, redCount: 10, totalRequests: 100 }],
+          totalRequests: 100,
+          currentStatus: "green",
+        },
+      ];
+      expect(calculateErrorRate(providers)).toBe(0.1); // 10%
+    });
+
+    test("should average error rates across providers", () => {
+      const providers: Provider[] = [
+        {
+          timeBuckets: [{ avgLatencyMs: 100, redCount: 10, totalRequests: 100 }],
+          totalRequests: 100,
+          currentStatus: "green",
+        },
+        {
+          timeBuckets: [{ avgLatencyMs: 100, redCount: 20, totalRequests: 100 }],
+          totalRequests: 100,
+          currentStatus: "yellow",
+        },
+      ];
+      expect(calculateErrorRate(providers)).toBeCloseTo(0.15, 10); // (10% + 20%) / 2
+    });
+
+    test("should handle providers with zero requests", () => {
+      const providers: Provider[] = [
+        {
+          timeBuckets: [{ avgLatencyMs: 0, redCount: 0, totalRequests: 0 }],
+          totalRequests: 0,
+          currentStatus: "unknown",
+        },
+      ];
+      expect(calculateErrorRate(providers)).toBe(0);
+    });
+  });
+
+  describe("countByStatus", () => {
+    const providers: Provider[] = [
+      { timeBuckets: [], totalRequests: 100, currentStatus: "green" },
+      { timeBuckets: [], totalRequests: 100, currentStatus: "green" },
+      { timeBuckets: [], totalRequests: 50, currentStatus: "yellow" },
+      { timeBuckets: [], totalRequests: 10, currentStatus: "red" },
+      { timeBuckets: [], totalRequests: 0, currentStatus: "unknown" },
+    ];
+
+    test("should count green providers", () => {
+      expect(countByStatus(providers, "green")).toBe(2);
+    });
+
+    test("should count yellow providers", () => {
+      expect(countByStatus(providers, "yellow")).toBe(1);
+    });
+
+    test("should count red providers", () => {
+      expect(countByStatus(providers, "red")).toBe(1);
+    });
+
+    test("should count unknown providers", () => {
+      expect(countByStatus(providers, "unknown")).toBe(1);
+    });
+
+    test("should return 0 for non-existent status", () => {
+      expect(countByStatus(providers, "nonexistent")).toBe(0);
+    });
+  });
+});
+
+describe("AvailabilityDashboard - auto-refresh intervals", () => {
+  test("provider tab should use 30 second interval", () => {
+    const activeTab = "provider";
+    const interval = activeTab === "provider" ? 30000 : 10000;
+    expect(interval).toBe(30000);
+  });
+
+  test("endpoint tab should use 10 second interval", () => {
+    const activeTab = "endpoint";
+    const interval = activeTab === "provider" ? 30000 : 10000;
+    expect(interval).toBe(10000);
+  });
+});

+ 136 - 0
tests/unit/dashboard/availability/confidence-badge.test.tsx

@@ -0,0 +1,136 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import { describe, expect, test } from "vitest";
+
+// Test the pure functions extracted from ConfidenceBadge component
+// These determine confidence levels based on request counts
+
+describe("ConfidenceBadge - getConfidenceLevel logic", () => {
+  type ConfidenceLevel = "low" | "medium" | "high";
+
+  function getConfidenceLevel(
+    count: number,
+    thresholds: { low: number; medium: number; high: number }
+  ): ConfidenceLevel {
+    if (count >= thresholds.high) return "high";
+    if (count >= thresholds.medium) return "medium";
+    return "low";
+  }
+
+  describe("with default thresholds (low: 10, medium: 50, high: 200)", () => {
+    const thresholds = { low: 10, medium: 50, high: 200 };
+
+    test("should return low confidence for counts < medium threshold", () => {
+      expect(getConfidenceLevel(0, thresholds)).toBe("low");
+      expect(getConfidenceLevel(5, thresholds)).toBe("low");
+      expect(getConfidenceLevel(10, thresholds)).toBe("low");
+      expect(getConfidenceLevel(49, thresholds)).toBe("low");
+    });
+
+    test("should return medium confidence for counts >= medium and < high", () => {
+      expect(getConfidenceLevel(50, thresholds)).toBe("medium");
+      expect(getConfidenceLevel(100, thresholds)).toBe("medium");
+      expect(getConfidenceLevel(199, thresholds)).toBe("medium");
+    });
+
+    test("should return high confidence for counts >= high threshold", () => {
+      expect(getConfidenceLevel(200, thresholds)).toBe("high");
+      expect(getConfidenceLevel(500, thresholds)).toBe("high");
+      expect(getConfidenceLevel(1000, thresholds)).toBe("high");
+    });
+  });
+
+  describe("with custom thresholds", () => {
+    const customThresholds = { low: 5, medium: 20, high: 100 };
+
+    test("should respect custom thresholds", () => {
+      expect(getConfidenceLevel(4, customThresholds)).toBe("low");
+      expect(getConfidenceLevel(5, customThresholds)).toBe("low");
+      expect(getConfidenceLevel(19, customThresholds)).toBe("low");
+      expect(getConfidenceLevel(20, customThresholds)).toBe("medium");
+      expect(getConfidenceLevel(99, customThresholds)).toBe("medium");
+      expect(getConfidenceLevel(100, customThresholds)).toBe("high");
+    });
+  });
+
+  describe("edge cases", () => {
+    const thresholds = { low: 10, medium: 50, high: 200 };
+
+    test("should handle zero requests", () => {
+      expect(getConfidenceLevel(0, thresholds)).toBe("low");
+    });
+
+    test("should handle negative values (treat as low)", () => {
+      expect(getConfidenceLevel(-1, thresholds)).toBe("low");
+      expect(getConfidenceLevel(-100, thresholds)).toBe("low");
+    });
+
+    test("should handle very large values", () => {
+      expect(getConfidenceLevel(1000000, thresholds)).toBe("high");
+    });
+
+    test("should handle exact threshold boundaries", () => {
+      // At exactly medium threshold
+      expect(getConfidenceLevel(50, thresholds)).toBe("medium");
+      // At exactly high threshold
+      expect(getConfidenceLevel(200, thresholds)).toBe("high");
+    });
+  });
+});
+
+describe("ConfidenceBadge - visual configuration", () => {
+  const confidenceConfig = {
+    low: {
+      bars: 1,
+      color: "bg-slate-400",
+      bgColor: "bg-slate-400/10",
+      borderStyle: "border-dashed border-slate-400/50",
+    },
+    medium: {
+      bars: 2,
+      color: "bg-amber-500",
+      bgColor: "bg-amber-500/10",
+      borderStyle: "border-solid border-amber-500/50",
+    },
+    high: {
+      bars: 3,
+      color: "bg-emerald-500",
+      bgColor: "bg-emerald-500/10",
+      borderStyle: "border-solid border-emerald-500/50",
+    },
+  };
+
+  test("low confidence should show 1 bar with dashed border", () => {
+    expect(confidenceConfig.low.bars).toBe(1);
+    expect(confidenceConfig.low.borderStyle).toContain("border-dashed");
+  });
+
+  test("medium confidence should show 2 bars with solid border", () => {
+    expect(confidenceConfig.medium.bars).toBe(2);
+    expect(confidenceConfig.medium.borderStyle).toContain("border-solid");
+  });
+
+  test("high confidence should show 3 bars with solid border", () => {
+    expect(confidenceConfig.high.bars).toBe(3);
+    expect(confidenceConfig.high.borderStyle).toContain("border-solid");
+  });
+
+  test("each level should have distinct colors", () => {
+    expect(confidenceConfig.low.color).not.toBe(confidenceConfig.medium.color);
+    expect(confidenceConfig.medium.color).not.toBe(confidenceConfig.high.color);
+    expect(confidenceConfig.low.color).not.toBe(confidenceConfig.high.color);
+  });
+
+  test("bar heights should increase progressively", () => {
+    // Bar heights are calculated as bar * 4px
+    const lowMaxHeight = confidenceConfig.low.bars * 4;
+    const mediumMaxHeight = confidenceConfig.medium.bars * 4;
+    const highMaxHeight = confidenceConfig.high.bars * 4;
+
+    expect(lowMaxHeight).toBe(4);
+    expect(mediumMaxHeight).toBe(8);
+    expect(highMaxHeight).toBe(12);
+  });
+});

+ 174 - 0
tests/unit/dashboard/availability/gauge-card.test.tsx

@@ -0,0 +1,174 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import { describe, expect, test } from "vitest";
+
+// Test the pure functions extracted from GaugeCard component
+// These are the core logic that determines gauge colors and trend indicators
+
+describe("GaugeCard - getGaugeColor logic", () => {
+  // Replicate the getGaugeColor function logic for testing
+  function getGaugeColor(
+    value: number,
+    thresholds: { warning: number; critical: number },
+    invertColors: boolean
+  ): string {
+    if (invertColors) {
+      // For metrics where lower is better (error rate, latency)
+      if (value <= thresholds.critical) return "text-emerald-500";
+      if (value <= thresholds.warning) return "text-amber-500";
+      return "text-rose-500";
+    }
+    // For metrics where higher is better (availability)
+    if (value >= thresholds.warning) return "text-emerald-500";
+    if (value >= thresholds.critical) return "text-amber-500";
+    return "text-rose-500";
+  }
+
+  describe("normal metrics (higher is better)", () => {
+    const thresholds = { warning: 80, critical: 50 };
+    const invertColors = false;
+
+    test("should return green for values >= warning threshold", () => {
+      expect(getGaugeColor(100, thresholds, invertColors)).toBe("text-emerald-500");
+      expect(getGaugeColor(95, thresholds, invertColors)).toBe("text-emerald-500");
+      expect(getGaugeColor(80, thresholds, invertColors)).toBe("text-emerald-500");
+    });
+
+    test("should return amber for values between critical and warning", () => {
+      expect(getGaugeColor(79, thresholds, invertColors)).toBe("text-amber-500");
+      expect(getGaugeColor(65, thresholds, invertColors)).toBe("text-amber-500");
+      expect(getGaugeColor(50, thresholds, invertColors)).toBe("text-amber-500");
+    });
+
+    test("should return red for values < critical threshold", () => {
+      expect(getGaugeColor(49, thresholds, invertColors)).toBe("text-rose-500");
+      expect(getGaugeColor(25, thresholds, invertColors)).toBe("text-rose-500");
+      expect(getGaugeColor(0, thresholds, invertColors)).toBe("text-rose-500");
+    });
+  });
+
+  describe("inverted metrics (lower is better)", () => {
+    const thresholds = { warning: 10, critical: 5 };
+    const invertColors = true;
+
+    test("should return green for values <= critical threshold", () => {
+      expect(getGaugeColor(0, thresholds, invertColors)).toBe("text-emerald-500");
+      expect(getGaugeColor(3, thresholds, invertColors)).toBe("text-emerald-500");
+      expect(getGaugeColor(5, thresholds, invertColors)).toBe("text-emerald-500");
+    });
+
+    test("should return amber for values between critical and warning", () => {
+      expect(getGaugeColor(6, thresholds, invertColors)).toBe("text-amber-500");
+      expect(getGaugeColor(8, thresholds, invertColors)).toBe("text-amber-500");
+      expect(getGaugeColor(10, thresholds, invertColors)).toBe("text-amber-500");
+    });
+
+    test("should return red for values > warning threshold", () => {
+      expect(getGaugeColor(11, thresholds, invertColors)).toBe("text-rose-500");
+      expect(getGaugeColor(50, thresholds, invertColors)).toBe("text-rose-500");
+      expect(getGaugeColor(100, thresholds, invertColors)).toBe("text-rose-500");
+    });
+  });
+});
+
+describe("GaugeCard - getTrendColor logic", () => {
+  function getTrendColor(direction: "up" | "down" | "stable", invertColors: boolean) {
+    if (direction === "stable") return "text-muted-foreground bg-muted/50";
+    if (invertColors) {
+      // For inverted metrics, down is good
+      return direction === "down"
+        ? "text-emerald-500 bg-emerald-500/10"
+        : "text-rose-500 bg-rose-500/10";
+    }
+    // For normal metrics, up is good
+    return direction === "up"
+      ? "text-emerald-500 bg-emerald-500/10"
+      : "text-rose-500 bg-rose-500/10";
+  }
+
+  describe("normal metrics", () => {
+    const invertColors = false;
+
+    test("should return green for upward trend", () => {
+      expect(getTrendColor("up", invertColors)).toBe("text-emerald-500 bg-emerald-500/10");
+    });
+
+    test("should return red for downward trend", () => {
+      expect(getTrendColor("down", invertColors)).toBe("text-rose-500 bg-rose-500/10");
+    });
+
+    test("should return muted for stable trend", () => {
+      expect(getTrendColor("stable", invertColors)).toBe("text-muted-foreground bg-muted/50");
+    });
+  });
+
+  describe("inverted metrics", () => {
+    const invertColors = true;
+
+    test("should return green for downward trend (lower is better)", () => {
+      expect(getTrendColor("down", invertColors)).toBe("text-emerald-500 bg-emerald-500/10");
+    });
+
+    test("should return red for upward trend (higher is worse)", () => {
+      expect(getTrendColor("up", invertColors)).toBe("text-rose-500 bg-rose-500/10");
+    });
+
+    test("should return muted for stable trend", () => {
+      expect(getTrendColor("stable", invertColors)).toBe("text-muted-foreground bg-muted/50");
+    });
+  });
+});
+
+describe("GaugeCard - SVG calculations", () => {
+  const sizeConfig = {
+    sm: { gauge: 64, stroke: 4 },
+    md: { gauge: 80, stroke: 5 },
+    lg: { gauge: 96, stroke: 6 },
+  };
+
+  test("should calculate correct radius for each size", () => {
+    for (const [size, config] of Object.entries(sizeConfig)) {
+      const radius = (config.gauge - config.stroke) / 2;
+      expect(radius).toBeGreaterThan(0);
+      // Radius should be less than half the gauge size
+      expect(radius).toBeLessThan(config.gauge / 2);
+    }
+  });
+
+  test("should calculate correct circumference", () => {
+    const config = sizeConfig.md;
+    const radius = (config.gauge - config.stroke) / 2;
+    const circumference = 2 * Math.PI * radius;
+    expect(circumference).toBeCloseTo(2 * Math.PI * 37.5, 2);
+  });
+
+  test("should calculate correct offset for different values", () => {
+    const config = sizeConfig.md;
+    const radius = (config.gauge - config.stroke) / 2;
+    const circumference = 2 * Math.PI * radius;
+
+    // 0% should have full offset (empty gauge)
+    const offset0 = circumference - (0 / 100) * circumference;
+    expect(offset0).toBe(circumference);
+
+    // 100% should have zero offset (full gauge)
+    const offset100 = circumference - (100 / 100) * circumference;
+    expect(offset100).toBe(0);
+
+    // 50% should have half offset
+    const offset50 = circumference - (50 / 100) * circumference;
+    expect(offset50).toBeCloseTo(circumference / 2, 2);
+  });
+
+  test("should clamp values between 0 and 100", () => {
+    const normalizeValue = (value: number) => Math.min(Math.max(value, 0), 100);
+
+    expect(normalizeValue(-10)).toBe(0);
+    expect(normalizeValue(0)).toBe(0);
+    expect(normalizeValue(50)).toBe(50);
+    expect(normalizeValue(100)).toBe(100);
+    expect(normalizeValue(150)).toBe(100);
+  });
+});

+ 247 - 0
tests/unit/dashboard/availability/probe-terminal.test.tsx

@@ -0,0 +1,247 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import { describe, expect, test } from "vitest";
+
+// Test the pure functions extracted from ProbeTerminal component
+// These handle log formatting, filtering, and status determination
+
+describe("ProbeTerminal - formatTime", () => {
+  function formatTime(date: Date | string): string {
+    const d = typeof date === "string" ? new Date(date) : date;
+    return d.toLocaleTimeString(undefined, {
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit",
+    });
+  }
+
+  test("should format Date object correctly", () => {
+    const date = new Date("2024-01-15T10:30:45Z");
+    const result = formatTime(date);
+    // Result format depends on locale, but should contain time components
+    expect(result).toMatch(/\d{1,2}:\d{2}:\d{2}/);
+  });
+
+  test("should format ISO string correctly", () => {
+    const result = formatTime("2024-01-15T10:30:45Z");
+    expect(result).toMatch(/\d{1,2}:\d{2}:\d{2}/);
+  });
+
+  test("should handle different date formats", () => {
+    const result1 = formatTime("2024-01-15T00:00:00Z");
+    const result2 = formatTime("2024-12-31T23:59:59Z");
+    expect(result1).toMatch(/\d{1,2}:\d{2}:\d{2}/);
+    expect(result2).toMatch(/\d{1,2}:\d{2}:\d{2}/);
+  });
+});
+
+describe("ProbeTerminal - formatLatency", () => {
+  function formatLatency(ms: number | null): string {
+    if (ms === null) return "-";
+    if (ms < 1000) return `${Math.round(ms)}ms`;
+    return `${(ms / 1000).toFixed(2)}s`;
+  }
+
+  test("should return dash for null values", () => {
+    expect(formatLatency(null)).toBe("-");
+  });
+
+  test("should format milliseconds for values < 1000", () => {
+    expect(formatLatency(0)).toBe("0ms");
+    expect(formatLatency(100)).toBe("100ms");
+    expect(formatLatency(500)).toBe("500ms");
+    expect(formatLatency(999)).toBe("999ms");
+  });
+
+  test("should format seconds for values >= 1000", () => {
+    expect(formatLatency(1000)).toBe("1.00s");
+    expect(formatLatency(1500)).toBe("1.50s");
+    expect(formatLatency(2345)).toBe("2.35s");
+    expect(formatLatency(10000)).toBe("10.00s");
+  });
+
+  test("should round milliseconds to nearest integer", () => {
+    expect(formatLatency(100.4)).toBe("100ms");
+    expect(formatLatency(100.5)).toBe("101ms");
+    expect(formatLatency(100.9)).toBe("101ms");
+  });
+});
+
+describe("ProbeTerminal - getLogLevel", () => {
+  type LogLevel = "success" | "error" | "warn";
+
+  interface ProbeLog {
+    ok: boolean;
+    errorType: string | null;
+  }
+
+  function getLogLevel(log: ProbeLog): LogLevel {
+    if (log.ok) return "success";
+    if (log.errorType === "timeout") return "warn";
+    return "error";
+  }
+
+  test("should return success for ok logs", () => {
+    expect(getLogLevel({ ok: true, errorType: null })).toBe("success");
+    expect(getLogLevel({ ok: true, errorType: "timeout" })).toBe("success");
+  });
+
+  test("should return warn for timeout errors", () => {
+    expect(getLogLevel({ ok: false, errorType: "timeout" })).toBe("warn");
+  });
+
+  test("should return error for other failures", () => {
+    expect(getLogLevel({ ok: false, errorType: null })).toBe("error");
+    expect(getLogLevel({ ok: false, errorType: "connection_refused" })).toBe("error");
+    expect(getLogLevel({ ok: false, errorType: "ssl_error" })).toBe("error");
+    expect(getLogLevel({ ok: false, errorType: "dns_error" })).toBe("error");
+  });
+});
+
+describe("ProbeTerminal - log filtering", () => {
+  interface MockLog {
+    id: number;
+    errorMessage: string | null;
+    errorType: string | null;
+    statusCode: number | null;
+  }
+
+  function filterLogs(logs: MockLog[], filter: string): MockLog[] {
+    if (!filter) return logs;
+    const searchLower = filter.toLowerCase();
+    return logs.filter((log) => {
+      return (
+        log.errorMessage?.toLowerCase().includes(searchLower) ||
+        log.errorType?.toLowerCase().includes(searchLower) ||
+        log.statusCode?.toString().includes(searchLower)
+      );
+    });
+  }
+
+  const mockLogs: MockLog[] = [
+    { id: 1, errorMessage: "Connection refused", errorType: "connection_error", statusCode: null },
+    { id: 2, errorMessage: null, errorType: "timeout", statusCode: null },
+    { id: 3, errorMessage: "SSL certificate error", errorType: "ssl_error", statusCode: null },
+    { id: 4, errorMessage: null, errorType: null, statusCode: 200 },
+    { id: 5, errorMessage: null, errorType: null, statusCode: 500 },
+    { id: 6, errorMessage: "Bad Gateway", errorType: "http_error", statusCode: 502 },
+  ];
+
+  test("should return all logs when filter is empty", () => {
+    expect(filterLogs(mockLogs, "")).toHaveLength(6);
+    expect(filterLogs(mockLogs, "")).toEqual(mockLogs);
+  });
+
+  test("should filter by error message", () => {
+    const result = filterLogs(mockLogs, "connection");
+    expect(result).toHaveLength(1);
+    expect(result[0].id).toBe(1);
+  });
+
+  test("should filter by error type", () => {
+    const result = filterLogs(mockLogs, "timeout");
+    expect(result).toHaveLength(1);
+    expect(result[0].id).toBe(2);
+  });
+
+  test("should filter by status code", () => {
+    const result = filterLogs(mockLogs, "500");
+    expect(result).toHaveLength(1);
+    expect(result[0].id).toBe(5);
+  });
+
+  test("should be case insensitive", () => {
+    const result1 = filterLogs(mockLogs, "SSL");
+    const result2 = filterLogs(mockLogs, "ssl");
+    expect(result1).toHaveLength(1);
+    expect(result2).toHaveLength(1);
+    expect(result1[0].id).toBe(result2[0].id);
+  });
+
+  test("should match partial strings", () => {
+    const result = filterLogs(mockLogs, "error");
+    // Should match: connection_error, ssl_error, http_error, and "SSL certificate error"
+    expect(result.length).toBeGreaterThan(0);
+  });
+
+  test("should return empty array when no matches", () => {
+    const result = filterLogs(mockLogs, "nonexistent");
+    expect(result).toHaveLength(0);
+  });
+});
+
+describe("ProbeTerminal - levelConfig", () => {
+  const levelConfig = {
+    success: {
+      label: "OK",
+      color: "text-emerald-500",
+      bgColor: "bg-emerald-500/5",
+      borderColor: "border-l-emerald-500",
+    },
+    error: {
+      label: "FAIL",
+      color: "text-rose-500",
+      bgColor: "bg-rose-500/5",
+      borderColor: "border-l-rose-500",
+    },
+    warn: {
+      label: "WARN",
+      color: "text-amber-500",
+      bgColor: "bg-amber-500/5",
+      borderColor: "border-l-amber-500",
+    },
+  };
+
+  test("success level should have green colors", () => {
+    expect(levelConfig.success.color).toContain("emerald");
+    expect(levelConfig.success.bgColor).toContain("emerald");
+    expect(levelConfig.success.borderColor).toContain("emerald");
+  });
+
+  test("error level should have red colors", () => {
+    expect(levelConfig.error.color).toContain("rose");
+    expect(levelConfig.error.bgColor).toContain("rose");
+    expect(levelConfig.error.borderColor).toContain("rose");
+  });
+
+  test("warn level should have amber colors", () => {
+    expect(levelConfig.warn.color).toContain("amber");
+    expect(levelConfig.warn.bgColor).toContain("amber");
+    expect(levelConfig.warn.borderColor).toContain("amber");
+  });
+
+  test("each level should have distinct labels", () => {
+    expect(levelConfig.success.label).toBe("OK");
+    expect(levelConfig.error.label).toBe("FAIL");
+    expect(levelConfig.warn.label).toBe("WARN");
+  });
+});
+
+describe("ProbeTerminal - maxLines slicing", () => {
+  function sliceLogs<T>(logs: T[], maxLines: number): T[] {
+    return logs.slice(-maxLines);
+  }
+
+  test("should return all logs when count <= maxLines", () => {
+    const logs = [1, 2, 3, 4, 5];
+    expect(sliceLogs(logs, 10)).toEqual([1, 2, 3, 4, 5]);
+    expect(sliceLogs(logs, 5)).toEqual([1, 2, 3, 4, 5]);
+  });
+
+  test("should return last N logs when count > maxLines", () => {
+    const logs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+    expect(sliceLogs(logs, 5)).toEqual([6, 7, 8, 9, 10]);
+    expect(sliceLogs(logs, 3)).toEqual([8, 9, 10]);
+  });
+
+  test("should handle empty array", () => {
+    expect(sliceLogs([], 10)).toEqual([]);
+  });
+
+  test("should handle maxLines of 1", () => {
+    const logs = [1, 2, 3];
+    expect(sliceLogs(logs, 1)).toEqual([3]);
+  });
+});