refactor: update cleanup and schedule config to use seconds for retentionDays and improve nextRun calculation

This commit is contained in:
Arunavo Ray
2025-05-24 20:12:27 +05:30
parent 7afe364a24
commit a28a766f8b
7 changed files with 212 additions and 41 deletions

View File

@@ -53,7 +53,7 @@ export function ConfigTabs() {
}, },
cleanupConfig: { cleanupConfig: {
enabled: false, enabled: false,
retentionDays: 7, retentionDays: 604800, // 7 days in seconds
}, },
}); });
const { user, refreshUser } = useAuth(); const { user, refreshUser } = useAuth();
@@ -181,6 +181,22 @@ export function ConfigTabs() {
// Removed refreshUser() call to prevent page reload // Removed refreshUser() call to prevent page reload
// Invalidate config cache so other components get fresh data // Invalidate config cache so other components get fresh data
invalidateConfigCache(); invalidateConfigCache();
// Fetch updated config to get the recalculated nextRun time
try {
const updatedResponse = await apiRequest<ConfigApiResponse>(
`/config?userId=${user.id}`,
{ method: 'GET' },
);
if (updatedResponse && !updatedResponse.error) {
setConfig(prev => ({
...prev,
scheduleConfig: updatedResponse.scheduleConfig || prev.scheduleConfig,
}));
}
} catch (fetchError) {
console.warn('Failed to fetch updated config after auto-save:', fetchError);
}
} else { } else {
toast.error( toast.error(
`Auto-save failed: ${result.message || 'Unknown error'}`, `Auto-save failed: ${result.message || 'Unknown error'}`,
@@ -233,6 +249,22 @@ export function ConfigTabs() {
// Silent success - no toast for auto-save // Silent success - no toast for auto-save
// Invalidate config cache so other components get fresh data // Invalidate config cache so other components get fresh data
invalidateConfigCache(); invalidateConfigCache();
// Fetch updated config to get the recalculated nextRun time
try {
const updatedResponse = await apiRequest<ConfigApiResponse>(
`/config?userId=${user.id}`,
{ method: 'GET' },
);
if (updatedResponse && !updatedResponse.error) {
setConfig(prev => ({
...prev,
cleanupConfig: updatedResponse.cleanupConfig || prev.cleanupConfig,
}));
}
} catch (fetchError) {
console.warn('Failed to fetch updated config after auto-save:', fetchError);
}
} else { } else {
toast.error( toast.error(
`Auto-save failed: ${result.message || 'Unknown error'}`, `Auto-save failed: ${result.message || 'Unknown error'}`,

View File

@@ -18,38 +18,79 @@ interface DatabaseCleanupConfigFormProps {
isAutoSaving?: boolean; isAutoSaving?: boolean;
} }
// Helper to calculate cleanup interval in hours (should match backend logic)
function calculateCleanupInterval(retentionSeconds: number): number {
const retentionDays = retentionSeconds / (24 * 60 * 60);
if (retentionDays <= 1) {
return 6;
} else if (retentionDays <= 3) {
return 12;
} else if (retentionDays <= 7) {
return 24;
} else if (retentionDays <= 30) {
return 48;
} else {
return 168;
}
}
export function DatabaseCleanupConfigForm({ export function DatabaseCleanupConfigForm({
config, config,
setConfig, setConfig,
onAutoSave, onAutoSave,
isAutoSaving = false, isAutoSaving = false,
}: DatabaseCleanupConfigFormProps) { }: DatabaseCleanupConfigFormProps) {
// Optimistically update nextRun when enabled or retention changes
const handleChange = ( const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => { ) => {
const { name, value, type } = e.target; const { name, value, type } = e.target;
const newConfig = { let newConfig = {
...config, ...config,
[name]: [name]: type === "checkbox" ? (e.target as HTMLInputElement).checked : value,
type === "checkbox" ? (e.target as HTMLInputElement).checked : value,
}; };
setConfig(newConfig);
// Trigger auto-save for cleanup config changes // If enabling or changing retention, recalculate nextRun
if (
(name === "enabled" && (e.target as HTMLInputElement).checked) ||
(name === "retentionDays" && config.enabled)
) {
const now = new Date();
const retentionSeconds =
name === "retentionDays"
? Number(value)
: Number(newConfig.retentionDays);
const intervalHours = calculateCleanupInterval(retentionSeconds);
const nextRun = new Date(now.getTime() + intervalHours * 60 * 60 * 1000);
newConfig = {
...newConfig,
nextRun,
};
}
// If disabling, clear nextRun
if (name === "enabled" && !(e.target as HTMLInputElement).checked) {
newConfig = {
...newConfig,
nextRun: undefined,
};
}
setConfig(newConfig);
if (onAutoSave) { if (onAutoSave) {
onAutoSave(newConfig); onAutoSave(newConfig);
} }
}; };
// Predefined retention periods // Predefined retention periods (in seconds, like schedule intervals)
const retentionOptions: { value: number; label: string }[] = [ const retentionOptions: { value: number; label: string }[] = [
{ value: 1, label: "1 day" }, { value: 86400, label: "1 day" }, // 24 * 60 * 60
{ value: 3, label: "3 days" }, { value: 259200, label: "3 days" }, // 3 * 24 * 60 * 60
{ value: 7, label: "7 days" }, { value: 604800, label: "7 days" }, // 7 * 24 * 60 * 60
{ value: 14, label: "14 days" }, { value: 1209600, label: "14 days" }, // 14 * 24 * 60 * 60
{ value: 30, label: "30 days" }, { value: 2592000, label: "30 days" }, // 30 * 24 * 60 * 60
{ value: 60, label: "60 days" }, { value: 5184000, label: "60 days" }, // 60 * 24 * 60 * 60
{ value: 90, label: "90 days" }, { value: 7776000, label: "90 days" }, // 90 * 24 * 60 * 60
]; ];
return ( return (
@@ -92,7 +133,7 @@ export function DatabaseCleanupConfigForm({
{config.enabled && ( {config.enabled && (
<div> <div>
<label className="block text-sm font-medium mb-2"> <label className="block text-sm font-medium mb-2">
Retention Period Data Retention Period
</label> </label>
<Select <Select
@@ -123,12 +164,18 @@ export function DatabaseCleanupConfigForm({
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
Activities and events older than this period will be automatically deleted. Activities and events older than this period will be automatically deleted.
</p> </p>
<div className="mt-2 p-2 bg-muted/50 rounded-md">
<p className="text-xs text-muted-foreground">
<strong>Cleanup Frequency:</strong> The cleanup process runs automatically at optimal intervals:
shorter retention periods trigger more frequent cleanups, longer periods trigger less frequent cleanups.
</p>
</div>
</div> </div>
)} )}
<div className="flex gap-x-4"> <div className="flex gap-x-4">
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium mb-1">Last Run</label> <label className="block text-sm font-medium mb-1">Last Cleanup</label>
<div className="text-sm"> <div className="text-sm">
{config.lastRun ? formatDate(config.lastRun) : "Never"} {config.lastRun ? formatDate(config.lastRun) : "Never"}
</div> </div>
@@ -136,9 +183,13 @@ export function DatabaseCleanupConfigForm({
{config.enabled && ( {config.enabled && (
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium mb-1">Next Run</label> <label className="block text-sm font-medium mb-1">Next Cleanup</label>
<div className="text-sm"> <div className="text-sm">
{config.nextRun ? formatDate(config.nextRun) : "Never"} {config.nextRun
? formatDate(config.nextRun)
: config.enabled
? "Calculating..."
: "Never"}
</div> </div>
</div> </div>
)} )}

View File

@@ -43,9 +43,6 @@ export function ScheduleConfigForm({
// Predefined intervals // Predefined intervals
const intervals: { value: number; label: string }[] = [ const intervals: { value: number; label: string }[] = [
// { value: 120, label: "2 minutes" }, //for testing
{ value: 900, label: "15 minutes" },
{ value: 1800, label: "30 minutes" },
{ value: 3600, label: "1 hour" }, { value: 3600, label: "1 hour" },
{ value: 7200, label: "2 hours" }, { value: 7200, label: "2 hours" },
{ value: 14400, label: "4 hours" }, { value: 14400, label: "4 hours" },
@@ -127,12 +124,18 @@ export function ScheduleConfigForm({
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
How often the mirroring process should run. How often the mirroring process should run.
</p> </p>
<div className="mt-2 p-2 bg-muted/50 rounded-md">
<p className="text-xs text-muted-foreground">
<strong>Sync Schedule:</strong> Repositories will be synchronized at the specified interval.
Choose shorter intervals for frequently updated repositories, longer intervals for stable ones.
</p>
</div>
</div> </div>
)} )}
<div className="flex gap-x-4"> <div className="flex gap-x-4">
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium mb-1">Last Run</label> <label className="block text-sm font-medium mb-1">Last Sync</label>
<div className="text-sm"> <div className="text-sm">
{config.lastRun ? formatDate(config.lastRun) : "Never"} {config.lastRun ? formatDate(config.lastRun) : "Never"}
</div> </div>
@@ -140,7 +143,7 @@ export function ScheduleConfigForm({
{config.enabled && ( {config.enabled && (
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium mb-1">Next Run</label> <label className="block text-sm font-medium mb-1">Next Sync</label>
<div className="text-sm"> <div className="text-sm">
{config.nextRun ? formatDate(config.nextRun) : "Never"} {config.nextRun ? formatDate(config.nextRun) : "Never"}
</div> </div>

View File

@@ -15,15 +15,39 @@ interface CleanupResult {
} }
/** /**
* Clean up old events and mirror jobs for a specific user * Calculate cleanup interval in hours based on retention period
* For shorter retention periods, run more frequently
* For longer retention periods, run less frequently
* @param retentionSeconds - Retention period in seconds
*/ */
async function cleanupForUser(userId: string, retentionDays: number): Promise<CleanupResult> { export function calculateCleanupInterval(retentionSeconds: number): number {
try { const retentionDays = retentionSeconds / (24 * 60 * 60); // Convert seconds to days
console.log(`Running cleanup for user ${userId} with ${retentionDays} days retention`);
// Calculate cutoff date if (retentionDays <= 1) {
return 6; // Every 6 hours for 1 day retention
} else if (retentionDays <= 3) {
return 12; // Every 12 hours for 1-3 days retention
} else if (retentionDays <= 7) {
return 24; // Daily for 4-7 days retention
} else if (retentionDays <= 30) {
return 48; // Every 2 days for 8-30 days retention
} else {
return 168; // Weekly for 30+ days retention
}
}
/**
* Clean up old events and mirror jobs for a specific user
* @param retentionSeconds - Retention period in seconds
*/
async function cleanupForUser(userId: string, retentionSeconds: number): Promise<CleanupResult> {
try {
const retentionDays = retentionSeconds / (24 * 60 * 60); // Convert to days for logging
console.log(`Running cleanup for user ${userId} with ${retentionDays} days retention (${retentionSeconds} seconds)`);
// Calculate cutoff date using seconds
const cutoffDate = new Date(); const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays); cutoffDate.setTime(cutoffDate.getTime() - retentionSeconds * 1000);
let eventsDeleted = 0; let eventsDeleted = 0;
let mirrorJobsDeleted = 0; let mirrorJobsDeleted = 0;
@@ -75,7 +99,9 @@ async function cleanupForUser(userId: string, retentionDays: number): Promise<Cl
async function updateCleanupConfig(userId: string, cleanupConfig: any) { async function updateCleanupConfig(userId: string, cleanupConfig: any) {
try { try {
const now = new Date(); const now = new Date();
const nextRun = new Date(now.getTime() + 24 * 60 * 60 * 1000); // Next day const retentionSeconds = cleanupConfig.retentionDays || 604800; // Default 7 days in seconds
const cleanupIntervalHours = calculateCleanupInterval(retentionSeconds);
const nextRun = new Date(now.getTime() + cleanupIntervalHours * 60 * 60 * 1000);
const updatedConfig = { const updatedConfig = {
...cleanupConfig, ...cleanupConfig,
@@ -91,7 +117,8 @@ async function updateCleanupConfig(userId: string, cleanupConfig: any) {
}) })
.where(eq(configs.userId, userId)); .where(eq(configs.userId, userId));
console.log(`Updated cleanup config for user ${userId}, next run: ${nextRun.toISOString()}`); const retentionDays = retentionSeconds / (24 * 60 * 60);
console.log(`Updated cleanup config for user ${userId}, next run: ${nextRun.toISOString()} (${cleanupIntervalHours}h interval for ${retentionDays}d retention)`);
} catch (error) { } catch (error) {
console.error(`Error updating cleanup config for user ${userId}:`, error); console.error(`Error updating cleanup config for user ${userId}:`, error);
} }
@@ -127,7 +154,7 @@ export async function runAutomaticCleanup(): Promise<CleanupResult[]> {
// If nextRun is null or in the past, run cleanup // If nextRun is null or in the past, run cleanup
if (!nextRun || now >= nextRun) { if (!nextRun || now >= nextRun) {
const result = await cleanupForUser(config.userId, cleanupConfig.retentionDays || 7); const result = await cleanupForUser(config.userId, cleanupConfig.retentionDays || 604800);
results.push(result); results.push(result);
// Update the cleanup config with new run times // Update the cleanup config with new run times

View File

@@ -54,7 +54,7 @@ export const configSchema = z.object({
}), }),
cleanupConfig: z.object({ cleanupConfig: z.object({
enabled: z.boolean().default(false), enabled: z.boolean().default(false),
retentionDays: z.number().min(1).default(7), // in days retentionDays: z.number().min(1).default(604800), // in seconds (default: 7 days)
lastRun: z.date().optional(), lastRun: z.date().optional(),
nextRun: z.date().optional(), nextRun: z.date().optional(),
}), }),

View File

@@ -2,6 +2,7 @@ import type { APIRoute } from "astro";
import { db, configs, users } from "@/lib/db"; import { db, configs, users } from "@/lib/db";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { calculateCleanupInterval } from "@/lib/cleanup-service";
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
@@ -56,6 +57,63 @@ export const POST: APIRoute = async ({ request }) => {
} }
} }
// Process schedule config - set/update nextRun if enabled, clear if disabled
const processedScheduleConfig = { ...scheduleConfig };
if (scheduleConfig.enabled) {
const now = new Date();
const interval = scheduleConfig.interval || 3600; // Default to 1 hour
// Check if we need to recalculate nextRun
// Recalculate if: no nextRun exists, or interval changed from existing config
let shouldRecalculate = !scheduleConfig.nextRun;
if (existingConfig && existingConfig.scheduleConfig) {
const existingScheduleConfig = existingConfig.scheduleConfig;
const existingInterval = existingScheduleConfig.interval || 3600;
// If interval changed, recalculate nextRun
if (interval !== existingInterval) {
shouldRecalculate = true;
}
}
if (shouldRecalculate) {
processedScheduleConfig.nextRun = new Date(now.getTime() + interval * 1000);
}
} else {
// Clear nextRun when disabled
processedScheduleConfig.nextRun = null;
}
// Process cleanup config - set/update nextRun if enabled, clear if disabled
const processedCleanupConfig = { ...cleanupConfig };
if (cleanupConfig.enabled) {
const now = new Date();
const retentionSeconds = cleanupConfig.retentionDays || 604800; // Default 7 days in seconds
const cleanupIntervalHours = calculateCleanupInterval(retentionSeconds);
// Check if we need to recalculate nextRun
// Recalculate if: no nextRun exists, or retention period changed from existing config
let shouldRecalculate = !cleanupConfig.nextRun;
if (existingConfig && existingConfig.cleanupConfig) {
const existingCleanupConfig = existingConfig.cleanupConfig;
const existingRetentionSeconds = existingCleanupConfig.retentionDays || 604800;
// If retention period changed, recalculate nextRun
if (retentionSeconds !== existingRetentionSeconds) {
shouldRecalculate = true;
}
}
if (shouldRecalculate) {
processedCleanupConfig.nextRun = new Date(now.getTime() + cleanupIntervalHours * 60 * 60 * 1000);
}
} else {
// Clear nextRun when disabled
processedCleanupConfig.nextRun = null;
}
if (existingConfig) { if (existingConfig) {
// Update path // Update path
await db await db
@@ -63,8 +121,8 @@ export const POST: APIRoute = async ({ request }) => {
.set({ .set({
githubConfig, githubConfig,
giteaConfig, giteaConfig,
scheduleConfig, scheduleConfig: processedScheduleConfig,
cleanupConfig, cleanupConfig: processedCleanupConfig,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(configs.id, existingConfig.id)); .where(eq(configs.id, existingConfig.id));
@@ -113,8 +171,8 @@ export const POST: APIRoute = async ({ request }) => {
giteaConfig, giteaConfig,
include: [], include: [],
exclude: [], exclude: [],
scheduleConfig, scheduleConfig: processedScheduleConfig,
cleanupConfig, cleanupConfig: processedCleanupConfig,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}); });
@@ -201,7 +259,7 @@ export const GET: APIRoute = async ({ request }) => {
}, },
cleanupConfig: { cleanupConfig: {
enabled: false, enabled: false,
retentionDays: 7, retentionDays: 604800, // 7 days in seconds
lastRun: null, lastRun: null,
nextRun: null, nextRun: null,
}, },

View File

@@ -20,7 +20,7 @@ export interface ScheduleConfig {
export interface DatabaseCleanupConfig { export interface DatabaseCleanupConfig {
enabled: boolean; enabled: boolean;
retentionDays: number; retentionDays: number; // Actually stores seconds, but keeping the name for compatibility
lastRun?: Date; lastRun?: Date;
nextRun?: Date; nextRun?: Date;
} }