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
31 changes: 18 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ CheckCle is an Open Source solution for seamless, real-time monitoring of full-s

## #️⃣ Getting Started

### Current Architecture Support
* ✅ 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
```bash
Expand Down Expand Up @@ -71,24 +75,25 @@ services:

## 📝 Development Roadmap

- [x] Health check & uptime monitoring (HTTP)
- [x] Dashboard UI with live stats
- [x] Auth with Multi-users system (admin)
- [x] Notifications (Telegram)
- [x] Docker containerization
- [x] CheckCle Website
- [x] CheckCle Demo Server
- [x] SSL & Domain Monitoring
- [x] Schedule Maintenance
- [x] Incident Management
- 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
- [ ] Uptime monitoring (PING - Inprogress)
- [ ] Infrastructure Server Monitoring
- [ ] Operational Status / Public Status Pages
- [ ] Uptime monitoring (TCP, PING, DNS)
- [x] System Setting Panel and Mail Settings
- [x] User Permission Roles
- System Setting Panel and Mail Settings
- User Permission Roles
- [ ] Notifications (Email/Slack/Discord/Signal)
- [x] Open-source release with full documentation
- ✅ 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
Expand Down
4 changes: 2 additions & 2 deletions application/src/components/dashboard/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ export const Sidebar = ({
<BookOpen className="h-4 w-4 mr-2" />
<span className="text-sm">{t("alertsTemplates")}</span>
</Link>
<div className={getMenuItemClasses(false)}>
<Link to={`/settings?panel=data-retention`} className={getMenuItemClasses(activeSettingsItem === 'data-retention')} onClick={() => handleSettingsItemClick('data-retention')}>
<Database className="h-4 w-4 mr-2" />
<span className="text-sm">{t("dataRetention")}</span>
</div>
</Link>
<Link to={`/settings?panel=about`} className={getMenuItemClasses(activeSettingsItem === 'about')} onClick={() => handleSettingsItemClick('about')}>
<Info className="h-4 w-4 mr-2" />
<span className="text-sm">{t("aboutSystem")}</span>
Expand Down
38 changes: 17 additions & 21 deletions application/src/components/profile/UpdateProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useToast } from "@/hooks/use-toast";
import { authService } from "@/services/authService";
import { AlertCircle, CheckCircle } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useNavigate } from "react-router-dom";

// Profile update form schema
const profileFormSchema = z.object({
Expand All @@ -33,10 +34,10 @@ interface UpdateProfileFormProps {

export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [emailChangeRequested, setEmailChangeRequested] = useState(false);
const [updateError, setUpdateError] = useState<string | null>(null);
const [updateSuccess, setUpdateSuccess] = useState<string | null>(null);
const { toast } = useToast();
const navigate = useNavigate();

// Initialize the form with current user data
const form = useForm<ProfileFormValues>({
Expand All @@ -52,7 +53,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
setIsSubmitting(true);
setUpdateError(null);
setUpdateSuccess(null);
setEmailChangeRequested(false);

try {
console.log("Submitting profile update with data:", data);
Expand All @@ -75,19 +75,25 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
// Update user data using the userService
await userService.updateUser(user.id, updateData);

// Refresh user data in auth context
await authService.refreshUserData();

// If email was changed, show a specific message
// If email was changed, show success message and auto-logout
if (isEmailChanged) {
setEmailChangeRequested(true);
setUpdateSuccess("A verification email has been sent to your new email address. Please check your inbox to complete the change.");
setUpdateSuccess("Email changed successfully! You will be logged out for security reasons. Please log in again with your new email.");

toast({
title: "Email verification sent",
description: "A verification email has been sent to your new email address. Please check your inbox and follow the instructions to complete the change.",
title: "Email changed successfully",
description: "You will be logged out for security reasons. Please log in again with your new email.",
variant: "default",
});

// Auto-logout after 3 seconds
setTimeout(() => {
authService.logout();
navigate("/login");
}, 3000);
} else {
// Refresh user data in auth context for other field changes
await authService.refreshUserData();

setUpdateSuccess("Your profile information has been updated successfully.");
toast({
title: "Profile updated",
Expand Down Expand Up @@ -132,16 +138,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
</AlertDescription>
</Alert>
)}

{emailChangeRequested && (
<Alert className="bg-yellow-50 border-yellow-200 text-yellow-800">
<AlertCircle className="h-4 w-4 text-yellow-600" />
<AlertDescription>
A verification email has been sent to your new email address.
Please check your inbox and follow the instructions to complete the change.
</AlertDescription>
</Alert>
)}

<FormField
control={form.control}
Expand Down Expand Up @@ -183,7 +179,7 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
<FormMessage />
{field.value !== user.email && (
<p className="text-xs text-muted-foreground mt-1">
Changing your email requires verification. A verification email will be sent.
Changing your email will log you out for security reasons. You will need to log in again with your new email.
</p>
)}
</FormItem>
Expand Down
53 changes: 21 additions & 32 deletions application/src/components/services/DateRangeFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { format, subDays, subHours, subMonths, subWeeks, subYears } from "date-fns";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils";

export type DateRangeOption = '60min' | '24h' | '7d' | '30d' | '1y' | 'custom';
export type DateRangeOption = '24h' | '7d' | '30d' | '1y' | 'custom';

interface DateRangeFilterProps {
onRangeChange: (startDate: Date, endDate: Date, option: DateRangeOption) => void;
selectedOption?: DateRangeOption;
}

// Define a proper type for the date range
interface DateRange {
from: Date | undefined;
to: Date | undefined;
Expand All @@ -45,57 +44,48 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR

const now = new Date();
let startDate: Date;
let endDate: Date = new Date(now.getTime() + (5 * 60 * 1000)); // Add 5 minutes buffer to future

switch (option) {
case '60min':
// Ensure we're getting exactly 60 minutes ago
startDate = new Date(now.getTime() - 60 * 60 * 1000);
console.log(`60min option selected: ${startDate.toISOString()} to ${now.toISOString()}`);
break;
case '24h':
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
startDate = new Date(now.getTime() - (24 * 60 * 60 * 1000));
break;
case '7d':
startDate = subDays(now, 7);
startDate = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
break;
case '30d':
startDate = subDays(now, 30);
startDate = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
break;
case '1y':
startDate = subYears(now, 1);
startDate = new Date(now.getTime() - (365 * 24 * 60 * 60 * 1000));
break;
case 'custom':
// Don't trigger onRangeChange for custom until both dates are selected
setIsCalendarOpen(true);
return;
default:
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
startDate = new Date(now.getTime() - (24 * 60 * 60 * 1000)); // Default to 24 hours
}

console.log(`DateRangeFilter: Option changed to ${option}, date range: ${startDate.toISOString()} to ${now.toISOString()}`);
onRangeChange(startDate, now, option);
console.log(`DateRangeFilter: ${option} selected, range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
onRangeChange(startDate, endDate, option);
};

