Merge pull request #27 from operacle/develop
feat: Implement uptime monitoring data retention
This commit is contained in:
@@ -117,10 +117,10 @@ export const Sidebar = ({
|
|||||||
<BookOpen className="h-4 w-4 mr-2" />
|
<BookOpen className="h-4 w-4 mr-2" />
|
||||||
<span className="text-sm">{t("alertsTemplates")}</span>
|
<span className="text-sm">{t("alertsTemplates")}</span>
|
||||||
</Link>
|
</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" />
|
<Database className="h-4 w-4 mr-2" />
|
||||||
<span className="text-sm">{t("dataRetention")}</span>
|
<span className="text-sm">{t("dataRetention")}</span>
|
||||||
</div>
|
</Link>
|
||||||
<Link to={`/settings?panel=about`} className={getMenuItemClasses(activeSettingsItem === 'about')} onClick={() => handleSettingsItemClick('about')}>
|
<Link to={`/settings?panel=about`} className={getMenuItemClasses(activeSettingsItem === 'about')} onClick={() => handleSettingsItemClick('about')}>
|
||||||
<Info className="h-4 w-4 mr-2" />
|
<Info className="h-4 w-4 mr-2" />
|
||||||
<span className="text-sm">{t("aboutSystem")}</span>
|
<span className="text-sm">{t("aboutSystem")}</span>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { authService } from "@/services/authService";
|
import { authService } from "@/services/authService";
|
||||||
import { AlertCircle, CheckCircle } from "lucide-react";
|
import { AlertCircle, CheckCircle } from "lucide-react";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
// Profile update form schema
|
// Profile update form schema
|
||||||
const profileFormSchema = z.object({
|
const profileFormSchema = z.object({
|
||||||
@@ -33,10 +34,10 @@ interface UpdateProfileFormProps {
|
|||||||
|
|
||||||
export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [emailChangeRequested, setEmailChangeRequested] = useState(false);
|
|
||||||
const [updateError, setUpdateError] = useState<string | null>(null);
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
const [updateSuccess, setUpdateSuccess] = useState<string | null>(null);
|
const [updateSuccess, setUpdateSuccess] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Initialize the form with current user data
|
// Initialize the form with current user data
|
||||||
const form = useForm<ProfileFormValues>({
|
const form = useForm<ProfileFormValues>({
|
||||||
@@ -52,7 +53,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setUpdateError(null);
|
setUpdateError(null);
|
||||||
setUpdateSuccess(null);
|
setUpdateSuccess(null);
|
||||||
setEmailChangeRequested(false);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("Submitting profile update with data:", data);
|
console.log("Submitting profile update with data:", data);
|
||||||
@@ -75,19 +75,25 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
// Update user data using the userService
|
// Update user data using the userService
|
||||||
await userService.updateUser(user.id, updateData);
|
await userService.updateUser(user.id, updateData);
|
||||||
|
|
||||||
// Refresh user data in auth context
|
// If email was changed, show success message and auto-logout
|
||||||
await authService.refreshUserData();
|
|
||||||
|
|
||||||
// If email was changed, show a specific message
|
|
||||||
if (isEmailChanged) {
|
if (isEmailChanged) {
|
||||||
setEmailChangeRequested(true);
|
setUpdateSuccess("Email changed successfully! You will be logged out for security reasons. Please log in again with your new email.");
|
||||||
setUpdateSuccess("A verification email has been sent to your new email address. Please check your inbox to complete the change.");
|
|
||||||
toast({
|
toast({
|
||||||
title: "Email verification sent",
|
title: "Email changed successfully",
|
||||||
description: "A verification email has been sent to your new email address. Please check your inbox and follow the instructions to complete the change.",
|
description: "You will be logged out for security reasons. Please log in again with your new email.",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-logout after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
authService.logout();
|
||||||
|
navigate("/login");
|
||||||
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
|
// Refresh user data in auth context for other field changes
|
||||||
|
await authService.refreshUserData();
|
||||||
|
|
||||||
setUpdateSuccess("Your profile information has been updated successfully.");
|
setUpdateSuccess("Your profile information has been updated successfully.");
|
||||||
toast({
|
toast({
|
||||||
title: "Profile updated",
|
title: "Profile updated",
|
||||||
@@ -133,16 +139,6 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
</Alert>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="full_name"
|
name="full_name"
|
||||||
@@ -183,7 +179,7 @@ export function UpdateProfileForm({ user }: UpdateProfileFormProps) {
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
{field.value !== user.email && (
|
{field.value !== user.email && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -14,18 +14,17 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 { Calendar as CalendarIcon } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type DateRangeOption = '60min' | '24h' | '7d' | '30d' | '1y' | 'custom';
|
export type DateRangeOption = '24h' | '7d' | '30d' | '1y' | 'custom';
|
||||||
|
|
||||||
interface DateRangeFilterProps {
|
interface DateRangeFilterProps {
|
||||||
onRangeChange: (startDate: Date, endDate: Date, option: DateRangeOption) => void;
|
onRangeChange: (startDate: Date, endDate: Date, option: DateRangeOption) => void;
|
||||||
selectedOption?: DateRangeOption;
|
selectedOption?: DateRangeOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a proper type for the date range
|
|
||||||
interface DateRange {
|
interface DateRange {
|
||||||
from: Date | undefined;
|
from: Date | undefined;
|
||||||
to: Date | undefined;
|
to: Date | undefined;
|
||||||
@@ -45,57 +44,48 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR
|
|||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let startDate: Date;
|
let startDate: Date;
|
||||||
|
let endDate: Date = new Date(now.getTime() + (5 * 60 * 1000)); // Add 5 minutes buffer to future
|
||||||
|
|
||||||
switch (option) {
|
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':
|
case '24h':
|
||||||
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
startDate = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||||
break;
|
break;
|
||||||
case '7d':
|
case '7d':
|
||||||
startDate = subDays(now, 7);
|
startDate = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
|
||||||
break;
|
break;
|
||||||
case '30d':
|
case '30d':
|
||||||
startDate = subDays(now, 30);
|
startDate = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||||
break;
|
break;
|
||||||
case '1y':
|
case '1y':
|
||||||
startDate = subYears(now, 1);
|
startDate = new Date(now.getTime() - (365 * 24 * 60 * 60 * 1000));
|
||||||
break;
|
break;
|
||||||
case 'custom':
|
case 'custom':
|
||||||
// Don't trigger onRangeChange for custom until both dates are selected
|
|
||||||
setIsCalendarOpen(true);
|
setIsCalendarOpen(true);
|
||||||
return;
|
return;
|
||||||
default:
|
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()}`);
|
console.log(`DateRangeFilter: ${option} selected, range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
||||||
onRangeChange(startDate, now, option);
|
onRangeChange(startDate, endDate, option);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle custom date range selection
|
|
||||||
const handleCustomRangeSelect = (range: DateRange | undefined) => {
|
const handleCustomRangeSelect = (range: DateRange | undefined) => {
|
||||||
if (!range) {
|
if (!range || !range.from || !range.to) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCustomDateRange(range);
|
setCustomDateRange(range);
|
||||||
|
|
||||||
if (range.from && range.to) {
|
const startOfDay = new Date(range.from);
|
||||||
// Ensure that we have both from and to dates before triggering the change
|
startOfDay.setHours(0, 0, 0, 0);
|
||||||
const startOfDay = new Date(range.from);
|
|
||||||
startOfDay.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
const endOfDay = new Date(range.to);
|
const endOfDay = new Date(range.to);
|
||||||
endOfDay.setHours(23, 59, 59, 999);
|
endOfDay.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
console.log(`DateRangeFilter: Custom range selected: ${startOfDay.toISOString()} to ${endOfDay.toISOString()}`);
|
console.log(`Custom range: ${startOfDay.toISOString()} to ${endOfDay.toISOString()}`);
|
||||||
onRangeChange(startOfDay, endOfDay, 'custom');
|
onRangeChange(startOfDay, endOfDay, 'custom');
|
||||||
setIsCalendarOpen(false);
|
setIsCalendarOpen(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -105,7 +95,6 @@ export function DateRangeFilter({ onRangeChange, selectedOption = '24h' }: DateR
|
|||||||
<SelectValue placeholder="Select time range" />
|
<SelectValue placeholder="Select time range" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="60min">Last 60 minutes</SelectItem>
|
|
||||||
<SelectItem value="24h">Last 24 hours</SelectItem>
|
<SelectItem value="24h">Last 24 hours</SelectItem>
|
||||||
<SelectItem value="7d">Last 7 days</SelectItem>
|
<SelectItem value="7d">Last 7 days</SelectItem>
|
||||||
<SelectItem value="30d">Last 30 days</SelectItem>
|
<SelectItem value="30d">Last 30 days</SelectItem>
|
||||||
|
|||||||
+17
-41
@@ -5,6 +5,7 @@ import { Service, UptimeData } from "@/types/service.types";
|
|||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { uptimeService } from "@/services/uptimeService";
|
import { uptimeService } from "@/services/uptimeService";
|
||||||
|
import { DateRangeOption } from "../../DateRangeFilter";
|
||||||
|
|
||||||
export const useServiceData = (serviceId: string | undefined, startDate: Date, endDate: Date) => {
|
export const useServiceData = (serviceId: string | undefined, startDate: Date, endDate: Date) => {
|
||||||
const [service, setService] = useState<Service | null>(null);
|
const [service, setService] = useState<Service | null>(null);
|
||||||
@@ -13,15 +14,12 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Handler for service status changes
|
|
||||||
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
|
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
|
||||||
if (!service || !serviceId) return;
|
if (!service || !serviceId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimistic UI update
|
|
||||||
setService({ ...service, status: newStatus as Service["status"] });
|
setService({ ...service, status: newStatus as Service["status"] });
|
||||||
|
|
||||||
// Update the service status in PocketBase
|
|
||||||
await pb.collection('services').update(serviceId, {
|
await pb.collection('services').update(serviceId, {
|
||||||
status: newStatus
|
status: newStatus
|
||||||
});
|
});
|
||||||
@@ -32,7 +30,6 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update service status:", error);
|
console.error("Failed to update service status:", error);
|
||||||
// Revert the optimistic update
|
|
||||||
setService(prevService => prevService);
|
setService(prevService => prevService);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -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?: DateRangeOption | string) => {
|
||||||
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange: string) => {
|
|
||||||
try {
|
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 = 500; // Default limit
|
||||||
let limit = 200; // default
|
|
||||||
|
|
||||||
// Adjust limits based on selected range
|
if (selectedRange === '24h') {
|
||||||
if (selectedRange === '60min') {
|
limit = 300;
|
||||||
limit = 300; // More points for shorter time ranges
|
|
||||||
} else if (selectedRange === '24h') {
|
|
||||||
limit = 200;
|
|
||||||
} else if (selectedRange === '7d') {
|
} else if (selectedRange === '7d') {
|
||||||
limit = 250;
|
limit = 400;
|
||||||
} else if (selectedRange === '30d' || selectedRange === '1y') {
|
|
||||||
limit = 300; // More points for longer time ranges
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Using limit ${limit} for range ${selectedRange}`);
|
console.log(`Using limit ${limit} for range ${selectedRange}`);
|
||||||
|
|
||||||
const history = await uptimeService.getUptimeHistory(serviceId, limit, start, end);
|
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) {
|
// Sort by timestamp (newest first)
|
||||||
console.log("No data returned from API, checking if we need to fetch with a higher limit");
|
const filteredHistory = [...history].sort((a, b) =>
|
||||||
// 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) =>
|
|
||||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
setUptimeData(sortedHistory);
|
setUptimeData(filteredHistory);
|
||||||
return sortedHistory;
|
return filteredHistory;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching uptime data:", error);
|
console.error("Error fetching uptime data:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -110,7 +86,6 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Add a timeout to prevent hanging
|
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
setTimeout(() => reject(new Error("Request timed out")), 10000);
|
setTimeout(() => reject(new Error("Request timed out")), 10000);
|
||||||
});
|
});
|
||||||
@@ -136,7 +111,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
|
|
||||||
setService(formattedService);
|
setService(formattedService);
|
||||||
|
|
||||||
// Fetch uptime history with date range
|
// Fetch initial uptime history with 24h default
|
||||||
await fetchUptimeData(serviceId, startDate, endDate, '24h');
|
await fetchUptimeData(serviceId, startDate, endDate, '24h');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching service:", error);
|
console.error("Error fetching service:", error);
|
||||||
@@ -156,10 +131,11 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
|
|
||||||
// Update data when date range changes
|
// Update data when date range changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serviceId && !isLoading) {
|
if (serviceId && !isLoading && service) {
|
||||||
fetchUptimeData(serviceId, startDate, endDate, '24h');
|
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 {
|
return {
|
||||||
service,
|
service,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { DateRangeOption } from "../DateRangeFilter";
|
import { DateRangeOption } from "../DateRangeFilter";
|
||||||
@@ -12,13 +11,17 @@ export const ServiceDetailContainer = () => {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Ensure we use exact timestamp for startDate
|
// Set default to 24h
|
||||||
const [startDate, setStartDate] = useState<Date>(() => {
|
const [startDate, setStartDate] = useState<Date>(() => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setHours(date.getHours() - 24);
|
date.setHours(date.getHours() - 24); // Go back 24 hours
|
||||||
|
return date;
|
||||||
|
});
|
||||||
|
const [endDate, setEndDate] = useState<Date>(() => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setMinutes(date.getMinutes() + 5); // Add 5 minutes buffer to future
|
||||||
return date;
|
return date;
|
||||||
});
|
});
|
||||||
const [endDate, setEndDate] = useState<Date>(new Date());
|
|
||||||
const [selectedRange, setSelectedRange] = useState<DateRangeOption>('24h');
|
const [selectedRange, setSelectedRange] = useState<DateRangeOption>('24h');
|
||||||
|
|
||||||
// State for sidebar collapse functionality (shared with Dashboard)
|
// State for sidebar collapse functionality (shared with Dashboard)
|
||||||
@@ -91,14 +94,16 @@ export const ServiceDetailContainer = () => {
|
|||||||
|
|
||||||
// Handle date range filter changes
|
// Handle date range filter changes
|
||||||
const handleDateRangeChange = useCallback((start: Date, end: Date, option: DateRangeOption) => {
|
const handleDateRangeChange = useCallback((start: Date, end: Date, option: DateRangeOption) => {
|
||||||
console.log(`Date range changed: ${start.toISOString()} to ${end.toISOString()}, option: ${option}`);
|
console.log(`ServiceDetailContainer: Date range changed: ${start.toISOString()} to ${end.toISOString()}, option: ${option}`);
|
||||||
|
|
||||||
|
// Update state which will trigger the useEffect in useServiceData
|
||||||
setStartDate(start);
|
setStartDate(start);
|
||||||
setEndDate(end);
|
setEndDate(end);
|
||||||
setSelectedRange(option);
|
setSelectedRange(option);
|
||||||
|
|
||||||
// Refetch uptime data with new date range, passing the selected range option
|
// Also explicitly fetch data with the new range to ensure immediate update
|
||||||
if (id) {
|
if (id) {
|
||||||
|
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
|
||||||
fetchUptimeData(id, start, end, option);
|
fetchUptimeData(id, start, end, option);
|
||||||
}
|
}
|
||||||
}, [id, fetchUptimeData]);
|
}, [id, fetchUptimeData]);
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ export const ServiceRow = ({
|
|||||||
<ServiceRowResponseTime responseTime={service.responseTime} />
|
<ServiceRowResponseTime responseTime={service.responseTime} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="w-52 py-4">
|
<TableCell className="w-52 py-4">
|
||||||
<UptimeBar uptime={service.uptime} status={service.status} serviceId={service.id} />
|
<UptimeBar
|
||||||
|
uptime={service.uptime}
|
||||||
|
status={service.status}
|
||||||
|
serviceId={service.id}
|
||||||
|
interval={service.interval}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="py-4">
|
<TableCell className="py-4">
|
||||||
<LastCheckedTime
|
<LastCheckedTime
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Check, X, AlertTriangle, Pause, Clock, Info, RefreshCcw } from "lucide-react";
|
import { Check, X, AlertTriangle, Pause, Clock, Info, RefreshCcw } from "lucide-react";
|
||||||
@@ -17,17 +16,18 @@ import { Button } from "@/components/ui/button";
|
|||||||
interface UptimeBarProps {
|
interface UptimeBarProps {
|
||||||
uptime: number;
|
uptime: number;
|
||||||
status: string;
|
status: string;
|
||||||
serviceId?: string; // Optional serviceId to fetch history
|
serviceId?: string;
|
||||||
|
interval?: number; // Service monitoring interval in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
|
export const UptimeBar = ({ uptime, status, serviceId, interval = 60 }: UptimeBarProps) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
||||||
|
|
||||||
// Fetch real uptime history data if serviceId is provided with improved caching and error handling
|
// Fetch real uptime history data if serviceId is provided with improved caching and error handling
|
||||||
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
||||||
queryKey: ['uptimeHistory', serviceId],
|
queryKey: ['uptimeHistory', serviceId],
|
||||||
queryFn: () => serviceId ? uptimeService.getUptimeHistory(serviceId, 20) : Promise.resolve([]),
|
queryFn: () => serviceId ? uptimeService.getUptimeHistory(serviceId, 50) : Promise.resolve([]),
|
||||||
enabled: !!serviceId,
|
enabled: !!serviceId,
|
||||||
refetchInterval: 30000, // Refresh every 30 seconds
|
refetchInterval: 30000, // Refresh every 30 seconds
|
||||||
staleTime: 15000, // Consider data fresh for 15 seconds
|
staleTime: 15000, // Consider data fresh for 15 seconds
|
||||||
@@ -36,10 +36,45 @@ export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
|
|||||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // Exponential backoff with max 10s
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // Exponential backoff with max 10s
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filter uptime data to respect the service interval
|
||||||
|
const filterUptimeDataByInterval = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
|
||||||
|
if (!data || data.length === 0) return [];
|
||||||
|
|
||||||
|
// Sort data by timestamp (newest first)
|
||||||
|
const sortedData = [...data].sort((a, b) =>
|
||||||
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
const filtered: UptimeData[] = [];
|
||||||
|
let lastIncludedTime: number | null = null;
|
||||||
|
const intervalMs = intervalSeconds * 1000; // Convert to milliseconds
|
||||||
|
|
||||||
|
// Include the most recent record first
|
||||||
|
if (sortedData.length > 0) {
|
||||||
|
filtered.push(sortedData[0]);
|
||||||
|
lastIncludedTime = new Date(sortedData[0].timestamp).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter subsequent records to maintain proper interval spacing
|
||||||
|
for (let i = 1; i < sortedData.length && filtered.length < 20; i++) {
|
||||||
|
const currentTime = new Date(sortedData[i].timestamp).getTime();
|
||||||
|
|
||||||
|
// Only include if enough time has passed since the last included record
|
||||||
|
if (lastIncludedTime && (lastIncludedTime - currentTime) >= intervalMs) {
|
||||||
|
filtered.push(sortedData[i]);
|
||||||
|
lastIncludedTime = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
};
|
||||||
|
|
||||||
// Update history items when data changes
|
// Update history items when data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uptimeData && uptimeData.length > 0) {
|
if (uptimeData && uptimeData.length > 0) {
|
||||||
setHistoryItems(uptimeData);
|
// Filter data based on the service interval
|
||||||
|
const filteredData = filterUptimeDataByInterval(uptimeData, interval);
|
||||||
|
setHistoryItems(filteredData);
|
||||||
} else if (status === "paused" || (uptimeData && uptimeData.length === 0)) {
|
} else if (status === "paused" || (uptimeData && uptimeData.length === 0)) {
|
||||||
// For paused services with no history, or empty history data, show all as paused
|
// For paused services with no history, or empty history data, show all as paused
|
||||||
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
||||||
@@ -49,13 +84,13 @@ export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
|
|||||||
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
|
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
|
||||||
id: `placeholder-${index}`,
|
id: `placeholder-${index}`,
|
||||||
serviceId: serviceId || "",
|
serviceId: serviceId || "",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
|
||||||
status: statusValue as "up" | "down" | "warning" | "paused",
|
status: statusValue as "up" | "down" | "warning" | "paused",
|
||||||
responseTime: 0
|
responseTime: 0
|
||||||
}));
|
}));
|
||||||
setHistoryItems(placeholderHistory);
|
setHistoryItems(placeholderHistory);
|
||||||
}
|
}
|
||||||
}, [uptimeData, serviceId, status]);
|
}, [uptimeData, serviceId, status, interval]);
|
||||||
|
|
||||||
// Get appropriate color classes for each status type
|
// Get appropriate color classes for each status type
|
||||||
const getStatusColor = (itemStatus: string) => {
|
const getStatusColor = (itemStatus: string) => {
|
||||||
@@ -153,13 +188,19 @@ export const UptimeBar = ({ uptime, status, serviceId }: UptimeBarProps) => {
|
|||||||
(status === "up" || status === "down" || status === "warning" || status === "paused") ?
|
(status === "up" || status === "down" || status === "warning" || status === "paused") ?
|
||||||
status as "up" | "down" | "warning" | "paused" : "paused";
|
status as "up" | "down" | "warning" | "paused" : "paused";
|
||||||
|
|
||||||
const paddingItems: UptimeData[] = Array(20 - displayItems.length).fill(null).map((_, index) => ({
|
// Generate padding items with proper time spacing
|
||||||
id: `padding-${index}`,
|
const paddingItems: UptimeData[] = Array(20 - displayItems.length).fill(null).map((_, index) => {
|
||||||
serviceId: serviceId || "",
|
const baseTime = lastItem ? new Date(lastItem.timestamp).getTime() : Date.now();
|
||||||
timestamp: new Date().toISOString(),
|
const timeOffset = (index + 1) * interval * 1000; // Respect the interval
|
||||||
status: lastStatus,
|
|
||||||
responseTime: 0
|
return {
|
||||||
}));
|
id: `padding-${index}`,
|
||||||
|
serviceId: serviceId || "",
|
||||||
|
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
||||||
|
status: lastStatus,
|
||||||
|
responseTime: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
displayItems.push(...paddingItems);
|
displayItems.push(...paddingItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,47 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
import React from 'react';
|
import { format } from 'date-fns';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
|
Card, CardContent, CardDescription, CardHeader, CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Github, FileText, Twitter, MessageCircle, Code2, ServerIcon } from "lucide-react";
|
import {
|
||||||
|
Github, FileText, Twitter, MessageCircle, Code2, ServerIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useLanguage } from "@/contexts/LanguageContext";
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
import { useSystemSettings } from "@/hooks/useSystemSettings";
|
||||||
|
|
||||||
export const AboutSystem: React.FC = () => {
|
export const AboutSystem: React.FC = () => {
|
||||||
const {
|
const { t } = useLanguage();
|
||||||
t
|
const { theme } = useTheme();
|
||||||
} = useLanguage();
|
const { systemName } = useSystemSettings();
|
||||||
const {
|
|
||||||
theme
|
const [version, setVersion] = useState<string>('...');
|
||||||
} = useTheme();
|
const [releaseDate, setReleaseDate] = useState<string>('...');
|
||||||
const {
|
|
||||||
systemName
|
useEffect(() => {
|
||||||
} = useSystemSettings();
|
const fetchLatestRelease = async () => {
|
||||||
return <div className="space-y-6 animate-fade-in">
|
try {
|
||||||
|
const res = await fetch('https://api.github.com/repos/operacle/checkcle/releases/latest');
|
||||||
|
const data = await res.json();
|
||||||
|
setVersion(data.tag_name || 'v1.x.x');
|
||||||
|
setReleaseDate(data.published_at ? format(new Date(data.published_at), 'MMMM d, yyyy') : t('unknown'));
|
||||||
|
} catch (err) {
|
||||||
|
setVersion('v1.x.x');
|
||||||
|
setReleaseDate(t('unknown'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchLatestRelease();
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 animate-fade-in">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
|
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
|
||||||
<p className="text-muted-foreground text-base leading-relaxed mt-2">{t('aboutCheckcle')}</p>
|
<p className="text-muted-foreground text-base leading-relaxed mt-2">
|
||||||
|
{t('aboutCheckcle')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -38,7 +59,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<div className="flex flex-col space-y-3 pt-2">
|
<div className="flex flex-col space-y-3 pt-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-muted-foreground">{t('systemVersion')}</span>
|
<span className="text-muted-foreground">{t('systemVersion')}</span>
|
||||||
<span className="text-foreground font-medium">{t('version')} 1.1.0</span>
|
<span className="text-foreground font-medium">{version}</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-1" />
|
<Separator className="my-1" />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@@ -48,7 +69,7 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<Separator className="my-1" />
|
<Separator className="my-1" />
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-muted-foreground">{t('releasedOn')}</span>
|
<span className="text-muted-foreground">{t('releasedOn')}</span>
|
||||||
<span className="text-foreground font-medium">May 16, 2025</span>
|
<span className="text-foreground font-medium">{releaseDate}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +82,9 @@ export const AboutSystem: React.FC = () => {
|
|||||||
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
|
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
|
||||||
<span>{t('links')}</span>
|
<span>{t('links')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="font-medium text-base">{systemName || 'ReamStack'} {t('resources').toLowerCase()}</CardDescription>
|
<CardDescription className="font-medium text-base">
|
||||||
|
{systemName || 'ReamStack'} {t('resources').toLowerCase()}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 pt-6">
|
<CardContent className="space-y-4 pt-6">
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
@@ -85,6 +108,8 @@ export const AboutSystem: React.FC = () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AboutSystem;
|
export default AboutSystem;
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Loader2, Database, Trash2, AlertTriangle, Globe, Server } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useLanguage } from "@/contexts/LanguageContext";
|
||||||
|
import { authService } from "@/services/authService";
|
||||||
|
import { dataRetentionService } from "@/services/dataRetentionService";
|
||||||
|
|
||||||
|
interface RetentionSettings {
|
||||||
|
uptimeRetentionDays: number;
|
||||||
|
serverRetentionDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataRetentionSettings = () => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [settings, setSettings] = useState<RetentionSettings>({
|
||||||
|
uptimeRetentionDays: 30,
|
||||||
|
serverRetentionDays: 30
|
||||||
|
});
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isUptimeShrinking, setIsUptimeShrinking] = useState(false);
|
||||||
|
const [isServerShrinking, setIsServerShrinking] = useState(false);
|
||||||
|
const [isFullShrinking, setIsFullShrinking] = useState(false);
|
||||||
|
const [lastCleanup, setLastCleanup] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Check if user is super admin
|
||||||
|
const currentUser = authService.getCurrentUser();
|
||||||
|
const isSuperAdmin = currentUser?.role === "superadmin";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
}, [isSuperAdmin]);
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const result = await dataRetentionService.getRetentionSettings();
|
||||||
|
if (result) {
|
||||||
|
setSettings({
|
||||||
|
uptimeRetentionDays: result.uptimeRetentionDays || 30,
|
||||||
|
serverRetentionDays: result.serverRetentionDays || 30
|
||||||
|
});
|
||||||
|
setLastCleanup(result.lastCleanup);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading retention settings:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load retention settings",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
await dataRetentionService.updateRetentionSettings(settings);
|
||||||
|
toast({
|
||||||
|
title: "Settings saved",
|
||||||
|
description: "Data retention settings have been updated",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving retention settings:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to save retention settings",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUptimeShrink = async () => {
|
||||||
|
try {
|
||||||
|
setIsUptimeShrinking(true);
|
||||||
|
const result = await dataRetentionService.manualUptimeCleanup();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Uptime cleanup completed",
|
||||||
|
description: `Deleted ${result.deletedRecords} old uptime records`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload settings to get updated last cleanup time
|
||||||
|
await loadSettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during uptime cleanup:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to perform uptime data cleanup",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsUptimeShrinking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServerShrink = async () => {
|
||||||
|
try {
|
||||||
|
setIsServerShrinking(true);
|
||||||
|
const result = await dataRetentionService.manualServerCleanup();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Server cleanup completed",
|
||||||
|
description: `Deleted ${result.deletedRecords} old server records`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload settings to get updated last cleanup time
|
||||||
|
await loadSettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during server cleanup:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to perform server data cleanup",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsServerShrinking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFullShrink = async () => {
|
||||||
|
try {
|
||||||
|
setIsFullShrinking(true);
|
||||||
|
const result = await dataRetentionService.manualCleanup();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Database cleanup completed",
|
||||||
|
description: `Deleted ${result.deletedRecords} old records`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload settings to get updated last cleanup time
|
||||||
|
await loadSettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during manual cleanup:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to perform database cleanup",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFullShrinking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show permission notice for admin users
|
||||||
|
if (!isSuperAdmin) {
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
{t("dataRetention", "settings")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<AlertDescription className="text-blue-700 dark:text-blue-300">
|
||||||
|
<span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
|
Loading retention settings...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
Data Retention Settings
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure how long monitoring data is kept in the system
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="uptimeRetention">Uptime Monitoring Retention (days)</Label>
|
||||||
|
<Input
|
||||||
|
id="uptimeRetention"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
value={settings.uptimeRetentionDays}
|
||||||
|
onChange={(e) => setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
uptimeRetentionDays: parseInt(e.target.value) || 30
|
||||||
|
}))}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Service uptime and incident data older than this will be automatically deleted
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="serverRetention">Server Monitoring Retention (days)</Label>
|
||||||
|
<Input
|
||||||
|
id="serverRetention"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
value={settings.serverRetentionDays}
|
||||||
|
onChange={(e) => setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
serverRetentionDays: parseInt(e.target.value) || 30
|
||||||
|
}))}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Server metrics and process data older than this will be automatically deleted
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lastCleanup && (
|
||||||
|
<Alert>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
Last automatic cleanup: {new Date(lastCleanup).toLocaleString()}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-wrap gap-2 w-full">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleUptimeShrink}
|
||||||
|
disabled={isUptimeShrinking}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isUptimeShrinking ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Shrink Uptime Data
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleServerShrink}
|
||||||
|
disabled={isServerShrinking}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isServerShrinking ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Shrink Server Data
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleFullShrink}
|
||||||
|
disabled={isFullShrinking}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isFullShrinking ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Full Database Shrink
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end w-full">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataRetentionSettings;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
import DataRetentionSettings from './DataRetentionSettings';
|
||||||
|
|
||||||
|
export default DataRetentionSettings;
|
||||||
|
export { DataRetentionSettings };
|
||||||
@@ -4,6 +4,8 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService";
|
import { userService, User, UpdateUserData, CreateUserData } from "@/services/userService";
|
||||||
import { UserFormValues, NewUserFormValues } from "../userForms";
|
import { UserFormValues, NewUserFormValues } from "../userForms";
|
||||||
import { avatarOptions } from "../avatarOptions";
|
import { avatarOptions } from "../avatarOptions";
|
||||||
|
import { authService } from "@/services/authService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const useUserOperations = (
|
export const useUserOperations = (
|
||||||
fetchUsers: () => Promise<void>,
|
fetchUsers: () => Promise<void>,
|
||||||
@@ -15,6 +17,7 @@ export const useUserOperations = (
|
|||||||
newUserFormReset: (values: any) => void
|
newUserFormReset: (values: any) => void
|
||||||
) => {
|
) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleDeleteUser = async (userToDelete: User | null) => {
|
const handleDeleteUser = async (userToDelete: User | null) => {
|
||||||
if (!userToDelete) return;
|
if (!userToDelete) return;
|
||||||
@@ -47,6 +50,11 @@ export const useUserOperations = (
|
|||||||
setUpdateError(null);
|
setUpdateError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get current logged-in user to check if we're editing ourselves
|
||||||
|
const loggedInUser = authService.getCurrentUser();
|
||||||
|
const isEditingSelf = loggedInUser?.id === currentUser.id;
|
||||||
|
const isEmailChanged = data.email !== currentUser.email;
|
||||||
|
|
||||||
// Create update object with only the fields we want to update
|
// Create update object with only the fields we want to update
|
||||||
const updateData: UpdateUserData = {
|
const updateData: UpdateUserData = {
|
||||||
full_name: data.full_name,
|
full_name: data.full_name,
|
||||||
@@ -57,7 +65,6 @@ export const useUserOperations = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// For avatar, only include if it's different from current one
|
// For avatar, only include if it's different from current one
|
||||||
// Note: We're still sending the avatar path, but our updated userService will handle it properly
|
|
||||||
if (data.avatar && data.avatar !== currentUser.avatar) {
|
if (data.avatar && data.avatar !== currentUser.avatar) {
|
||||||
updateData.avatar = data.avatar;
|
updateData.avatar = data.avatar;
|
||||||
}
|
}
|
||||||
@@ -66,9 +73,26 @@ export const useUserOperations = (
|
|||||||
|
|
||||||
await userService.updateUser(currentUser.id, updateData);
|
await userService.updateUser(currentUser.id, updateData);
|
||||||
|
|
||||||
// After successful update, refresh the auth user data if this is the current user
|
// Handle email change for current user
|
||||||
// In a real app, you'd check if the updated user is the current logged-in user
|
if (isEditingSelf && isEmailChanged) {
|
||||||
|
toast({
|
||||||
|
title: "Email changed successfully",
|
||||||
|
description: "You will be logged out for security reasons. Please log in again with your new email.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
|
||||||
|
// Auto-logout after 2 seconds if editing own email
|
||||||
|
setTimeout(() => {
|
||||||
|
authService.logout();
|
||||||
|
navigate("/login");
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return; // Don't continue with normal flow
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal success flow for other users or non-email changes
|
||||||
toast({
|
toast({
|
||||||
title: "User updated",
|
title: "User updated",
|
||||||
description: `${data.full_name || data.username}'s profile has been updated.`,
|
description: `${data.full_name || data.username}'s profile has been updated.`,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import UserManagement from "@/components/settings/user-management";
|
|||||||
import { NotificationSettings } from "@/components/settings/notification-settings";
|
import { NotificationSettings } from "@/components/settings/notification-settings";
|
||||||
import { AlertsTemplates } from "@/components/settings/alerts-templates";
|
import { AlertsTemplates } from "@/components/settings/alerts-templates";
|
||||||
import { AboutSystem } from "@/components/settings/about-system";
|
import { AboutSystem } from "@/components/settings/about-system";
|
||||||
|
import DataRetentionSettings from "@/components/settings/data-retention/DataRetentionSettings";
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
// State for sidebar collapse functionality
|
// State for sidebar collapse functionality
|
||||||
@@ -58,6 +59,7 @@ const Settings = () => {
|
|||||||
{activePanel === "users" && <UserManagement />}
|
{activePanel === "users" && <UserManagement />}
|
||||||
{activePanel === "notifications" && <NotificationSettings />}
|
{activePanel === "notifications" && <NotificationSettings />}
|
||||||
{activePanel === "templates" && <AlertsTemplates />}
|
{activePanel === "templates" && <AlertsTemplates />}
|
||||||
|
{activePanel === "data-retention" && <DataRetentionSettings />}
|
||||||
{activePanel === "about" && <AboutSystem />}
|
{activePanel === "about" && <AboutSystem />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
|
||||||
|
import { pb } from '@/lib/pocketbase';
|
||||||
|
|
||||||
|
interface RetentionSettings {
|
||||||
|
uptimeRetentionDays: number;
|
||||||
|
serverRetentionDays: number;
|
||||||
|
lastCleanup?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CleanupResult {
|
||||||
|
deletedRecords: number;
|
||||||
|
collections: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataRetentionService = {
|
||||||
|
async getRetentionSettings(): Promise<RetentionSettings | null> {
|
||||||
|
try {
|
||||||
|
// Try to get existing settings from data_settings collection
|
||||||
|
const records = await pb.collection('data_settings').getFullList({
|
||||||
|
sort: '-created'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (records.length > 0) {
|
||||||
|
const settings = records[0];
|
||||||
|
return {
|
||||||
|
uptimeRetentionDays: settings.uptime_retention_days || 30,
|
||||||
|
serverRetentionDays: settings.server_retention_days || 30,
|
||||||
|
lastCleanup: settings.last_cleanup
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return default settings if none exist
|
||||||
|
return {
|
||||||
|
uptimeRetentionDays: 30,
|
||||||
|
serverRetentionDays: 30
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching retention settings:", error);
|
||||||
|
// Return default settings on error
|
||||||
|
return {
|
||||||
|
uptimeRetentionDays: 30,
|
||||||
|
serverRetentionDays: 30
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateRetentionSettings(settings: RetentionSettings): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if settings already exist
|
||||||
|
const existingRecords = await pb.collection('data_settings').getFullList({
|
||||||
|
sort: '-created'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
uptime_retention_days: settings.uptimeRetentionDays,
|
||||||
|
server_retention_days: settings.serverRetentionDays,
|
||||||
|
retention_days: Math.max(settings.uptimeRetentionDays, settings.serverRetentionDays), // General retention days
|
||||||
|
backup: "auto", // Default backup setting
|
||||||
|
updated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingRecords.length > 0) {
|
||||||
|
// Update existing record
|
||||||
|
await pb.collection('data_settings').update(existingRecords[0].id, data);
|
||||||
|
} else {
|
||||||
|
// Create new record
|
||||||
|
await pb.collection('data_settings').create({
|
||||||
|
...data,
|
||||||
|
created: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Retention settings updated successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating retention settings:", error);
|
||||||
|
throw new Error("Failed to update retention settings");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async manualUptimeCleanup(): Promise<CleanupResult> {
|
||||||
|
try {
|
||||||
|
const settings = await this.getRetentionSettings();
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error("Could not load retention settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalDeleted = 0;
|
||||||
|
const cleanedCollections: string[] = [];
|
||||||
|
|
||||||
|
// Calculate cutoff date for uptime data
|
||||||
|
const uptimeCutoffDate = new Date();
|
||||||
|
uptimeCutoffDate.setDate(uptimeCutoffDate.getDate() - settings.uptimeRetentionDays);
|
||||||
|
|
||||||
|
console.log(`Starting uptime cleanup - Cutoff: ${uptimeCutoffDate.toISOString()}`);
|
||||||
|
|
||||||
|
// Clean uptime_data collection
|
||||||
|
try {
|
||||||
|
const uptimeRecords = await pb.collection('uptime_data').getFullList({
|
||||||
|
filter: `created < "${uptimeCutoffDate.toISOString()}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${uptimeRecords.length} uptime records to delete`);
|
||||||
|
|
||||||
|
for (const record of uptimeRecords) {
|
||||||
|
await pb.collection('uptime_data').delete(record.id);
|
||||||
|
totalDeleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uptimeRecords.length > 0) {
|
||||||
|
cleanedCollections.push('uptime_data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error cleaning uptime_data:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Uptime cleanup completed. Deleted ${totalDeleted} records`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedRecords: totalDeleted,
|
||||||
|
collections: cleanedCollections
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during uptime cleanup:", error);
|
||||||
|
throw new Error("Failed to perform uptime data cleanup");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async manualServerCleanup(): Promise<CleanupResult> {
|
||||||
|
try {
|
||||||
|
const settings = await this.getRetentionSettings();
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error("Could not load retention settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalDeleted = 0;
|
||||||
|
const cleanedCollections: string[] = [];
|
||||||
|
|
||||||
|
// Calculate cutoff date for server data
|
||||||
|
const serverCutoffDate = new Date();
|
||||||
|
serverCutoffDate.setDate(serverCutoffDate.getDate() - settings.serverRetentionDays);
|
||||||
|
|
||||||
|
console.log(`Starting server cleanup - Cutoff: ${serverCutoffDate.toISOString()}`);
|
||||||
|
|
||||||
|
// Clean ping_data collection
|
||||||
|
try {
|
||||||
|
const pingRecords = await pb.collection('ping_data').getFullList({
|
||||||
|
filter: `created < "${serverCutoffDate.toISOString()}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${pingRecords.length} ping records to delete`);
|
||||||
|
|
||||||
|
for (const record of pingRecords) {
|
||||||
|
await pb.collection('ping_data').delete(record.id);
|
||||||
|
totalDeleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pingRecords.length > 0) {
|
||||||
|
cleanedCollections.push('ping_data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error cleaning ping_data:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Server cleanup completed. Deleted ${totalDeleted} records`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedRecords: totalDeleted,
|
||||||
|
collections: cleanedCollections
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during server cleanup:", error);
|
||||||
|
throw new Error("Failed to perform server data cleanup");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async manualCleanup(): Promise<CleanupResult> {
|
||||||
|
try {
|
||||||
|
// Get current retention settings
|
||||||
|
const settings = await this.getRetentionSettings();
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error("Could not load retention settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalDeleted = 0;
|
||||||
|
const cleanedCollections: string[] = [];
|
||||||
|
|
||||||
|
// Calculate cutoff dates
|
||||||
|
const uptimeCutoffDate = new Date();
|
||||||
|
uptimeCutoffDate.setDate(uptimeCutoffDate.getDate() - settings.uptimeRetentionDays);
|
||||||
|
|
||||||
|
const serverCutoffDate = new Date();
|
||||||
|
serverCutoffDate.setDate(serverCutoffDate.getDate() - settings.serverRetentionDays);
|
||||||
|
|
||||||
|
console.log(`Starting manual cleanup - Uptime cutoff: ${uptimeCutoffDate.toISOString()}, Server cutoff: ${serverCutoffDate.toISOString()}`);
|
||||||
|
|
||||||
|
// Clean uptime_data collection
|
||||||
|
try {
|
||||||
|
const uptimeRecords = await pb.collection('uptime_data').getFullList({
|
||||||
|
filter: `created < "${uptimeCutoffDate.toISOString()}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${uptimeRecords.length} uptime records to delete`);
|
||||||
|
|
||||||
|
for (const record of uptimeRecords) {
|
||||||
|
await pb.collection('uptime_data').delete(record.id);
|
||||||
|
totalDeleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uptimeRecords.length > 0) {
|
||||||
|
cleanedCollections.push('uptime_data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error cleaning uptime_data:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean ping_data collection
|
||||||
|
try {
|
||||||
|
const pingRecords = await pb.collection('ping_data').getFullList({
|
||||||
|
filter: `created < "${serverCutoffDate.toISOString()}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${pingRecords.length} ping records to delete`);
|
||||||
|
|
||||||
|
for (const record of pingRecords) {
|
||||||
|
await pb.collection('ping_data').delete(record.id);
|
||||||
|
totalDeleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pingRecords.length > 0) {
|
||||||
|
cleanedCollections.push('ping_data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error cleaning ping_data:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last cleanup timestamp
|
||||||
|
await this.updateLastCleanupTime();
|
||||||
|
|
||||||
|
console.log(`Manual cleanup completed. Deleted ${totalDeleted} records from collections: ${cleanedCollections.join(', ')}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedRecords: totalDeleted,
|
||||||
|
collections: cleanedCollections
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during manual cleanup:", error);
|
||||||
|
throw new Error("Failed to perform database cleanup");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateLastCleanupTime(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const existingRecords = await pb.collection('data_settings').getFullList({
|
||||||
|
sort: '-created'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
last_cleanup: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingRecords.length > 0) {
|
||||||
|
await pb.collection('data_settings').update(existingRecords[0].id, data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating last cleanup time:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async scheduleAutomaticCleanup(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const settings = await this.getRetentionSettings();
|
||||||
|
if (!settings) return;
|
||||||
|
|
||||||
|
// Check if enough time has passed since last cleanup (run daily)
|
||||||
|
if (settings.lastCleanup) {
|
||||||
|
const lastCleanup = new Date(settings.lastCleanup);
|
||||||
|
const now = new Date();
|
||||||
|
const hoursSinceLastCleanup = (now.getTime() - lastCleanup.getTime()) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (hoursSinceLastCleanup < 24) {
|
||||||
|
console.log("Skipping automatic cleanup - last cleanup was less than 24 hours ago");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Starting scheduled automatic cleanup");
|
||||||
|
const result = await this.manualCleanup();
|
||||||
|
console.log(`Automatic cleanup completed. Deleted ${result.deletedRecords} records`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during automatic cleanup:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,29 +2,24 @@
|
|||||||
import { pb } from '@/lib/pocketbase';
|
import { pb } from '@/lib/pocketbase';
|
||||||
import { UptimeData } from '@/types/service.types';
|
import { UptimeData } from '@/types/service.types';
|
||||||
|
|
||||||
// Simple in-memory cache to avoid excessive requests
|
|
||||||
const uptimeCache = new Map<string, {
|
const uptimeCache = new Map<string, {
|
||||||
data: UptimeData[],
|
data: UptimeData[],
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
expiresIn: number
|
expiresIn: number
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Cache time-to-live in milliseconds
|
const CACHE_TTL = 3000; // 3 seconds for faster updates
|
||||||
const CACHE_TTL = 15000; // 15 seconds
|
|
||||||
|
|
||||||
export const uptimeService = {
|
export const uptimeService = {
|
||||||
async recordUptimeData(data: UptimeData): Promise<void> {
|
async recordUptimeData(data: UptimeData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`Recording uptime data for service ${data.serviceId}: Status ${data.status}, Response time: ${data.responseTime}ms`);
|
console.log(`Recording uptime data for service ${data.serviceId}: Status ${data.status}, Response time: ${data.responseTime}ms`);
|
||||||
|
|
||||||
// Create a custom request options object to disable auto-cancellation
|
|
||||||
const options = {
|
const options = {
|
||||||
$autoCancel: false, // Disable auto-cancellation for this request
|
$autoCancel: false,
|
||||||
$cancelKey: `uptime_record_${data.serviceId}_${Date.now()}` // Unique key for this request
|
$cancelKey: `uptime_record_${data.serviceId}_${Date.now()}`
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store uptime history in the uptime_data collection
|
|
||||||
// Format data for PocketBase (use snake_case)
|
|
||||||
const record = await pb.collection('uptime_data').create({
|
const record = await pb.collection('uptime_data').create({
|
||||||
service_id: data.serviceId,
|
service_id: data.serviceId,
|
||||||
timestamp: data.timestamp,
|
timestamp: data.timestamp,
|
||||||
@@ -32,8 +27,9 @@ export const uptimeService = {
|
|||||||
response_time: data.responseTime
|
response_time: data.responseTime
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
// Invalidate cache for this service after recording new data
|
// Invalidate cache for this service
|
||||||
uptimeCache.delete(`uptime_${data.serviceId}`);
|
const keysToDelete = Array.from(uptimeCache.keys()).filter(key => key.includes(`uptime_${data.serviceId}`));
|
||||||
|
keysToDelete.forEach(key => uptimeCache.delete(key));
|
||||||
|
|
||||||
console.log(`Uptime data recorded successfully with ID: ${record.id}`);
|
console.log(`Uptime data recorded successfully with ID: ${record.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -49,10 +45,9 @@ export const uptimeService = {
|
|||||||
endDate?: Date
|
endDate?: Date
|
||||||
): Promise<UptimeData[]> {
|
): Promise<UptimeData[]> {
|
||||||
try {
|
try {
|
||||||
// Create cache key based on parameters
|
|
||||||
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}`;
|
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}`;
|
||||||
|
|
||||||
// Check if we have a valid cached result
|
// Check cache
|
||||||
const cached = uptimeCache.get(cacheKey);
|
const cached = uptimeCache.get(cacheKey);
|
||||||
if (cached && (Date.now() - cached.timestamp) < cached.expiresIn) {
|
if (cached && (Date.now() - cached.timestamp) < cached.expiresIn) {
|
||||||
console.log(`Using cached uptime history for service ${serviceId}`);
|
console.log(`Using cached uptime history for service ${serviceId}`);
|
||||||
@@ -61,80 +56,74 @@ export const uptimeService = {
|
|||||||
|
|
||||||
console.log(`Fetching uptime history for service ${serviceId}, limit: ${limit}`);
|
console.log(`Fetching uptime history for service ${serviceId}, limit: ${limit}`);
|
||||||
|
|
||||||
// Base filter for a specific service
|
|
||||||
let filter = `service_id='${serviceId}'`;
|
let filter = `service_id='${serviceId}'`;
|
||||||
|
|
||||||
// Add date range filtering if provided
|
// Add date range filtering if provided
|
||||||
if (startDate && endDate) {
|
if (startDate && endDate) {
|
||||||
// Convert to ISO strings for PocketBase filtering
|
// Convert dates to UTC strings in the format PocketBase expects
|
||||||
const startISO = startDate.toISOString();
|
const startUTC = startDate.toISOString();
|
||||||
const endISO = endDate.toISOString();
|
const endUTC = endDate.toISOString();
|
||||||
|
|
||||||
// Log the date range we're filtering by
|
console.log(`Date filter: ${startUTC} to ${endUTC}`);
|
||||||
console.log(`Date range filter: ${startISO} to ${endISO}`);
|
|
||||||
|
|
||||||
filter += ` && timestamp >= '${startISO}' && timestamp <= '${endISO}'`;
|
// Use proper PocketBase date filtering syntax
|
||||||
|
filter += ` && timestamp >= "${startUTC}" && timestamp <= "${endUTC}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time difference to determine if it's a short range (like 60min)
|
|
||||||
const isShortTimeRange = startDate && endDate &&
|
|
||||||
(endDate.getTime() - startDate.getTime() <= 60 * 60 * 1000);
|
|
||||||
|
|
||||||
// For very short time ranges, adjust sorting and limit
|
|
||||||
const sort = '-timestamp'; // Default: newest first
|
|
||||||
const actualLimit = isShortTimeRange ? Math.max(limit, 300) : limit; // Ensure adequate points for short ranges
|
|
||||||
|
|
||||||
// Create custom request options to disable auto-cancellation
|
|
||||||
const options = {
|
const options = {
|
||||||
filter: filter,
|
filter: filter,
|
||||||
sort: sort,
|
sort: '-timestamp',
|
||||||
$autoCancel: false, // Disable auto-cancellation for this request
|
$autoCancel: false,
|
||||||
$cancelKey: `uptime_history_${serviceId}_${Date.now()}` // Unique key for this request
|
$cancelKey: `uptime_history_${serviceId}_${Date.now()}`
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
console.log(`Filter query: ${filter}`);
|
||||||
// Get uptime history for a specific service with date filtering
|
|
||||||
const response = await pb.collection('uptime_data').getList(1, actualLimit, options);
|
|
||||||
|
|
||||||
console.log(`Fetched ${response.items.length} uptime records for service ${serviceId}`);
|
const response = await pb.collection('uptime_data').getList(1, limit, options);
|
||||||
|
|
||||||
// Map and return the data
|
console.log(`Fetched ${response.items.length} uptime records for service ${serviceId}`);
|
||||||
const uptimeData = response.items.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
serviceId: item.service_id,
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
status: item.status,
|
|
||||||
responseTime: item.response_time || 0,
|
|
||||||
date: item.timestamp, // Mapping timestamp to date
|
|
||||||
uptime: 100 // Default value for uptime
|
|
||||||
}));
|
|
||||||
|
|
||||||
// For short time ranges with few data points, we might need to ensure we have enough points
|
if (response.items.length > 0) {
|
||||||
const shouldAddPlaceholderData = isShortTimeRange && uptimeData.length <= 2;
|
console.log(`Date range in results: ${response.items[response.items.length - 1].timestamp} to ${response.items[0].timestamp}`);
|
||||||
|
} else {
|
||||||
|
console.log(`No records found for filter: ${filter}`);
|
||||||
|
|
||||||
let finalData = uptimeData;
|
// Try a fallback query without date filter to see if there's any data at all
|
||||||
|
const fallbackResponse = await pb.collection('uptime_data').getList(1, 10, {
|
||||||
if (shouldAddPlaceholderData && uptimeData.length > 0) {
|
filter: `service_id='${serviceId}'`,
|
||||||
// We'll add some additional data points to ensure graph is visible
|
sort: '-timestamp',
|
||||||
console.log("Adding placeholder data points to ensure graph visibility");
|
$autoCancel: false
|
||||||
finalData = [...uptimeData];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
uptimeCache.set(cacheKey, {
|
|
||||||
data: finalData,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
expiresIn: CACHE_TTL
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return finalData;
|
console.log(`Fallback query found ${fallbackResponse.items.length} total records for service`);
|
||||||
} catch (err) {
|
if (fallbackResponse.items.length > 0) {
|
||||||
throw err;
|
console.log(`Latest record timestamp: ${fallbackResponse.items[0].timestamp}`);
|
||||||
|
console.log(`Oldest record timestamp: ${fallbackResponse.items[fallbackResponse.items.length - 1].timestamp}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uptimeData = response.items.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
serviceId: item.service_id,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
status: item.status,
|
||||||
|
responseTime: item.response_time || 0,
|
||||||
|
date: item.timestamp,
|
||||||
|
uptime: 100
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
uptimeCache.set(cacheKey, {
|
||||||
|
data: uptimeData,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
expiresIn: CACHE_TTL
|
||||||
|
});
|
||||||
|
|
||||||
|
return uptimeData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching uptime history:", error);
|
console.error("Error fetching uptime history:", error);
|
||||||
|
|
||||||
// Try to return cached data even if it's expired, as a fallback
|
// Try to return cached data as fallback
|
||||||
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}`;
|
const cacheKey = `uptime_${serviceId}_${limit}_${startDate?.toISOString() || ''}_${endDate?.toISOString() || ''}`;
|
||||||
const cached = uptimeCache.get(cacheKey);
|
const cached = uptimeCache.get(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
|||||||
@@ -148,21 +148,10 @@ export const userService = {
|
|||||||
delete cleanData.role;
|
delete cleanData.role;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle email updates separately
|
// Handle email updates with proper error handling
|
||||||
const hasEmailChange = cleanData.email !== undefined;
|
const hasEmailChange = cleanData.email !== undefined;
|
||||||
const emailToUpdate = hasEmailChange ? cleanData.email : null;
|
const emailToUpdate = hasEmailChange ? cleanData.email : null;
|
||||||
|
|
||||||
// Remove email from regular update if it's being changed
|
|
||||||
if (hasEmailChange) {
|
|
||||||
console.log("Email change detected, will handle separately:", emailToUpdate);
|
|
||||||
delete cleanData.email;
|
|
||||||
|
|
||||||
// For email changes, we should always set emailVisibility
|
|
||||||
if (cleanData.emailVisibility === undefined) {
|
|
||||||
cleanData.emailVisibility = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let updatedUser: User | null = null;
|
let updatedUser: User | null = null;
|
||||||
|
|
||||||
// First, determine if this is currently a regular user or superadmin
|
// First, determine if this is currently a regular user or superadmin
|
||||||
@@ -194,7 +183,6 @@ export const userService = {
|
|||||||
...cleanData
|
...cleanData
|
||||||
};
|
};
|
||||||
|
|
||||||
// We need to create in the new collection and delete from the old one
|
|
||||||
try {
|
try {
|
||||||
if (targetRole === "superadmin") {
|
if (targetRole === "superadmin") {
|
||||||
// Create in superadmin collection
|
// Create in superadmin collection
|
||||||
@@ -217,7 +205,6 @@ export const userService = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular update without changing collections
|
// Regular update without changing collections
|
||||||
// Only perform the regular update if there are fields to update
|
|
||||||
if (Object.keys(cleanData).length > 0) {
|
if (Object.keys(cleanData).length > 0) {
|
||||||
console.log("Final update payload to PocketBase:", cleanData);
|
console.log("Final update payload to PocketBase:", cleanData);
|
||||||
|
|
||||||
@@ -227,47 +214,31 @@ export const userService = {
|
|||||||
const updatedRecord = await pb.collection(collection).update(id, cleanData);
|
const updatedRecord = await pb.collection(collection).update(id, cleanData);
|
||||||
updatedUser = convertToUserType(updatedRecord, isCurrentlySuperadmin ? "superadmin" : "admin");
|
updatedUser = convertToUserType(updatedRecord, isCurrentlySuperadmin ? "superadmin" : "admin");
|
||||||
|
|
||||||
console.log("PocketBase update response for regular fields:", updatedUser);
|
console.log("PocketBase update response:", updatedUser);
|
||||||
|
|
||||||
|
// If email was updated successfully, show success message
|
||||||
|
if (hasEmailChange) {
|
||||||
|
console.log("Email updated successfully to:", emailToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating user regular fields:", error);
|
console.error("Error updating user:", error);
|
||||||
|
|
||||||
|
// Provide more specific error messages for email issues
|
||||||
|
if (hasEmailChange && error instanceof Error) {
|
||||||
|
if (error.message.includes("email")) {
|
||||||
|
throw new Error("Email update failed. The email address may already be in use or invalid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If no other fields to update, get the current user
|
// If no fields to update, get the current user
|
||||||
updatedUser = await this.getUser(id);
|
updatedUser = await this.getUser(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now handle email change separately if needed
|
|
||||||
if (emailToUpdate && updatedUser) {
|
|
||||||
try {
|
|
||||||
console.log("Processing email change request for new email:", emailToUpdate);
|
|
||||||
|
|
||||||
// For email changes, we need to directly update the email field instead of using requestEmailChange
|
|
||||||
// This is because requestEmailChange has permission issues when cross-collection requests are made
|
|
||||||
const collection = updatedUser.role === "superadmin" ? '_superusers' : 'users';
|
|
||||||
|
|
||||||
console.log(`Updating email directly in ${collection} collection`);
|
|
||||||
|
|
||||||
// Update the email directly in the user record
|
|
||||||
const emailUpdateData = {
|
|
||||||
email: emailToUpdate,
|
|
||||||
emailVisibility: true,
|
|
||||||
verified: false // Reset verification status when email changes
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedRecord = await pb.collection(collection).update(id, emailUpdateData);
|
|
||||||
updatedUser = convertToUserType(updatedRecord, updatedUser.role === "superadmin" ? "superadmin" : "admin");
|
|
||||||
|
|
||||||
console.log("Email updated successfully:", updatedUser);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update email:", error);
|
|
||||||
// Don't throw error for email update failure, just log it
|
|
||||||
console.log("Email update failed, but other fields were updated successfully");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update user:", error);
|
console.error("Failed to update user:", error);
|
||||||
|
|||||||
Reference in New Issue
Block a user