diff --git a/LICENSE.md b/LICENSE.md index 134eb81..3d3d1dc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md index a39ee3c..0cb8ef2 100644 --- a/README.md +++ b/README.md @@ -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 \ @@ -40,7 +47,7 @@ docker run -d \ operacle/checkcle:latest ``` -2. Docker Compose - Recommended +3. Install with Docker compose Configuration. ```bash version: '3.9' @@ -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 diff --git a/application/src/App.tsx b/application/src/App.tsx index 9c79abf..46c461c 100644 --- a/application/src/App.tsx +++ b/application/src/App.tsx @@ -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 }) => { @@ -111,6 +113,16 @@ const App = () => { } /> + + + + } + /> + {/* Public status page route */} + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/application/src/components/dashboard/Sidebar.tsx b/application/src/components/dashboard/Sidebar.tsx index 24b4c6d..f543936 100644 --- a/application/src/components/dashboard/Sidebar.tsx +++ b/application/src/components/dashboard/Sidebar.tsx @@ -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"; @@ -71,10 +70,10 @@ export const Sidebar = ({ {!collapsed && {t("scheduleIncident")}} -
+ {!collapsed && {t("operationalPage")}} -
+
{!collapsed && {t("reports")}} diff --git a/application/src/components/operational-page/ComponentsSelector.tsx b/application/src/components/operational-page/ComponentsSelector.tsx new file mode 100644 index 0000000..e291a9f --- /dev/null +++ b/application/src/components/operational-page/ComponentsSelector.tsx @@ -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[]; + onComponentsChange: (components: Partial[]) => 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 = { + ...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 ( + + + + + Status Page Components + + + Add monitoring components like uptime services, SSL certificates, and incident tracking + + + + {selectedComponents.length > 0 && ( +
+ +
+ {selectedComponents.map((component, index) => ( +
+
+
{component.name}
+ {component.description && ( +
{component.description}
+ )} +
+ {component.service_id && ( + + Service: {services.find(s => s.id === component.service_id)?.name || component.service_id} + + )} + {component.server_id && ( + + Server: {component.server_id} + + )} +
+
+ +
+ ))} +
+
+ )} + + {!showAddForm ? ( + + ) : ( +
+
+
+ + setNewComponent({ ...newComponent, name: e.target.value })} + /> +
+
+ + setNewComponent({ ...newComponent, display_order: parseInt(e.target.value) || 1 })} + /> +
+
+ +
+ +