Merge pull request #53 from operacle/develop
Integrate regional agents in service-operation and Enhance Regional Agent Installation
This commit is contained in:
+69
-17
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
|
||||||
import { ThemeProvider } from '@/contexts/ThemeContext';
|
import { ThemeProvider } from '@/contexts/ThemeContext';
|
||||||
import { LanguageProvider } from '@/contexts/LanguageContext';
|
import { LanguageProvider } from '@/contexts/LanguageContext';
|
||||||
import { SidebarProvider } from '@/contexts/SidebarContext';
|
import { SidebarProvider } from '@/contexts/SidebarContext';
|
||||||
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
@@ -13,51 +13,103 @@ import Index from '@/pages/Index';
|
|||||||
import Login from '@/pages/Login';
|
import Login from '@/pages/Login';
|
||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
import ServiceDetail from '@/pages/ServiceDetail';
|
import ServiceDetail from '@/pages/ServiceDetail';
|
||||||
import Profile from '@/pages/Profile';
|
|
||||||
import Settings from '@/pages/Settings';
|
import Settings from '@/pages/Settings';
|
||||||
import OperationalPage from '@/pages/OperationalPage';
|
import Profile from '@/pages/Profile';
|
||||||
import ScheduleIncident from '@/pages/ScheduleIncident';
|
|
||||||
import SslDomain from '@/pages/SslDomain';
|
import SslDomain from '@/pages/SslDomain';
|
||||||
|
import ScheduleIncident from '@/pages/ScheduleIncident';
|
||||||
|
import OperationalPage from '@/pages/OperationalPage';
|
||||||
import PublicStatusPage from '@/pages/PublicStatusPage';
|
import PublicStatusPage from '@/pages/PublicStatusPage';
|
||||||
|
import RegionalMonitoring from '@/pages/RegionalMonitoring';
|
||||||
import NotFound from '@/pages/NotFound';
|
import NotFound from '@/pages/NotFound';
|
||||||
|
|
||||||
|
import { authService } from '@/services/authService';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
retry: 1,
|
retry: 2,
|
||||||
refetchOnWindowFocus: false,
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Protected Route wrapper
|
||||||
|
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const isAuthenticated = authService.isAuthenticated();
|
||||||
|
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<ErrorBoundary>
|
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/public/:pageId" element={<PublicStatusPage />} />
|
||||||
<Route path="/service/:id" element={<ServiceDetail />} />
|
|
||||||
<Route path="/profile" element={<Profile />} />
|
{/* Protected Routes */}
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/dashboard" element={
|
||||||
<Route path="/operational-page" element={<OperationalPage />} />
|
<ProtectedRoute>
|
||||||
<Route path="/schedule-incident" element={<ScheduleIncident />} />
|
<Dashboard />
|
||||||
<Route path="/ssl-domain" element={<SslDomain />} />
|
</ProtectedRoute>
|
||||||
<Route path="/status/:slug" element={<PublicStatusPage />} />
|
} />
|
||||||
|
|
||||||
|
<Route path="/service/:id" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ServiceDetail />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/settings" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Settings />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/profile" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Profile />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/ssl-domain" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<SslDomain />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/schedule-incident" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ScheduleIncident />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/operational-page" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<OperationalPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/regional-monitoring" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<RegionalMonitoring />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</ErrorBoundary>
|
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, MapPin, Settings, User, Bell, Database, Info, BookOpen } from "lucide-react";
|
||||||
import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, FileText, Settings, User, Bell, Database, Info, BookOpen } from "lucide-react";
|
|
||||||
|
|
||||||
export const mainMenuItems = [
|
export const mainMenuItems = [
|
||||||
{
|
{
|
||||||
@@ -51,12 +50,12 @@ export const mainMenuItems = [
|
|||||||
hasNavigation: false
|
hasNavigation: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'api-documentation',
|
id: 'regional-monitoring',
|
||||||
path: null,
|
path: '/regional-monitoring',
|
||||||
icon: FileText,
|
icon: MapPin,
|
||||||
translationKey: 'apiDocumentation',
|
translationKey: 'regionalMonitoring',
|
||||||
color: 'text-indigo-400',
|
color: 'text-indigo-400',
|
||||||
hasNavigation: false
|
hasNavigation: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Copy, Download, Terminal, CheckCircle, Zap, Play } from "lucide-react";
|
||||||
|
import { regionalService } from "@/services/regionalService";
|
||||||
|
import { InstallCommand } from "@/types/regional.types";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface AddRegionalAgentDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onAgentAdded: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onAgentAdded
|
||||||
|
}) => {
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [regionName, setRegionName] = useState("");
|
||||||
|
const [agentIp, setAgentIp] = useState("");
|
||||||
|
const [installCommand, setInstallCommand] = useState<InstallCommand | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const handleCreateAgent = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!regionName.trim() || !agentIp.trim()) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result = await regionalService.createRegionalService({
|
||||||
|
region_name: regionName,
|
||||||
|
agent_ip_address: agentIp,
|
||||||
|
});
|
||||||
|
|
||||||
|
setInstallCommand(result.installCommand);
|
||||||
|
setStep(2);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to create regional agent configuration.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
toast({
|
||||||
|
title: "Copied!",
|
||||||
|
description: "Installation script copied to clipboard.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Copy failed",
|
||||||
|
description: "Failed to copy to clipboard.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadScript = () => {
|
||||||
|
if (!installCommand) return;
|
||||||
|
|
||||||
|
const blob = new Blob([installCommand.bash_script], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `install-regional-agent-${installCommand.agent_id}.sh`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Downloaded!",
|
||||||
|
description: "Installation script downloaded successfully.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
setRegionName("");
|
||||||
|
setAgentIp("");
|
||||||
|
setInstallCommand(null);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetDialog = () => {
|
||||||
|
setStep(1);
|
||||||
|
setRegionName("");
|
||||||
|
setAgentIp("");
|
||||||
|
setInstallCommand(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Regional Monitoring Agent</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Deploy a regional monitoring agent with automatic one-click installation.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<form onSubmit={handleCreateAgent} className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="regionName">Region Name</Label>
|
||||||
|
<Input
|
||||||
|
id="regionName"
|
||||||
|
placeholder="e.g., us-east-1, europe-west-1, asia-pacific-1"
|
||||||
|
value={regionName}
|
||||||
|
onChange={(e) => setRegionName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="agentIp">Agent Server IP Address</Label>
|
||||||
|
<Input
|
||||||
|
id="agentIp"
|
||||||
|
placeholder="e.g., 192.168.1.100 or your-server.example.com"
|
||||||
|
value={agentIp}
|
||||||
|
onChange={(e) => setAgentIp(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Generating..." : "Generate Installation"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && installCommand && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<CheckCircle className="h-12 w-12 text-green-600 mx-auto" />
|
||||||
|
<h3 className="text-lg font-semibold">Agent Configuration Ready!</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
One-click installation script generated with automatic configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="oneclicK" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
<TabsTrigger value="oneclicK">One-Click Install</TabsTrigger>
|
||||||
|
<TabsTrigger value="details">Agent Details</TabsTrigger>
|
||||||
|
<TabsTrigger value="manual">Manual Install</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="oneclicK" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Zap className="h-5 w-5 text-yellow-500" />
|
||||||
|
One-Click Automatic Installation
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Complete installation, configuration, and service startup in one command
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 text-green-800 font-medium mb-2">
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
What this script does automatically:
|
||||||
|
</div>
|
||||||
|
<ul className="text-sm text-green-700 space-y-1 ml-6">
|
||||||
|
<li>• Downloads the latest .deb package</li>
|
||||||
|
<li>• Installs the regional monitoring agent</li>
|
||||||
|
<li>• Creates configuration file with your settings</li>
|
||||||
|
<li>• Starts and enables the service</li>
|
||||||
|
<li>• Runs health checks</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Run this command on your target server:</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
readOnly
|
||||||
|
value={`# One-click installation command:
|
||||||
|
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}"`}
|
||||||
|
className="font-mono text-sm min-h-[120px] pr-12"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
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}"`)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={downloadScript} variant="outline" className="flex-1">
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Download Complete Script
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => copyToClipboard(installCommand.bash_script)}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Copy Full Script
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="details" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Generated Agent Configuration</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
These values will be automatically configured during installation
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium text-foreground">Agent ID</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
value={installCommand.agent_id}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium text-foreground">Region Name</Label>
|
||||||
|
<Input
|
||||||
|
value={regionName}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium text-foreground">Server IP</Label>
|
||||||
|
<Input
|
||||||
|
value={agentIp}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium text-foreground">API Endpoint</Label>
|
||||||
|
<Input
|
||||||
|
value={installCommand.api_endpoint}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium text-foreground">Authentication Token</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
value={installCommand.token}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm bg-muted/50 border-muted-foreground/20 text-foreground pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
<strong>Configuration file location:</strong> /etc/regional-check-agent/regional-check-agent.env
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="manual" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Terminal className="h-5 w-5" />
|
||||||
|
Manual Installation Steps
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Step-by-step manual installation process
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="text-sm space-y-4">
|
||||||
|
<div className="border-l-4 border-blue-500 pl-4">
|
||||||
|
<p className="font-medium">Step 1: Download Package</p>
|
||||||
|
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||||
|
wget https://github.com/checkcle/distributed-regional-monitoring/releases/latest/download/distributed-regional-check-agent_1.0.0_amd64.deb
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-l-4 border-blue-500 pl-4">
|
||||||
|
<p className="font-medium">Step 2: Install Package</p>
|
||||||
|
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||||
|
sudo dpkg -i distributed-regional-check-agent_1.0.0_amd64.deb<br/>
|
||||||
|
sudo apt-get install -f
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-l-4 border-blue-500 pl-4">
|
||||||
|
<p className="font-medium">Step 3: Configure Agent</p>
|
||||||
|
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||||
|
sudo nano /etc/regional-check-agent/regional-check-agent.env
|
||||||
|
</code>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Add the configuration values shown in the Agent Details tab</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-l-4 border-blue-500 pl-4">
|
||||||
|
<p className="font-medium">Step 4: Start Service</p>
|
||||||
|
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||||
|
sudo systemctl enable regional-check-agent<br/>
|
||||||
|
sudo systemctl start regional-check-agent
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-l-4 border-green-500 pl-4">
|
||||||
|
<p className="font-medium">Step 5: Verify Installation</p>
|
||||||
|
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||||
|
sudo systemctl status regional-check-agent<br/>
|
||||||
|
curl http://localhost:8091/health
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<Button variant="outline" onClick={resetDialog}>
|
||||||
|
Add Another Agent
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleComplete}>
|
||||||
|
Complete Setup
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
|
import { MapPin, Wifi, WifiOff, MoreVertical, Trash2, Terminal, Copy } from "lucide-react";
|
||||||
|
import { RegionalService } from "@/types/regional.types";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface RegionalAgentCardProps {
|
||||||
|
agent: RegionalService;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RegionalAgentCard: React.FC<RegionalAgentCardProps> = ({ agent, onDelete }) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Check if this is the default agent that cannot be removed
|
||||||
|
const isDefaultAgent = agent.agent_id === "1" || agent.region_name === "Default";
|
||||||
|
|
||||||
|
const copyAgentId = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(agent.agent_id);
|
||||||
|
toast({
|
||||||
|
title: "Copied!",
|
||||||
|
description: "Agent ID copied to clipboard.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Copy failed",
|
||||||
|
description: "Failed to copy agent ID.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConnectionStatus = () => {
|
||||||
|
if (agent.connection === 'online') {
|
||||||
|
return {
|
||||||
|
icon: <Wifi className="h-4 w-4" />,
|
||||||
|
label: 'Online',
|
||||||
|
variant: 'default' as const,
|
||||||
|
className: 'bg-green-100 text-green-800 hover:bg-green-100'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
icon: <WifiOff className="h-4 w-4" />,
|
||||||
|
label: 'Offline',
|
||||||
|
variant: 'secondary' as const,
|
||||||
|
className: 'bg-red-100 text-red-800 hover:bg-red-100'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectionStatus = getConnectionStatus();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="relative">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<MapPin className="h-5 w-5 text-blue-600" />
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
{agent.region_name}
|
||||||
|
{isDefaultAgent && (
|
||||||
|
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
|
||||||
|
Default
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="flex items-center gap-1 text-sm">
|
||||||
|
{agent.agent_ip_address}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={copyAgentId}>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Copy Agent ID
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{!isDefaultAgent && (
|
||||||
|
<DropdownMenuItem onClick={onDelete} className="text-red-600">
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Remove Agent
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Badge
|
||||||
|
variant={connectionStatus.variant}
|
||||||
|
className={connectionStatus.className}
|
||||||
|
>
|
||||||
|
{connectionStatus.icon}
|
||||||
|
<span className="ml-1">{connectionStatus.label}</span>
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{agent.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Agent ID:</span>
|
||||||
|
<span className="font-mono text-xs">{agent.agent_id.substring(0, 12)}...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Last Updated:</span>
|
||||||
|
<span className="text-xs">
|
||||||
|
{formatDistanceToNow(new Date(agent.updated), { addSuffix: true })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agent.connection === 'online' && (
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<div className="flex items-center text-xs text-green-600">
|
||||||
|
<div className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse"></div>
|
||||||
|
Active monitoring
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agent.connection === 'offline' && (
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<div className="flex items-center text-xs text-red-600">
|
||||||
|
<div className="w-2 h-2 bg-red-600 rounded-full mr-2"></div>
|
||||||
|
Connection lost
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Plus, MapPin, Activity, Wifi, WifiOff } from "lucide-react";
|
||||||
|
import { regionalService } from "@/services/regionalService";
|
||||||
|
import { RegionalService } from "@/types/regional.types";
|
||||||
|
import { AddRegionalAgentDialog } from "./AddRegionalAgentDialog";
|
||||||
|
import { RegionalAgentCard } from "./RegionalAgentCard";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
export const RegionalMonitoringContent = () => {
|
||||||
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: regionalServices = [], isLoading, error } = useQuery({
|
||||||
|
queryKey: ['regional-services'],
|
||||||
|
queryFn: regionalService.getRegionalServices,
|
||||||
|
refetchInterval: 30000, // Refetch every 30 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAgentAdded = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
||||||
|
toast({
|
||||||
|
title: "Regional Agent Added",
|
||||||
|
description: "The regional monitoring agent has been successfully configured.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAgent = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await regionalService.deleteRegionalService(id);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['regional-services'] });
|
||||||
|
toast({
|
||||||
|
title: "Agent Removed",
|
||||||
|
description: "The regional monitoring agent has been removed.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to remove the regional monitoring agent.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onlineAgents = regionalServices.filter(agent => agent.connection === 'online').length;
|
||||||
|
const totalAgents = regionalServices.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Regional Monitoring</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Manage distributed monitoring agents across different regions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setAddDialogOpen(true)}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Regional Agent
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Agents</CardTitle>
|
||||||
|
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalAgents}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Regional monitoring agents
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Online Agents</CardTitle>
|
||||||
|
<Wifi className="h-4 w-4 text-green-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-green-600">{onlineAgents}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Currently connected
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Offline Agents</CardTitle>
|
||||||
|
<WifiOff className="h-4 w-4 text-red-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-red-600">{totalAgents - onlineAgents}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Disconnected agents
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agents List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Regional Agents</h2>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Card key={i} className="animate-pulse">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||||
|
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-3 bg-gray-200 rounded"></div>
|
||||||
|
<div className="h-3 bg-gray-200 rounded w-5/6"></div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : regionalServices.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||||
|
<MapPin className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">No Regional Agents</h3>
|
||||||
|
<p className="text-muted-foreground text-center mb-4">
|
||||||
|
Get started by adding your first regional monitoring agent to extend your monitoring coverage.
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => setAddDialogOpen(true)}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add First Agent
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{regionalServices.map((agent) => (
|
||||||
|
<RegionalAgentCard
|
||||||
|
key={agent.id}
|
||||||
|
agent={agent}
|
||||||
|
onDelete={() => handleDeleteAgent(agent.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddRegionalAgentDialog
|
||||||
|
open={addDialogOpen}
|
||||||
|
onOpenChange={setAddDialogOpen}
|
||||||
|
onAgentAdded={handleAgentAdded}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export { RegionalMonitoringContent } from './RegionalMonitoringContent';
|
||||||
|
export { AddRegionalAgentDialog } from './AddRegionalAgentDialog';
|
||||||
|
export { RegionalAgentCard } from './RegionalAgentCard';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Header } from "@/components/dashboard/Header";
|
||||||
|
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||||
|
import { authService } from "@/services/authService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useSidebar } from "@/contexts/SidebarContext";
|
||||||
|
import { RegionalMonitoringContent } from "@/components/regional-monitoring/RegionalMonitoringContent";
|
||||||
|
|
||||||
|
const RegionalMonitoring = () => {
|
||||||
|
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
authService.logout();
|
||||||
|
navigate("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-background text-foreground">
|
||||||
|
<Sidebar collapsed={sidebarCollapsed} />
|
||||||
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
|
<Header
|
||||||
|
currentUser={currentUser}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
|
toggleSidebar={toggleSidebar}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<RegionalMonitoringContent />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegionalMonitoring;
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
|
||||||
|
import { pb } from '@/lib/pocketbase';
|
||||||
|
import { RegionalService, CreateRegionalServiceParams, InstallCommand } from '@/types/regional.types';
|
||||||
|
|
||||||
|
// Generate a random token
|
||||||
|
const generateToken = (): string => {
|
||||||
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate a random agent ID
|
||||||
|
const generateAgentId = (): string => {
|
||||||
|
return 'agent_' + Math.random().toString(36).substring(2, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const regionalService = {
|
||||||
|
async getRegionalServices(): Promise<RegionalService[]> {
|
||||||
|
try {
|
||||||
|
const response = await pb.collection('regional_service').getFullList({
|
||||||
|
sort: '-created',
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.map(record => ({
|
||||||
|
id: record.id,
|
||||||
|
region_name: record.region_name,
|
||||||
|
status: record.status,
|
||||||
|
agent_id: record.agent_id,
|
||||||
|
agent_ip_address: record.agent_ip_address,
|
||||||
|
token: record.token,
|
||||||
|
connection: record.connection,
|
||||||
|
created: record.created,
|
||||||
|
updated: record.updated,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching regional services:', error);
|
||||||
|
throw new Error('Failed to fetch regional services');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createRegionalService(params: CreateRegionalServiceParams): Promise<{ service: RegionalService; installCommand: InstallCommand }> {
|
||||||
|
try {
|
||||||
|
const token = generateToken();
|
||||||
|
const agentId = generateAgentId();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
region_name: params.region_name,
|
||||||
|
status: 'pending',
|
||||||
|
agent_id: agentId,
|
||||||
|
agent_ip_address: params.agent_ip_address,
|
||||||
|
token: token,
|
||||||
|
connection: 'offline',
|
||||||
|
};
|
||||||
|
|
||||||
|
const record = await pb.collection('regional_service').create(data);
|
||||||
|
|
||||||
|
const service: RegionalService = {
|
||||||
|
id: record.id,
|
||||||
|
region_name: record.region_name,
|
||||||
|
status: record.status,
|
||||||
|
agent_id: record.agent_id,
|
||||||
|
agent_ip_address: record.agent_ip_address,
|
||||||
|
token: record.token,
|
||||||
|
connection: record.connection,
|
||||||
|
created: record.created,
|
||||||
|
updated: record.updated,
|
||||||
|
};
|
||||||
|
|
||||||
|
const installCommand: InstallCommand = {
|
||||||
|
token: token,
|
||||||
|
agent_id: agentId,
|
||||||
|
api_endpoint: pb.baseUrl,
|
||||||
|
bash_script: this.generateAutomaticInstallScript(token, agentId, pb.baseUrl, params.agent_ip_address, params.region_name)
|
||||||
|
};
|
||||||
|
|
||||||
|
return { service, installCommand };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating regional service:', error);
|
||||||
|
throw new Error('Failed to create regional service');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteRegionalService(id: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await pb.collection('regional_service').delete(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting regional service:', error);
|
||||||
|
throw new Error('Failed to delete regional service');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
generateAutomaticInstallScript(token: string, agentId: string, apiEndpoint: string, agentIp: string, regionName: string): string {
|
||||||
|
return `#!/bin/bash
|
||||||
|
|
||||||
|
# CheckCle Regional Monitoring Agent - Automatic Installation Script
|
||||||
|
# Generated on: $(date)
|
||||||
|
# This script will automatically detect architecture, download, install, configure and start the regional monitoring agent
|
||||||
|
|
||||||
|
echo "🚀 CheckCle Regional Monitoring Agent - Automatic Installation"
|
||||||
|
echo "=============================================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Configuration variables
|
||||||
|
REGION_NAME="${regionName}"
|
||||||
|
AGENT_ID="${agentId}"
|
||||||
|
AGENT_IP_ADDRESS="${agentIp}"
|
||||||
|
AGENT_TOKEN="${token}"
|
||||||
|
POCKETBASE_URL="${apiEndpoint}"
|
||||||
|
|
||||||
|
# Base package information
|
||||||
|
BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/releases"
|
||||||
|
PACKAGE_VERSION="1.0.0"
|
||||||
|
SERVICE_NAME="regional-check-agent"
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "❌ This script must be run as root (use sudo)"
|
||||||
|
echo " Usage: sudo bash install-regional-agent.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect operating system
|
||||||
|
echo "🔍 Detecting system information..."
|
||||||
|
OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo " Operating System: $OS_TYPE"
|
||||||
|
|
||||||
|
# Check if it's a supported OS
|
||||||
|
if [[ "$OS_TYPE" != "linux" ]]; then
|
||||||
|
echo "❌ Unsupported operating system: $OS_TYPE"
|
||||||
|
echo " This installer only supports Linux systems"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
echo " Hardware Architecture: $ARCH"
|
||||||
|
|
||||||
|
# Map architecture to package architecture
|
||||||
|
case $ARCH in
|
||||||
|
x86_64|amd64)
|
||||||
|
PKG_ARCH="amd64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
PKG_ARCH="arm64"
|
||||||
|
;;
|
||||||
|
armv7l|armv6l)
|
||||||
|
PKG_ARCH="arm64"
|
||||||
|
echo "⚠️ ARM 32-bit detected, using ARM64 package (may require compatibility layer)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Unsupported architecture: $ARCH"
|
||||||
|
echo " Supported architectures: x86_64 (amd64), aarch64 (arm64)"
|
||||||
|
echo " Please contact support for your architecture: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo " Package Architecture: $PKG_ARCH"
|
||||||
|
|
||||||
|
# Construct package URLs and names
|
||||||
|
PACKAGE_URL="$BASE_PACKAGE_URL/distributed-regional-check-agent_\${PACKAGE_VERSION}_\${PKG_ARCH}.deb"
|
||||||
|
PACKAGE_NAME="distributed-regional-check-agent_\${PACKAGE_VERSION}_\${PKG_ARCH}.deb"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📋 Installation Configuration:"
|
||||||
|
echo " Region Name: $REGION_NAME"
|
||||||
|
echo " Agent ID: $AGENT_ID"
|
||||||
|
echo " Agent IP: $AGENT_IP_ADDRESS"
|
||||||
|
echo " PocketBase URL: $POCKETBASE_URL"
|
||||||
|
echo " Package URL: $PACKAGE_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create temporary directory
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
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
|
||||||
|
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"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify package integrity
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Verifying package..."
|
||||||
|
if dpkg-deb --info "$PACKAGE_NAME" > /dev/null 2>&1; then
|
||||||
|
echo "✅ Package verification successful"
|
||||||
|
else
|
||||||
|
echo "❌ Package verification failed - corrupted download"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install the package
|
||||||
|
echo ""
|
||||||
|
echo "📦 Installing Regional Monitoring Agent package..."
|
||||||
|
if dpkg -i "$PACKAGE_NAME"; then
|
||||||
|
echo "✅ Package installed successfully"
|
||||||
|
else
|
||||||
|
echo "⚠️ Package installation had issues, attempting to fix dependencies..."
|
||||||
|
if apt-get install -f -y; then
|
||||||
|
echo "✅ Dependencies fixed and package installed successfully"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to install package and fix dependencies"
|
||||||
|
echo " This might be due to:"
|
||||||
|
echo " - Missing system dependencies"
|
||||||
|
echo " - Architecture compatibility issues"
|
||||||
|
echo " - Insufficient disk space"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure the agent
|
||||||
|
echo ""
|
||||||
|
echo "⚙️ Configuring Regional Monitoring Agent..."
|
||||||
|
|
||||||
|
# Create the environment configuration file
|
||||||
|
cat > /etc/regional-check-agent/regional-check-agent.env << EOF
|
||||||
|
# Distributed Regional Check Agent Configuration
|
||||||
|
# Auto-generated on $(date)
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=8091
|
||||||
|
|
||||||
|
# Operation defaults
|
||||||
|
DEFAULT_COUNT=4
|
||||||
|
DEFAULT_TIMEOUT=3s
|
||||||
|
MAX_COUNT=20
|
||||||
|
MAX_TIMEOUT=30s
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
ENABLE_LOGGING=true
|
||||||
|
|
||||||
|
# PocketBase integration
|
||||||
|
POCKETBASE_ENABLED=true
|
||||||
|
POCKETBASE_URL=$POCKETBASE_URL
|
||||||
|
|
||||||
|
# Regional Agent Configuration - Auto-configured
|
||||||
|
REGION_NAME=$REGION_NAME
|
||||||
|
AGENT_ID=$AGENT_ID
|
||||||
|
AGENT_IP_ADDRESS=$AGENT_IP_ADDRESS
|
||||||
|
AGENT_TOKEN=$AGENT_TOKEN
|
||||||
|
|
||||||
|
# Monitoring configuration
|
||||||
|
CHECK_INTERVAL=30s
|
||||||
|
MAX_RETRIES=3
|
||||||
|
REQUEST_TIMEOUT=10s
|
||||||
|
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
|
||||||
|
|
||||||
|
# Enable and start the service
|
||||||
|
echo ""
|
||||||
|
echo "🔧 Starting Regional Monitoring Agent service..."
|
||||||
|
|
||||||
|
# Reload systemd daemon
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service for auto-start
|
||||||
|
if systemctl enable $SERVICE_NAME; then
|
||||||
|
echo "✅ Service enabled for auto-start"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to enable service"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
if systemctl start $SERVICE_NAME; then
|
||||||
|
echo "✅ Service started successfully"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to start service"
|
||||||
|
echo " Check the configuration and try: sudo systemctl start $SERVICE_NAME"
|
||||||
|
echo " View logs with: sudo journalctl -u $SERVICE_NAME -f"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait a moment for service to initialize
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
echo ""
|
||||||
|
echo "📊 Service Status:"
|
||||||
|
systemctl status $SERVICE_NAME --no-pager -l
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
echo ""
|
||||||
|
echo "🩺 Testing agent health endpoint..."
|
||||||
|
if curl -s -f http://localhost:8091/health > /dev/null; then
|
||||||
|
echo "✅ Agent health endpoint is responding"
|
||||||
|
else
|
||||||
|
echo "⚠️ Agent health endpoint not responding yet (this is normal, may take a few moments)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Regional Monitoring Agent Installation Complete!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Installation Summary:"
|
||||||
|
echo " Agent ID: $AGENT_ID"
|
||||||
|
echo " Region: $REGION_NAME"
|
||||||
|
echo " Architecture: $PKG_ARCH"
|
||||||
|
echo " Status: $(systemctl is-active $SERVICE_NAME)"
|
||||||
|
echo " Health URL: http://localhost:8091/health"
|
||||||
|
echo " Service endpoint: http://localhost:8091/operation"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Useful commands:"
|
||||||
|
echo " Check status: sudo systemctl status $SERVICE_NAME"
|
||||||
|
echo " View logs: sudo journalctl -u $SERVICE_NAME -f"
|
||||||
|
echo " Restart: sudo systemctl restart $SERVICE_NAME"
|
||||||
|
echo " Stop: sudo systemctl stop $SERVICE_NAME"
|
||||||
|
echo ""
|
||||||
|
echo "✨ The agent is now monitoring and reporting to your CheckCle dashboard!"
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ export const menuTranslations: MenuTranslations = {
|
|||||||
scheduleIncident: "Zeitplan & Vorfall",
|
scheduleIncident: "Zeitplan & Vorfall",
|
||||||
operationalPage: "Betriebsstatus-Seite",
|
operationalPage: "Betriebsstatus-Seite",
|
||||||
reports: "Berichte",
|
reports: "Berichte",
|
||||||
apiDocumentation: "API-Dokumentation",
|
regionalMonitoring: "Regional Monitoring",
|
||||||
// Einstellungen-Panel
|
// Einstellungen-Panel
|
||||||
settingPanel: "Einstellungsbereich",
|
settingPanel: "Einstellungsbereich",
|
||||||
generalSettings: "Allgemeine Einstellungen",
|
generalSettings: "Allgemeine Einstellungen",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const menuTranslations: MenuTranslations = {
|
|||||||
scheduleIncident: "Schedule & Incident",
|
scheduleIncident: "Schedule & Incident",
|
||||||
operationalPage: "Operational Page",
|
operationalPage: "Operational Page",
|
||||||
reports: "Reports",
|
reports: "Reports",
|
||||||
apiDocumentation: "API Documentation",
|
regionalMonitoring: "Regional Monitoring",
|
||||||
settingPanel: "Setting Panel",
|
settingPanel: "Setting Panel",
|
||||||
generalSettings: "General Settings",
|
generalSettings: "General Settings",
|
||||||
userManagement: "User Management",
|
userManagement: "User Management",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const menuTranslations: MenuTranslations = {
|
|||||||
scheduleIncident: "កាលវិភាគនិងឧបទ្ទវហេតុ",
|
scheduleIncident: "កាលវិភាគនិងឧបទ្ទវហេតុ",
|
||||||
operationalPage: "ទំព័រប្រតិបត្តិការ",
|
operationalPage: "ទំព័រប្រតិបត្តិការ",
|
||||||
reports: "របាយការណ៍",
|
reports: "របាយការណ៍",
|
||||||
apiDocumentation: "ឯកសារ API",
|
regionalMonitoring: "ការត្រួតពិនិត្យតំបន់",
|
||||||
settingPanel: "ផ្ទាំងការកំណត់",
|
settingPanel: "ផ្ទាំងការកំណត់",
|
||||||
generalSettings: "ការកំណត់ទូទៅ",
|
generalSettings: "ការកំណត់ទូទៅ",
|
||||||
userManagement: "ការគ្រប់គ្រងអ្នកប្រើប្រាស់",
|
userManagement: "ការគ្រប់គ្រងអ្នកប្រើប្រាស់",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface MenuTranslations {
|
|||||||
scheduleIncident: string;
|
scheduleIncident: string;
|
||||||
operationalPage: string;
|
operationalPage: string;
|
||||||
reports: string;
|
reports: string;
|
||||||
apiDocumentation: string;
|
regionalMonitoring: string;
|
||||||
settingPanel: string;
|
settingPanel: string;
|
||||||
generalSettings: string;
|
generalSettings: string;
|
||||||
userManagement: string;
|
userManagement: string;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
export interface RegionalService {
|
||||||
|
id: string;
|
||||||
|
region_name: string;
|
||||||
|
status: string;
|
||||||
|
agent_id: string;
|
||||||
|
agent_ip_address: string;
|
||||||
|
token: string;
|
||||||
|
connection: "online" | "offline";
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateRegionalServiceParams {
|
||||||
|
region_name: string;
|
||||||
|
agent_ip_address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstallCommand {
|
||||||
|
token: string;
|
||||||
|
agent_id: string;
|
||||||
|
bash_script: string;
|
||||||
|
api_endpoint: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
REGION_NAME=""
|
||||||
|
AGENT_ID=""
|
||||||
|
AGENT_IP_ADDRESS=""
|
||||||
|
AGENT_TOKEN=""
|
||||||
|
POCKETBASE_URL=""
|
||||||
|
BASE_PACKAGE_URL="https://github.com/operacle/Distributed-Regional-Monitoring/releases"
|
||||||
|
PACKAGE_VERSION="1.0.0"
|
||||||
|
SERVICE_NAME="regional-check-agent"
|
||||||
|
|
||||||
|
# Function to show usage
|
||||||
|
show_usage() {
|
||||||
|
echo "Usage: $0 [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --region-name=NAME Set the region name (required)"
|
||||||
|
echo " --agent-id=ID Set the agent ID (required)"
|
||||||
|
echo " --agent-ip=IP Set the agent IP address (required)"
|
||||||
|
echo " --token=TOKEN Set the authentication token (required)"
|
||||||
|
echo " --pocketbase-url=URL Set the PocketBase API URL (required)"
|
||||||
|
echo " --package-version=VER Set package version (default: $PACKAGE_VERSION)"
|
||||||
|
echo " --help Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 --region-name=\"us-east-1\" --agent-id=\"agent_abc123\" \\"
|
||||||
|
echo " --agent-ip=\"192.168.1.100\" --token=\"your-token\" \\"
|
||||||
|
echo " --pocketbase-url=\"https://your-pb.com\""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--region-name=*)
|
||||||
|
REGION_NAME="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--agent-id=*)
|
||||||
|
AGENT_ID="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--agent-ip=*)
|
||||||
|
AGENT_IP_ADDRESS="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--token=*)
|
||||||
|
AGENT_TOKEN="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--pocketbase-url=*)
|
||||||
|
POCKETBASE_URL="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--package-version=*)
|
||||||
|
PACKAGE_VERSION="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
show_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
show_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate required parameters
|
||||||
|
if [[ -z "$REGION_NAME" || -z "$AGENT_ID" || -z "$AGENT_IP_ADDRESS" || -z "$AGENT_TOKEN" || -z "$POCKETBASE_URL" ]]; then
|
||||||
|
echo "❌ Error: Missing required parameters"
|
||||||
|
echo ""
|
||||||
|
show_usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🚀 CheckCle Regional Monitoring Agent - Universal Installation"
|
||||||
|
echo "=============================================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "❌ This script must be run as root (use sudo)"
|
||||||
|
echo " Usage: sudo bash $0 [options]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect operating system
|
||||||
|
echo "🔍 Detecting system information..."
|
||||||
|
OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo " Operating System: $OS_TYPE"
|
||||||
|
|
||||||
|
# Check if it's a supported OS
|
||||||
|
if [[ "$OS_TYPE" != "linux" ]]; then
|
||||||
|
echo "❌ Unsupported operating system: $OS_TYPE"
|
||||||
|
echo " This installer only supports Linux systems"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
echo " Hardware Architecture: $ARCH"
|
||||||
|
|
||||||
|
# Map architecture to package architecture
|
||||||
|
case $ARCH in
|
||||||
|
x86_64|amd64)
|
||||||
|
PKG_ARCH="amd64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
PKG_ARCH="arm64"
|
||||||
|
;;
|
||||||
|
armv7l|armv6l)
|
||||||
|
PKG_ARCH="arm64"
|
||||||
|
echo "⚠️ ARM 32-bit detected, using ARM64 package (may require compatibility layer)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Unsupported architecture: $ARCH"
|
||||||
|
echo " Supported architectures: x86_64 (amd64), aarch64 (arm64)"
|
||||||
|
echo " Please contact support for your architecture: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo " Package Architecture: $PKG_ARCH"
|
||||||
|
|
||||||
|
# Construct package URLs and names
|
||||||
|
PACKAGE_URL="$BASE_PACKAGE_URL/distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb"
|
||||||
|
PACKAGE_NAME="distributed-regional-check-agent_${PACKAGE_VERSION}_${PKG_ARCH}.deb"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📋 Installation Configuration:"
|
||||||
|
echo " Region Name: $REGION_NAME"
|
||||||
|
echo " Agent ID: $AGENT_ID"
|
||||||
|
echo " Agent IP: $AGENT_IP_ADDRESS"
|
||||||
|
echo " Package Architecture: $PKG_ARCH"
|
||||||
|
echo " Package URL: $PACKAGE_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for required tools
|
||||||
|
echo "🔧 Checking system requirements..."
|
||||||
|
MISSING_TOOLS=()
|
||||||
|
|
||||||
|
if ! command -v wget >/dev/null 2>&1; then
|
||||||
|
MISSING_TOOLS+=("wget")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v dpkg >/dev/null 2>&1; then
|
||||||
|
MISSING_TOOLS+=("dpkg")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v systemctl >/dev/null 2>&1; then
|
||||||
|
MISSING_TOOLS+=("systemd")
|
||||||
|
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"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ System requirements satisfied"
|
||||||
|
|
||||||
|
# Create temporary directory
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
echo "📁 Created temporary directory: $TEMP_DIR"
|
||||||
|
|
||||||
|
# Download the .deb package
|
||||||
|
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
|
||||||
|
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 " - Firewall/proxy settings"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify package integrity
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Verifying package..."
|
||||||
|
if dpkg-deb --info "$PACKAGE_NAME" > /dev/null 2>&1; then
|
||||||
|
echo "✅ Package verification successful"
|
||||||
|
|
||||||
|
# Show package info
|
||||||
|
echo "📦 Package Information:"
|
||||||
|
dpkg-deb --field "$PACKAGE_NAME" Package Version Architecture Description | head -4
|
||||||
|
else
|
||||||
|
echo "❌ Package verification failed - corrupted download"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install the package
|
||||||
|
echo ""
|
||||||
|
echo "📦 Installing Regional Monitoring Agent package..."
|
||||||
|
if dpkg -i "$PACKAGE_NAME" 2>/dev/null; then
|
||||||
|
echo "✅ Package installed successfully"
|
||||||
|
else
|
||||||
|
echo "⚠️ Package installation had dependency issues, attempting to fix..."
|
||||||
|
if apt-get update && apt-get install -f -y; then
|
||||||
|
echo "✅ Dependencies fixed and package installed successfully"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to install package and fix dependencies"
|
||||||
|
echo " This might be due to:"
|
||||||
|
echo " - Missing system dependencies"
|
||||||
|
echo " - Architecture compatibility issues"
|
||||||
|
echo " - Package conflicts"
|
||||||
|
echo " - Insufficient disk space"
|
||||||
|
echo ""
|
||||||
|
echo " Manual resolution:"
|
||||||
|
echo " 1. Run: sudo apt-get update"
|
||||||
|
echo " 2. Run: sudo apt-get install -f"
|
||||||
|
echo " 3. Retry installation"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure the agent
|
||||||
|
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
|
||||||
|
# Auto-generated on $(date)
|
||||||
|
# Architecture: $PKG_ARCH
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=8091
|
||||||
|
|
||||||
|
# Operation defaults
|
||||||
|
DEFAULT_COUNT=4
|
||||||
|
DEFAULT_TIMEOUT=10s
|
||||||
|
MAX_COUNT=20
|
||||||
|
MAX_TIMEOUT=30s
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
ENABLE_LOGGING=true
|
||||||
|
|
||||||
|
# PocketBase integration
|
||||||
|
POCKETBASE_ENABLED=true
|
||||||
|
POCKETBASE_URL=$POCKETBASE_URL
|
||||||
|
|
||||||
|
# Regional Agent Configuration - Auto-configured
|
||||||
|
REGION_NAME=$REGION_NAME
|
||||||
|
AGENT_ID=$AGENT_ID
|
||||||
|
AGENT_IP_ADDRESS=$AGENT_IP_ADDRESS
|
||||||
|
AGENT_TOKEN=$AGENT_TOKEN
|
||||||
|
|
||||||
|
# Monitoring configuration
|
||||||
|
CHECK_INTERVAL=30s
|
||||||
|
MAX_RETRIES=3
|
||||||
|
REQUEST_TIMEOUT=10s
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Configuration file created at /etc/regional-check-agent/regional-check-agent.env"
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
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
|
||||||
|
echo "✅ Configuration file permissions set"
|
||||||
|
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 ""
|
||||||
|
echo "🔧 Starting Regional Monitoring Agent service..."
|
||||||
|
|
||||||
|
# Reload systemd daemon
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service for auto-start
|
||||||
|
if systemctl enable $SERVICE_NAME; then
|
||||||
|
echo "✅ Service enabled for auto-start"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to enable service"
|
||||||
|
echo " Check systemd configuration"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
if systemctl start $SERVICE_NAME; then
|
||||||
|
echo "✅ Service started successfully"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to start service"
|
||||||
|
echo " Common issues:"
|
||||||
|
echo " - Configuration errors"
|
||||||
|
echo " - Port 8091 already in use"
|
||||||
|
echo " - Permission issues"
|
||||||
|
echo ""
|
||||||
|
echo " Troubleshooting:"
|
||||||
|
echo " - Check logs: sudo journalctl -u $SERVICE_NAME -f"
|
||||||
|
echo " - Check config: sudo nano /etc/regional-check-agent/regional-check-agent.env"
|
||||||
|
echo " - Manual start: sudo systemctl start $SERVICE_NAME"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait a moment for service to initialize
|
||||||
|
echo "⏳ Waiting for service to initialize..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
echo ""
|
||||||
|
echo "📊 Service Status:"
|
||||||
|
systemctl status $SERVICE_NAME --no-pager -l --lines=5
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
echo ""
|
||||||
|
echo "🩺 Testing agent health endpoint..."
|
||||||
|
HEALTH_CHECK_ATTEMPTS=3
|
||||||
|
HEALTH_CHECK_DELAY=2
|
||||||
|
|
||||||
|
for i in $(seq 1 $HEALTH_CHECK_ATTEMPTS); do
|
||||||
|
if curl -s -f --connect-timeout 5 http://localhost:8091/health > /dev/null; then
|
||||||
|
echo "✅ Agent health endpoint is responding"
|
||||||
|
HEALTH_OK=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
if [ $i -lt $HEALTH_CHECK_ATTEMPTS ]; then
|
||||||
|
echo "⏳ Health check attempt $i/$HEALTH_CHECK_ATTEMPTS failed, retrying in ${HEALTH_CHECK_DELAY}s..."
|
||||||
|
sleep $HEALTH_CHECK_DELAY
|
||||||
|
else
|
||||||
|
echo "⚠️ Agent health endpoint not responding after $HEALTH_CHECK_ATTEMPTS attempts"
|
||||||
|
echo " This may be normal if the service is still starting up"
|
||||||
|
echo " Check status later with: curl http://localhost:8091/health"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Regional Monitoring Agent Installation Complete!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Installation Summary:"
|
||||||
|
echo " Agent ID: $AGENT_ID"
|
||||||
|
echo " Region: $REGION_NAME"
|
||||||
|
echo " Architecture: $PKG_ARCH ($ARCH)"
|
||||||
|
echo " Status: $(systemctl is-active $SERVICE_NAME 2>/dev/null || echo 'unknown')"
|
||||||
|
echo " Health URL: http://localhost:8091/health"
|
||||||
|
echo " Service endpoint: http://localhost:8091/operation"
|
||||||
|
echo " Config file: /etc/regional-check-agent/regional-check-agent.env"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Useful commands:"
|
||||||
|
echo " Check status: sudo systemctl status $SERVICE_NAME"
|
||||||
|
echo " View logs: sudo journalctl -u $SERVICE_NAME -f"
|
||||||
|
echo " Restart: sudo systemctl restart $SERVICE_NAME"
|
||||||
|
echo " Stop: sudo systemctl stop $SERVICE_NAME"
|
||||||
|
echo " Health check: curl http://localhost:8091/health"
|
||||||
|
echo ""
|
||||||
|
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 " 4. Check port availability: sudo netstat -tlnp | grep 8091"
|
||||||
|
echo ""
|
||||||
|
echo "✨ The agent is now monitoring and reporting to your CheckCle dashboard!"
|
||||||
@@ -4,7 +4,7 @@ PORT=8091
|
|||||||
|
|
||||||
# Operation defaults
|
# Operation defaults
|
||||||
DEFAULT_COUNT=4
|
DEFAULT_COUNT=4
|
||||||
DEFAULT_TIMEOUT=3s
|
DEFAULT_TIMEOUT=10s
|
||||||
MAX_COUNT=20
|
MAX_COUNT=20
|
||||||
MAX_TIMEOUT=30s
|
MAX_TIMEOUT=30s
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ PORT=8091
|
|||||||
|
|
||||||
# Operation defaults
|
# Operation defaults
|
||||||
DEFAULT_COUNT=4
|
DEFAULT_COUNT=4
|
||||||
DEFAULT_TIMEOUT=3s
|
DEFAULT_TIMEOUT=10s
|
||||||
MAX_COUNT=20
|
MAX_COUNT=20
|
||||||
MAX_TIMEOUT=30s
|
MAX_TIMEOUT=30s
|
||||||
|
|
||||||
@@ -13,4 +13,4 @@ ENABLE_LOGGING=true
|
|||||||
|
|
||||||
# PocketBase integration (no authentication required)
|
# PocketBase integration (no authentication required)
|
||||||
POCKETBASE_ENABLED=true
|
POCKETBASE_ENABLED=true
|
||||||
POCKETBASE_URL=https://pb-api.k8sops.asia
|
POCKETBASE_URL=http://localhost:8090
|
||||||
@@ -117,13 +117,6 @@ Environment variables:
|
|||||||
```bash
|
```bash
|
||||||
go run main.go
|
go run main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
|
||||||
```bash
|
|
||||||
docker build -t service-operation .
|
|
||||||
docker run -p 8080:8080 service-operation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Go 1.21+
|
- Go 1.21+
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -32,15 +31,15 @@ func main() {
|
|||||||
if err := pbClient.TestConnection(); err != nil {
|
if err := pbClient.TestConnection(); err != nil {
|
||||||
log.Printf("Warning: PocketBase connection test failed: %v", err)
|
log.Printf("Warning: PocketBase connection test failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
// Initialize and start monitoring service
|
// Initialize and start monitoring service with regional support
|
||||||
monitoringService = monitoring.NewMonitoringService(pbClient)
|
monitoringService = monitoring.NewMonitoringService(pbClient)
|
||||||
go monitoringService.Start()
|
go monitoringService.Start()
|
||||||
log.Println("Monitoring service started (public access mode)")
|
log.Println("Monitoring service started with regional agent support")
|
||||||
|
|
||||||
// Initialize and start SSL monitoring service
|
// Initialize and start SSL monitoring service (unchanged - no regional support)
|
||||||
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
|
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
|
||||||
go sslMonitoringService.Start()
|
go sslMonitoringService.Start()
|
||||||
log.Println("SSL monitoring service started")
|
log.Println("SSL monitoring service started (independent of regional agents)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,10 +66,10 @@ func main() {
|
|||||||
log.Printf("PocketBase integration enabled at %s (public access)", pbClient.GetBaseURL())
|
log.Printf("PocketBase integration enabled at %s (public access)", pbClient.GetBaseURL())
|
||||||
}
|
}
|
||||||
if monitoringService != nil {
|
if monitoringService != nil {
|
||||||
log.Printf("Automatic service monitoring enabled")
|
log.Printf("Automatic service monitoring enabled with regional agent support")
|
||||||
}
|
}
|
||||||
if sslMonitoringService != nil {
|
if sslMonitoringService != nil {
|
||||||
log.Printf("SSL certificate monitoring enabled")
|
log.Printf("SSL certificate monitoring enabled (independent)")
|
||||||
}
|
}
|
||||||
log.Printf("Endpoints:")
|
log.Printf("Endpoints:")
|
||||||
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http, ssl)")
|
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http, ssl)")
|
||||||
@@ -79,6 +78,7 @@ func main() {
|
|||||||
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
||||||
log.Printf(" GET /health - Health check")
|
log.Printf(" GET /health - Health check")
|
||||||
log.Printf("Supported operations: ping, dns, tcp, http, ssl")
|
log.Printf("Supported operations: ping, dns, tcp, http, ssl")
|
||||||
|
log.Printf("Regional monitoring: Tracks 'Default' region connection status")
|
||||||
|
|
||||||
// Setup graceful shutdown
|
// Setup graceful shutdown
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ func (ms *MonitoringService) performCheck(service pocketbase.Service) {
|
|||||||
log.Printf("Failed to update service status for %s: %v", latestService.Name, err)
|
log.Printf("Failed to update service status for %s: %v", latestService.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save metrics data in ONE place to prevent duplicates
|
// Save metrics data with regional information
|
||||||
if result != nil {
|
if result != nil {
|
||||||
metricsSaver := savers.NewMetricsSaver(ms.pbClient)
|
regionName, agentID := ms.GetRegionalInfo()
|
||||||
|
metricsSaver := savers.NewMetricsSaverWithRegion(ms.pbClient, regionName, agentID)
|
||||||
metricsSaver.SaveMetricsForService(*latestService, result)
|
metricsSaver.SaveMetricsForService(*latestService, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegionalMonitor struct {
|
||||||
|
pbClient *pocketbase.PocketBaseClient
|
||||||
|
regionalService *pocketbase.RegionalService
|
||||||
|
isOnline bool
|
||||||
|
ticker *time.Ticker
|
||||||
|
stopChan chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegionalMonitor(pbClient *pocketbase.PocketBaseClient) *RegionalMonitor {
|
||||||
|
return &RegionalMonitor{
|
||||||
|
pbClient: pbClient,
|
||||||
|
isOnline: false,
|
||||||
|
stopChan: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *RegionalMonitor) Start() {
|
||||||
|
// Get or create the "Default" regional service
|
||||||
|
service, err := rm.pbClient.GetDefaultRegionalService()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Could not get default regional service: %v", err)
|
||||||
|
log.Printf("Regional monitoring will continue with fallback values")
|
||||||
|
// Continue with fallback service
|
||||||
|
service = &pocketbase.RegionalService{
|
||||||
|
ID: "default",
|
||||||
|
RegionName: "Default",
|
||||||
|
Status: "active",
|
||||||
|
AgentID: "1",
|
||||||
|
AgentIPAddress: "127.0.0.1",
|
||||||
|
Connection: "offline",
|
||||||
|
Token: "default-token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rm.regionalService = service
|
||||||
|
rm.ticker = time.NewTicker(30 * time.Second) // Check connection every 30 seconds
|
||||||
|
|
||||||
|
// Initial connection status update
|
||||||
|
rm.updateConnectionStatus("online")
|
||||||
|
rm.isOnline = true
|
||||||
|
|
||||||
|
log.Printf("Regional monitor started for region: %s (Agent ID: %s)",
|
||||||
|
service.RegionName, service.AgentID)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-rm.ticker.C:
|
||||||
|
rm.checkConnection()
|
||||||
|
case <-rm.stopChan:
|
||||||
|
rm.ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *RegionalMonitor) Stop() {
|
||||||
|
if rm.regionalService != nil {
|
||||||
|
rm.updateConnectionStatus("offline")
|
||||||
|
log.Printf("Regional monitor stopped for region: %s", rm.regionalService.RegionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rm.ticker != nil {
|
||||||
|
rm.ticker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
rm.stopChan <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *RegionalMonitor) checkConnection() {
|
||||||
|
// Test PocketBase connection to determine if we're online
|
||||||
|
err := rm.pbClient.TestConnection()
|
||||||
|
|
||||||
|
if err != nil && rm.isOnline {
|
||||||
|
// We were online but now we're offline
|
||||||
|
rm.updateConnectionStatus("offline")
|
||||||
|
rm.isOnline = false
|
||||||
|
log.Printf("Regional agent went offline: %v", err)
|
||||||
|
} else if err == nil && !rm.isOnline {
|
||||||
|
// We were offline but now we're online
|
||||||
|
rm.updateConnectionStatus("online")
|
||||||
|
rm.isOnline = true
|
||||||
|
log.Printf("Regional agent back online")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *RegionalMonitor) updateConnectionStatus(status string) {
|
||||||
|
if rm.regionalService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rm.pbClient.UpdateRegionalServiceConnection(rm.regionalService.ID, status); err != nil {
|
||||||
|
// Don't log errors for update failures - might be due to collection access issues
|
||||||
|
// log.Printf("Failed to update regional service connection status: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *RegionalMonitor) GetRegionalInfo() (string, string) {
|
||||||
|
if rm.regionalService == nil {
|
||||||
|
return "Default", "1" // Fallback values
|
||||||
|
}
|
||||||
|
return rm.regionalService.RegionName, rm.regionalService.AgentID
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package monitoring
|
package monitoring
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,6 +11,7 @@ import (
|
|||||||
type MonitoringService struct {
|
type MonitoringService struct {
|
||||||
pbClient *pocketbase.PocketBaseClient
|
pbClient *pocketbase.PocketBaseClient
|
||||||
activeServices map[string]*ServiceMonitor
|
activeServices map[string]*ServiceMonitor
|
||||||
|
regionalMonitor *RegionalMonitor
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
stopChan chan bool
|
stopChan chan bool
|
||||||
isRunning bool
|
isRunning bool
|
||||||
@@ -21,6 +21,7 @@ func NewMonitoringService(pbClient *pocketbase.PocketBaseClient) *MonitoringServ
|
|||||||
return &MonitoringService{
|
return &MonitoringService{
|
||||||
pbClient: pbClient,
|
pbClient: pbClient,
|
||||||
activeServices: make(map[string]*ServiceMonitor),
|
activeServices: make(map[string]*ServiceMonitor),
|
||||||
|
regionalMonitor: NewRegionalMonitor(pbClient),
|
||||||
stopChan: make(chan bool),
|
stopChan: make(chan bool),
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
}
|
}
|
||||||
@@ -38,6 +39,9 @@ func (ms *MonitoringService) Start() {
|
|||||||
ms.isRunning = true
|
ms.isRunning = true
|
||||||
log.Println("Starting monitoring service...")
|
log.Println("Starting monitoring service...")
|
||||||
|
|
||||||
|
// Start regional monitoring
|
||||||
|
ms.regionalMonitor.Start()
|
||||||
|
|
||||||
// Start monitoring all services from PocketBase
|
// Start monitoring all services from PocketBase
|
||||||
go ms.monitoringLoop()
|
go ms.monitoringLoop()
|
||||||
}
|
}
|
||||||
@@ -53,6 +57,9 @@ func (ms *MonitoringService) Stop() {
|
|||||||
log.Println("Stopping monitoring service...")
|
log.Println("Stopping monitoring service...")
|
||||||
ms.isRunning = false
|
ms.isRunning = false
|
||||||
|
|
||||||
|
// Stop regional monitoring
|
||||||
|
ms.regionalMonitor.Stop()
|
||||||
|
|
||||||
// Stop all active monitors
|
// Stop all active monitors
|
||||||
for serviceID, monitor := range ms.activeServices {
|
for serviceID, monitor := range ms.activeServices {
|
||||||
ms.stopMonitor(serviceID, monitor)
|
ms.stopMonitor(serviceID, monitor)
|
||||||
@@ -61,6 +68,10 @@ func (ms *MonitoringService) Stop() {
|
|||||||
ms.stopChan <- true
|
ms.stopChan <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ms *MonitoringService) GetRegionalInfo() (string, string) {
|
||||||
|
return ms.regionalMonitor.GetRegionalInfo()
|
||||||
|
}
|
||||||
|
|
||||||
func (ms *MonitoringService) monitoringLoop() {
|
func (ms *MonitoringService) monitoringLoop() {
|
||||||
ticker := time.NewTicker(30 * time.Second) // Check for new services every 30 seconds
|
ticker := time.NewTicker(30 * time.Second) // Check for new services every 30 seconds
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
package pocketbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) GetRegionalServices() ([]RegionalService, error) {
|
||||||
|
resp, err := c.httpClient.Get(fmt.Sprintf("%s/api/collections/regional_service/records", c.baseURL))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusForbidden {
|
||||||
|
// Collection might require authentication or doesn't exist
|
||||||
|
fmt.Printf("Warning: Cannot access regional_service collection (403). Regional monitoring will be disabled.\n")
|
||||||
|
return nil, fmt.Errorf("access denied to regional_service collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to get regional services: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response RegionalServicesResponse
|
||||||
|
if err := c.parseResponse(resp, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) GetDefaultRegionalService() (*RegionalService, error) {
|
||||||
|
services, err := c.GetRegionalServices()
|
||||||
|
if err != nil {
|
||||||
|
// Try to create a default regional service if we can't get existing ones
|
||||||
|
return c.CreateDefaultRegionalService()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for a service with region_name "Default"
|
||||||
|
for _, service := range services {
|
||||||
|
if service.RegionName == "Default" {
|
||||||
|
return &service, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no default service found, try to create one
|
||||||
|
return c.CreateDefaultRegionalService()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) CreateDefaultRegionalService() (*RegionalService, error) {
|
||||||
|
defaultService := map[string]interface{}{
|
||||||
|
"region_name": "Default",
|
||||||
|
"status": "active",
|
||||||
|
"agent_id": "1",
|
||||||
|
"agent_ip_address": "127.0.0.1",
|
||||||
|
"connection": "offline",
|
||||||
|
"token": fmt.Sprintf("default-%d", time.Now().Unix()),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.createRecord("regional_service", defaultService)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Could not create default regional service: %v\n", err)
|
||||||
|
// Return a fallback service for local operations
|
||||||
|
return &RegionalService{
|
||||||
|
ID: "default",
|
||||||
|
RegionName: "Default",
|
||||||
|
Status: "active",
|
||||||
|
AgentID: "1",
|
||||||
|
AgentIPAddress: "127.0.0.1",
|
||||||
|
Connection: "offline",
|
||||||
|
Token: "default-token",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch the created service
|
||||||
|
services, err := c.GetRegionalServices()
|
||||||
|
if err == nil {
|
||||||
|
for _, service := range services {
|
||||||
|
if service.RegionName == "Default" {
|
||||||
|
return &service, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return fallback if still can't get the created service
|
||||||
|
return &RegionalService{
|
||||||
|
ID: "default",
|
||||||
|
RegionName: "Default",
|
||||||
|
Status: "active",
|
||||||
|
AgentID: "1",
|
||||||
|
AgentIPAddress: "127.0.0.1",
|
||||||
|
Connection: "offline",
|
||||||
|
Token: "default-token",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) UpdateRegionalServiceConnection(serviceID, connection string) error {
|
||||||
|
// Determine status based on connection
|
||||||
|
status := "inactive"
|
||||||
|
if connection == "online" {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"connection": connection,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.updateRecord("regional_service", serviceID, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) SaveSSLData(sslData SSLDataRecord) error {
|
||||||
|
return c.createRecord("ssl_data", sslData)
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package pocketbase
|
package pocketbase
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
@@ -46,6 +45,8 @@ type PingDataRecord struct {
|
|||||||
RTTs string `json:"rtts"`
|
RTTs string `json:"rtts"`
|
||||||
Details string `json:"details,omitempty"`
|
Details string `json:"details,omitempty"`
|
||||||
ErrorMessage string `json:"error_message,omitempty"`
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
RegionName string `json:"region_name,omitempty"`
|
||||||
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UptimeDataRecord struct {
|
type UptimeDataRecord struct {
|
||||||
@@ -61,6 +62,8 @@ type UptimeDataRecord struct {
|
|||||||
Details string `json:"details"`
|
Details string `json:"details"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
RegionID string `json:"region_id,omitempty"`
|
RegionID string `json:"region_id,omitempty"`
|
||||||
|
RegionName string `json:"region_name,omitempty"`
|
||||||
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSDataRecord struct {
|
type DNSDataRecord struct {
|
||||||
@@ -94,6 +97,46 @@ type TCPDataRecord struct {
|
|||||||
AgentID string `json:"agent_id,omitempty"`
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSL Data Record remains unchanged - no regional agent fields
|
||||||
|
type SSLDataRecord struct {
|
||||||
|
ServiceID string `json:"service_id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
ResponseTime int64 `json:"response_time"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ValidFrom string `json:"valid_from"`
|
||||||
|
ValidTill string `json:"valid_till"`
|
||||||
|
DaysLeft int `json:"days_left"`
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
SerialNumber string `json:"serial_number"`
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
SANs string `json:"sans"`
|
||||||
|
ResolvedIP string `json:"resolved_ip"`
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regional Service record structure
|
||||||
|
type RegionalService struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
RegionName string `json:"region_name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
AgentID string `json:"agent_id"`
|
||||||
|
AgentIPAddress string `json:"agent_ip_address"`
|
||||||
|
Connection string `json:"connection"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Updated string `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionalServicesResponse struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PerPage int `json:"perPage"`
|
||||||
|
TotalItems int `json:"totalItems"`
|
||||||
|
TotalPages int `json:"totalPages"`
|
||||||
|
Items []RegionalService `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -9,6 +9,50 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) parseResponse(resp *http.Response, target interface{}) error {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(body, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) updateRecord(collection string, recordID string, data interface{}) error {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("PATCH",
|
||||||
|
fmt.Sprintf("%s/api/collections/%s/records/%s", c.baseURL, collection, recordID),
|
||||||
|
bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
bodyString := string(bodyBytes)
|
||||||
|
|
||||||
|
fmt.Printf("Failed to update record in %s collection. Status: %d, Response: %s\n",
|
||||||
|
collection, resp.StatusCode, bodyString)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to update record in %s, status: %d, response: %s",
|
||||||
|
collection, resp.StatusCode, bodyString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *PocketBaseClient) createRecord(collection string, data interface{}) error {
|
func (c *PocketBaseClient) createRecord(collection string, data interface{}) error {
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ func (ms *MetricsSaver) SaveDNSDataToPocketBase(result *types.OperationResult, s
|
|||||||
Authority: "", // Not available in current implementation
|
Authority: "", // Not available in current implementation
|
||||||
ErrorMessage: result.Error,
|
ErrorMessage: result.Error,
|
||||||
Details: details, // Short, clean message
|
Details: details, // Short, clean message
|
||||||
RegionName: "default", // You can make this configurable
|
RegionName: ms.regionName, // Add regional fields
|
||||||
AgentID: "1", // You can make this configurable
|
AgentID: ms.agentID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ms.pbClient.SaveDNSData(dnsData); err != nil {
|
if err := ms.pbClient.SaveDNSData(dnsData); err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
package savers
|
package savers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"service-operation/pocketbase"
|
"service-operation/pocketbase"
|
||||||
@@ -10,11 +10,23 @@ import (
|
|||||||
|
|
||||||
type MetricsSaver struct {
|
type MetricsSaver struct {
|
||||||
pbClient *pocketbase.PocketBaseClient
|
pbClient *pocketbase.PocketBaseClient
|
||||||
|
regionName string
|
||||||
|
agentID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMetricsSaver(pbClient *pocketbase.PocketBaseClient) *MetricsSaver {
|
func NewMetricsSaver(pbClient *pocketbase.PocketBaseClient) *MetricsSaver {
|
||||||
return &MetricsSaver{
|
return &MetricsSaver{
|
||||||
pbClient: pbClient,
|
pbClient: pbClient,
|
||||||
|
regionName: "Default", // Default fallback
|
||||||
|
agentID: "1", // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMetricsSaverWithRegion(pbClient *pocketbase.PocketBaseClient, regionName, agentID string) *MetricsSaver {
|
||||||
|
return &MetricsSaver{
|
||||||
|
pbClient: pbClient,
|
||||||
|
regionName: regionName,
|
||||||
|
agentID: agentID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +62,8 @@ func (ms *MetricsSaver) SaveMetricsToPocketBase(result *types.OperationResult, s
|
|||||||
ms.SaveDNSDataToPocketBase(result, serviceID)
|
ms.SaveDNSDataToPocketBase(result, serviceID)
|
||||||
case types.OperationTCP:
|
case types.OperationTCP:
|
||||||
ms.SaveTCPDataToPocketBase(result, serviceID)
|
ms.SaveTCPDataToPocketBase(result, serviceID)
|
||||||
|
case types.OperationSSL:
|
||||||
|
ms.SaveSSLDataToPocketBase(result, serviceID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,5 +100,46 @@ func (ms *MetricsSaver) SaveMetricsForService(service pocketbase.Service, result
|
|||||||
ms.SaveUptimeDataToPocketBase(result, service.ID)
|
ms.SaveUptimeDataToPocketBase(result, service.ID)
|
||||||
case "tcp":
|
case "tcp":
|
||||||
ms.SaveTCPDataToPocketBase(result, service.ID)
|
ms.SaveTCPDataToPocketBase(result, service.ID)
|
||||||
|
case "ssl":
|
||||||
|
ms.SaveSSLDataToPocketBase(result, service.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSL Data saver - remains unchanged (no regional fields)
|
||||||
|
func (ms *MetricsSaver) SaveSSLDataToPocketBase(result *types.OperationResult, serviceID string) {
|
||||||
|
// Create SSL data record without regional fields
|
||||||
|
var details string
|
||||||
|
|
||||||
|
if result.Success {
|
||||||
|
details = fmt.Sprintf("✅ SSL Certificate Valid - Expires in %d days", result.SSLDaysLeft)
|
||||||
|
details += fmt.Sprintf(" | Valid until: %s", result.SSLValidTill.Format("2006-01-02"))
|
||||||
|
|
||||||
|
if result.SSLIssuer != "" {
|
||||||
|
details += fmt.Sprintf(" | Issuer: %s", result.SSLIssuer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
details = fmt.Sprintf("❌ SSL Certificate Issue - %s", GetShortErrorMessage(result.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
sslData := pocketbase.SSLDataRecord{
|
||||||
|
ServiceID: serviceID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
ResponseTime: result.ResponseTime.Milliseconds(),
|
||||||
|
Status: GetStatusString(result.Success),
|
||||||
|
ValidFrom: result.SSLValidFrom.Format(time.RFC3339),
|
||||||
|
ValidTill: result.SSLValidTill.Format(time.RFC3339),
|
||||||
|
DaysLeft: result.SSLDaysLeft,
|
||||||
|
Issuer: result.SSLIssuer,
|
||||||
|
Subject: result.SSLSubject,
|
||||||
|
SerialNumber: result.SSLSerialNumber,
|
||||||
|
Algorithm: result.SSLAlgorithm,
|
||||||
|
SANs: result.SSLSANs,
|
||||||
|
ResolvedIP: result.SSLResolvedIP,
|
||||||
|
ErrorMessage: result.Error,
|
||||||
|
Details: details,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ms.pbClient.SaveSSLData(sslData); err != nil {
|
||||||
|
println("Failed to save SSL data to PocketBase:", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,8 @@ func (ms *MetricsSaver) SavePingDataToPocketBase(result *types.OperationResult,
|
|||||||
Latency: fmt.Sprintf("%.2fms", float64(result.AvgRTT.Nanoseconds())/1000000),
|
Latency: fmt.Sprintf("%.2fms", float64(result.AvgRTT.Nanoseconds())/1000000),
|
||||||
ErrorMessage: result.Error,
|
ErrorMessage: result.Error,
|
||||||
Details: details, // Short, clean message
|
Details: details, // Short, clean message
|
||||||
|
RegionName: ms.regionName, // Add regional fields
|
||||||
|
AgentID: ms.agentID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ms.pbClient.SavePingData(pingData); err != nil {
|
if err := ms.pbClient.SavePingData(pingData); err != nil {
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ func (ms *MetricsSaver) SaveTCPDataToPocketBase(result *types.OperationResult, s
|
|||||||
Port: strconv.Itoa(result.Port),
|
Port: strconv.Itoa(result.Port),
|
||||||
ErrorMessage: result.Error,
|
ErrorMessage: result.Error,
|
||||||
Details: details,
|
Details: details,
|
||||||
RegionName: "default",
|
RegionName: ms.regionName, // Add regional fields
|
||||||
AgentID: "1",
|
AgentID: ms.agentID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ms.pbClient.SaveTCPData(tcpData); err != nil {
|
if err := ms.pbClient.SaveTCPData(tcpData); err != nil {
|
||||||
|
|||||||
@@ -57,8 +57,10 @@ func (ms *MetricsSaver) SaveUptimeDataToPocketBase(result *types.OperationResult
|
|||||||
Keyword: "", // Can be populated later if needed
|
Keyword: "", // Can be populated later if needed
|
||||||
ErrorMessage: result.Error,
|
ErrorMessage: result.Error,
|
||||||
Details: details, // Short, clean message
|
Details: details, // Short, clean message
|
||||||
Region: "default", // You can make this configurable
|
Region: ms.regionName, // Legacy field
|
||||||
RegionID: "1", // You can make this configurable
|
RegionID: ms.agentID, // Legacy field
|
||||||
|
RegionName: ms.regionName, // Add regional fields
|
||||||
|
AgentID: ms.agentID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ms.pbClient.SaveUptimeData(uptimeData); err != nil {
|
if err := ms.pbClient.SaveUptimeData(uptimeData); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user