// Handle custom date range selection
const handleCustomRangeSelect = (range: DateRange | undefined) => {
if (!range) {
if (!range || !range.from || !range.to) {
return;
}

setCustomDateRange(range);

if (range.from && range.to) {
// Ensure that we have both from and to dates before triggering the change
const startOfDay = new Date(range.from);
startOfDay.setHours(0, 0, 0, 0);

const endOfDay = new Date(range.to);
endOfDay.setHours(23, 59, 59, 999);

console.log(`DateRangeFilter: Custom range selected: ${startOfDay.toISOString()} to ${endOfDay.toISOString()}`);
onRangeChange(startOfDay, endOfDay, 'custom');
setIsCalendarOpen(false);
}
const startOfDay = new Date(range.from);
startOfDay.setHours(0, 0, 0, 0);

const endOfDay = new Date(range.to);
endOfDay.setHours(23, 59, 59, 999);

console.log(`Custom range: ${startOfDay.toISOString()} to ${endOfDay.toISOString()}`);
onRangeChange(startOfDay, endOfDay, 'custom');
setIsCalendarOpen(false);
};

return (
Expand All @@ -105,7 +95,6 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR
<SelectValue placeholder="Select time range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="60min">Last 60 minutes</SelectItem>
<SelectItem value="24h">Last 24 hours</SelectItem>
<SelectItem value="7d">Last 7 days</SelectItem>
<SelectItem value="30d">Last 30 days</SelectItem>
Expand Down Expand Up @@ -153,4 +142,4 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR
)}
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Service, UptimeData } from "@/types/service.types";
import { useToast } from "@/hooks/use-toast";
import { useNavigate } from "react-router-dom";
import { uptimeService } from "@/services/uptimeService";
import { DateRangeOption } from "../../DateRangeFilter";

