Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025, Tola Leng
MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
Copyright (c) 2025 Tola Leng

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ 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)

### Installation with Docker Run and Compose
1. Copy ready docker run command
### Install CheckCle using one of the options below.


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 \
Expand All @@ -40,7 +47,7 @@ docker run -d \
operacle/checkcle:latest

```
2. Docker Compose - Recommended
3. Install with Docker compose Configuration.
```bash

version: '3.9'
Expand Down Expand Up @@ -87,7 +94,7 @@ services:
- ✅ Incident Management
- [ ] Uptime monitoring (PING - Inprogress)
- [ ] Infrastructure Server Monitoring
- [ ] Operational Status / Public Status Pages
- Operational Status / Public Status Pages
- [ ] Uptime monitoring (TCP, PING, DNS)
- ✅ System Setting Panel and Mail Settings
- ✅ User Permission Roles
Expand Down
12 changes: 12 additions & 0 deletions application/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import Profile from "./pages/Profile";
import NotFound from "./pages/NotFound";
import SslDomain from "./pages/SslDomain";
import ScheduleIncident from "./pages/ScheduleIncident";
import OperationalPage from "./pages/OperationalPage";
import PublicStatusPage from "./pages/PublicStatusPage";

// Create a Protected route component
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
Expand Down Expand Up @@ -111,6 +113,16 @@ const App = () => {
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
{/* Public status page route */}
<Route path="/status/:slug" element={<PublicStatusPage />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
Expand Down
5 changes: 2 additions & 3 deletions application/src/components/dashboard/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, FileText, Settings, User, UserCog, Bell, FileClock, Database, RefreshCw, Info, ChevronDown, BookOpen } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { Link, useLocation } from "react-router-dom";
Expand Down Expand Up @@ -71,10 +70,10 @@ export const Sidebar = ({
<Calendar className={`${mainIconSize} text-emerald-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("scheduleIncident")}</span>}
</Link>
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<Link to="/operational-page" className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${location.pathname === '/operational-page' ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<BarChart2 className={`${mainIconSize} text-amber-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("operationalPage")}</span>}
</div>
</Link>
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<LineChart className={`${mainIconSize} text-rose-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("reports")}</span>}
Expand Down
243 changes: 243 additions & 0 deletions application/src/components/operational-page/ComponentsSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
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 { 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 { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
import { useQuery } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';

interface ComponentsSelectorProps {
selectedComponents: Partial<StatusPageComponentRecord>[];
onComponentsChange: (components: Partial<StatusPageComponentRecord>[]) => void;
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({
name: '',
description: '',
service_id: '',
server_id: '',
display_order: selectedComponents.length + 1,
});

// Fetch uptime services for the dropdown
const { data: services = [] } = useQuery({
queryKey: ['services'],
queryFn: serviceService.getServices,
});

const addComponent = () => {
if (!newComponent.name.trim()) return;

const component: Partial<StatusPageComponentRecord> = {
...newComponent,
operational_status_id: '', // Will be set when page is created
};

onComponentsChange([...selectedComponents, component]);
setNewComponent({
name: '',
description: '',
service_id: '',
server_id: '',
display_order: selectedComponents.length + 2,
});
setShowAddForm(false);
};

const removeComponent = async (index: number) => {
const component = selectedComponents[index];

// If component has an ID, it exists in database and needs to be deleted
if (component.id && onComponentDelete) {
await onComponentDelete(component.id);
} else {
// For new components not yet saved, just remove from local state
const updated = selectedComponents.filter((_, i) => i !== index);
onComponentsChange(updated);
}
};

return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
Status Page Components
</CardTitle>
<CardDescription>
Add monitoring components like uptime services, SSL certificates, and incident tracking
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedComponents.length > 0 && (
<div className="space-y-2">
<Label>Selected Components</Label>
<div className="space-y-2">
{selectedComponents.map((component, index) => (
<div key={component.id || index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex-1">
<div className="font-medium">{component.name}</div>
{component.description && (
<div className="text-sm text-muted-foreground">{component.description}</div>
)}
<div className="flex gap-2 mt-1">
{component.service_id && (
<Badge variant="secondary" className="text-xs">
Service: {services.find(s => s.id === component.service_id)?.name || component.service_id}
</Badge>
)}
{component.server_id && (
<Badge variant="secondary" className="text-xs">
Server: {component.server_id}
</Badge>
)}
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeComponent(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}

{!showAddForm ? (
<Button
variant="outline"
onClick={() => setShowAddForm(true)}
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
Add Component
</Button>
) : (
<div className="border rounded-lg p-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="component-name">Component Name</Label>
<Input
id="component-name"
placeholder="e.g., Main Website"
value={newComponent.name}
onChange={(e) => setNewComponent({ ...newComponent, name: e.target.value })}
/>
</div>
<div>
<Label htmlFor="display-order">Display Order</Label>
<Input
id="display-order"
type="number"
value={newComponent.display_order}
onChange={(e) => setNewComponent({ ...newComponent, display_order: parseInt(e.target.value) || 1 })}
/>
</div>
</div>

<div>
<Label htmlFor="component-description">Description (Optional)</Label>
<Textarea
id="component-description"
placeholder="Brief description of this component"
value={newComponent.description}
onChange={(e) => setNewComponent({ ...newComponent, description: e.target.value })}
/>
</div>

<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="service-id">Uptime Service (Optional)</Label>
<Select onValueChange={(value) => setNewComponent({ ...newComponent, service_id: value })}>
<SelectTrigger>
<SelectValue placeholder="Select an uptime service" />
</SelectTrigger>
<SelectContent className="z-50 bg-white border shadow-lg">
{services.map((service) => (
<SelectItem key={service.id} value={service.id}>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
service.status === 'up' ? 'bg-green-500' :
service.status === 'down' ? 'bg-red-500' :
'bg-yellow-500'
}`} />
{service.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="server-id">Server ID (Optional)</Label>
<Input
id="server-id"
placeholder="server_456"
value={newComponent.server_id}
onChange={(e) => setNewComponent({ ...newComponent, server_id: e.target.value })}
/>
</div>
</div>

<div className="flex gap-2">
<Button onClick={addComponent} disabled={!newComponent.name.trim()}>
Add Component
</Button>
<Button variant="outline" onClick={() => setShowAddForm(false)}>
Cancel
</Button>
</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>
);
};
Loading