Merge pull request #112 from operacle/develop

feat: add docker compose for server docker agent and Fix: Add tar.gz fallback for unsupported systems
and other improvement
This commit is contained in:
Tola Leng
2025-07-31 04:10:06 +07:00
committed by GitHub
31 changed files with 1414 additions and 250 deletions
+43 -50
View File
@@ -42,8 +42,9 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
User: admin@example.com | Passwd: Admin123456
## 🌟 Core Features
### 📝 Roadmap : [DEVELOPMENT_ROADMAP](docs/DEVELOPMENT_ROADMAP.md)
### Uptime Services & Infrastructure Server Monitoring
### Uptime Services & Infrastructure Server Monitoring
- Monitor HTTP, DNS, and Ping protocols
- Monitor TCP-based, API services (e.g., FTP, SMTP, HTTP)
- Track detail uptime, response times, and performance issues
@@ -63,26 +64,9 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s
* ✅ x86_64 PCs, laptops, servers (amd64)
* ✅ Modern Raspberry Pi 3/4/5 with (64-bit OS), Apple Silicon Macs (arm64)
### Install CheckCle using one of the options below.
### Install CheckCle using one of the following methods:
1. CheckCle One-Click Installation - Just copy and run on terminal
```bash
curl -fsSL https://checkcle.io/install.sh | bash
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Install with Docker compose Configuration.
1. Install with Docker Compose Configuration (Recommended)
```bash
version: '3.9'
@@ -102,6 +86,18 @@ services:
hard: 8192
```
2. Install with docker run. Just copy ready docker run command below
```bash
docker run -d \
--name checkcle \
--restart unless-stopped \
-p 8090:8090 \
-v /opt/pb_data:/mnt/pb_data \
--ulimit nofile=4096:8192 \
operacle/checkcle:latest
```
3. Admin Web Management
Default URL: http://0.0.0.0:8090
@@ -118,28 +114,6 @@ services:
![Schedule Maintenance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-schedule-maintenance.png)
## 📝 Development Roadmap
- ✅ Health check & uptime monitoring (HTTP)
- ✅ Dashboard UI with live stats
- ✅ Auth with Multi-users system (admin)
- ✅ Notifications (Telegram)
- ✅ Docker containerization
- ✅ CheckCle Website
- ✅ CheckCle Demo Server
- ✅ SSL & Domain Monitoring
- ✅ Schedule Maintenance
- ✅ Incident Management
- ✅ Infrastructure Server Monitoring
- ✅ Operational Status / Public Status Pages
- ✅ Uptime monitoring (HTTP, TCP, PING, DNS) Full functionality
- ✅ Distributed Regional Monitoring Agent
- ✅ System Setting Panel and Mail Settings
- ✅ User Permission Roles
- [ ] Notifications (Email/Slack/Discord/Signal)
- ✅ Data Retention & Automate Strink (Muti Options to Shrink Data & Database )
- ✅ Open-source release with full documentation
## 🌟 CheckCle for Communities?
- **Built with Passion**: Created by an open-source enthusiast for the community
- **Free & Open Source**: Completely free to use with no hidden costs
@@ -147,6 +121,30 @@ services:
---
## Sponsors
🙏 We're incredibly thankful to our sponsors. Your contributions enable us to maintain infrastructure (hosting, domains), run tests, and continue developing valuable features.
### 🥈 Silver Uptime Ally
<a href="https://github.com/sponsors/tolaleng">
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%" />
</a>
### 🧡 Ping Supporter
<a href="https://github.com/sponsors/tolaleng">
<img src="https://avatars.githubusercontent.com/u/4575656?v=4" width="75" height="75" style="border-radius: 50%" />
</a>
---
## 👥 Contributors
Thank you for contributing and continuously making CheckCle better, you're awesome 🫶
[![](https://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors)
---
## 🤝 Ways to Contribute
Here are some ways you can help improve CheckCle:
@@ -155,25 +153,20 @@ Here are some ways you can help improve CheckCle:
- 🌟 **Suggest Features** Have an idea? Start a [Discussion](https://github.com/operacle/checkcle/discussions) or open a Feature Request issue.
- 🛠 **Submit Pull Requests** Improve the code, fix bugs, add features, or enhance the docs.
- 📝 **Improve Documentation** Even a typo fix helps!
- 🌍 **Spread the Word** Star ⭐ the repo, share it on socials, and invite others to contribute!
- 🌍 **Spread the Word** Star ⭐ the [CheckCle](https://github.com/operacle/checkcle.git) repo, share it on socials, and invite others to contribute!
---
## 🌍 Stay Connected
- Website: [checkcle.io](https://checkcle.io)
- Documentation: [docs.checkcle.io](https://docs.checkcle.io)
- GitHub Repository: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
- Reddit Community: [reddit](https://www.reddit.com/r/checkcle)
- Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- X: [@tlengoss](https://x.com/tlengoss)
- Chat on Discord: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- Follow us on X: [@checkcle_oss](https://x.com/checkcle_oss)
## 📜 License
CheckCle is released under the MIT License.
---
## 👥 Contributors
Thank you for contributing and continuously making CheckCle better, you're awesome 🫶
[![](https://contrib.rocks/image?repo=operacle/checkcle)](https://github.com/operacle/checkcle/graphs/contributors)
+2 -2
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CheckCle is an open-source monitoring stack</title>
<title>CheckCle</title>
<meta name="description" content="An open-source monitoring platform offering real-time insights into server and service health, incident management, and operational transparency." />
<meta name="author" content="Tola Leng" />
@@ -14,7 +14,7 @@
<meta property="og:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@tlengoss" />
<meta name="twitter:site" content="@checkcle_oss" />
<meta name="twitter:image" content="https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png" />
</head>
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

@@ -162,7 +162,7 @@ export const Header = ({
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://x.com/tlengoss", "_blank")}
onClick={() => window.open("https://x.com/checkcle_oss", "_blank")}
>
<span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" />
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -6,7 +7,7 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Plus, X, Server, Shield, AlertTriangle } from 'lucide-react';
import { Plus, X, Server } from 'lucide-react';
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useQuery } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';
@@ -17,12 +18,6 @@ interface ComponentsSelectorProps {
onComponentDelete?: (componentId: string) => void;
}
const componentTypes = [
{ value: 'uptime', label: 'Uptime Service', icon: Server },
{ value: 'ssl', label: 'SSL Certificate', icon: Shield },
{ value: 'incident', label: 'Incident Monitoring', icon: AlertTriangle },
];
export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onComponentDelete }: ComponentsSelectorProps) => {
const [showAddForm, setShowAddForm] = useState(false);
const [newComponent, setNewComponent] = useState({
@@ -169,7 +164,7 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
<SelectTrigger>
<SelectValue placeholder="Select an uptime service" />
</SelectTrigger>
<SelectContent className="z-50 bg-white border shadow-lg">
<SelectContent>
{services.map((service) => (
<SelectItem key={service.id} value={service.id}>
<div className="flex items-center gap-2">
@@ -206,37 +201,6 @@ export const ComponentsSelector = ({ selectedComponents, onComponentsChange, onC
</div>
</div>
)}
<div className="mt-4">
<Label>Quick Add Templates</Label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-2">
{componentTypes.map((type) => {
const Icon = type.icon;
return (
<Button
key={type.value}
variant="outline"
size="sm"
onClick={() => {
const component: Partial<StatusPageComponentRecord> = {
name: type.label,
description: `Monitor ${type.label.toLowerCase()}`,
service_id: '',
server_id: '',
display_order: selectedComponents.length + 1,
operational_status_id: '',
};
onComponentsChange([...selectedComponents, component]);
}}
className="justify-start"
>
<Icon className="h-4 w-4 mr-2" />
{type.label}
</Button>
);
})}
</div>
</div>
</CardContent>
</Card>
);
@@ -67,13 +67,13 @@ export const CreateOperationalPageDialog = () => {
page_style: data.page_style || '',
};
console.log('Creating operational page with payload:', payload);
// console.log('Creating operational page with payload:', payload);
const createdPage = await createMutation.mutateAsync(payload);
console.log('Created page:', createdPage);
// console.log('Created page:', createdPage);
// Create components after page is created
if (selectedComponents.length > 0) {
console.log('Creating components for page:', createdPage.id);
// console.log('Creating components for page:', createdPage.id);
for (const component of selectedComponents) {
const componentPayload = {
operational_status_id: createdPage.id,
@@ -84,7 +84,7 @@ export const CreateOperationalPageDialog = () => {
display_order: component.display_order || 1,
};
console.log('Creating component with payload:', componentPayload);
// console.log('Creating component with payload:', componentPayload);
await createComponentMutation.mutateAsync(componentPayload);
}
}
@@ -93,7 +93,7 @@ export const CreateOperationalPageDialog = () => {
form.reset();
setSelectedComponents([]);
} catch (error) {
console.error('Error creating operational page:', error);
// console.error('Error creating operational page:', error);
}
};
@@ -41,11 +41,11 @@ export const OperationalPageCard = ({ page, onEdit, onView, onDelete }: Operatio
</div>
<div>
<span className="font-medium text-muted-foreground">Public:</span>
<p className="mt-1">
<div className="mt-1">
<Badge variant={page.is_public === 'true' ? 'default' : 'secondary'}>
{page.is_public === 'true' ? 'Yes' : 'No'}
</Badge>
</p>
</div>
</div>
<div>
<span className="font-medium text-muted-foreground">Updated:</span>
@@ -12,7 +12,7 @@ import { PublicStatusPageFooter } from './PublicStatusPageFooter';
export const PublicStatusPage = () => {
const { slug } = useParams<{ slug: string }>();
console.log('PublicStatusPage - slug from params:', slug);
// console.log('PublicStatusPage - slug from params:', slug);
const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(slug);
const [lastUpdated, setLastUpdated] = useState(new Date());
@@ -51,7 +51,7 @@ export const PublicStatusPage = () => {
};
}, [page?.theme]);
console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
// console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
if (loading) {
return (
@@ -19,61 +19,61 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
useEffect(() => {
const fetchPublicPage = async () => {
if (!slug) {
console.log('No slug provided');
// console.log('No slug provided');
setError('No status page slug provided');
setLoading(false);
return;
}
try {
console.log('Fetching public status page for slug:', slug);
// console.log('Fetching public status page for slug:', slug);
setLoading(true);
setError(null);
// Fetch operational page
console.log('Fetching operational pages...');
// console.log('Fetching operational pages...');
const pages = await operationalPageService.getOperationalPages();
console.log('All pages:', pages);
// console.log('All pages:', pages);
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
console.log('Found page:', foundPage);
// console.log('Found page:', foundPage);
if (!foundPage) {
console.log('Page not found or not public');
// console.log('Page not found or not public');
setError('Status page not found or not public');
setLoading(false);
return;
}
setPage(foundPage);
console.log('Page set successfully');
// console.log('Page set successfully');
// Fetch components for this page
console.log('Fetching components for page:', foundPage.id);
// console.log('Fetching components for page:', foundPage.id);
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
console.log('Components found:', pageComponents);
// console.log('Components found:', pageComponents);
setComponents(pageComponents);
// Fetch all services
console.log('Fetching all services...');
// console.log('Fetching all services...');
const allServices = await serviceService.getServices();
console.log('Services found:', allServices);
// console.log('Services found:', allServices);
setServices(allServices);
// Fetch uptime data for each component that has a service
console.log('Fetching uptime data...');
// console.log('Fetching uptime data...');
const uptimePromises = pageComponents
.filter(component => component.service_id)
.map(async (component) => {
try {
console.log('Fetching uptime for service:', component.service_id);
// console.log('Fetching uptime for service:', component.service_id);
const endDate = new Date();
const startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Last 90 days
const history = await uptimeService.getUptimeHistory(component.service_id, 2000, startDate, endDate);
console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
// console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
return { serviceId: component.service_id, history };
} catch (error) {
console.error(`Error fetching uptime for service ${component.service_id}:`, error);
// console.error(`Error fetching uptime for service ${component.service_id}:`, error);
return { serviceId: component.service_id, history: [] };
}
});
@@ -84,12 +84,12 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
uptimeMap[result.serviceId] = result.history;
});
setUptimeData(uptimeMap);
console.log('Uptime data set successfully');
// console.log('Uptime data set successfully');
console.log('All data fetched successfully');
// console.log('All data fetched successfully');
} catch (err) {
console.error('Error fetching public page:', err);
// console.error('Error fetching public page:', err);
setError(`Failed to load status page: ${err}`);
} finally {
setLoading(false);
@@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getCurrentEndpoint } from "@/lib/pocketbase";
import { ServerAgentConfigForm } from "./ServerAgentConfigForm";
import { OneClickInstallTab } from "./OneClickInstallTab";
import { DockerOneClickTab } from "./DockerOneClickTab";
import { ManualInstallTab } from "./ManualInstallTab";
interface AddServerAgentDialogProps {
@@ -123,9 +124,10 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="configure">Configure Agent</TabsTrigger>
<TabsTrigger value="one-click">One-Click Install</TabsTrigger>
<TabsTrigger value="docker-one-click">Docker One-Click</TabsTrigger>
<TabsTrigger value="manual">Manual Installation</TabsTrigger>
</TabsList>
@@ -150,6 +152,16 @@ export const AddServerAgentDialog: React.FC<AddServerAgentDialogProps> = ({
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="docker-one-click" className="space-y-6">
<DockerOneClickTab
serverToken={serverToken}
currentPocketBaseUrl={currentPocketBaseUrl}
formData={formData}
serverId={serverId}
onDialogClose={handleDialogClose}
/>
</TabsContent>
<TabsContent value="manual" className="space-y-6">
<ManualInstallTab
@@ -0,0 +1,176 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Copy, Download, Container } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { copyToClipboard } from "@/utils/copyUtils";
interface DockerOneClickTabProps {
serverToken: string;
currentPocketBaseUrl: string;
formData: {
serverName: string;
osType: string;
checkInterval: string;
retryAttempt: string;
};
serverId: string;
onDialogClose: () => void;
}
export const DockerOneClickTab: React.FC<DockerOneClickTabProps> = ({
serverToken,
currentPocketBaseUrl,
formData,
serverId,
onDialogClose,
}) => {
const getDockerOneClickCommand = () => {
const scriptUrl = "https://cdn.checkcle.io/scripts/server-docker-agent.sh";
return `curl -L -o server-docker-agent.sh "${scriptUrl}"
chmod +x server-docker-agent.sh
SERVER_TOKEN="${serverToken}" \\
POCKETBASE_URL="${currentPocketBaseUrl}" \\
SERVER_NAME="${formData.serverName}" \\
AGENT_ID="${serverId}" \\
sudo -E bash ./server-docker-agent.sh`;
};
const getDirectDockerCommand = () => {
return `docker run -d \\
--name monitoring-agent \\
--restart unless-stopped \\
-p 8081:8081 \\
--group-add 999 \\
-e AGENT_ID="${serverId}" \\
-e SERVER_NAME="${formData.serverName}" \\
-e SERVER_TOKEN="${serverToken}" \\
-e POCKETBASE_URL="${currentPocketBaseUrl}" \\
-e POCKETBASE_ENABLED=true \\
-v /proc:/host/proc:ro \\
-v /etc:/host/etc:ro \\
-v /sys:/host/sys:ro \\
-v /:/host/root:ro \\
-v /var/run:/host/var/run:ro \\
-v /dev:/host/dev:ro \\
-v /var/run/docker.sock:/var/run/docker.sock:ro \\
-v monitoring_data:/var/lib/monitoring-agent \\
-v monitoring_logs:/var/log/monitoring-agent \\
operacle/checkcle-server-agent:latest`;
};
const handleCopyOneClickCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
console.log('Copy one-click command button clicked');
const command = getDockerOneClickCommand();
console.log('Copying one-click command:', command);
await copyToClipboard(command);
};
const handleCopyDockerCommand = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
console.log('Copy docker command button clicked');
const command = getDirectDockerCommand();
console.log('Copying docker command:', command);
await copyToClipboard(command);
};
return (
<div className="space-y-6">
{/* One-Click Docker Installation */}
<Card className="border-blue-500/20 bg-blue-50/50 dark:bg-blue-950/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-blue-700 dark:text-blue-400">
<Container className="h-5 w-5" />
Docker One-Click Install (Recommended)
</CardTitle>
<CardDescription className="text-blue-600 dark:text-blue-300">
Automated Docker container installation with system monitoring capabilities
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-blue-700 dark:text-blue-400">Docker One-Click Command</Label>
<div className="relative">
<pre className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all text-blue-800 dark:text-blue-200">
<code>{getDockerOneClickCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2 bg-blue-50 dark:bg-blue-950/50 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-400"
onClick={handleCopyOneClickCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 p-3 rounded-md">
<p className="font-medium mb-1">This script will automatically:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Download and setup the Docker monitoring agent</li>
<li>Configure all required environment variables</li>
<li>Start the container with proper system access</li>
<li>Setup monitoring data persistence</li>
</ol>
</div>
</CardContent>
</Card>
{/* Direct Docker Run Command */}
<Card className="border-gray-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Container className="h-5 w-5" />
Direct Docker Run Command
</CardTitle>
<CardDescription>
If you prefer to run the Docker container directly without the script
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Docker Run Command</Label>
<div className="relative">
<pre className="bg-muted border p-4 rounded-md text-sm overflow-x-auto whitespace-pre-wrap break-all">
<code>{getDirectDockerCommand()}</code>
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="absolute top-2 right-2"
onClick={handleCopyDockerCommand}
>
<Copy className="h-4 w-4 mr-1" />
Copy
</Button>
</div>
</div>
<div className="text-sm text-muted-foreground bg-muted/50 border p-3 rounded-md">
<p className="font-medium mb-1">Prerequisites for direct Docker run:</p>
<ol className="list-decimal list-inside space-y-1 text-xs">
<li>Docker must be installed and running</li>
<li>The operacle/checkcle-server-agent image must be available</li>
<li>Run as root or with sudo privileges</li>
</ol>
</div>
</CardContent>
</Card>
<div className="flex justify-end pt-4">
<Button onClick={onDialogClose}>
Done
</Button>
</div>
</div>
);
};
@@ -25,7 +25,7 @@ export const ManualInstallTab: React.FC<ManualInstallTabProps> = ({
onDialogClose,
}) => {
const getManualInstallSteps = () => {
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return [
{
@@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
onDialogClose,
}) => {
const getOneClickInstallCommand = () => {
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
const scriptUrl = "https://cdn.checkcle.io/scripts/server-agent.sh";
return `curl -L -o server-agent.sh "${scriptUrl}"
chmod +x server-agent.sh
@@ -269,7 +269,11 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
const isProcessing = pausingServers.has(server.id);
return (
<TableRow key={server.id} className="hover:bg-muted/50">
<TableRow
key={server.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => handleViewDetails(server.id)}
>
<TableCell className="font-medium">
<div className="truncate" title={server.name}>
{server.name}
@@ -325,7 +329,7 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
{new Date(server.last_checked).toLocaleString()}
</div>
</TableCell>
<TableCell className="text-right">
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
@@ -338,19 +342,19 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewDetails(server.id); }}>
<Eye className="mr-2 h-4 w-4" />
View Server Detail
</DropdownMenuItem>
{server.docker === 'true' && (
<DropdownMenuItem onClick={() => handleViewContainers(server.id)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleViewContainers(server.id); }}>
<Activity className="mr-2 h-4 w-4" />
Container Monitoring
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handlePauseResume(server)}
onClick={(e) => { e.stopPropagation(); handlePauseResume(server); }}
disabled={isProcessing}
>
{isPaused ? (
@@ -366,12 +370,12 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleEdit(server)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleEdit(server); }}>
<Edit className="mr-2 h-4 w-4" />
Edit Server
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDelete(server)}
onClick={(e) => { e.stopPropagation(); handleDelete(server); }}
className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -427,4 +431,4 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
</AlertDialog>
</>
);
};
};
@@ -70,10 +70,10 @@ export function ServiceForm({
if (onSubmitStart) onSubmitStart();
try {
console.log("Form data being submitted:", data);
// console.log("Form data being submitted:", data);
const serviceData = mapFormDataToServiceData(data);
console.log("Service data being sent:", serviceData);
// console.log("Service data being sent:", serviceData);
if (isEdit && initialData) {
// Update existing service
@@ -67,7 +67,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
placeholder={getPlaceholder()}
{...field}
onChange={(e) => {
console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
// console.log(`${serviceType === "dns" ? "Domain" : serviceType === "tcp" ? "Host" : "URL"} field changed:`, e.target.value);
field.onChange(e);
}}
/>
@@ -93,7 +93,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
type="number"
{...field}
onChange={(e) => {
console.log("Port field changed:", e.target.value);
// console.log("Port field changed:", e.target.value);
field.onChange(e);
}}
/>
@@ -95,7 +95,7 @@ export const AboutSystem: React.FC = () => {
<FileText className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
<span>{t('viewDocumentation')}</span>
</Button>
<Button variant="outline" className="flex items-center justify-start gap-3 h-12 hover:bg-muted/50 transition-all duration-200" onClick={() => window.open("https://x.com/tlengoss", "_blank")}>
<Button variant="outline" className="flex items-center justify-start gap-3 h-12 hover:bg-muted/50 transition-all duration-200" onClick={() => window.open("https://x.com/checkcle_oss", "_blank")}>
<Twitter className={`h-5 w-5 ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`} />
<span>{t('followOnX')}</span>
</Button>
@@ -45,20 +45,20 @@ export const SSLCertificateActions = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onView(certificate)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onView(certificate); }}>
<Eye className="mr-2 h-4 w-4" />
{t('view')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleCheck}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); handleCheck(); }}>
<RefreshCw className="mr-2 h-4 w-4" />
Check
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onEdit(certificate)}>
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onEdit(certificate); }}>
<Edit className="mr-2 h-4 w-4" />
{t('edit')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(certificate)}
onClick={(e) => { e.stopPropagation(); onDelete(certificate); }}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -67,4 +67,4 @@ export const SSLCertificateActions = ({
</DropdownMenuContent>
</DropdownMenu>
);
};
};
@@ -33,11 +33,13 @@ import { AddSSLCertificateForm } from "./AddSSLCertificateForm";
import { EditSSLCertificateForm } from "./EditSSLCertificateForm";
import { SSLCertificateActions } from "./SSLCertificateActions";
import { SSLCertificateDetailDialog } from "./SSLCertificateDetailDialog";
import { SSLPagination } from "./SSLPagination";
import { fetchSSLCertificates, addSSLCertificate, deleteSSLCertificate } from "@/services/sslCertificateService";
import { pb } from "@/lib/pocketbase";
import { SSLCertificate } from "@/types/ssl.types";
import { useLanguage } from "@/contexts/LanguageContext";
import { useTheme } from "@/contexts/ThemeContext";
import { useSSLPagination } from "@/hooks/useSSLPagination";
import { toast } from "sonner";
export const SSLCertificatesTable = () => {
@@ -56,6 +58,16 @@ export const SSLCertificatesTable = () => {
queryFn: fetchSSLCertificates,
});
const {
paginatedCertificates,
currentPage,
totalPages,
pageSize,
totalItems,
handlePageChange,
handlePageSizeChange,
} = useSSLPagination({ certificates });
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading certificates</div>;
@@ -67,7 +79,6 @@ export const SSLCertificatesTable = () => {
setShowAddDialog(false);
toast.success(t('certificateAdded'));
} catch (error) {
// console.error("Error adding certificate:", error);
toast.error(t('failedToAddCertificate'));
} finally {
setIsSubmitting(false);
@@ -89,7 +100,6 @@ export const SSLCertificatesTable = () => {
setSelectedCertificate(null);
toast.success(t('certificateUpdated'));
} catch (error) {
// console.error("Error updating certificate:", error);
toast.error(t('failedToUpdateCertificate'));
} finally {
setIsSubmitting(false);
@@ -107,7 +117,6 @@ export const SSLCertificatesTable = () => {
setSelectedCertificate(null);
toast.success(t('certificateDeleted'));
} catch (error) {
// console.error("Error deleting certificate:", error);
toast.error(t('failedToDeleteCertificate'));
} finally {
setIsSubmitting(false);
@@ -136,53 +145,68 @@ export const SSLCertificatesTable = () => {
{t('noCertificatesFound')}
</div>
) : (
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
<Table>
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('domain')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('issuer')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('validUntil')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('daysLeft')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Check Interval</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right w-[50px]`}>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{certificates.map((certificate) => (
<TableRow key={certificate.id} className="hover:bg-muted/50">
<TableCell className="font-medium">
{certificate.domain}
</TableCell>
<TableCell>
<SSLStatusBadge status={certificate.status} />
</TableCell>
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
<TableCell>
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
</TableCell>
<TableCell>
<span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
{certificate.days_left} {t('days')}
</span>
</TableCell>
<TableCell>
{certificate.check_interval || 1} {t('days')}
</TableCell>
<TableCell className="text-right">
<SSLCertificateActions
certificate={certificate}
onView={openViewDialog}
onEdit={openEditDialog}
onDelete={openDeleteDialog}
/>
</TableCell>
<>
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
<Table>
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('domain')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('status')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('issuer')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('validUntil')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>{t('daysLeft')}</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Check Interval</TableHead>
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right w-[50px]`}>Actions</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TableHeader>
<TableBody>
{paginatedCertificates.map((certificate) => (
<TableRow
key={certificate.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => openViewDialog(certificate)}
>
<TableCell className="font-medium">
{certificate.domain}
</TableCell>
<TableCell>
<SSLStatusBadge status={certificate.status} />
</TableCell>
<TableCell>{certificate.issuer_o || certificate.issuer_cn || 'Unknown'}</TableCell>
<TableCell>
{certificate.valid_till ? new Date(certificate.valid_till).toLocaleDateString() : 'N/A'}
</TableCell>
<TableCell>
<span className={certificate.days_left <= 7 ? 'text-red-600 font-semibold' : certificate.days_left <= 30 ? 'text-yellow-600 font-semibold' : 'text-green-600'}>
{certificate.days_left} {t('days')}
</span>
</TableCell>
<TableCell>
{certificate.check_interval || 1} {t('days')}
</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<SSLCertificateActions
certificate={certificate}
onView={openViewDialog}
onEdit={openEditDialog}
onDelete={openDeleteDialog}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<SSLPagination
currentPage={currentPage}
totalPages={totalPages}
pageSize={pageSize}
totalItems={totalItems}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
</>
)}
{/* View Certificate Dialog */}
@@ -30,12 +30,12 @@ export const SSLDomainContent = () => {
queryKey: ['ssl-certificates'],
queryFn: async () => {
try {
console.log("Fetching SSL certificates from SSLDomainContent...");
// console.log("Fetching SSL certificates from SSLDomainContent...");
const result = await fetchSSLCertificates();
console.log("Received SSL certificates:", result);
// console.log("Received SSL certificates:", result);
return result;
} catch (error) {
console.error("Error fetching certificates:", error);
// console.error("Error fetching certificates:", error);
toast.error(t('failedToLoadCertificates'));
throw error;
}
@@ -61,7 +61,7 @@ export const SSLDomainContent = () => {
// Edit certificate mutation - Updated to ensure thresholds are properly updated
const editMutation = useMutation({
mutationFn: async (certificate: SSLCertificate) => {
console.log("Updating certificate with data:", certificate);
// console.log("Updating certificate with data:", certificate);
// Create the update data object
const updateData = {
@@ -70,12 +70,12 @@ export const SSLDomainContent = () => {
notification_channel: certificate.notification_channel,
};
console.log("Update data to be sent:", updateData);
// console.log("Update data to be sent:", updateData);
// Update certificate in the database using PocketBase directly
const updated = await pb.collection('ssl_certificates').update(certificate.id, updateData);
console.log("PocketBase update response:", updated);
// console.log("PocketBase update response:", updated);
// After updating the settings, refresh the certificate to ensure it's up to date
// This will also check if notification needs to be sent based on updated thresholds
@@ -90,7 +90,7 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateUpdated'));
},
onError: (error) => {
console.error("Error updating SSL certificate:", error);
// console.error("Error updating SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToUpdateCertificate'));
}
});
@@ -103,7 +103,7 @@ export const SSLDomainContent = () => {
toast.success(t('sslCertificateDeleted'));
},
onError: (error) => {
console.error("Error deleting SSL certificate:", error);
// console.error("Error deleting SSL certificate:", error);
toast.error(error instanceof Error ? error.message : t('failedToDeleteCertificate'));
}
});
@@ -117,7 +117,7 @@ export const SSLDomainContent = () => {
// Removed individual success toast notification
},
onError: (error) => {
console.error("Error refreshing SSL certificate:", error);
// console.error("Error refreshing SSL certificate:", error);
setRefreshingId(null);
// Still refresh the data to show any partial information that was saved
@@ -142,7 +142,7 @@ export const SSLDomainContent = () => {
}
},
onError: (error) => {
console.error("Error refreshing all certificates:", error);
// console.error("Error refreshing all certificates:", error);
toast.error(t('failedToCheckCertificate'));
setIsRefreshingAll(false);
@@ -0,0 +1,131 @@
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import { SSLPageSize } from "@/hooks/useSSLPagination";
import { useLanguage } from "@/contexts/LanguageContext";
interface SSLPaginationProps {
currentPage: number;
totalPages: number;
pageSize: SSLPageSize;
totalItems: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: SSLPageSize) => void;
}
export function SSLPagination({
currentPage,
totalPages,
pageSize,
totalItems,
onPageChange,
onPageSizeChange,
}: SSLPaginationProps) {
const { t } = useLanguage();
// Generate page numbers to display
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
const halfVisible = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfVisible);
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
// Adjust start page if we don't have enough pages at the end
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const startItem = Math.min((currentPage - 1) * pageSize + 1, totalItems);
const endItem = Math.min(currentPage * pageSize, totalItems);
return (
<div className="flex items-center justify-between py-4 px-4 border-t border-border">
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">
{t("rowsPerPage") || "Rows per page"}:
</span>
<Select
value={pageSize.toString()}
onValueChange={(value) => onPageSizeChange(parseInt(value) as SSLPageSize)}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="30">30</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground">
{totalItems > 0
? `${startItem}-${endItem} of ${totalItems} ${t("certificates") || "certificates"}`
: `0 ${t("certificates") || "certificates"}`
}
</span>
</div>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
className={
currentPage === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{getPageNumbers().map((page) => (
<PaginationItem key={page}>
<PaginationLink
isActive={page === currentPage}
onClick={() => onPageChange(page)}
className="cursor-pointer"
>
{page}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { useState, useMemo } from 'react';
import { SSLCertificate } from '@/types/ssl.types';
export type SSLPageSize = 10 | 30 | 50;
interface UseSSLPaginationProps {
certificates: SSLCertificate[];
initialPageSize?: SSLPageSize;
}
export const useSSLPagination = ({
certificates,
initialPageSize = 10
}: UseSSLPaginationProps) => {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState<SSLPageSize>(initialPageSize);
const { paginatedCertificates, totalPages } = useMemo(() => {
const totalItems = certificates.length;
const pages = Math.ceil(totalItems / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return {
paginatedCertificates: certificates.slice(startIndex, endIndex),
totalPages: Math.max(1, pages)
};
}, [certificates, currentPage, pageSize]);
// Reset to first page when page size changes or certificates change
const handlePageSizeChange = (newPageSize: SSLPageSize) => {
setPageSize(newPageSize);
setCurrentPage(1);
};
// Reset to first page if current page exceeds total pages
const handlePageChange = (page: number) => {
const validPage = Math.min(Math.max(1, page), totalPages);
setCurrentPage(validPage);
};
return {
paginatedCertificates,
currentPage,
totalPages,
pageSize,
totalItems: certificates.length,
handlePageChange,
handlePageSizeChange,
};
};
@@ -13,7 +13,7 @@ export async function pauseMonitoring(serviceId: string): Promise<void> {
if (intervalId) {
clearInterval(intervalId);
monitoringIntervals.delete(serviceId);
console.log(`Monitoring paused for service ${serviceId}`);
// console.log(`Monitoring paused for service ${serviceId}`);
}
// Get current timestamp formatted as a string
@@ -27,7 +27,7 @@ export const operationalPageService = {
const result = await response.json();
return result.items || [];
} catch (error) {
console.error('Error fetching operational pages:', error);
// console.error('Error fetching operational pages:', error);
throw error;
}
},
@@ -55,7 +55,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
console.error('Error fetching operational page:', error);
// console.error('Error fetching operational page:', error);
throw error;
}
},
@@ -84,7 +84,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
console.error('Error updating operational page:', error);
// console.error('Error updating operational page:', error);
throw error;
}
},
@@ -113,7 +113,7 @@ export const operationalPageService = {
return await response.json();
} catch (error) {
console.error('Error creating operational page:', error);
// console.error('Error creating operational page:', error);
throw error;
}
},
@@ -139,7 +139,7 @@ export const operationalPageService = {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('Error deleting operational page:', error);
// console.error('Error deleting operational page:', error);
throw error;
}
}
@@ -27,7 +27,7 @@ export const statusPageComponentsService = {
const result = await response.json();
return result.items || [];
} catch (error) {
console.error('Error fetching status page components:', error);
// console.error('Error fetching status page components:', error);
throw error;
}
},
@@ -56,7 +56,7 @@ export const statusPageComponentsService = {
const result = await response.json();
return result.items || [];
} catch (error) {
console.error('Error fetching status page components by operational ID:', error);
// console.error('Error fetching status page components by operational ID:', error);
throw error;
}
},
@@ -79,7 +79,7 @@ export const statusPageComponentsService = {
headers['Authorization'] = `Bearer ${authToken}`;
}
console.log('Creating status page component with data:', data);
// console.log('Creating status page component with data:', data);
const baseUrl = getCurrentEndpoint();
const response = await fetch(`${baseUrl}/api/collections/status_page_components/records`, {
@@ -95,10 +95,10 @@ export const statusPageComponentsService = {
}
const result = await response.json();
console.log('Created component:', result);
// console.log('Created component:', result);
return result;
} catch (error) {
console.error('Error creating status page component:', error);
// console.error('Error creating status page component:', error);
throw error;
}
},
@@ -124,7 +124,7 @@ export const statusPageComponentsService = {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('Error deleting status page component:', error);
// console.error('Error deleting status page component:', error);
throw error;
}
}
+35
View File
@@ -0,0 +1,35 @@
name: checkcle-server-agent
services:
checkcle-server-agent:
container_name: monitoring-agent
restart: unless-stopped
ports:
- "8081:8081"
group_add:
- 999
environment:
AGENT_ID: agent_124343535
SERVER_NAME: "test"
SERVER_TOKEN: srv_123243434
POCKETBASE_URL: http://x.x.x.x:8090
POCKETBASE_ENABLED: "true"
volumes:
- /proc:/host/proc:ro
- /etc:/host/etc:ro
- /sys:/host/sys:ro
- /:/host/root:ro
- /var/run:/host/var/run:ro
- /dev:/host/dev:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- monitoring_data:/var/lib/monitoring-agent
- monitoring_logs:/var/log/monitoring-agent
image: operacle/checkcle-server-agent:latest
volumes:
monitoring_data:
external: true
name: monitoring_data
monitoring_logs:
external: true
name: monitoring_logs
+119
View File
@@ -0,0 +1,119 @@
# 📍 Development Roadmap
Welcome to the official development roadmap for this project. This document outlines our plans, priorities, and direction for upcoming releases. It is intended to keep contributors, users, and stakeholders informed about the projects evolution.
> 🔄 **Note**: This roadmap is subject to change based on feedback, urgency, or shifts in priorities.
---
## ✅ Goals and Vision
Our mission is to deliver a robust, scalable, and user-friendly solution for "uptime monitoring and server infrastructure insights".
**Core objectives:**
- Provide high performance with low resource usage
- Ensure scalability and modularity
- Focus on excellent user experience (UX)
- Maintain open-source transparency and community-driven features
---
## 🧩 Key Main Features
#### Delivery Status:
- ✅ Setup authentication and user management
- ✅ Core monitoring dashboard
- ✅ Agent communication with backend
- ✅ Docker containerization
- ✅ CheckCle Website
- ✅ CheckCle Demo Server
- ✅ SSL & Domain Monitoring
- ✅ Schedule Maintenance
- ✅ Incident Management
- ✅ Infrastructure Server Monitoring
- ✅ Operational Status / Public Status Pages
- ✅ Uptime Monitoring (HTTP, TCP, PING, DNS)
- ✅ Distributed Regional Monitoring Agent
- ✅ System Setting Panel and Mail Settings
- ✅ Data Retention & Automate Shrink
- ✅ Open-source release with documentation
---
## 🚦 Roadmap Stages
The roadmap is divided into the following stages:
| Stage | Description |
|-------|-------------|
| 🎯 Planned | Approved features or improvements that are on the schedule |
| 🔧 In Progress | Actively being developed |
| ✅ Completed | Fully implemented and tested |
| ⏳ Backlog | On hold or pending prioritization |
| 🧪 Experimental | Testing new concepts or prototypes |
---
## 🗂 Milestone Overview
### 📦 v1.0.0-1.3.0 Initial Release _(Target: Q2 2025)_
**Status:** ✅ Completed
**Goal:** MVP feature completion, API stability, and core use-case readiness.
#### Key Deliverables: https://github.com/operacle/checkcle/releases
---
### 🚀 v1.4 - 1.5 Feature Enhancements _(Target: Q3 2025)_
**Status:** 🔧 In Progress
#### Tentative Features:
- [ ] ✅ Server and Service Table row clickable to detail page.
- [ ] ✅ Implement pagination for the SSL dashboard table
- [ ] ✅ Server Agent (RPM, Docker container, and general binary package)
- [ ] 🔧 Notification System (Webhook, Telegram, Discord, Slack, Email, Google Chat)
- [ ] 🔧 Improve Uptime Service and Server connection update based on status and notification.
- [ ] 🔧 Improve SSL perform the initial check automatically after creation
- [ ] 🔧 Rate limiting and abuse protection
- [ ] 🎯 Improve the Operational status page
- [ ] 🎯 Improve the Schedule and Incident for automation
- [ ] 🎯 Server Windows Agent
- [ ] 🎯 More Uptime Service Type (HTTP keyword, HTTP json)
- [ ] 🎯 Server support with Multiple Network Interfaces
- [ ] 🎯 Server support with cpu temperature
- [ ] 🎯 Server upport with multiple disks
- [ ] 🎯 Add 2FA support
- [ ] 🎯 Rate limiting and abuse protection
---
## 🧠 Ideas / Community Wishlist
These are community-suggested or experimental features under review:
- [ ] Grouping uptime services
- [ ] OAuth2 integration
- [ ] Mobile PWA support
- [ ] Cloud-native Helm/Kubernetes deployment
Youre welcome to propose features via [GitHub Discussions](https://github.com/operacle/checkcle/discussions) or open an issue with the `feature-request` template.
---
## 📌 How to Contribute
We encourage contributions from the community!
To get involved:
- Review [CONTRIBUTING.md](../CONTRIBUTING.md) | https://docs.checkcle.io
- Check out the [Open Issues](https://github.com/operacle/checkcle/issues)
- Join us in shaping the roadmap via [Discussions](https://github.com/operacle/checkcle/discussions)
---
## 📅 Last Updated
_This roadmap was last updated on **July 26, 2025**._
---
With ❤️ from the CheckCle
[Website](https://checkcle.io) | [GitHub](https://github.com/operacle/checkcle)
+1 -1
View File
@@ -162,7 +162,7 @@ CheckCleの改善にご協力いただける方法をご紹介します:
- GitHubリポジトリ: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
- コミュニティチャンネル: ディスカッションやissuesで参加してください!
- Discord: コミュニティに参加 [@discord](https://discord.gg/xs9gbubGwX)
- X: [@tlengoss](https://x.com/tlengoss)
- X: [@checkcle_oss](https://x.com/checkcle_oss)
## 📜 ライセンス
+1 -1
View File
@@ -163,7 +163,7 @@ services:
- ឃ្លាំងផ្ទុកកូដ: ⭐ [CheckCle](https://github.com/operacle/checkcle.git)
- ទំនាក់ទំនងក្រុម: Engage via discussions and issues!
- បណ្តាញសង្គម: Join our community [@discord](https://discord.gg/xs9gbubGwX)
- X: [@tlengoss](https://x.com/tlengoss)
- X: [@checkcle_oss](https://x.com/checkcle_oss)
## 📜 License
+330 -37
View File
@@ -1,4 +1,3 @@
#!/bin/bash
# CheckCle Server Monitoring Agent - One-Click Installation Script
@@ -44,7 +43,7 @@ check_root() {
fi
}
# Detect system architecture and package format
# Detect system architecture and package format with fallback
detect_system() {
log_info "Detecting system architecture and package format..."
@@ -64,28 +63,89 @@ detect_system() {
;;
esac
# Detect package format preference
if command -v dpkg >/dev/null 2>&1; then
PACKAGE_FORMAT="deb"
PACKAGE_MANAGER="dpkg"
elif command -v rpm >/dev/null 2>&1; then
PACKAGE_FORMAT="rpm"
if command -v yum >/dev/null 2>&1; then
PACKAGE_MANAGER="yum"
elif command -v dnf >/dev/null 2>&1; then
PACKAGE_MANAGER="dnf"
else
PACKAGE_MANAGER="rpm"
fi
else
log_error "No supported package manager found (dpkg or rpm required)"
log_info "This script supports Debian/Ubuntu (.deb) and RHEL/CentOS/Fedora (.rpm) systems"
exit 1
# Detect OS and preferred package format
PREFERRED_FORMAT=""
PREFERRED_MANAGER=""
if [[ -f /etc/os-release ]]; then
OS_ID=$(grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"')
OS_LIKE=$(grep "^ID_LIKE=" /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "")
log_info "Detected OS: $OS_ID"
[[ -n "$OS_LIKE" ]] && log_info "OS family: $OS_LIKE"
# Determine preferred package format based on OS
case "$OS_ID" in
debian|ubuntu|linuxmint|elementary|pop)
PREFERRED_FORMAT="deb"
PREFERRED_MANAGER="dpkg"
;;
rhel|centos|fedora|rocky|almalinux|oracle|amazonlinux)
PREFERRED_FORMAT="rpm"
if command -v dnf >/dev/null 2>&1; then
PREFERRED_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PREFERRED_MANAGER="yum"
else
PREFERRED_MANAGER="rpm"
fi
;;
arch|manjaro|artix|endeavouros)
# Arch-based systems - use tar.gz fallback
log_info "Arch-based system detected, using tar.gz format"
PREFERRED_FORMAT="tar.gz"
PREFERRED_MANAGER="tar"
;;
*)
# Check OS_LIKE for family detection
if [[ "$OS_LIKE" == *"debian"* ]] || [[ "$OS_LIKE" == *"ubuntu"* ]]; then
PREFERRED_FORMAT="deb"
PREFERRED_MANAGER="dpkg"
elif [[ "$OS_LIKE" == *"rhel"* ]] || [[ "$OS_LIKE" == *"fedora"* ]]; then
PREFERRED_FORMAT="rpm"
if command -v dnf >/dev/null 2>&1; then
PREFERRED_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PREFERRED_MANAGER="yum"
else
PREFERRED_MANAGER="rpm"
fi
elif [[ "$OS_LIKE" == *"arch"* ]]; then
PREFERRED_FORMAT="tar.gz"
PREFERRED_MANAGER="tar"
fi
;;
esac
fi
# Construct package filename and URL
PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}"
PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}"
# If no preferred format detected, check available package managers
if [[ -z "$PREFERRED_FORMAT" ]]; then
if command -v dpkg >/dev/null 2>&1; then
PREFERRED_FORMAT="deb"
PREFERRED_MANAGER="dpkg"
elif command -v rpm >/dev/null 2>&1; then
PREFERRED_FORMAT="rpm"
if command -v dnf >/dev/null 2>&1; then
PREFERRED_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PREFERRED_MANAGER="yum"
else
PREFERRED_MANAGER="rpm"
fi
else
# Fallback to tar.gz for unsupported systems
log_info "No native package manager found, using tar.gz fallback"
PREFERRED_FORMAT="tar.gz"
PREFERRED_MANAGER="tar"
fi
fi
# Set initial package format and manager
PACKAGE_FORMAT="$PREFERRED_FORMAT"
PACKAGE_MANAGER="$PREFERRED_MANAGER"
# Construct initial package filename and URL
construct_package_url
log_success "System detection complete:"
log_info " Architecture: $ARCH -> $PACKAGE_ARCH"
@@ -94,6 +154,16 @@ detect_system() {
log_info " Package URL: $PACKAGE_URL"
}
# Construct package URL based on current format
construct_package_url() {
if [[ "$PACKAGE_FORMAT" == "tar.gz" ]]; then
PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.tar.gz"
else
PACKAGE_FILENAME="monitoring-agent_1.0.0_${PACKAGE_ARCH}.${PACKAGE_FORMAT}"
fi
PACKAGE_URL="${GITHUB_BASE_URL}/${PACKAGE_FILENAME}"
}
# Validate required environment variables - check both current and sudo environments
validate_environment() {
# Check if SERVER_TOKEN is available in current environment or passed as argument
@@ -116,7 +186,7 @@ validate_environment() {
[[ -n "$AGENT_ID" ]] && log_info "AGENT_ID: $AGENT_ID"
}
# Download package based on detected system
# Download package with fallback to tar.gz
download_package() {
local temp_dir="/tmp/monitoring-agent-install"
mkdir -p "$temp_dir"
@@ -124,17 +194,211 @@ download_package() {
log_info "Downloading monitoring agent package..."
log_info "URL: $PACKAGE_URL"
if curl -L -f -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL"; then
DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME"
log_success "Package downloaded successfully: $PACKAGE_FILENAME"
# Try to download the preferred package format first
if curl -L -f -s -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL" 2>/dev/null; then
# Verify the file was actually downloaded and has content
if [[ -s "$temp_dir/$PACKAGE_FILENAME" ]]; then
DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME"
log_success "Package downloaded successfully: $PACKAGE_FILENAME"
return 0
else
log_warning "Downloaded file is empty, trying fallback..."
rm -f "$temp_dir/$PACKAGE_FILENAME"
fi
else
log_error "Failed to download package from: $PACKAGE_URL"
log_info "Please check:"
log_info " 1. Internet connectivity"
log_info " 2. Package availability for your architecture ($PACKAGE_ARCH)"
log_info " 3. GitHub repository access"
log_warning "Failed to download preferred package format: $PACKAGE_FORMAT"
fi
# If preferred format failed and it's not tar.gz, try tar.gz fallback
if [[ "$PACKAGE_FORMAT" != "tar.gz" ]]; then
log_warning "Native package ($PACKAGE_FORMAT) not available, trying tar.gz fallback..."
# Switch to tar.gz format
PACKAGE_FORMAT="tar.gz"
PACKAGE_MANAGER="tar"
construct_package_url
log_info "Fallback URL: $PACKAGE_URL"
if curl -L -f -s -o "$temp_dir/$PACKAGE_FILENAME" "$PACKAGE_URL" 2>/dev/null; then
# Verify the file was actually downloaded and has content
if [[ -s "$temp_dir/$PACKAGE_FILENAME" ]]; then
DOWNLOADED_PACKAGE="$temp_dir/$PACKAGE_FILENAME"
log_success "Fallback package downloaded successfully: $PACKAGE_FILENAME"
return 0
else
log_error "Downloaded fallback file is empty"
rm -f "$temp_dir/$PACKAGE_FILENAME"
fi
else
log_error "Failed to download tar.gz fallback"
fi
fi
# Both attempts failed
log_error "Failed to download package from: $PACKAGE_URL"
log_info "Please check:"
log_info " 1. Internet connectivity"
log_info " 2. Package availability for your architecture ($PACKAGE_ARCH)"
log_info " 3. GitHub repository access"
exit 1
}
# Install tar.gz package
install_tar_package() {
log_info "Installing monitoring agent from tar.gz package..."
local temp_extract_dir="/tmp/monitoring-agent-extract"
mkdir -p "$temp_extract_dir"
# Extract tar.gz package
if tar -xzf "$DOWNLOADED_PACKAGE" -C "$temp_extract_dir"; then
log_success "Package extracted successfully"
else
log_error "Failed to extract tar.gz package"
exit 1
fi
# Install files to their proper locations
log_info "Installing files to system directories..."
# Create necessary directories
mkdir -p /usr/bin
mkdir -p /etc/monitoring-agent
mkdir -p /var/lib/monitoring-agent
mkdir -p /var/log/monitoring-agent
# Detect systemd service directory based on OS
if [[ -d /lib/systemd/system ]]; then
SYSTEMD_DIR="/lib/systemd/system"
elif [[ -d /usr/lib/systemd/system ]]; then
SYSTEMD_DIR="/usr/lib/systemd/system"
else
SYSTEMD_DIR="/etc/systemd/system"
fi
mkdir -p "$SYSTEMD_DIR"
# Copy files from extracted package
if [[ -f "$temp_extract_dir/usr/bin/monitoring-agent" ]]; then
cp "$temp_extract_dir/usr/bin/monitoring-agent" /usr/bin/
chmod +x /usr/bin/monitoring-agent
log_success "Binary installed to /usr/bin/monitoring-agent"
else
log_error "Binary not found in package"
exit 1
fi
if [[ -f "$temp_extract_dir/etc/monitoring-agent/monitoring-agent.env" ]]; then
cp "$temp_extract_dir/etc/monitoring-agent/monitoring-agent.env" /etc/monitoring-agent/
log_success "Configuration template installed"
fi
# Create monitoring-agent user and group if they don't exist
if ! getent group monitoring-agent >/dev/null 2>&1; then
groupadd --system monitoring-agent
log_success "Created monitoring-agent group"
else
log_info "monitoring-agent group already exists"
fi
if ! getent passwd monitoring-agent >/dev/null 2>&1; then
useradd --system --gid monitoring-agent --home-dir /var/lib/monitoring-agent --shell /bin/false --comment "Monitoring Agent" monitoring-agent
log_success "Created monitoring-agent user"
else
log_info "monitoring-agent user already exists"
fi
# Set proper ownership and permissions
chown -R monitoring-agent:monitoring-agent /var/lib/monitoring-agent
chown -R monitoring-agent:monitoring-agent /var/log/monitoring-agent
chown -R monitoring-agent:monitoring-agent /etc/monitoring-agent
chmod 755 /var/lib/monitoring-agent
chmod 755 /var/log/monitoring-agent
chmod 755 /etc/monitoring-agent
# Check if Docker is installed and configure service file accordingly
DOCKER_AVAILABLE=false
if command -v docker >/dev/null 2>&1 && getent group docker >/dev/null 2>&1; then
DOCKER_AVAILABLE=true
# Add monitoring-agent user to docker group if Docker is available
usermod -a -G docker monitoring-agent
log_info "Docker detected, added monitoring-agent user to docker group"
else
log_info "Docker not detected, skipping Docker group configuration"
fi
# Create the systemd service file with proper Docker configuration
create_systemd_service_file "$SYSTEMD_DIR" "$DOCKER_AVAILABLE"
# Clean up extraction directory
rm -rf "$temp_extract_dir"
log_success "Tar.gz package installation completed"
}
# Create systemd service file with Docker configuration
create_systemd_service_file() {
local systemd_dir="$1"
local docker_available="$2"
log_info "Creating systemd service file at $systemd_dir/monitoring-agent.service"
cat > "$systemd_dir/monitoring-agent.service" << 'EOF'
[Unit]
Description=CheckCle Server Monitoring Agent
Documentation=https://github.com/operacle/checkcle-server-agent
After=network.target
Wants=network.target
[Service]
Type=simple
User=monitoring-agent
Group=monitoring-agent
EOF
# Add Docker group only if Docker is available
if [[ "$docker_available" == "true" ]]; then
echo "# Add docker group for Docker monitoring access" >> "$systemd_dir/monitoring-agent.service"
echo "SupplementaryGroups=docker" >> "$systemd_dir/monitoring-agent.service"
else
echo "# Docker not available - skipping Docker group configuration" >> "$systemd_dir/monitoring-agent.service"
fi
# Continue with the rest of the service configuration
cat >> "$systemd_dir/monitoring-agent.service" << 'EOF'
ExecStart=/usr/bin/monitoring-agent
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Security settings
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
RestrictRealtime=yes
# Allow access to system information and configuration
ReadWritePaths=/var/log/monitoring-agent
ReadOnlyPaths=/proc /sys /etc/monitoring-agent
# Network access
PrivateNetwork=no
# Environment
EnvironmentFile=/etc/monitoring-agent/monitoring-agent.env
WorkingDirectory=/var/lib/monitoring-agent
[Install]
WantedBy=multi-user.target
EOF
log_success "Created systemd service file with Docker support: $docker_available"
}
# Install package based on detected package manager
@@ -185,6 +449,10 @@ install_package() {
fi
;;
tar)
install_tar_package
;;
*)
log_error "Unsupported package manager: $PACKAGE_MANAGER"
exit 1
@@ -232,7 +500,7 @@ configure_agent() {
log_info "Final configuration:"
log_info " Server Name: $SERVER_NAME"
log_info " Agent ID: $AGENT_ID"
log_info " PB API URL: $POCKETBASE_URL"
log_info " PocketBase URL: $POCKETBASE_URL"
log_info " IP Address: $IP_ADDRESS"
log_info " OS Type: $OS_TYPE"
log_info " Health Check Port: $HEALTH_CHECK_PORT"
@@ -341,7 +609,7 @@ test_installation() {
show_post_install_info() {
echo
echo "============================================="
echo " CheckCle Server Agent Installation Complete!"
echo " Installation Complete!"
echo "============================================="
echo
log_success "CheckCle Monitoring Agent installed and configured successfully"
@@ -350,6 +618,11 @@ show_post_install_info() {
echo " Architecture: $ARCH ($PACKAGE_ARCH)"
echo " Package: $PACKAGE_FILENAME"
echo " Package Manager: $PACKAGE_MANAGER"
if [[ "$PACKAGE_FORMAT" == "tar.gz" ]]; then
echo " Installation Type: Manual (tar.gz)"
else
echo " Installation Type: Native package ($PACKAGE_FORMAT)"
fi
echo
echo "Configuration: $CONFIG_FILE"
echo "Service status: systemctl status monitoring-agent"
@@ -416,7 +689,12 @@ case "${1:-}" in
echo "System Requirements:"
echo " - Linux with systemd"
echo " - Supported architectures: x86_64 (amd64), aarch64/arm64"
echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm)"
echo " - Supported distributions: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm), or any Linux (tar.gz fallback)"
echo
echo "Package Installation Logic:"
echo " 1. Detects OS and tries native package format first (.deb for Debian-based, .rpm for RHEL-based)"
echo " 2. If native package is not available, automatically falls back to tar.gz"
echo " 3. tar.gz works on any Linux distribution with systemd"
echo
echo "Required Environment Variables:"
echo " SERVER_TOKEN Server authentication token"
@@ -444,13 +722,28 @@ case "${1:-}" in
systemctl disable monitoring-agent 2>/dev/null || true
# Remove based on detected package manager
if command -v dpkg >/dev/null 2>&1; then
if command -v dpkg >/dev/null 2>&1 && dpkg -l monitoring-agent >/dev/null 2>&1; then
dpkg -r monitoring-agent 2>/dev/null || true
elif command -v rpm >/dev/null 2>&1; then
log_success "DEB package removed"
elif command -v rpm >/dev/null 2>&1 && rpm -q monitoring-agent >/dev/null 2>&1; then
rpm -e monitoring-agent 2>/dev/null || true
log_success "RPM package removed"
else
# Manual removal for tar.gz installations
log_info "Performing manual cleanup for tar.gz installation..."
rm -f /usr/bin/monitoring-agent 2>/dev/null || true
rm -rf /etc/monitoring-agent 2>/dev/null || true
rm -f /lib/systemd/system/monitoring-agent.service 2>/dev/null || true
rm -f /usr/lib/systemd/system/monitoring-agent.service 2>/dev/null || true
rm -f /etc/systemd/system/monitoring-agent.service 2>/dev/null || true
userdel monitoring-agent 2>/dev/null || true
groupdel monitoring-agent 2>/dev/null || true
rm -rf /var/lib/monitoring-agent 2>/dev/null || true
rm -rf /var/log/monitoring-agent 2>/dev/null || true
log_success "Manual cleanup completed"
fi
rm -rf /etc/monitoring-agent 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
log_success "Monitoring agent uninstalled"
exit 0
;;
+361
View File
@@ -0,0 +1,361 @@
#!/bin/bash
# CheckCle Server Monitoring Agent - Docker One-Click Installation Script
# This script provides fully automated Docker installation using environment variables
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root (use sudo)"
exit 1
fi
}
# Check if Docker is installed
check_docker() {
if ! command -v docker >/dev/null 2>&1; then
log_error "Docker is not installed"
log_info "Please install Docker first:"
log_info " Ubuntu/Debian: sudo apt-get update && sudo apt-get install docker.io"
log_info " CentOS/RHEL: sudo yum install docker"
log_info " Or visit: https://docs.docker.com/engine/install/"
exit 1
fi
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
log_error "Docker daemon is not running"
log_info "Start Docker with: sudo systemctl start docker"
exit 1
fi
log_success "Docker is installed and running"
}
# Validate required environment variables
validate_environment() {
local missing_vars=()
# Check required variables
[[ -z "$SERVER_TOKEN" ]] && missing_vars+=("SERVER_TOKEN")
[[ -z "$POCKETBASE_URL" ]] && missing_vars+=("POCKETBASE_URL")
[[ -z "$SERVER_NAME" ]] && missing_vars+=("SERVER_NAME")
[[ -z "$AGENT_ID" ]] && missing_vars+=("AGENT_ID")
if [[ ${#missing_vars[@]} -ne 0 ]]; then
log_error "Missing required environment variables: ${missing_vars[*]}"
log_info ""
log_info "Usage examples:"
log_info " SERVER_TOKEN=your-token POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent sudo -E bash $0"
log_info " sudo SERVER_TOKEN=your-token POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent bash $0"
log_info ""
log_info "Required variables:"
log_info " SERVER_TOKEN Server authentication token"
log_info " POCKETBASE_URL PocketBase URL (e.g., http://194.233.80.126:8090)"
log_info " SERVER_NAME Server name (e.g., DB-Server01)"
log_info " AGENT_ID Agent identifier (e.g., agent_hogr3np88u7dq5xf9balzl)"
log_info ""
log_info "Optional variables:"
log_info " HEALTH_CHECK_PORT Health check port (default: 8081)"
log_info " CONTAINER_NAME Container name (default: monitoring-agent)"
log_info " DOCKER_IMAGE Docker image (default: operacle/checkcle-server-agent:latest)"
exit 1
fi
log_success "Environment validation passed"
log_info "SERVER_TOKEN: ${SERVER_TOKEN:0:8}..."
log_info "POCKETBASE_URL: $POCKETBASE_URL"
log_info "SERVER_NAME: $SERVER_NAME"
log_info "AGENT_ID: $AGENT_ID"
}
# Set default values for optional variables
set_defaults() {
HEALTH_CHECK_PORT="${HEALTH_CHECK_PORT:-8081}"
CONTAINER_NAME="${CONTAINER_NAME:-monitoring-agent}"
DOCKER_IMAGE="${DOCKER_IMAGE:-operacle/checkcle-server-agent:latest}"
POCKETBASE_ENABLED="${POCKETBASE_ENABLED:-true}"
log_info "Configuration:"
log_info " Container name: $CONTAINER_NAME"
log_info " Docker image: $DOCKER_IMAGE"
log_info " Health check port: $HEALTH_CHECK_PORT"
log_info " PocketBase enabled: $POCKETBASE_ENABLED"
}
# Stop and remove existing container if it exists
cleanup_existing_container() {
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_info "Stopping and removing existing container: $CONTAINER_NAME"
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
log_success "Existing container removed"
fi
}
# Create Docker volumes
create_volumes() {
log_info "Creating Docker volumes..."
# Create volumes if they don't exist
if ! docker volume ls --format '{{.Name}}' | grep -q "^monitoring_data$"; then
docker volume create monitoring_data
log_success "Created monitoring_data volume"
else
log_info "monitoring_data volume already exists"
fi
if ! docker volume ls --format '{{.Name}}' | grep -q "^monitoring_logs$"; then
docker volume create monitoring_logs
log_success "Created monitoring_logs volume"
else
log_info "monitoring_logs volume already exists"
fi
}
# Get Docker group ID
get_docker_group_id() {
local docker_gid
docker_gid=$(getent group docker | cut -d: -f3)
if [[ -z "$docker_gid" ]]; then
log_warning "Docker group not found, using default GID 999"
docker_gid=999
fi
echo "$docker_gid"
}
# Run the Docker container
run_docker_container() {
log_info "Starting monitoring agent Docker container..."
local docker_gid
docker_gid=$(get_docker_group_id)
log_info "Docker group ID: $docker_gid"
# Run the Docker container with all the required parameters
docker run -d \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-p "$HEALTH_CHECK_PORT:8081" \
--group-add "$docker_gid" \
-e AGENT_ID="$AGENT_ID" \
-e SERVER_NAME="$SERVER_NAME" \
-e SERVER_TOKEN="$SERVER_TOKEN" \
-e POCKETBASE_URL="$POCKETBASE_URL" \
-e POCKETBASE_ENABLED="$POCKETBASE_ENABLED" \
-v /proc:/host/proc:ro \
-v /etc:/host/etc:ro \
-v /sys:/host/sys:ro \
-v /:/host/root:ro \
-v /var/run:/host/var/run:ro \
-v /dev:/host/dev:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v monitoring_data:/var/lib/monitoring-agent \
-v monitoring_logs:/var/log/monitoring-agent \
"$DOCKER_IMAGE"
log_success "Docker container started successfully"
}
# Wait for container to be ready
wait_for_container() {
log_info "Waiting for container to be ready..."
local max_attempts=30
local attempt=0
while [[ $attempt -lt $max_attempts ]]; do
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_success "Container is running"
return 0
fi
sleep 1
((attempt++))
done
log_error "Container failed to start within expected time"
return 1
}
# Test the installation
test_installation() {
log_info "Testing installation..."
# Check container status
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_error "Container is not running"
log_info "Check container logs: docker logs $CONTAINER_NAME"
return 1
fi
# Wait a moment for the service to start
sleep 3
# Test health endpoint
log_info "Testing health endpoint at http://localhost:$HEALTH_CHECK_PORT/health"
if curl -s -f "http://localhost:$HEALTH_CHECK_PORT/health" >/dev/null 2>&1; then
log_success "Health endpoint is responding"
else
log_warning "Health endpoint not responding yet (service may still be starting)"
fi
# Show recent container logs
log_info "Recent container logs:"
docker logs --tail 10 "$CONTAINER_NAME"
}
# Show post-installation information
show_post_install_info() {
echo
echo "============================================="
echo " Docker Installation Complete!"
echo "============================================="
echo
log_success "CheckCle Monitoring Agent Docker container installed and running successfully"
echo
echo "Container Information:"
echo " Container name: $CONTAINER_NAME"
echo " Docker image: $DOCKER_IMAGE"
echo " Health check port: $HEALTH_CHECK_PORT"
echo " Agent ID: $AGENT_ID"
echo " Server name: $SERVER_NAME"
echo
echo "Useful commands:"
echo " Container status: docker ps -f name=$CONTAINER_NAME"
echo " Container logs: docker logs -f $CONTAINER_NAME"
echo " Stop container: docker stop $CONTAINER_NAME"
echo " Start container: docker start $CONTAINER_NAME"
echo " Remove container: docker rm -f $CONTAINER_NAME"
echo " Health check: curl http://localhost:$HEALTH_CHECK_PORT/health"
echo
echo "The monitoring agent is now running in a Docker container and will appear in your dashboard."
echo
}
# Main installation function
main() {
echo "============================================="
echo " CheckCle Server Monitoring Agent"
echo " Docker One-Click Installation"
echo "============================================="
echo
check_root
check_docker
validate_environment
set_defaults
log_info "Starting Docker container installation..."
# Setup and run container
cleanup_existing_container
create_volumes
run_docker_container
if wait_for_container; then
test_installation
show_post_install_info
else
log_error "Container installation failed"
log_info "Check container logs: docker logs $CONTAINER_NAME"
exit 1
fi
}
# Handle script arguments
case "${1:-}" in
--help|-h)
echo "CheckCle Server Monitoring Agent - Docker One-Click Installer"
echo
echo "Usage: SERVER_TOKEN=token POCKETBASE_URL=url SERVER_NAME=name AGENT_ID=id sudo bash $0"
echo
echo "System Requirements:"
echo " - Docker installed and running"
echo " - Root privileges (sudo)"
echo " - Internet access to pull Docker image"
echo
echo "Required Environment Variables:"
echo " SERVER_TOKEN Server authentication token"
echo " POCKETBASE_URL PocketBase URL (e.g., http://194.233.80.126:8090)"
echo " SERVER_NAME Server name (e.g., DB-Server01)"
echo " AGENT_ID Agent identifier"
echo
echo "Optional Environment Variables:"
echo " HEALTH_CHECK_PORT Health check port (default: 8081)"
echo " CONTAINER_NAME Container name (default: monitoring-agent)"
echo " DOCKER_IMAGE Docker image (default: operacle/checkcle-server-agent:latest)"
echo " POCKETBASE_ENABLED Enable PocketBase (default: true)"
echo
echo "Examples:"
echo " SERVER_TOKEN=abc123 POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent sudo -E bash $0"
echo " sudo SERVER_TOKEN=abc123 POCKETBASE_URL=http://host:8090 SERVER_NAME=server AGENT_ID=agent bash $0"
echo
echo "Options:"
echo " --help, -h Show this help message"
echo " --uninstall Remove the monitoring agent container"
echo
exit 0
;;
--uninstall)
check_root
check_docker
set_defaults
log_info "Uninstalling monitoring agent Docker container..."
# Stop and remove container
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
log_success "Container removed"
else
log_info "Container not found"
fi
# Optionally remove volumes (ask user)
echo -n "Remove data volumes? (y/N): "
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
docker volume rm monitoring_data monitoring_logs >/dev/null 2>&1 || true
log_success "Data volumes removed"
else
log_info "Data volumes preserved"
fi
log_success "Monitoring agent Docker container uninstalled"
exit 0
;;
*)
main "$@"
;;
esac