export const useServiceData = (serviceId: string | undefined, startDate: Date, endDate: Date) => {
const [service, setService] = useState<Service | null>(null);
Expand All @@ -13,15 +14,12 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
const { toast } = useToast();
const navigate = useNavigate();

// Handler for service status changes
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
if (!service || !serviceId) return;

try {
// Optimistic UI update
setService({ ...service, status: newStatus as Service["status"] });

// Update the service status in PocketBase
await pb.collection('services').update(serviceId, {
status: newStatus
});
Expand All @@ -32,7 +30,6 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
});
} catch (error) {
console.error("Failed to update service status:", error);
// Revert the optimistic update
setService(prevService => prevService);

toast({
Expand All @@ -43,51 +40,30 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
}
};

// Function to fetch uptime data with date filters
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange: string) => {
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string) => {
try {
console.log(`Fetching uptime data from ${start.toISOString()} to ${end.toISOString()}`);
console.log(`Fetching uptime data: ${start.toISOString()} to ${end.toISOString()} for range: ${selectedRange}`);

// Set appropriate limits based on time range to ensure enough granularity
let limit = 200; // default
let limit = 500; // Default limit

// Adjust limits based on selected range
if (selectedRange === '60min') {
limit = 300; // More points for shorter time ranges
} else if (selectedRange === '24h') {
limit = 200;
if (selectedRange === '24h') {
limit = 300;
} else if (selectedRange === '7d') {
limit = 250;
} else if (selectedRange === '30d' || selectedRange === '1y') {
limit = 300; // More points for longer time ranges
limit = 400;
}

console.log(`Using limit ${limit} for range ${selectedRange}`);

const history = await uptimeService.getUptimeHistory(serviceId, limit, start, end);
console.log(`Fetched ${history.length} uptime records for time range ${selectedRange}`);
console.log(`Retrieved ${history.length} uptime records`);

if (history.length === 0) {
console.log("No data returned from API, checking if we need to fetch with a higher limit");
// If no data, try with a higher limit as fallback
if (limit < 500) {
const extendedHistory = await uptimeService.getUptimeHistory(serviceId, 500, start, end);
console.log(`Fallback: Fetched ${extendedHistory.length} uptime records with higher limit`);

if (extendedHistory.length > 0) {
setUptimeData(extendedHistory);
return extendedHistory;
}
}
}

// Sort data by timestamp (newest first)
const sortedHistory = [...history].sort((a, b) =>
// Sort by timestamp (newest first)
const filteredHistory = [...history].sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);

setUptimeData(sortedHistory);
return sortedHistory;
setUptimeData(filteredHistory);
return filteredHistory;
} catch (error) {
console.error("Error fetching uptime data:", error);
toast({
Expand All @@ -110,7 +86,6 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e

setIsLoading(true);

// Add a timeout to prevent hanging
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timed out")), 10000);
});
Expand All @@ -136,7 +111,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e

setService(formattedService);

// Fetch uptime history with date range
// Fetch initial uptime history with 24h default
await fetchUptimeData(serviceId, startDate, endDate, '24h');
} catch (error) {
console.error("Error fetching service:", error);
Expand All @@ -156,10 +131,11 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e

// Update data when date range changes
useEffect(() => {
if (serviceId && !isLoading) {
fetchUptimeData(serviceId, startDate, endDate, '24h');
if (serviceId && !isLoading && service) {
console.log(`Date range changed, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
fetchUptimeData(serviceId, startDate, endDate);
}
}, [startDate, endDate]);
}, [startDate, endDate, serviceId, isLoading, service]);

return {
service,
Expand All @@ -170,4 +146,4 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
handleStatusChange,
fetchUptimeData
};
};
};
Loading