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:
@@ -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>
|
||||
)}
|
||||
|
||||
+91
-11
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!"
|
||||
Reference in New Issue
Block a user