Merge pull request #57 from operacle/develop

Refactor: Separate data display by monitoring source and feat: Implement "Regional Monitoring" option
This commit is contained in:
Tola Leng
2025-06-26 16:28:32 +07:00
committed by GitHub
27 changed files with 1720 additions and 331 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ function App() {
<Routes>
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
<Route path="/public/:pageId" element={<PublicStatusPage />} />
<Route path="/public/:slug" element={<PublicStatusPage />} />
{/* Protected Routes */}
<Route path="/dashboard" element={
@@ -41,14 +41,6 @@ export const mainMenuItems = [
color: 'text-amber-400',
hasNavigation: true
},
{
id: 'reports',
path: null,
icon: LineChart,
translationKey: 'reports',
color: 'text-rose-400',
hasNavigation: false
},
{
id: 'regional-monitoring',
path: '/regional-monitoring',
@@ -56,6 +48,14 @@ export const mainMenuItems = [
translationKey: 'regionalMonitoring',
color: 'text-indigo-400',
hasNavigation: true
},
{
id: 'reports',
path: null,
icon: LineChart,
translationKey: 'reports',
color: 'text-rose-400',
hasNavigation: false
}
];
@@ -39,8 +39,8 @@ export const OperationalPageContent = () => {
if (page.custom_domain) {
window.open(`https://${page.custom_domain}`, '_blank');
} else {
// Navigate to the public status page route
window.open(`/status/${page.slug}`, '_blank');
// Navigate to the public status page route using the correct format
window.open(`/public/${page.slug}`, '_blank');
}
};
@@ -1,4 +1,3 @@
import React, { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
@@ -54,19 +53,69 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
}
};
const copyToClipboard = async (text: string) => {
const copyToClipboard = async (text: string, description: string = "Content") => {
try {
await navigator.clipboard.writeText(text);
toast({
title: "Copied!",
description: "Installation script copied to clipboard.",
});
// Try the modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
toast({
title: "Copied!",
description: `${description} copied to clipboard.`,
});
return;
}
// Fallback for older browsers or non-secure contexts (like localhost)
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
toast({
title: "Copied!",
description: `${description} copied to clipboard.`,
});
} else {
throw new Error('execCommand failed');
}
} catch (error) {
console.error('Failed to copy to clipboard:', error);
// Show the text in a modal or alert as final fallback
const userAgent = navigator.userAgent.toLowerCase();
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
let errorMessage = "Failed to copy to clipboard.";
if (isLocalhost) {
errorMessage += " This is common in local development. Please manually copy the text.";
} else if (userAgent.includes('chrome')) {
errorMessage += " Try using HTTPS or enable clipboard permissions.";
}
toast({
title: "Copy failed",
description: "Failed to copy to clipboard.",
description: errorMessage,
variant: "destructive",
});
// As a last resort, select the text for manual copying
try {
const textarea = document.querySelector('textarea[readonly]') as HTMLTextAreaElement;
if (textarea) {
textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices
}
} catch (selectError) {
console.error('Failed to select text:', selectError);
}
}
};
@@ -89,16 +138,6 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
});
};
const copyOneClickCommand = () => {
if (!installCommand) return;
const oneClickCommand = `curl -fsSL -H "User-Agent: CheckCle-Installer" \\
"data:text/plain;base64,$(echo '${installCommand.bash_script}' | base64 -w 0)" \\
| sudo bash`;
copyToClipboard(oneClickCommand);
};
const handleComplete = () => {
onAgentAdded();
setStep(1);
@@ -211,7 +250,7 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
<Textarea
readOnly
value={`# One-click installation command:
curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/regional-agent.sh | sudo bash -s -- \\
curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh | sudo bash -s -- \\
--region-name="${regionName}" \\
--agent-id="${installCommand.agent_id}" \\
--agent-ip="${agentIp}" \\
@@ -223,7 +262,7 @@ curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/regional-agent
size="sm"
variant="outline"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(`curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/regional-agent.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`)}
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
>
<Copy className="h-3 w-3" />
</Button>
@@ -236,7 +275,7 @@ curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/regional-agent
Download Complete Script
</Button>
<Button
onClick={() => copyToClipboard(installCommand.bash_script)}
onClick={() => copyToClipboard(installCommand.bash_script, "Installation script")}
variant="outline"
className="flex-1"
>
@@ -244,6 +283,16 @@ curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/regional-agent
Copy Full Script
</Button>
</div>
{/* Local development notice */}
{(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800">
<strong>Local Development Notice:</strong> If clipboard copying fails, this is normal in local development.
You can use the "Download Complete Script" button instead or manually copy the text.
</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
@@ -270,7 +319,7 @@ curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/regional-agent
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.agent_id)}
onClick={() => copyToClipboard(installCommand.agent_id, "Agent ID")}
>
<Copy className="h-3 w-3" />
</Button>
@@ -314,7 +363,7 @@ curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/regional-agent
size="sm"
variant="ghost"
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 hover:bg-muted"
onClick={() => copyToClipboard(installCommand.token)}
onClick={() => copyToClipboard(installCommand.token, "Authentication token")}
>
<Copy className="h-3 w-3" />
</Button>
@@ -0,0 +1,92 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useQuery } from "@tanstack/react-query";
import { regionalService } from "@/services/regionalService";
import { MapPin, Loader2, BarChart3 } from "lucide-react";
interface RegionalAgentFilterProps {
selectedAgent: string;
onAgentChange: (agent: string) => void;
}
export function RegionalAgentFilter({ selectedAgent, onAgentChange }: RegionalAgentFilterProps) {
const { data: regionalAgents = [], isLoading } = useQuery({
queryKey: ['regional-services'],
queryFn: regionalService.getRegionalServices,
});
// Filter only online agents
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
const getCurrentAgentDisplay = () => {
if (!selectedAgent || selectedAgent === "all") {
return "All Monitoring";
}
const [regionName] = selectedAgent.split("|");
const agent = onlineAgents.find(agent =>
`${agent.region_name}|${agent.agent_id}` === selectedAgent
);
if (agent) {
return `${agent.region_name} (${agent.agent_ip_address})`;
}
return regionName || "All Monitoring";
};
return (
<div className="w-64">
<label className="text-sm font-medium flex items-center gap-2 mb-2">
<MapPin className="h-4 w-4" />
Monitoring Source
</label>
<Select
onValueChange={onAgentChange}
value={selectedAgent || "all"}
disabled={isLoading}
>
<SelectTrigger>
<SelectValue placeholder={
isLoading
? "Loading agents..."
: getCurrentAgentDisplay()
} />
</SelectTrigger>
<SelectContent>
{isLoading ? (
<SelectItem value="loading" disabled>
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Loading agents...
</div>
</SelectItem>
) : (
<>
<SelectItem value="all">
<div className="flex items-center gap-2">
<BarChart3 className="h-4 w-4 text-purple-500" />
<span className="font-medium">All Monitoring</span>
</div>
</SelectItem>
{onlineAgents.length > 0 && onlineAgents.map((agent) => (
<SelectItem key={agent.id} value={`${agent.region_name}|${agent.agent_id}`}>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="font-medium">{agent.region_name}</span>
<span className="text-muted-foreground">({agent.agent_ip_address})</span>
</div>
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
{onlineAgents.length === 0 && !isLoading && (
<p className="text-xs text-amber-600 mt-1">
No regional agents available. Using default monitoring only.
</p>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from "recharts";
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line } from "recharts";
import { UptimeData } from "@/types/service.types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useTheme } from "@/contexts/ThemeContext";
@@ -13,94 +13,290 @@ interface ResponseTimeChartProps {
export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
const { theme } = useTheme();
// Modern color palette for different chart lines (only for All Monitoring)
const modernColors = [
{ stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.1)' }, // Blue
{ stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.1)' }, // Emerald
{ stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.1)' }, // Amber
{ stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.1)' }, // Red
{ stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.1)' }, // Violet
{ stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.1)' }, // Cyan
{ stroke: '#f97316', fill: 'rgba(249, 115, 22, 0.1)' }, // Orange
{ stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.1)' }, // Lime
];
// Check if we have data from multiple sources
const hasMultipleSources = useMemo(() => {
const sources = new Set();
uptimeData.forEach(data => {
const source = data.source || 'default';
sources.add(source);
});
return sources.size > 1;
}, [uptimeData]);
// Format data for the chart with enhanced time formatting
const chartData = useMemo(() => {
if (!uptimeData || uptimeData.length === 0) return [];
// Sort by timestamp ascending (oldest to newest)
const sortedData = [...uptimeData].sort((a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
// Get time format based on data density
const isShortTimeRange = uptimeData.length > 0 &&
(new Date(uptimeData[0].timestamp).getTime() - new Date(uptimeData[uptimeData.length - 1].timestamp).getTime()) < 2 * 60 * 60 * 1000;
return sortedData.map(data => {
const timestamp = new Date(data.timestamp);
if (hasMultipleSources) {
// Group data by timestamp for multi-source display
const timeGroups = new Map();
return {
time: isShortTimeRange
? format(timestamp, 'HH:mm:ss') // Include seconds for short time ranges like 60min
: format(timestamp, 'HH:mm'),
rawTime: timestamp.getTime(),
date: format(timestamp, 'MMM dd, yyyy'),
value: data.status === "paused" ? null : data.responseTime,
status: data.status,
// Separate values for different statuses with proper positioning
upValue: data.status === "up" ? data.responseTime : null,
downValue: data.status === "down" ? data.responseTime : null,
warningValue: data.status === "warning" ? data.responseTime : null,
};
});
}, [uptimeData]);
uptimeData.forEach(data => {
const timestamp = new Date(data.timestamp);
// Round to nearest minute for better grouping
const roundedTime = new Date(timestamp.getFullYear(), timestamp.getMonth(), timestamp.getDate(),
timestamp.getHours(), timestamp.getMinutes());
const timeKey = roundedTime.getTime();
const source = data.source || 'default';
if (!timeGroups.has(timeKey)) {
timeGroups.set(timeKey, {
time: format(roundedTime, 'HH:mm'),
rawTime: timeKey,
date: format(roundedTime, 'MMM dd, yyyy'),
timestamp: data.timestamp
});
}
const group = timeGroups.get(timeKey);
// Handle different data sources properly
if (source === 'default') {
// Only add pure default if we don't already have regional data with agent_id 1
const hasRegionalDefault = uptimeData.some(d =>
d.region_name === 'Default' && (d.agent_id === '1' || d.agent_id === 1)
);
if (!hasRegionalDefault) {
group.defaultValue = data.status === "paused" ? null : data.responseTime;
group.defaultStatus = data.status;
}
} else {
// Regional monitoring data - distinguish by region and agent
const sourceKey = `regional_${source.replace('|', '_')}`;
// Group by the same timestamp more accurately
const currentValue = group[`${sourceKey}_value`];
const newValue = data.status === "paused" ? null : data.responseTime;
if (currentValue !== undefined && newValue !== null) {
// Average multiple values for the same time slot
const count = group[`${sourceKey}_count`] || 1;
group[`${sourceKey}_value`] = ((currentValue * count) + newValue) / (count + 1);
group[`${sourceKey}_count`] = count + 1;
} else if (newValue !== null) {
group[`${sourceKey}_value`] = newValue;
group[`${sourceKey}_count`] = 1;
}
group[`${sourceKey}_status`] = data.status;
group[`${sourceKey}_label`] = data.region_name || source;
group[`${sourceKey}_agent_id`] = data.agent_id;
}
});
return Array.from(timeGroups.values())
.sort((a, b) => a.rawTime - b.rawTime)
.slice(-50); // Show last 50 data points for better performance
} else {
// Single source display - use original styling
const sortedData = [...uptimeData].sort((a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
const isShortTimeRange = uptimeData.length > 0 &&
(new Date(uptimeData[0].timestamp).getTime() - new Date(uptimeData[uptimeData.length - 1].timestamp).getTime()) < 2 * 60 * 60 * 1000;
return sortedData.map(data => {
const timestamp = new Date(data.timestamp);
return {
time: isShortTimeRange
? format(timestamp, 'HH:mm:ss')
: format(timestamp, 'HH:mm'),
rawTime: timestamp.getTime(),
date: format(timestamp, 'MMM dd, yyyy'),
value: data.status === "paused" ? null : data.responseTime,
status: data.status,
upValue: data.status === "up" ? data.responseTime : null,
downValue: data.status === "down" ? data.responseTime : null,
warningValue: data.status === "warning" ? data.responseTime : null,
};
});
}
}, [uptimeData, hasMultipleSources]);
// Calculate Y-axis domain for better positioning
const yAxisDomain = useMemo(() => {
if (!chartData.length) return ['dataMin - 10', 'dataMax + 10'];
if (!chartData.length) return [0, 100];
const allValues = chartData
.filter(d => d.value !== null && d.status !== 'paused')
.map(d => d.value);
let allValues: number[] = [];
if (hasMultipleSources) {
chartData.forEach(d => {
Object.keys(d).forEach(key => {
if (key.endsWith('_value') || key === 'defaultValue') {
const value = d[key as keyof typeof d];
if (value !== null && value !== undefined && typeof value === 'number') {
allValues.push(value);
}
}
});
});
} else {
allValues = chartData
.filter(d => d.value !== null && d.status !== 'paused')
.map(d => d.value as number)
.filter(v => typeof v === 'number');
}
if (allValues.length === 0) return [0, 100];
const minValue = Math.min(...allValues);
const maxValue = Math.max(...allValues);
const padding = (maxValue - minValue) * 0.1 || 10;
const padding = Math.max((maxValue - minValue) * 0.1, 10);
return [Math.max(0, minValue - padding), maxValue + padding];
}, [chartData]);
}, [chartData, hasMultipleSources]);
// Get unique sources for legend with proper labeling and colors
const sources = useMemo(() => {
if (!hasMultipleSources) return [];
const sourceSet = new Set<string>();
const sourceInfo = new Map<string, any>();
uptimeData.forEach(data => {
const source = data.source || 'default';
if (source !== 'default') {
sourceSet.add(source);
sourceInfo.set(source, data);
}
});
const regionalSources = Array.from(sourceSet).map((source, index) => {
const data = sourceInfo.get(source);
const [regionName, agentId] = source.split('|');
// Create proper label with region and agent info
let label = regionName;
if (data?.agent_id && data.agent_id !== '1') {
label = `${regionName} (${data.agent_id})`;
} else if (regionName === 'Default' && data?.agent_id === '1') {
label = `Default (Agent 1)`;
}
const colorIndex = index % modernColors.length;
return {
key: `regional_${source.replace('|', '_')}`,
label: label,
stroke: modernColors[colorIndex].stroke,
fill: modernColors[colorIndex].fill
};
});
// Only add pure default monitoring if we have it AND no regional Default with agent_id 1
const hasRegionalDefault = uptimeData.some(data =>
data.region_name === 'Default' && (data.agent_id === '1' || data.agent_id === 1)
);
const hasPureDefault = uptimeData.some(data => data.source === 'default');
const defaultSources = (hasPureDefault && !hasRegionalDefault) ? [{
key: 'default',
label: 'Default',
stroke: modernColors[regionalSources.length % modernColors.length].stroke,
fill: modernColors[regionalSources.length % modernColors.length].fill
}] : [];
return [...defaultSources, ...regionalSources];
}, [uptimeData, hasMultipleSources]);
// Create a custom tooltip for the chart
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
// Set background color based on status
let statusColor = "bg-emerald-800";
let statusText = "Up";
if (data.status === "down") {
statusColor = "bg-red-800";
statusText = "Down";
} else if (data.status === "warning") {
statusColor = "bg-yellow-800";
statusText = "Warning";
} else if (data.status === "paused") {
statusColor = "bg-gray-800";
statusText = "Paused";
}
return (
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} p-2 border ${theme === 'dark' ? 'border-gray-700' : 'border-gray-200'} rounded shadow-md`}>
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-muted-foreground">{data.date}</p>
<div className={`flex items-center gap-2 mt-1 ${data.status === "paused" ? "opacity-70" : ""}`}>
<div className={`w-3 h-3 rounded-full ${statusColor}`}></div>
<span>{statusText}</span>
</div>
<p className="mt-1 font-mono text-sm">
{data.status === "paused" ? "Monitoring paused" :
data.value !== null ? `${data.value} ms` : "No data"}
</p>
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} p-3 border ${theme === 'dark' ? 'border-gray-700' : 'border-gray-200'} rounded shadow-md min-w-48`}>
<p className="text-sm font-medium">{String(label)}</p>
<p className="text-xs text-muted-foreground mb-2">{String(data.date)}</p>
{hasMultipleSources ? (
// Multi-source tooltip
<div className="space-y-2">
{sources.map(source => {
const valueKey = source.key === 'default' ? 'defaultValue' : `${source.key}_value`;
const statusKey = source.key === 'default' ? 'defaultStatus' : `${source.key}_status`;
const value = data[valueKey];
const status = data[statusKey];
if (value === undefined && status === undefined) return null;
let statusColor = "bg-gray-800";
let statusText = "No Data";
if (status === "up") {
statusColor = "bg-emerald-800";
statusText = "Up";
} else if (status === "down") {
statusColor = "bg-red-800";
statusText = "Down";
} else if (status === "warning") {
statusColor = "bg-yellow-800";
statusText = "Warning";
} else if (status === "paused") {
statusColor = "bg-gray-800";
statusText = "Paused";
}
return (
<div key={source.key} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: source.stroke }}
></div>
<span className="text-xs">{String(source.label)}</span>
</div>
<div className="text-xs font-mono">
{status === "paused" ? "Paused" :
value !== null && value !== undefined ? `${value} ms` : "No data"}
</div>
</div>
);
})}
</div>
) : (
// Single source tooltip - showing status with color
<>
<div className={`flex items-center gap-2 mt-1 ${data.status === "paused" ? "opacity-70" : ""}`}>
<div className={`w-3 h-3 rounded-full ${
data.status === "up" ? "bg-emerald-800" :
data.status === "down" ? "bg-red-800" :
data.status === "warning" ? "bg-yellow-800" : "bg-gray-800"
}`}></div>
<span>{
data.status === "up" ? "Up" :
data.status === "down" ? "Down" :
data.status === "warning" ? "Warning" : "Paused"
}</span>
</div>
<p className="mt-1 font-mono text-sm">
{data.status === "paused" ? "Monitoring paused" :
data.value !== null ? `${data.value} ms` : "No data"}
</p>
</>
)}
</div>
);
}
return null;
};
// Check if we have any data to display - be more lenient by checking raw uptimeData
// Check if we have any data to display
const hasData = uptimeData.length > 0;
// Get date range for display
@@ -130,6 +326,19 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
</span>
)}
</CardTitle>
{hasMultipleSources && (
<div className="flex flex-wrap gap-4 text-sm">
{sources.map(source => (
<div key={source.key} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: source.stroke }}
></div>
<span>{String(source.label)}</span>
</div>
))}
</div>
)}
</CardHeader>
<CardContent>
{!hasData ? (
@@ -139,82 +348,140 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
) : (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
<defs>
<linearGradient id="colorUp" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0.2}/>
</linearGradient>
<linearGradient id="colorDown" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#ef4444" stopOpacity={0.2}/>
</linearGradient>
<linearGradient id="colorWarning" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0.2}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
<XAxis
dataKey="time"
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
angle={-45}
textAnchor="end"
tick={{ fontSize: 10 }}
height={60}
interval="preserveStartEnd"
minTickGap={5}
/>
<YAxis
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
allowDecimals={false}
domain={yAxisDomain}
/>
<Tooltip content={<CustomTooltip />} />
{/* Separate area charts for each status - positioned closer together */}
<Area
type="monotone"
dataKey="upValue"
stroke="#10b981"
strokeWidth={2}
fillOpacity={0.4}
fill="url(#colorUp)"
connectNulls={false}
/>
<Area
type="monotone"
dataKey="downValue"
stroke="#ef4444"
strokeWidth={2}
fillOpacity={0.4}
fill="url(#colorDown)"
connectNulls={false}
/>
<Area
type="monotone"
dataKey="warningValue"
stroke="#f59e0b"
strokeWidth={2}
fillOpacity={0.4}
fill="url(#colorWarning)"
connectNulls={false}
/>
{/* Add reference lines for paused periods */}
{chartData.map((entry, index) =>
entry.status === 'paused' ? (
<ReferenceLine
key={`ref-${index}`}
x={entry.time}
stroke="#9ca3af"
strokeDasharray="3 3"
{hasMultipleSources ? (
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
<defs>
{sources.map(source => (
<linearGradient key={`gradient-${source.key}`} id={`gradient-${source.key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={source.stroke} stopOpacity={0.3}/>
<stop offset="95%" stopColor={source.stroke} stopOpacity={0.05}/>
</linearGradient>
))}
</defs>
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
<XAxis
dataKey="time"
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
angle={-45}
textAnchor="end"
tick={{ fontSize: 10 }}
height={60}
interval="preserveStartEnd"
minTickGap={5}
/>
<YAxis
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
allowDecimals={false}
domain={yAxisDomain}
/>
<Tooltip content={<CustomTooltip />} />
{/* Default monitoring area with modern background - only if exists and no regional default */}
{sources.find(s => s.key === 'default') && (
<Area
type="monotone"
dataKey="defaultValue"
stroke={sources.find(s => s.key === 'default')?.stroke}
fill={`url(#gradient-default)`}
strokeWidth={2.5}
dot={false}
connectNulls={false}
/>
) : null
)}
</AreaChart>
)}
{/* Regional monitoring areas with modern backgrounds */}
{sources.filter(s => s.key !== 'default').map((source) => (
<Area
key={source.key}
type="monotone"
dataKey={`${source.key}_value`}
stroke={source.stroke}
fill={`url(#gradient-${source.key})`}
strokeWidth={2.5}
dot={false}
connectNulls={false}
/>
))}
</AreaChart>
) : (
// For single regional agent or default monitoring only - use original AreaChart styling
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
<defs>
<linearGradient id="colorUp" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0.05}/>
</linearGradient>
<linearGradient id="colorDown" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#ef4444" stopOpacity={0.05}/>
</linearGradient>
<linearGradient id="colorWarning" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0.05}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
<XAxis
dataKey="time"
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
angle={-45}
textAnchor="end"
tick={{ fontSize: 10 }}
height={60}
interval="preserveStartEnd"
minTickGap={5}
/>
<YAxis
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
allowDecimals={false}
domain={yAxisDomain}
/>
<Tooltip content={<CustomTooltip />} />
{/* Original area charts for different statuses with background colors */}
<Area
type="monotone"
dataKey="upValue"
stroke="#10b981"
fill="url(#colorUp)"
strokeWidth={2.5}
dot={false}
connectNulls={false}
/>
<Area
type="monotone"
dataKey="downValue"
stroke="#ef4444"
fill="url(#colorDown)"
strokeWidth={2.5}
dot={false}
connectNulls={false}
/>
<Area
type="monotone"
dataKey="warningValue"
stroke="#f59e0b"
fill="url(#colorWarning)"
strokeWidth={2.5}
dot={false}
connectNulls={false}
/>
{/* Reference lines for paused status */}
{chartData.map((entry, index) =>
entry.status === 'paused' ? (
<ReferenceLine
key={`ref-${index}`}
x={entry.time}
stroke="#9ca3af"
strokeDasharray="3 3"
/>
) : null
)}
</AreaChart>
)}
</ResponsiveContainer>
</div>
)}
@@ -1,4 +1,3 @@
import { useState, useEffect } from "react";
import { pb } from "@/lib/pocketbase";
import { Service, UptimeData } from "@/types/service.types";
@@ -6,14 +5,24 @@ import { useToast } from "@/hooks/use-toast";
import { useNavigate } from "react-router-dom";
import { uptimeService } from "@/services/uptimeService";
import { DateRangeOption } from "../../DateRangeFilter";
import { useQuery } from "@tanstack/react-query";
import { regionalService } from "@/services/regionalService";
export const useServiceData = (serviceId: string | undefined, startDate: Date, endDate: Date) => {
const [service, setService] = useState<Service | null>(null);
const [uptimeData, setUptimeData] = useState<UptimeData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedRegionalAgent, setSelectedRegionalAgent] = useState<string>("all");
const { toast } = useToast();
const navigate = useNavigate();
// Get regional agents for "all" monitoring
const { data: regionalAgents = [] } = useQuery({
queryKey: ['regional-services'],
queryFn: regionalService.getRegionalServices,
enabled: selectedRegionalAgent === "all"
});
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
if (!service || !serviceId) return;
@@ -40,14 +49,18 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
}
};
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string) => {
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => {
try {
if (!service) {
console.log('No service data available for uptime fetch');
return [];
}
console.log(`Fetching uptime data: ${start.toISOString()} to ${end.toISOString()} for range: ${selectedRange}, service type: ${service.type}`);
const currentAgent = regionalAgent || selectedRegionalAgent;
console.log(`Fetching uptime data: ${start.toISOString()} to ${end.toISOString()} for range: ${selectedRange}, service type: ${service.type}, regional agent: ${currentAgent}`);
// Clear existing data immediately when switching agents
setUptimeData([]);
let limit = 500; // Default limit
@@ -59,15 +72,66 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
console.log(`Using limit ${limit} for range ${selectedRange}`);
// Use the service type to fetch from the correct collection
const history = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
console.log(`Retrieved ${history.length} uptime records from collection for ${service.type} service`);
let history: UptimeData[] = [];
if (currentAgent === "all") {
// Fetch data from all sources (default + all online regional agents)
console.log(`Fetching data from all monitoring sources`);
// Fetch default monitoring data
const defaultData = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
console.log(`Retrieved ${defaultData.length} default monitoring records`);
// Mark default data with source identifier
const markedDefaultData = defaultData.map(record => ({
...record,
source: 'default' as const,
region_name: undefined,
agent_id: undefined
}));
history = [...markedDefaultData];
// Fetch regional monitoring data from all online agents
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
for (const agent of onlineAgents) {
try {
const regionalData = await uptimeService.getUptimeHistoryByRegionalAgent(
serviceId, limit, start, end, service.type, agent.region_name, agent.agent_id
);
console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`);
// Mark regional data with source identifier
const markedRegionalData = regionalData.map(record => ({
...record,
source: `${agent.region_name}|${agent.agent_id}` as const,
region_name: agent.region_name,
agent_id: agent.agent_id
}));
history = [...history, ...markedRegionalData];
} catch (error) {
console.error(`Error fetching data from ${agent.region_name}:`, error);
}
}
console.log(`Total combined records: ${history.length}`);
} else {
// Fetch regional agent specific data
const [regionName, agentId] = currentAgent.split("|");
console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
console.log(`Retrieved ${history.length} regional monitoring records`);
}
// Sort by timestamp (newest first)
const filteredHistory = [...history].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`);
setUptimeData(filteredHistory);
return filteredHistory;
} catch (error) {
@@ -81,6 +145,20 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
}
};
const handleRegionalAgentChange = (agent: string) => {
console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
// Clear data immediately when switching
setUptimeData([]);
setSelectedRegionalAgent(agent);
// Refetch data with new agent selection
if (serviceId && !isLoading && service) {
console.log(`Refetching data for new agent: ${agent}`);
fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
}
};
// Initial data loading
useEffect(() => {
const fetchServiceData = async () => {
@@ -121,8 +199,8 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
setService(formattedService);
// Fetch initial uptime history with 24h default - wait for service to be set
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay to ensure state is updated
// Small delay to ensure state is updated before fetching uptime data
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.error("Error fetching service:", error);
toast({
@@ -143,9 +221,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
useEffect(() => {
if (serviceId && !isLoading && service) {
console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
fetchUptimeData(serviceId, startDate, endDate, '24h');
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
}
}, [startDate, endDate, serviceId, isLoading, service]);
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]);
return {
service,
@@ -154,6 +232,8 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
setUptimeData,
isLoading,
handleStatusChange,
fetchUptimeData
fetchUptimeData,
selectedRegionalAgent,
handleRegionalAgentChange
};
};
@@ -80,7 +80,9 @@ export const ServiceDetailContainer = () => {
handleStatusChange,
fetchUptimeData,
setService,
setUptimeData
setUptimeData,
selectedRegionalAgent,
handleRegionalAgentChange
} = useServiceData(id, startDate, endDate);
// Set up real-time updates
@@ -104,9 +106,9 @@ export const ServiceDetailContainer = () => {
// Also explicitly fetch data with the new range to ensure immediate update
if (id) {
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
fetchUptimeData(id, start, end, option);
fetchUptimeData(id, start, end, option, selectedRegionalAgent);
}
}, [id, fetchUptimeData]);
}, [id, fetchUptimeData, selectedRegionalAgent]);
return (
<ServiceDetailWrapper
@@ -124,6 +126,8 @@ export const ServiceDetailContainer = () => {
onDateRangeChange={handleDateRangeChange}
onStatusChange={handleStatusChange}
selectedDateOption={selectedRange}
selectedRegionalAgent={selectedRegionalAgent}
onRegionalAgentChange={handleRegionalAgentChange}
/>
)}
</ServiceDetailWrapper>
@@ -14,6 +14,8 @@ interface ServiceDetailContentProps {
onDateRangeChange: (start: Date, end: Date, option: DateRangeOption) => void;
onStatusChange: (newStatus: "up" | "down" | "paused" | "warning") => void;
selectedDateOption: DateRangeOption;
selectedRegionalAgent: string;
onRegionalAgentChange: (agent: string) => void;
}
export const ServiceDetailContent = ({
@@ -21,28 +23,37 @@ export const ServiceDetailContent = ({
uptimeData,
onDateRangeChange,
onStatusChange,
selectedDateOption
selectedDateOption,
selectedRegionalAgent,
onRegionalAgentChange
}: ServiceDetailContentProps) => {
// Check if data is available
const hasUptimeData = uptimeData && uptimeData.length > 0;
return (
<div className="p-4 md:p-6 pb-0 h-full overflow-auto">
<ServiceHeader service={service} onStatusChange={onStatusChange} />
<ServiceHeader
service={service}
onStatusChange={onStatusChange}
selectedRegionalAgent={selectedRegionalAgent}
onRegionalAgentChange={onRegionalAgentChange}
/>
<ServiceStatsCards service={service} uptimeData={uptimeData} />
<div className="mb-4 md:mb-6 mt-6 md:mt-8 flex flex-col md:flex-row justify-between items-start md:items-center gap-4 md:gap-0">
<div className="mb-4 md:mb-6 mt-6 md:mt-8 flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
<h2 className="text-lg md:text-xl font-medium">Response Time History</h2>
<div className="flex flex-col md:flex-row items-start md:items-center gap-2">
<span className="text-sm text-muted-foreground">
Collection: {service.type.toLowerCase() === 'ping' || service.type.toLowerCase() === 'icmp' ? 'ping_data' :
service.type.toLowerCase() === 'dns' ? 'dns_data' :
service.type.toLowerCase() === 'tcp' ? 'tcp_data' : 'uptime_data'}
</span>
<DateRangeFilter
onRangeChange={onDateRangeChange}
selectedOption={selectedDateOption}
/>
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
<div className="flex flex-col md:flex-row items-start md:items-center gap-2">
<span className="text-sm text-muted-foreground">
Collection: {service.type.toLowerCase() === 'ping' || service.type.toLowerCase() === 'icmp' ? 'ping_data' :
service.type.toLowerCase() === 'dns' ? 'dns_data' :
service.type.toLowerCase() === 'tcp' ? 'tcp_data' : 'uptime_data'}
</span>
<DateRangeFilter
onRangeChange={onDateRangeChange}
selectedOption={selectedDateOption}
/>
</div>
</div>
</div>
@@ -53,7 +64,8 @@ export const ServiceDetailContent = ({
<AlertTriangle className="h-10 w-10 md:h-12 md:w-12 text-amber-500 mx-auto mb-2 md:mb-3 opacity-70" />
<h3 className="text-base md:text-lg font-medium mb-1">No uptime data available</h3>
<p className="text-muted-foreground max-w-md text-sm md:text-base">
There's no monitoring data for this service in the selected time period.
There's no monitoring data for this service in the selected time period
{selectedRegionalAgent !== "default" ? " from the selected regional agent" : ""}.
This could be because the service was recently added or monitoring is paused.
</p>
</div>
@@ -12,6 +12,7 @@ import { ServiceNotificationFields } from "./add-service/ServiceNotificationFiel
import { ServiceFormActions } from "./add-service/ServiceFormActions";
import { serviceService } from "@/services/serviceService";
import { Service } from "@/types/service.types";
import { ServiceRegionalFields } from "./add-service/ServiceRegionalFields";
interface ServiceFormProps {
onSuccess: () => void;
@@ -43,6 +44,8 @@ export function ServiceForm({
retries: "3",
notificationChannel: "",
alertTemplate: "",
regionalMonitoringEnabled: false,
regionalAgent: "",
},
mode: "onBlur",
});
@@ -71,6 +74,11 @@ export function ServiceForm({
urlValue = initialData.url || "";
}
// Handle regional monitoring data - ensure proper assignment display
const regionalAgent = initialData.region_name && initialData.agent_id
? `${initialData.region_name}|${initialData.agent_id}`
: "";
// Reset the form with initial data values
form.reset({
name: initialData.name || "",
@@ -81,10 +89,20 @@ export function ServiceForm({
retries: String(initialData.retries || 3),
notificationChannel: initialData.notificationChannel === "none" ? "" : initialData.notificationChannel || "",
alertTemplate: initialData.alertTemplate === "default" ? "" : initialData.alertTemplate || "",
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
regionalAgent: regionalAgent,
});
// Log for debugging
console.log("Populating form with data:", { type: validType, url: urlValue, port: portValue });
console.log("Populating form with data:", {
type: validType,
url: urlValue,
port: portValue,
regionalAgent,
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
region_name: initialData.region_name,
agent_id: initialData.agent_id
});
}
}, [initialData, isEdit, form]);
@@ -97,6 +115,17 @@ export function ServiceForm({
try {
console.log("Form data being submitted:", data); // Debug log for submitted data
// Parse regional agent selection
let regionName = "";
let agentId = "";
// Only set region and agent if regional monitoring is enabled AND an agent is selected (not unassign)
if (data.regionalMonitoringEnabled && data.regionalAgent && data.regionalAgent !== "") {
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
regionName = parsedRegionName || "";
agentId = parsedAgentId || "";
}
// Prepare service data with proper field mapping
const serviceData = {
name: data.name,
@@ -105,6 +134,10 @@ export function ServiceForm({
retries: parseInt(data.retries),
notificationChannel: data.notificationChannel === "none" ? "" : data.notificationChannel,
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
regionalMonitoringEnabled: data.regionalMonitoringEnabled || false,
// Always set region_name and agent_id - empty strings when unassigned
regionName: regionName,
agentId: agentId,
// Map the URL field to appropriate database field based on service type
...(data.type === "dns"
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
@@ -115,6 +148,8 @@ export function ServiceForm({
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
)
};
console.log("Service data being sent:", serviceData);
if (isEdit && initialData) {
// Update existing service
@@ -164,6 +199,11 @@ export function ServiceForm({
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
<ServiceConfigFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
<ServiceRegionalFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
@@ -1,9 +1,10 @@
import { ArrowLeft, Globe, MoreVertical, FileText, Github, Twitter, MessageSquare, Bell } from "lucide-react";
import { ArrowLeft, Globe } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/services/StatusBadge";
import { ServiceMonitoringButton } from "@/components/services/ServiceMonitoringButton";
import { RegionalAgentFilter } from "@/components/services/RegionalAgentFilter";
import { Service } from "@/types/service.types";
import { useLanguage } from "@/contexts/LanguageContext";
import { cn } from "@/lib/utils";
@@ -11,9 +12,16 @@ import { cn } from "@/lib/utils";
interface ServiceHeaderProps {
service: Service;
onStatusChange?: (newStatus: "up" | "down" | "paused" | "warning") => void;
selectedRegionalAgent?: string;
onRegionalAgentChange?: (agent: string) => void;
}
export function ServiceHeader({ service, onStatusChange }: ServiceHeaderProps) {
export function ServiceHeader({
service,
onStatusChange,
selectedRegionalAgent,
onRegionalAgentChange
}: ServiceHeaderProps) {
const navigate = useNavigate();
const { t } = useLanguage();
@@ -72,12 +80,14 @@ export function ServiceHeader({ service, onStatusChange }: ServiceHeaderProps) {
<div className="flex items-center space-x-4">
<StatusBadge status={service.status} size="lg" />
<ServiceMonitoringButton service={service} onStatusChange={onStatusChange} />
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<span className="sr-only">More options</span>
<MoreVertical className="w-4 h-4" />
</Button>
{selectedRegionalAgent !== undefined && onRegionalAgentChange && (
<RegionalAgentFilter
selectedAgent={selectedRegionalAgent}
onAgentChange={onRegionalAgentChange}
/>
)}
</div>
</div>
</div>
);
}
}
@@ -64,6 +64,7 @@ export const ServiceHistoryDialog = ({
{selectedService && (
<ServiceUptimeHistory
serviceId={selectedService.id}
serviceType={selectedService.type}
startDate={startDate}
endDate={endDate}
/>
@@ -71,4 +72,4 @@ export const ServiceHistoryDialog = ({
</DialogContent>
</Dialog>
);
};
};
@@ -55,7 +55,7 @@ export const ServiceRow = ({
<TableCell className="py-4">
<ServiceRowResponseTime responseTime={service.responseTime} />
</TableCell>
<TableCell className="w-52 py-4">
<TableCell className="w-60 py-4">
<UptimeBar
uptime={service.uptime}
status={service.status}
+117 -32
View File
@@ -1,52 +1,137 @@
import React from "react";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useUptimeData } from "./hooks/useUptimeData";
import { UptimeStatusItem } from "./uptime/UptimeStatusItem";
import { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData";
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { UptimeSummary } from "./uptime/UptimeSummary";
import { UptimeLoadingState } from "./uptime/UptimeLoadingState";
import { UptimeErrorState } from "./uptime/UptimeErrorState";
import { formatRelative } from "date-fns";
interface UptimeBarProps {
uptime: number;
status: string;
serviceId?: string;
interval?: number; // Service monitoring interval in seconds
serviceType?: string; // Add service type for proper data fetching
serviceId: string;
interval: number;
serviceType?: string;
}
export const UptimeBar = ({ uptime, status, serviceId, interval = 60, serviceType }: UptimeBarProps) => {
const { displayItems, isLoading, error, isFetching, refetch } = useUptimeData({
export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }: UptimeBarProps) => {
// Use consolidated hook to get properly merged data
const { consolidatedItems, isLoading } = useConsolidatedUptimeData({
serviceId,
serviceType,
status,
interval
});
// If still loading and no history, show improved loading state
if ((isLoading || isFetching) && displayItems.length === 0) {
return <UptimeLoadingState />;
}
// If there's an error and no history, show improved error state with retry button
if (error && displayItems.length === 0) {
return <UptimeErrorState uptime={uptime} onRetry={refetch} />;
}
return (
<TooltipProvider delayDuration={300}>
<div className="flex flex-col w-full gap-1">
<div className="flex items-center space-x-0.5 w-full h-6">
{displayItems.map((item, index) => (
<UptimeStatusItem
key={item.id || `status-${index}`}
item={item}
index={index}
/>
const getStatusColor = (itemStatus: string, hasData: boolean = true) => {
if (!hasData) {
return "bg-gray-300"; // No data color
}
switch (itemStatus) {
case "up":
return "bg-emerald-500";
case "down":
return "bg-red-500";
case "warning":
return "bg-yellow-500";
case "paused":
return "bg-gray-500"; // Distinct paused color (darker grey)
default:
return "bg-gray-300"; // Default fallback
}
};
if (isLoading) {
return (
<div className="w-52">
<div className="flex items-center gap-1">
{Array(20).fill(null).map((_, index) => (
<div key={index} className="h-6 w-1 bg-gray-200 rounded animate-pulse" />
))}
</div>
<UptimeSummary uptime={uptime} interval={interval} />
</div>
</TooltipProvider>
);
}
console.log('UptimeBar rendering consolidated items:', consolidatedItems.length);
return (
<div className="w-52">
<TooltipProvider>
<div className="flex items-center gap-1">
{consolidatedItems.map((slot, index) => {
// Check if we have actual monitoring data with valid status
const hasValidData = slot.items.length > 0 && slot.items.some(item =>
item.status && ['up', 'down', 'warning', 'paused'].includes(item.status)
);
// Determine the primary status for bar color (prioritize worst status)
let primaryStatus = 'unknown';
if (hasValidData) {
const statuses = slot.items.map(item => item.status);
primaryStatus = statuses.includes('down') ? 'down' :
statuses.includes('warning') ? 'warning' :
statuses.includes('up') ? 'up' :
statuses.includes('paused') ? 'paused' : 'unknown';
}
console.log(`Bar ${index} - Timestamp: ${slot.timestamp}, Items: ${slot.items.length}, Primary Status: ${primaryStatus}, Has Valid Data: ${hasValidData}`);
slot.items.forEach((item, itemIndex) => {
console.log(` Item ${itemIndex}: Source=${item.source}, Status=${item.status}, ResponseTime=${item.responseTime}, IsDefault=${item.isDefault}`);
});
return (
<Tooltip key={index}>
<TooltipTrigger asChild>
<div
className={`h-6 w-1 rounded cursor-pointer ${getStatusColor(primaryStatus, hasValidData)}`}
/>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<div className="text-sm">
<div className="font-medium mb-2 text-center">
{formatRelative(new Date(slot.timestamp), new Date())}
</div>
{hasValidData ? (
<div className="space-y-2">
{slot.items.map((item, itemIndex) => (
<div key={itemIndex} className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0">
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
item.status === 'up' ? 'bg-emerald-500' :
item.status === 'down' ? 'bg-red-500' :
item.status === 'warning' ? 'bg-yellow-500' :
item.status === 'paused' ? 'bg-gray-500' : 'bg-gray-300'
}`} />
<span className="text-xs font-medium truncate">{item.source}</span>
</div>
<div className="text-xs font-mono text-right flex-shrink-0">
{item.status === 'paused' ? 'Paused' :
item.responseTime && item.responseTime > 0 ? `${item.responseTime}ms` :
'No response'}
</div>
</div>
))}
{slot.items.length > 1 && (
<div className="border-t pt-1 mt-2 text-xs text-muted-foreground text-center">
{slot.items.length} monitoring sources
</div>
)}
</div>
) : (
<div className="text-xs text-muted-foreground text-center">
No monitoring data available
</div>
)}
</div>
</TooltipContent>
</Tooltip>
);
})}
</div>
</TooltipProvider>
<UptimeSummary uptime={uptime} interval={interval} />
</div>
);
}
};
@@ -13,6 +13,7 @@ import { ServiceNotificationFields } from "./ServiceNotificationFields";
import { ServiceFormActions } from "./ServiceFormActions";
import { serviceService } from "@/services/serviceService";
import { Service } from "@/types/service.types";
import { ServiceRegionalFields } from "./ServiceRegionalFields";
interface ServiceFormProps {
onSuccess: () => void;
@@ -44,6 +45,8 @@ export function ServiceForm({
retries: "3",
notificationChannel: "",
alertTemplate: "",
regionalMonitoringEnabled: false,
regionalAgent: "",
},
mode: "onBlur",
});
@@ -72,6 +75,11 @@ export function ServiceForm({
urlValue = initialData.url || "";
}
// Handle regional monitoring data - ensure proper assignment display
const regionalAgent = initialData.region_name && initialData.agent_id
? `${initialData.region_name}|${initialData.agent_id}`
: "";
// Reset the form with initial data values
form.reset({
name: initialData.name || "",
@@ -82,10 +90,20 @@ export function ServiceForm({
retries: String(initialData.retries || 3),
notificationChannel: initialData.notificationChannel || "",
alertTemplate: initialData.alertTemplate || "",
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
regionalAgent: regionalAgent,
});
// Log for debugging
console.log("Populating form with data:", { type: validType, url: urlValue, port: portValue });
console.log("Populating form with data:", {
type: validType,
url: urlValue,
port: portValue,
regionalAgent,
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
region_name: initialData.region_name,
agent_id: initialData.agent_id
});
}
}, [initialData, isEdit, form]);
@@ -98,6 +116,17 @@ export function ServiceForm({
try {
console.log("Form data being submitted:", data); // Debug log for submitted data
// Parse regional agent selection
let regionName = "";
let agentId = "";
// Only set region and agent if regional monitoring is enabled AND an agent is selected (not unassign)
if (data.regionalMonitoringEnabled && data.regionalAgent && data.regionalAgent !== "") {
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
regionName = parsedRegionName || "";
agentId = parsedAgentId || "";
}
// Prepare service data with proper field mapping
const serviceData = {
name: data.name,
@@ -106,6 +135,10 @@ export function ServiceForm({
retries: parseInt(data.retries),
notificationChannel: data.notificationChannel || undefined,
alertTemplate: data.alertTemplate || undefined,
regionalMonitoringEnabled: data.regionalMonitoringEnabled || false,
// Always set region_name and agent_id - empty strings when unassigned
regionName: regionName,
agentId: agentId,
// Map the URL field to appropriate database field based on service type
...(data.type === "dns"
? { domain: data.url, url: "", host: "", port: undefined } // DNS: store in domain field
@@ -116,6 +149,8 @@ export function ServiceForm({
: { url: data.url, domain: "", host: "", port: undefined } // HTTP: store in url field
)
};
console.log("Service data being sent:", serviceData);
if (isEdit && initialData) {
// Update existing service
@@ -165,6 +200,11 @@ export function ServiceForm({
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
<ServiceConfigFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
<ServiceRegionalFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
@@ -0,0 +1,154 @@
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types";
import { useQuery } from "@tanstack/react-query";
import { regionalService } from "@/services/regionalService";
import { MapPin, Loader2, X } from "lucide-react";
interface ServiceRegionalFieldsProps {
form: UseFormReturn<ServiceFormData>;
}
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
const currentRegionalAgent = form.watch("regionalAgent");
const { data: regionalAgents = [], isLoading } = useQuery({
queryKey: ['regional-services'],
queryFn: regionalService.getRegionalServices,
enabled: regionalMonitoringEnabled,
});
// Filter only online agents
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
// Find the current agent name for display
const getCurrentAgentDisplay = () => {
if (!currentRegionalAgent || currentRegionalAgent === "unassign") {
return "Select a regional agent or unassign";
}
const [regionName] = currentRegionalAgent.split("|");
const agent = onlineAgents.find(agent =>
`${agent.region_name}|${agent.agent_id}` === currentRegionalAgent
);
if (agent) {
return `${agent.region_name} (${agent.agent_ip_address})`;
}
// If agent is not found in online agents, it might be offline but still assigned
return regionName || "Select a regional agent or unassign";
};
return (
<div className="space-y-4">
<FormField
control={form.control}
name="regionalMonitoringEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base font-medium flex items-center gap-2">
<MapPin className="h-4 w-4" />
Regional Monitoring
</FormLabel>
<div className="text-sm text-muted-foreground">
Assign this service to a regional monitoring agent for distributed monitoring
</div>
</div>
<FormControl>
<Switch
checked={field.value || false}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{regionalMonitoringEnabled && (
<FormField
control={form.control}
name="regionalAgent"
render={({ field }) => (
<FormItem>
<FormLabel>Regional Agent</FormLabel>
<Select
onValueChange={(value) => {
// Handle the unassign case by setting to empty string
field.onChange(value === "unassign" ? "" : value);
}}
value={field.value || "unassign"}
disabled={isLoading}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={
isLoading
? "Loading agents..."
: getCurrentAgentDisplay()
} />
</SelectTrigger>
</FormControl>
<SelectContent>
{isLoading ? (
<SelectItem value="loading" disabled>
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Loading agents...
</div>
</SelectItem>
) : (
<>
<SelectItem value="unassign">
<div className="flex items-center gap-2">
<X className="h-4 w-4 text-red-500" />
<span className="text-red-600">Unassign (No Regional Agent)</span>
</div>
</SelectItem>
{onlineAgents.length === 0 ? (
<SelectItem value="no-agents" disabled>
No online regional agents available
</SelectItem>
) : (
onlineAgents.map((agent) => (
<SelectItem key={agent.id} value={`${agent.region_name}|${agent.agent_id}`}>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="font-medium">{agent.region_name}</span>
<span className="text-muted-foreground">({agent.agent_ip_address})</span>
</div>
</SelectItem>
))
)}
</>
)}
</SelectContent>
</Select>
<FormMessage />
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
<p className="text-sm text-amber-600">
No online regional agents found. Services will use default monitoring.
</p>
)}
{currentRegionalAgent && currentRegionalAgent !== "" && currentRegionalAgent !== "unassign" && (
<p className="text-sm text-green-600">
Currently assigned to: {getCurrentAgentDisplay()}
</p>
)}
{(!currentRegionalAgent || currentRegionalAgent === "" || currentRegionalAgent === "unassign") && (
<p className="text-sm text-orange-600">
Service is unassigned and will use default monitoring.
</p>
)}
</FormItem>
)}
/>
)}
</div>
);
}
@@ -13,6 +13,9 @@ export const serviceSchema = z.object({
retries: z.string(),
notificationChannel: z.string().optional(),
alertTemplate: z.string().optional(),
// Regional monitoring fields
regionalMonitoringEnabled: z.boolean().optional(),
regionalAgent: z.string().optional(),
});
export type ServiceFormData = z.infer<typeof serviceSchema>;
@@ -0,0 +1,209 @@
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { uptimeService } from '@/services/uptimeService';
import { UptimeData } from '@/types/service.types';
interface UseConsolidatedUptimeDataProps {
serviceId?: string;
serviceType?: string;
status: string;
interval: number;
}
interface ConsolidatedTimeSlot {
timestamp: string;
items: Array<UptimeData & { source: string; isDefault: boolean }>;
}
// Helper function to safely check if a PocketBase field is actually undefined/null
const isFieldEmpty = (field: any): boolean => {
if (field === undefined || field === null) return true;
if (typeof field === 'object' && field._type === 'undefined') return true;
if (typeof field === 'string' && (field === '' || field === 'undefined')) return true;
return false;
};
// Helper function to safely get field value from PocketBase
const getFieldValue = (field: any): string | number | null => {
if (isFieldEmpty(field)) return null;
if (typeof field === 'object' && field.value !== undefined) return field.value;
return field;
};
// Helper function to normalize timestamp to minute precision
const normalizeTimestamp = (timestamp: string): string => {
const date = new Date(timestamp);
// Round down to the nearest minute
date.setSeconds(0, 0);
return date.toISOString();
};
export const useConsolidatedUptimeData = ({ serviceId, serviceType, status, interval }: UseConsolidatedUptimeDataProps) => {
const [consolidatedItems, setConsolidatedItems] = useState<ConsolidatedTimeSlot[]>([]);
// Fetch ALL uptime history data in a single query
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
queryKey: ['consolidatedUptimeHistory', serviceId, serviceType],
queryFn: async () => {
if (!serviceId) {
console.log('No serviceId provided, skipping fetch');
return [];
}
console.log(`Fetching consolidated uptime data for service ${serviceId} of type ${serviceType}`);
// Get ALL uptime history data - this should include both default and regional data
const rawData = await uptimeService.getUptimeHistory(serviceId, 100, undefined, undefined, serviceType);
console.log(`Retrieved ${rawData.length} total records for service ${serviceId}`);
console.log('Raw data sample:', rawData.slice(0, 5).map(d => ({
timestamp: d.timestamp,
region_name: d.region_name,
agent_id: d.agent_id,
status: d.status,
service_id: d.service_id,
response_time: d.responseTime
})));
return rawData;
},
enabled: !!serviceId,
refetchInterval: 30000,
staleTime: 15000,
placeholderData: (previousData) => previousData,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
// Process and consolidate uptime data
const processConsolidatedData = (data: UptimeData[]): ConsolidatedTimeSlot[] => {
if (!data || data.length === 0) return [];
console.log(`Processing ${data.length} uptime records for consolidation`);
// Create a map to group records by normalized timestamp (minute precision)
const timeSlotMap = new Map<string, Array<UptimeData & { source: string; isDefault: boolean }>>();
data.forEach(record => {
// Normalize timestamp to minute precision to avoid duplicate bars
const normalizedTimestamp = normalizeTimestamp(record.timestamp);
// Safely extract region and agent information
const regionName = getFieldValue(record.region_name);
const agentId = getFieldValue(record.agent_id);
// Determine the source name and whether it's default monitoring
let sourceName: string;
let isDefault: boolean;
if (regionName && agentId) {
// Regional monitoring data
sourceName = `${regionName} (Agent ${agentId})`;
isDefault = false;
console.log(`Found regional data: ${sourceName} for normalized timestamp ${normalizedTimestamp}`);
} else if (agentId && !regionName) {
// Default monitoring with specific agent
sourceName = `Default (Agent ${agentId})`;
isDefault = true;
console.log(`Found default monitoring: ${sourceName} for normalized timestamp ${normalizedTimestamp}`);
} else {
// Default monitoring fallback
sourceName = 'Default (Agent 1)';
isDefault = true;
console.log(`Using fallback default monitoring for normalized timestamp ${normalizedTimestamp}`);
}
// Get or create the array for this normalized timestamp
if (!timeSlotMap.has(normalizedTimestamp)) {
timeSlotMap.set(normalizedTimestamp, []);
}
// Check if we already have an entry from this source for this time slot
const existingItems = timeSlotMap.get(normalizedTimestamp)!;
const existingFromSource = existingItems.find(item => item.source === sourceName);
if (!existingFromSource) {
// Add the record with source information only if we don't already have one from this source
timeSlotMap.get(normalizedTimestamp)!.push({
...record,
timestamp: normalizedTimestamp, // Use normalized timestamp
source: sourceName,
isDefault
});
console.log(`Added record to normalized timestamp ${normalizedTimestamp}: Source=${sourceName}, Status=${record.status}, IsDefault=${isDefault}, ResponseTime=${record.responseTime}ms`);
} else {
console.log(`Skipping duplicate record for source ${sourceName} at timestamp ${normalizedTimestamp}`);
}
});
// Convert map to consolidated time slots and sort by timestamp (newest first)
const consolidatedTimeline: ConsolidatedTimeSlot[] = Array.from(timeSlotMap.entries())
.map(([timestamp, items]) => ({
timestamp,
items: items.sort((a, b) => {
// Sort so default monitoring appears first
if (a.isDefault && !b.isDefault) return -1;
if (!a.isDefault && b.isDefault) return 1;
return 0;
})
}))
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 20); // Take the most recent 20 time slots
console.log(`Created ${consolidatedTimeline.length} consolidated time slots with normalized timestamps`);
consolidatedTimeline.forEach((slot, index) => {
console.log(`Slot ${index} - Normalized Timestamp: ${slot.timestamp}, Items: ${slot.items.length}`);
slot.items.forEach((item, itemIndex) => {
console.log(` Item ${itemIndex}: Source=${item.source}, Status=${item.status}, ResponseTime=${item.responseTime}ms, IsDefault=${item.isDefault}`);
});
});
return consolidatedTimeline;
};
// Update consolidated items when data changes
useEffect(() => {
if (uptimeData && uptimeData.length > 0) {
console.log(`Processing consolidated uptime data for service ${serviceId}`);
const processedData = processConsolidatedData(uptimeData);
setConsolidatedItems(processedData);
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
// Generate placeholder data when no real data is available
console.log(`No uptime data available for service ${serviceId}, generating placeholder`);
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
? status
: "paused";
const placeholderHistory: ConsolidatedTimeSlot[] = Array(20).fill(null).map((_, index) => {
const timestamp = new Date(Date.now() - (index * interval * 1000));
timestamp.setSeconds(0, 0); // Normalize to minute precision
return {
timestamp: timestamp.toISOString(),
items: [{
id: `placeholder-${serviceId}-${index}`,
service_id: serviceId || "",
serviceId: serviceId || "",
timestamp: timestamp.toISOString(),
status: statusValue as "up" | "down" | "warning" | "paused",
responseTime: 0,
source: 'Default (Agent 1)',
isDefault: true
}]
};
});
setConsolidatedItems(placeholderHistory);
}
}, [uptimeData, serviceId, status, interval]);
return {
consolidatedItems,
isLoading,
error,
isFetching,
refetch
};
};
@@ -0,0 +1,180 @@
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { uptimeService } from '@/services/uptimeService';
import { UptimeData } from '@/types/service.types';
interface UseDefaultUptimeDataProps {
serviceId?: string;
serviceType?: string;
status: string;
interval: number;
}
export const useDefaultUptimeData = ({ serviceId, serviceType, status, interval }: UseDefaultUptimeDataProps) => {
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
// Fetch ONLY Default (Agent ID 1) uptime history data - NO regional data whatsoever
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
queryKey: ['strictDefaultUptimeHistory', serviceId, serviceType],
queryFn: async () => {
if (!serviceId) {
console.log('No serviceId provided, skipping fetch');
return [];
}
console.log(`Fetching STRICT Default (Agent ID 1) uptime data for service ${serviceId} of type ${serviceType}`);
// Get raw uptime history data
const rawData = await uptimeService.getUptimeHistory(serviceId, 50, undefined, undefined, serviceType);
// ULTRA-STRICT filtering: ONLY Default monitoring records (Agent ID 1)
// This means: NO region_name, NO agent_id, NO regional monitoring whatsoever
const strictDefaultData = rawData.filter(record => {
// Must have NO regional monitoring info at all
const isStrictDefault = (
!record.region_name &&
!record.agent_id &&
record.service_id === serviceId
);
if (isStrictDefault) {
console.log(`✓ ACCEPTED Default record: ${record.id} - ${record.timestamp} - Pure default monitoring`);
} else {
console.log(`✗ REJECTED non-default record: ${record.id} - region: ${record.region_name || 'none'}, agent: ${record.agent_id || 'none'}`);
}
return isStrictDefault;
});
console.log(`ULTRA-STRICT FILTER: Reduced ${rawData.length} records to ${strictDefaultData.length} pure default monitoring records`);
return strictDefaultData;
},
enabled: !!serviceId,
refetchInterval: 30000,
staleTime: 15000,
placeholderData: (previousData) => previousData,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
// Process uptime data with absolute timestamp uniqueness
const processUptimeData = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
if (!data || data.length === 0) return [];
console.log(`Processing ${data.length} strict default monitoring records for service ${serviceId}`);
// Sort data by timestamp (newest first)
const sortedData = [...data].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
// ABSOLUTE timestamp uniqueness - use Map for O(1) lookup
const timestampMap = new Map<string, UptimeData>();
sortedData.forEach(record => {
const exactTimestamp = record.timestamp;
if (!timestampMap.has(exactTimestamp)) {
timestampMap.set(exactTimestamp, record);
console.log(`✓ Added unique default record: ${record.id} - ${exactTimestamp}`);
} else {
console.log(`✗ REJECTED absolute duplicate timestamp: ${record.id} - ${exactTimestamp} (exact timestamp match)`);
}
});
// Convert Map back to array and take most recent 20
const uniqueRecords = Array.from(timestampMap.values());
const recentData = uniqueRecords.slice(0, 20);
console.log(`FINAL RESULT: ${recentData.length} absolutely unique default monitoring records`);
return recentData;
};
// Update history items when data changes
useEffect(() => {
if (uptimeData && uptimeData.length > 0) {
console.log(`Received ${uptimeData.length} strict default monitoring records for service ${serviceId}`);
const processedData = processUptimeData(uptimeData, interval);
setHistoryItems(processedData);
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
// Generate placeholder data when no real data is available
console.log(`No strict default monitoring data available for service ${serviceId}, generating placeholder`);
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
? status
: "paused";
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
id: `placeholder-${serviceId}-${index}`,
service_id: serviceId || "",
serviceId: serviceId || "",
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
status: statusValue as "up" | "down" | "warning" | "paused",
responseTime: 0
}));
setHistoryItems(placeholderHistory);
}
}, [uptimeData, serviceId, status, interval]);
// Ensure we always have exactly 20 items for consistent display
const getDisplayItems = (): UptimeData[] => {
const items = [...historyItems];
// If we have fewer than 20 items, pad with older placeholder data
if (items.length < 20) {
const lastItem = items.length > 0 ? items[items.length - 1] : null;
const lastStatus = lastItem ? lastItem.status :
(status === "up" || status === "down" || status === "warning" || status === "paused") ?
status as "up" | "down" | "warning" | "paused" : "paused";
const paddingCount = 20 - items.length;
const paddingItems: UptimeData[] = Array(paddingCount).fill(null).map((_, index) => {
const baseTime = lastItem ? new Date(lastItem.timestamp).getTime() : Date.now();
const timeOffset = (index + 1) * interval * 1000;
return {
id: `padding-${serviceId}-${index}`,
service_id: serviceId || "",
serviceId: serviceId || "",
timestamp: new Date(baseTime - timeOffset).toISOString(),
status: lastStatus,
responseTime: 0
};
});
items.push(...paddingItems);
}
// Final absolute validation - use Set for guaranteed uniqueness
const finalItems = items.slice(0, 20);
const seenTimestamps = new Set<string>();
const absolutelyUniqueItems: UptimeData[] = [];
finalItems.forEach(item => {
if (!seenTimestamps.has(item.timestamp)) {
seenTimestamps.add(item.timestamp);
absolutelyUniqueItems.push(item);
} else {
console.log(`FINAL VALIDATION: Removing absolute duplicate timestamp ${item.timestamp} from display`);
}
});
// Sort by timestamp (newest first) and ensure exactly 20 items
const sortedValidated = absolutelyUniqueItems.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
console.log(`DISPLAY READY: ${sortedValidated.length} absolutely validated default monitoring items`);
return sortedValidated.slice(0, 20);
};
return {
displayItems: getDisplayItems(),
isLoading,
error,
isFetching,
refetch
};
};
@@ -14,16 +14,24 @@ interface UseUptimeDataProps {
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
// Fetch real uptime history data if serviceId is provided
// Fetch ALL uptime history data including regional monitoring data
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
queryKey: ['uptimeHistory', serviceId, serviceType],
queryFn: () => {
queryKey: ['allUptimeHistory', serviceId, serviceType],
queryFn: async () => {
if (!serviceId) {
console.log('No serviceId provided, skipping fetch');
return Promise.resolve([]);
return [];
}
console.log(`Fetching uptime data for service ${serviceId} of type ${serviceType}`);
return uptimeService.getUptimeHistory(serviceId, 50, undefined, undefined, serviceType);
console.log(`Fetching ALL uptime data for service ${serviceId} of type ${serviceType} - including regional monitoring`);
// Get raw uptime history data
const rawData = await uptimeService.getUptimeHistory(serviceId, 50, undefined, undefined, serviceType);
// Include ALL monitoring data - both default and regional
const allMonitoringData = rawData.filter(record => record.service_id === serviceId);
console.log(`Retrieved ${rawData.length} total records, filtered to ${allMonitoringData.length} records for service ${serviceId}`);
return allMonitoringData;
},
enabled: !!serviceId,
refetchInterval: 30000,
@@ -33,21 +41,21 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
// Filter and process uptime data
// Process uptime data to include both default and regional monitoring
const processUptimeData = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
if (!data || data.length === 0) return [];
console.log(`Processing ${data.length} uptime records for service ${serviceId}`);
console.log(`Processing ${data.length} uptime records (all monitoring data) for service ${serviceId}`);
// Sort data by timestamp (newest first)
const sortedData = [...data].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
// Take the most recent 20 records to ensure we have enough data
const recentData = sortedData.slice(0, 20);
// Take the most recent records to ensure we have enough data for both default and regional
const recentData = sortedData.slice(0, 50);
console.log(`Using ${recentData.length} most recent records`);
console.log(`Using ${recentData.length} most recent records including both default and regional monitoring`);
return recentData;
};
@@ -55,7 +63,7 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
// Update history items when data changes
useEffect(() => {
if (uptimeData && uptimeData.length > 0) {
console.log(`Received ${uptimeData.length} uptime records for service ${serviceId}`);
console.log(`Received ${uptimeData.length} uptime records (all monitoring) for service ${serviceId}`);
const processedData = processUptimeData(uptimeData, interval);
setHistoryItems(processedData);
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
@@ -68,8 +76,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
id: `placeholder-${serviceId}-${index}`,
service_id: serviceId || "", // Include service_id
serviceId: serviceId || "", // Keep for backward compatibility
service_id: serviceId || "",
serviceId: serviceId || "",
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
status: statusValue as "up" | "down" | "warning" | "paused",
responseTime: 0
@@ -79,39 +87,9 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
}
}, [uptimeData, serviceId, status, interval]);
// Ensure we always have exactly 20 items for consistent display
// Return display items - all monitoring data for tooltip usage
const getDisplayItems = (): UptimeData[] => {
const items = [...historyItems];
// If we have fewer than 20 items, pad with older placeholder data
if (items.length < 20) {
const lastItem = items.length > 0 ? items[items.length - 1] : null;
const lastStatus = lastItem ? lastItem.status :
(status === "up" || status === "down" || status === "warning" || status === "paused") ?
status as "up" | "down" | "warning" | "paused" : "paused";
const paddingCount = 20 - items.length;
const paddingItems: UptimeData[] = Array(paddingCount).fill(null).map((_, index) => {
const baseTime = lastItem ? new Date(lastItem.timestamp).getTime() : Date.now();
const timeOffset = (index + 1) * interval * 1000;
return {
id: `padding-${serviceId}-${index}`,
service_id: serviceId || "", // Include service_id
serviceId: serviceId || "", // Keep for backward compatibility
timestamp: new Date(baseTime - timeOffset).toISOString(),
status: lastStatus,
responseTime: 0
};
});
items.push(...paddingItems);
}
// Return exactly 20 items, sorted by timestamp (newest first)
return items.slice(0, 20).sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
return historyItems;
};
return {
@@ -8,12 +8,12 @@ interface UptimeSummaryProps {
export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => {
return (
<div className="flex items-center justify-between text-xs">
<div className="flex items-center justify-between text-xs mt-1">
<span className="text-muted-foreground">
{Math.round(uptime)}% uptime
</span>
<span className="text-xs text-muted-foreground">
Last 20 checks
Last 20 checks
</span>
</div>
);
@@ -1,4 +1,3 @@
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -19,7 +18,7 @@ const formSchema = z.object({
domain: z.string().min(1, "Domain is required"),
warning_threshold: z.coerce.number().int().min(1).max(365),
expiry_threshold: z.coerce.number().int().min(1).max(30),
notification_channel: z.string().min(1, "Notification channel is required"),
notification_channel: z.string().optional(), // Make it optional to allow empty string for "None"
check_interval: z.coerce.number().int().min(1).max(30).optional()
});
@@ -44,7 +43,7 @@ export const AddSSLCertificateForm = ({
domain: "",
warning_threshold: 30,
expiry_threshold: 7,
notification_channel: "",
notification_channel: "none",
check_interval: 1
}
});
@@ -66,14 +65,6 @@ export const AddSSLCertificateForm = ({
return config.enabled === true;
});
setAlertConfigs(enabledConfigs);
// Set default if available, preferring Telegram channels
const telegramChannel = enabledConfigs.find(c => c.notification_type === "telegram");
const defaultChannel = telegramChannel || enabledConfigs[0];
if (defaultChannel?.id) {
form.setValue("notification_channel", defaultChannel.id);
}
} catch (error) {
console.error("Error fetching notification channels:", error);
toast.error(t('failedToLoadCertificates'));
@@ -92,7 +83,7 @@ export const AddSSLCertificateForm = ({
domain: values.domain,
warning_threshold: values.warning_threshold,
expiry_threshold: values.expiry_threshold,
notification_channel: values.notification_channel,
notification_channel: values.notification_channel === "none" ? "" : (values.notification_channel || ""), // Convert "none" to empty string
check_interval: values.check_interval
};
@@ -180,13 +171,14 @@ export const AddSSLCertificateForm = ({
render={({ field }) => (
<FormItem>
<FormLabel>{t('notificationChannel')}</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<Select onValueChange={field.onChange} value={field.value || "none"}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('chooseChannel')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">{t('none')}</SelectItem>
{alertConfigs.length > 0 ? (
alertConfigs.map((config) => (
<SelectItem key={config.id} value={config.id || ""}>
@@ -196,13 +188,13 @@ export const AddSSLCertificateForm = ({
) : isLoading ? (
<SelectItem value="loading" disabled>{t('loadingChannels')}</SelectItem>
) : (
<SelectItem value="none" disabled>{t('noChannelsFound')}</SelectItem>
<SelectItem value="none-disabled" disabled>{t('noChannelsFound')}</SelectItem>
)}
</SelectContent>
</Select>
<FormDescription className="flex items-center gap-1">
<Bell className="h-4 w-4" />
{t('chooseChannel')}
{t('whereToSend')}
</FormDescription>
<FormMessage />
</FormItem>
+47 -9
View File
@@ -1,4 +1,3 @@
import { pb } from '@/lib/pocketbase';
import { RegionalService, CreateRegionalServiceParams, InstallCommand } from '@/types/regional.types';
@@ -106,7 +105,7 @@ AGENT_TOKEN="${token}"
POCKETBASE_URL="${apiEndpoint}"
# Base package information
BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/releases"
BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0"
PACKAGE_VERSION="1.0.0"
SERVICE_NAME="regional-check-agent"
@@ -175,14 +174,44 @@ echo "📁 Created temporary directory: $TEMP_DIR"
# Download the .deb package
echo "📥 Downloading Regional Monitoring Agent package for $PKG_ARCH..."
cd "$TEMP_DIR"
if wget -q --show-progress "$PACKAGE_URL" -O "$PACKAGE_NAME"; then
echo "✅ Package downloaded successfully"
else
# Test if package exists first
echo "🔍 Checking package availability..."
if command -v curl >/dev/null 2>&1; then
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -I "$PACKAGE_URL")
if [ "$HTTP_STATUS" != "200" ]; then
echo "❌ Package not found at $PACKAGE_URL (HTTP $HTTP_STATUS)"
echo " Available packages should be:"
echo " - distributed-regional-check-agent_\${PACKAGE_VERSION}_amd64.deb"
echo " - distributed-regional-check-agent_\${PACKAGE_VERSION}_arm64.deb"
rm -rf "$TEMP_DIR"
exit 1
fi
echo "✅ Package found, proceeding with download..."
fi
# Try wget first, then curl as fallback
DOWNLOAD_SUCCESS=false
if command -v wget >/dev/null 2>&1; then
if wget -q --show-progress --timeout=30 --tries=3 "$PACKAGE_URL" -O "$PACKAGE_NAME"; then
DOWNLOAD_SUCCESS=true
echo "✅ Package downloaded successfully using wget"
fi
elif command -v curl >/dev/null 2>&1; then
if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 2 -o "$PACKAGE_NAME" "$PACKAGE_URL"; then
DOWNLOAD_SUCCESS=true
echo "✅ Package downloaded successfully using curl"
fi
fi
if [ "$DOWNLOAD_SUCCESS" = false ]; then
echo "❌ Failed to download package from $PACKAGE_URL"
echo " Please check:"
echo " - Internet connection"
echo " - Package availability for $PKG_ARCH architecture"
echo " - GitHub repository access"
echo " - GitHub repository access: https://github.com/operacle/Distributed-Regional-Monitoring/releases"
echo " - Firewall/proxy settings"
rm -rf "$TEMP_DIR"
exit 1
fi
@@ -222,6 +251,9 @@ fi
echo ""
echo "⚙️ Configuring Regional Monitoring Agent..."
# Ensure configuration directory exists
mkdir -p /etc/regional-check-agent
# Create the environment configuration file
cat > /etc/regional-check-agent/regional-check-agent.env << EOF
# Distributed Regional Check Agent Configuration
@@ -232,7 +264,7 @@ PORT=8091
# Operation defaults
DEFAULT_COUNT=4
DEFAULT_TIMEOUT=3s
DEFAULT_TIMEOUT=10s
MAX_COUNT=20
MAX_TIMEOUT=30s
@@ -258,8 +290,14 @@ EOF
echo "✅ Configuration file created at /etc/regional-check-agent/regional-check-agent.env"
# Set proper permissions
chown root:regional-check-agent /etc/regional-check-agent/regional-check-agent.env
chmod 640 /etc/regional-check-agent/regional-check-agent.env
if id "regional-check-agent" &>/dev/null; then
chown root:regional-check-agent /etc/regional-check-agent/regional-check-agent.env
chmod 640 /etc/regional-check-agent/regional-check-agent.env
else
echo "⚠️ regional-check-agent user not found, using root permissions"
chown root:root /etc/regional-check-agent/regional-check-agent.env
chmod 600 /etc/regional-check-agent/regional-check-agent.env
fi
# Enable and start the service
echo ""
@@ -32,6 +32,10 @@ export const serviceService = {
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
alerts: item.alerts || "unmuted", // Store actual database field
muteChangedAt: item.mute_changed_at,
// Regional monitoring fields
region_name: item.region_name || "",
agent_id: item.agent_id || "",
regional_monitoring_enabled: item.regional_monitoring_enabled || false,
}));
} catch (error) {
console.error("Error fetching services:", error);
@@ -58,6 +62,10 @@ export const serviceService = {
max_retries: params.retries,
notification_id: params.notificationChannel,
template_id: params.alertTemplate,
// Regional monitoring fields
regional_monitoring_enabled: params.regionalMonitoringEnabled || false,
region_name: params.regionName || "",
agent_id: params.agentId || "",
// Conditionally add fields based on service type
...(serviceType === "dns"
? { domain: params.domain, url: "", host: "", port: null } // DNS: store in domain field
@@ -90,6 +98,9 @@ export const serviceService = {
retries: record.max_retries || 3,
notificationChannel: record.notification_id,
alertTemplate: record.template_id,
regional_monitoring_enabled: record.regional_monitoring_enabled || false,
region_name: record.region_name || "",
agent_id: record.agent_id || "",
} as Service;
// Immediately start monitoring for the new service
@@ -117,6 +128,10 @@ export const serviceService = {
max_retries: params.retries,
notification_id: params.notificationChannel || null,
template_id: params.alertTemplate || null,
// Regional monitoring fields
regional_monitoring_enabled: params.regionalMonitoringEnabled || false,
region_name: params.regionName || "",
agent_id: params.agentId || "",
// Conditionally update fields based on service type
...(serviceType === "dns"
? { domain: params.domain, url: "", host: "", port: null } // DNS: update domain field
@@ -156,6 +171,9 @@ export const serviceService = {
retries: record.max_retries || 3,
notificationChannel: record.notification_id,
alertTemplate: record.template_id,
regional_monitoring_enabled: record.regional_monitoring_enabled || false,
region_name: record.region_name || "",
agent_id: record.agent_id || "",
} as Service;
return updatedService;
+83 -7
View File
@@ -70,7 +70,7 @@ export const uptimeService = {
return [];
}
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}`;
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}_default`;
// Check cache
const cached = uptimeCache.get(cacheKey);
@@ -81,7 +81,7 @@ export const uptimeService = {
// Determine the correct collection based on service type
const collection = serviceType ? getCollectionForServiceType(serviceType) : 'uptime_data';
console.log(`Fetching uptime history for service ${serviceId} from collection ${collection}, limit: ${limit}`);
console.log(`Fetching default uptime history for service ${serviceId} from collection ${collection}, limit: ${limit}`);
// Build filter to get records for specific service_id
let filter = `service_id='${serviceId}'`;
@@ -102,11 +102,11 @@ export const uptimeService = {
$cancelKey: `uptime_history_${serviceId}_${Date.now()}`
};
console.log(`Filter query: ${filter} on collection: ${collection}`);
console.log(`Filter query for default data: ${filter} on collection: ${collection}`);
const response = await pb.collection(collection).getList(1, limit, options);
console.log(`Fetched ${response.items.length} uptime records for service ${serviceId} from ${collection}`);
console.log(`Fetched ${response.items.length} records for service ${serviceId} from ${collection}`);
if (response.items.length > 0) {
console.log(`Date range in results: ${response.items[response.items.length - 1].timestamp} to ${response.items[0].timestamp}`);
@@ -117,8 +117,8 @@ export const uptimeService = {
// Transform the response items to UptimeData format
const uptimeData = response.items.map(item => ({
id: item.id,
service_id: item.service_id, // Include service_id
serviceId: item.service_id, // Keep for backward compatibility
service_id: item.service_id,
serviceId: item.service_id,
timestamp: item.timestamp,
status: item.status as "up" | "down" | "warning" | "paused",
responseTime: item.response_time || 0,
@@ -140,7 +140,7 @@ export const uptimeService = {
console.error(`Error fetching uptime history for service ${serviceId}:`, error);
// Try to return cached data as fallback
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}`;
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}_default`;
const cached = uptimeCache.get(cacheKey);
if (cached) {
console.log(`Using expired cached data for service ${serviceId} due to fetch error`);
@@ -151,5 +151,81 @@ export const uptimeService = {
console.log(`Returning empty array for service ${serviceId} due to fetch error`);
return [];
}
},
async getUptimeHistoryByRegionalAgent(
serviceId: string,
limit: number = 50,
startDate?: Date,
endDate?: Date,
serviceType?: string,
regionName?: string,
agentId?: string
): Promise<UptimeData[]> {
try {
if (!regionName || !agentId) {
console.log('No region name or agent ID provided for regional query');
return [];
}
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}_${serviceType || 'default'}_${regionName}_${agentId}`;
// Check cache
const cached = uptimeCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < cached.expiresIn) {
console.log(`Using cached regional uptime history for service ${serviceId}`);
return cached.data;
}
// Determine the correct collection based on service type
const collection = serviceType ? getCollectionForServiceType(serviceType) : 'uptime_data';
console.log(`Fetching regional uptime history from collection: ${collection} for service: ${serviceId}, region: ${regionName}, agent: ${agentId}`);
// Build filter for regional agent data
let filter = `service_id="${serviceId}" && region_name="${regionName}" && agent_id="${agentId}"`;
if (startDate && endDate) {
const startISO = startDate.toISOString();
const endISO = endDate.toISOString();
filter += ` && timestamp>="${startISO}" && timestamp<="${endISO}"`;
}
console.log(`Regional filter query: ${filter} on collection: ${collection}`);
const records = await pb.collection(collection).getList(1, limit, {
sort: '-timestamp',
filter: filter,
$autoCancel: false,
$cancelKey: `regional_uptime_history_${serviceId}_${Date.now()}`
});
console.log(`Retrieved ${records.items.length} regional uptime records from ${collection} for region ${regionName}, agent ${agentId}`);
const uptimeData = records.items.map(item => ({
id: item.id,
service_id: item.service_id,
serviceId: item.service_id,
timestamp: item.timestamp,
status: item.status as "up" | "down" | "paused" | "warning",
responseTime: item.response_time || item.responseTime || 0,
error_message: item.error_message,
details: item.details,
created: item.created,
updated: item.updated
}));
// Cache the result
uptimeCache.set(cacheKey, {
data: uptimeData,
timestamp: Date.now(),
expiresIn: CACHE_TTL
});
return uptimeData;
} catch (error) {
const collectionForError = serviceType ? getCollectionForServiceType(serviceType) : 'uptime_data';
console.error(`Error fetching regional uptime history from ${collectionForError}:`, error);
return [];
}
}
};
+13 -1
View File
@@ -1,4 +1,3 @@
export interface Service {
id: string;
name: string;
@@ -32,6 +31,10 @@ export interface Service {
headers?: string;
body?: string;
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
// Regional monitoring fields
region_name?: string;
agent_id?: string;
regional_monitoring_enabled?: boolean;
}
export interface CreateServiceParams {
@@ -45,6 +48,10 @@ export interface CreateServiceParams {
retries: number;
notificationChannel?: string;
alertTemplate?: string;
// Regional monitoring params
regionalMonitoringEnabled?: boolean;
regionName?: string;
agentId?: string;
}
export interface UptimeData {
@@ -60,6 +67,11 @@ export interface UptimeData {
updated?: string;
date?: string; // Keep for backward compatibility
uptime?: number; // Keep for backward compatibility
// Regional monitoring fields
region_name?: string;
agent_id?: string | number;
// Source identifier for multi-source display
source?: string;
}
export interface PingData {
+60 -11
View File
@@ -3,7 +3,7 @@
# CheckCle Regional Monitoring Agent - Universal Installation Script
# This script automatically detects system architecture and installs the appropriate package
# Usage: curl -fsSL https://github.com/operacle/checkcle/blob/main/scripts/install-regional-agent.sh | sudo bash -s -- [options]
# Usage: curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/main/scripts/install-regional-agent.sh | sudo bash -s -- [options]
set -e
@@ -13,7 +13,7 @@ AGENT_ID=""
AGENT_IP_ADDRESS=""
AGENT_TOKEN=""
POCKETBASE_URL=""
BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/releases"
BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0"
PACKAGE_VERSION="1.0.0"
SERVICE_NAME="regional-check-agent"
@@ -149,8 +149,8 @@ echo ""
echo "🔧 Checking system requirements..."
MISSING_TOOLS=()
if ! command -v wget >/dev/null 2>&1; then
MISSING_TOOLS+=("wget")
if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then
MISSING_TOOLS+=("wget or curl")
fi
if ! command -v dpkg >/dev/null 2>&1; then
@@ -164,7 +164,7 @@ fi
if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then
echo "❌ Missing required tools: ${MISSING_TOOLS[*]}"
echo " Please install missing tools and try again"
echo " On Debian/Ubuntu: sudo apt-get update && sudo apt-get install wget"
echo " On Debian/Ubuntu: sudo apt-get update && sudo apt-get install wget curl"
exit 1
fi
@@ -179,15 +179,62 @@ echo ""
echo "📥 Downloading Regional Monitoring Agent package for $PKG_ARCH..."
cd "$TEMP_DIR"
if wget -q --show-progress --timeout=30 --tries=3 "$PACKAGE_URL" -O "$PACKAGE_NAME"; then
echo "✅ Package downloaded successfully"
else
# Test if package exists first - Accept both 200 and 302 (redirect) as success
echo "🔍 Checking package availability..."
if command -v curl >/dev/null 2>&1; then
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -I "$PACKAGE_URL")
if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then
echo "❌ Package not found at $PACKAGE_URL (HTTP $HTTP_STATUS)"
echo " Available packages should be:"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_amd64.deb"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_arm64.deb"
echo ""
echo " Please check the GitHub releases page:"
echo " https://github.com/operacle/Distributed-Regional-Monitoring/releases"
rm -rf "$TEMP_DIR"
exit 1
fi
echo "✅ Package found (HTTP $HTTP_STATUS), proceeding with download..."
fi
# Try wget first, then curl as fallback
DOWNLOAD_SUCCESS=false
if command -v wget >/dev/null 2>&1; then
echo "📥 Downloading with wget..."
if wget -q --show-progress --timeout=30 --tries=3 "$PACKAGE_URL" -O "$PACKAGE_NAME"; then
DOWNLOAD_SUCCESS=true
echo "✅ Package downloaded successfully using wget"
fi
fi
if [ "$DOWNLOAD_SUCCESS" = false ] && command -v curl >/dev/null 2>&1; then
echo "📥 Downloading with curl..."
if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 2 -o "$PACKAGE_NAME" "$PACKAGE_URL" --progress-bar; then
DOWNLOAD_SUCCESS=true
echo "✅ Package downloaded successfully using curl"
fi
fi
if [ "$DOWNLOAD_SUCCESS" = false ]; then
echo "❌ Failed to download package from $PACKAGE_URL"
echo " Please check:"
echo " - Internet connection"
echo " - Package availability for $PKG_ARCH architecture"
echo " - GitHub repository access"
echo " - GitHub repository access: https://github.com/operacle/Distributed-Regional-Monitoring/releases"
echo " - Firewall/proxy settings"
echo ""
echo " Available packages should be:"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_amd64.deb"
echo " - distributed-regional-check-agent_${PACKAGE_VERSION}_arm64.deb"
rm -rf "$TEMP_DIR"
exit 1
fi
# Verify download was successful
if [ ! -f "$PACKAGE_NAME" ] || [ ! -s "$PACKAGE_NAME" ]; then
echo "❌ Downloaded package is empty or missing"
echo " File size: $(ls -lh "$PACKAGE_NAME" 2>/dev/null | awk '{print $5}' || echo 'file not found')"
rm -rf "$TEMP_DIR"
exit 1
fi
@@ -203,6 +250,8 @@ if dpkg-deb --info "$PACKAGE_NAME" > /dev/null 2>&1; then
dpkg-deb --field "$PACKAGE_NAME" Package Version Architecture Description | head -4
else
echo "❌ Package verification failed - corrupted download"
echo " File size: $(ls -lh "$PACKAGE_NAME" | awk '{print $5}')"
echo " Try downloading manually from: $PACKAGE_URL"
rm -rf "$TEMP_DIR"
exit 1
fi
@@ -227,7 +276,7 @@ else
echo " Manual resolution:"
echo " 1. Run: sudo apt-get update"
echo " 2. Run: sudo apt-get install -f"
echo " 3. Retry installation"
echo " 3. Retry installation: sudo dpkg -i $PACKAGE_NAME"
rm -rf "$TEMP_DIR"
exit 1
fi
@@ -379,7 +428,7 @@ echo "🔧 Troubleshooting:"
echo " If the service fails to start:"
echo " 1. Check logs: sudo journalctl -u $SERVICE_NAME -n 50"
echo " 2. Verify config: cat /etc/regional-check-agent/regional-check-agent.env"
echo " 3. Test connectivity: ping $POCKETBASE_URL"
echo " 3. Test connectivity: ping $(echo $POCKETBASE_URL | sed 's|https\?://||' | sed 's|/.*||')"
echo " 4. Check port availability: sudo netstat -tlnp | grep 8091"
echo ""
echo "✨ The agent is now monitoring and reporting to your CheckCle dashboard!"