14a32bdc0d
- Unified monorepo with backend (Express), frontend (Next.js), and devops - Backend: ESLint, Prettier, Jest tests (3 passing), health endpoint, .env.example - Frontend: Fixed build errors, fixed all lint errors (0 remaining), tests passing - DevOps: Docker Compose with PostgreSQL, backend, frontend + healthchecks - CI/CD: 3 GitHub Actions workflows (backend, frontend, docker integration) - DX: Husky pre-commit hooks with smart change detection - Docs: Root README with architecture, CONTRIBUTING.md, PR template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
431 lines
15 KiB
TypeScript
431 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { DashboardLayout } from "@/components/layouts/DashboardLayout";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/layout/Card";
|
|
import { Button } from "@/components/ui/forms/Button";
|
|
import { Badge } from "@/components/ui/layout/Badge";
|
|
import {
|
|
Activity,
|
|
CheckCircle,
|
|
XCircle,
|
|
Clock,
|
|
AlertTriangle,
|
|
Play,
|
|
Pause,
|
|
Settings,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Zap,
|
|
Loader2,
|
|
} from "lucide-react";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { supabase } from "@/lib/supabase";
|
|
import { logError, getUserFriendlyErrorMessage, extractSupabaseErrorInfo } from "@/utils/errorUtils";
|
|
|
|
interface MonitoringStatus {
|
|
id: string;
|
|
website_name: string;
|
|
website_url: string;
|
|
is_monitoring: boolean;
|
|
last_check: string;
|
|
status: "up" | "down" | "warning";
|
|
response_time: number;
|
|
uptime_percentage: number;
|
|
incidents_count: number;
|
|
created_at: string;
|
|
}
|
|
|
|
interface UptimeMetric {
|
|
website_id: string;
|
|
timestamp: string;
|
|
status: "up" | "down" | "warning";
|
|
response_time: number;
|
|
error_message?: string;
|
|
}
|
|
|
|
export default function MonitoringPage() {
|
|
const { userDetails } = useAuth();
|
|
const [websites, setWebsites] = useState<MonitoringStatus[]>([]);
|
|
const [recentChecks, setRecentChecks] = useState<UptimeMetric[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [updating, setUpdating] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (userDetails?.organization_id) {
|
|
loadMonitoringData();
|
|
// Set up real-time updates
|
|
const interval = setInterval(loadMonitoringData, 30000); // Update every 30 seconds
|
|
return () => clearInterval(interval);
|
|
}
|
|
}, [userDetails]);
|
|
|
|
const loadMonitoringData = async () => {
|
|
if (!userDetails?.organization_id) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Fetch websites with monitoring status
|
|
const { data: websitesData, error: websitesError } = await supabase
|
|
.from("websites")
|
|
.select(`
|
|
id,
|
|
name,
|
|
base_url,
|
|
is_active,
|
|
created_at,
|
|
uptime_checks (
|
|
id,
|
|
status,
|
|
response_time,
|
|
checked_at,
|
|
error_message
|
|
)
|
|
`)
|
|
.eq("organization_id", userDetails.organization_id)
|
|
.eq("is_active", true)
|
|
.order("created_at", { ascending: false });
|
|
|
|
if (websitesError) throw websitesError;
|
|
|
|
// Process monitoring data
|
|
const monitoringData: MonitoringStatus[] = websitesData?.map((website: any) => {
|
|
const checks = website.uptime_checks || [];
|
|
const recentChecks = checks
|
|
.filter((check: any) => {
|
|
const checkDate = new Date(check.checked_at);
|
|
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
return checkDate >= dayAgo;
|
|
})
|
|
.sort((a: any, b: any) => new Date(b.checked_at).getTime() - new Date(a.checked_at).getTime());
|
|
|
|
const latestCheck = recentChecks[0];
|
|
const upChecks = recentChecks.filter((check: any) => check.status === "up").length;
|
|
const totalChecks = recentChecks.length;
|
|
const uptimePercentage = totalChecks > 0 ? Math.round((upChecks / totalChecks) * 100) : 0;
|
|
const incidents = recentChecks.filter((check: any) => check.status === "down").length;
|
|
|
|
return {
|
|
id: website.id,
|
|
website_name: website.name,
|
|
website_url: website.base_url,
|
|
is_monitoring: true, // Assume monitoring is enabled for active websites
|
|
last_check: latestCheck?.checked_at || website.created_at,
|
|
status: latestCheck?.status || "warning",
|
|
response_time: latestCheck?.response_time || 0,
|
|
uptime_percentage: uptimePercentage,
|
|
incidents_count: incidents,
|
|
created_at: website.created_at,
|
|
};
|
|
}) || [];
|
|
|
|
setWebsites(monitoringData);
|
|
|
|
// Get recent checks for the timeline
|
|
const allChecks: UptimeMetric[] = [];
|
|
websitesData?.forEach((website: any) => {
|
|
const checks = website.uptime_checks || [];
|
|
checks.slice(0, 10).forEach((check: any) => {
|
|
allChecks.push({
|
|
website_id: website.id,
|
|
timestamp: check.checked_at,
|
|
status: check.status,
|
|
response_time: check.response_time,
|
|
error_message: check.error_message,
|
|
});
|
|
});
|
|
});
|
|
|
|
allChecks.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
setRecentChecks(allChecks.slice(0, 20));
|
|
|
|
} catch (error) {
|
|
const errorInfo = extractSupabaseErrorInfo(error);
|
|
logError("Error loading monitoring data", error, {
|
|
organizationId: userDetails.organization_id,
|
|
function: "loadMonitoringData",
|
|
supabaseError: errorInfo
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const toggleMonitoring = async (websiteId: string, currentStatus: boolean) => {
|
|
try {
|
|
setUpdating(websiteId);
|
|
|
|
// In a real implementation, you would update monitoring settings
|
|
// For now, we'll just simulate the action
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Update local state
|
|
setWebsites(prev => prev.map(website =>
|
|
website.id === websiteId
|
|
? { ...website, is_monitoring: !currentStatus }
|
|
: website
|
|
));
|
|
|
|
} catch (error) {
|
|
console.error("Error toggling monitoring:", error);
|
|
} finally {
|
|
setUpdating(null);
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case "up":
|
|
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
case "down":
|
|
return <XCircle className="w-4 h-4 text-red-500" />;
|
|
case "warning":
|
|
return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
|
|
default:
|
|
return <Clock className="w-4 h-4 text-gray-500" />;
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case "up":
|
|
return "bg-green-100 text-green-800";
|
|
case "down":
|
|
return "bg-red-100 text-red-800";
|
|
case "warning":
|
|
return "bg-yellow-100 text-yellow-800";
|
|
default:
|
|
return "bg-gray-100 text-gray-800";
|
|
}
|
|
};
|
|
|
|
const getUptimeColor = (percentage: number) => {
|
|
if (percentage >= 99) return "text-green-600";
|
|
if (percentage >= 95) return "text-yellow-600";
|
|
return "text-red-600";
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
const totalWebsites = websites.length;
|
|
const activeMonitoring = websites.filter(w => w.is_monitoring).length;
|
|
const upWebsites = websites.filter(w => w.status === "up").length;
|
|
const downWebsites = websites.filter(w => w.status === "down").length;
|
|
const avgUptime = websites.length > 0
|
|
? Math.round(websites.reduce((sum, w) => sum + w.uptime_percentage, 0) / websites.length)
|
|
: 0;
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
|
<Activity className="w-6 h-6" />
|
|
Website Monitoring
|
|
</h1>
|
|
<p className="text-gray-600 mt-1">
|
|
Real-time uptime monitoring for your websites
|
|
</p>
|
|
</div>
|
|
<Button
|
|
onClick={loadMonitoringData}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Zap className="w-4 h-4" />
|
|
Refresh Status
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Total Websites</p>
|
|
<p className="text-2xl font-bold">{totalWebsites}</p>
|
|
</div>
|
|
<Activity className="w-8 h-8 text-blue-600" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Monitoring Active</p>
|
|
<p className="text-2xl font-bold text-green-600">{activeMonitoring}</p>
|
|
</div>
|
|
<CheckCircle className="w-8 h-8 text-green-600" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Websites Up</p>
|
|
<p className="text-2xl font-bold text-green-600">{upWebsites}</p>
|
|
</div>
|
|
<TrendingUp className="w-8 h-8 text-green-600" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-600">Average Uptime</p>
|
|
<p className={`text-2xl font-bold ${getUptimeColor(avgUptime)}`}>
|
|
{avgUptime}%
|
|
</p>
|
|
</div>
|
|
<TrendingUp className="w-8 h-8 text-purple-600" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Monitoring Status */}
|
|
<div className="space-y-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Website Status</h2>
|
|
|
|
{websites.length > 0 ? (
|
|
<div className="grid gap-4">
|
|
{websites.map((website) => (
|
|
<Card key={website.id}>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
{getStatusIcon(website.status)}
|
|
<div>
|
|
<h3 className="font-semibold text-gray-900">{website.website_name}</h3>
|
|
<p className="text-sm text-gray-500">{website.website_url}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-right">
|
|
<Badge className={getStatusColor(website.status)}>
|
|
{website.status.toUpperCase()}
|
|
</Badge>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Last check: {new Date(website.last_check).toLocaleTimeString()}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium">
|
|
{website.response_time > 0 ? `${website.response_time}ms` : "—"}
|
|
</p>
|
|
<p className="text-xs text-gray-500">Response time</p>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<p className={`text-sm font-medium ${getUptimeColor(website.uptime_percentage)}`}>
|
|
{website.uptime_percentage}%
|
|
</p>
|
|
<p className="text-xs text-gray-500">24h uptime</p>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium text-red-600">
|
|
{website.incidents_count}
|
|
</p>
|
|
<p className="text-xs text-gray-500">Incidents</p>
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => toggleMonitoring(website.id, website.is_monitoring)}
|
|
disabled={updating === website.id}
|
|
className="flex items-center gap-1"
|
|
>
|
|
{updating === website.id ? (
|
|
<Loader2 className="w-3 h-3 animate-spin" />
|
|
) : website.is_monitoring ? (
|
|
<Pause className="w-3 h-3" />
|
|
) : (
|
|
<Play className="w-3 h-3" />
|
|
)}
|
|
{website.is_monitoring ? "Pause" : "Start"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="p-12 text-center">
|
|
<Activity className="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
No websites being monitored
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
Add websites and enable monitoring to see uptime status here
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
|
|
{/* Recent Activity */}
|
|
{recentChecks.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Clock className="w-5 h-5" />
|
|
Recent Activity
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{recentChecks.slice(0, 10).map((check, index) => {
|
|
const website = websites.find(w => w.id === check.website_id);
|
|
return (
|
|
<div key={index} className="flex items-center justify-between py-2 border-b border-gray-100 last:border-0">
|
|
<div className="flex items-center gap-3">
|
|
{getStatusIcon(check.status)}
|
|
<div>
|
|
<p className="text-sm font-medium">{website?.website_name || "Unknown"}</p>
|
|
<p className="text-xs text-gray-500">
|
|
{new Date(check.timestamp).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm">{check.response_time}ms</p>
|
|
{check.error_message && (
|
|
<p className="text-xs text-red-500">{check.error_message}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
} |