diff --git a/application/src/components/dashboard/Sidebar.tsx b/application/src/components/dashboard/Sidebar.tsx index 6fe582c..24b4c6d 100644 --- a/application/src/components/dashboard/Sidebar.tsx +++ b/application/src/components/dashboard/Sidebar.tsx @@ -117,10 +117,10 @@ export const Sidebar = ({ {t("alertsTemplates")} -
+ handleSettingsItemClick('data-retention')}> {t("dataRetention")} -
+ handleSettingsItemClick('about')}> {t("aboutSystem")} diff --git a/application/src/components/settings/data-retention/DataRetentionSettings.tsx b/application/src/components/settings/data-retention/DataRetentionSettings.tsx new file mode 100644 index 0000000..15c1d58 --- /dev/null +++ b/application/src/components/settings/data-retention/DataRetentionSettings.tsx @@ -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({ + 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(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 ( +
+ + + + + {t("dataRetention", "settings")} + + + + + + + Permission Notice: As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins. + + + + +
+ ); + } + + if (isLoading) { + return ( +
+ + Loading retention settings... +
+ ); + } + + return ( +
+ + + + + Data Retention Settings + + + Configure how long monitoring data is kept in the system + + + +
+
+ + setSettings(prev => ({ + ...prev, + uptimeRetentionDays: parseInt(e.target.value) || 30 + }))} + className="mt-1" + /> +

+ Service uptime and incident data older than this will be automatically deleted +

+
+ +
+ + setSettings(prev => ({ + ...prev, + serverRetentionDays: parseInt(e.target.value) || 30 + }))} + className="mt-1" + /> +

+ Server metrics and process data older than this will be automatically deleted +

+
+
+ + {lastCleanup && ( + + + + Last automatic cleanup: {new Date(lastCleanup).toLocaleString()} + + + )} +
+ +
+ + + + + +
+ +
+ +
+
+
+
+ ); +}; + +export default DataRetentionSettings; \ No newline at end of file diff --git a/application/src/components/settings/data-retention/index.ts b/application/src/components/settings/data-retention/index.ts new file mode 100644 index 0000000..bc25d28 --- /dev/null +++ b/application/src/components/settings/data-retention/index.ts @@ -0,0 +1,5 @@ + +import DataRetentionSettings from './DataRetentionSettings'; + +export default DataRetentionSettings; +export { DataRetentionSettings }; \ No newline at end of file diff --git a/application/src/pages/Settings.tsx b/application/src/pages/Settings.tsx index 5c23f96..6dd3516 100644 --- a/application/src/pages/Settings.tsx +++ b/application/src/pages/Settings.tsx @@ -9,6 +9,7 @@ import UserManagement from "@/components/settings/user-management"; import { NotificationSettings } from "@/components/settings/notification-settings"; import { AlertsTemplates } from "@/components/settings/alerts-templates"; import { AboutSystem } from "@/components/settings/about-system"; +import DataRetentionSettings from "@/components/settings/data-retention/DataRetentionSettings"; const Settings = () => { // State for sidebar collapse functionality @@ -58,6 +59,7 @@ const Settings = () => { {activePanel === "users" && } {activePanel === "notifications" && } {activePanel === "templates" && } + {activePanel === "data-retention" && } {activePanel === "about" && } @@ -65,4 +67,4 @@ const Settings = () => { ); }; -export default Settings; +export default Settings; \ No newline at end of file diff --git a/application/src/services/dataRetentionService.ts b/application/src/services/dataRetentionService.ts new file mode 100644 index 0000000..05c7e76 --- /dev/null +++ b/application/src/services/dataRetentionService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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); + } + } +}; \ No newline at end of file