mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-07 03:56:46 +03:00
feat: Implement automatic database cleanup feature with configuration options and API support
This commit is contained in:
@@ -30,3 +30,9 @@ JWT_SECRET=change-this-to-a-secure-random-string-in-production
|
|||||||
# GITEA_ORGANIZATION=github-mirrors
|
# GITEA_ORGANIZATION=github-mirrors
|
||||||
# GITEA_ORG_VISIBILITY=public
|
# GITEA_ORG_VISIBILITY=public
|
||||||
# DELAY=3600
|
# DELAY=3600
|
||||||
|
|
||||||
|
# Optional Database Cleanup Configuration (configured via web UI)
|
||||||
|
# These environment variables are optional and only used as defaults
|
||||||
|
# Users can configure cleanup settings through the web interface
|
||||||
|
# CLEANUP_ENABLED=false
|
||||||
|
# CLEANUP_RETENTION_DAYS=7
|
||||||
|
|||||||
@@ -197,6 +197,33 @@ async function ensureTablesExist() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: Add cleanup_config column to existing configs table
|
||||||
|
try {
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
|
// Check if cleanup_config column exists
|
||||||
|
const tableInfo = db.query(`PRAGMA table_info(configs)`).all();
|
||||||
|
const hasCleanupConfig = tableInfo.some((column: any) => column.name === 'cleanup_config');
|
||||||
|
|
||||||
|
if (!hasCleanupConfig) {
|
||||||
|
console.log("Adding cleanup_config column to configs table...");
|
||||||
|
|
||||||
|
// Add the column with a default value
|
||||||
|
const defaultCleanupConfig = JSON.stringify({
|
||||||
|
enabled: false,
|
||||||
|
retentionDays: 7,
|
||||||
|
lastRun: null,
|
||||||
|
nextRun: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
db.exec(`ALTER TABLE configs ADD COLUMN cleanup_config TEXT NOT NULL DEFAULT '${defaultCleanupConfig}'`);
|
||||||
|
console.log("✅ cleanup_config column added successfully.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error during cleanup_config migration:", error);
|
||||||
|
// Don't exit here as this is not critical for basic functionality
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -328,6 +355,7 @@ async function initializeDatabase() {
|
|||||||
include TEXT NOT NULL DEFAULT '["*"]',
|
include TEXT NOT NULL DEFAULT '["*"]',
|
||||||
exclude TEXT NOT NULL DEFAULT '[]',
|
exclude TEXT NOT NULL DEFAULT '[]',
|
||||||
schedule_config TEXT NOT NULL,
|
schedule_config TEXT NOT NULL,
|
||||||
|
cleanup_config TEXT NOT NULL,
|
||||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
@@ -459,10 +487,16 @@ async function initializeDatabase() {
|
|||||||
lastRun: null,
|
lastRun: null,
|
||||||
nextRun: null,
|
nextRun: null,
|
||||||
});
|
});
|
||||||
|
const cleanupConfig = JSON.stringify({
|
||||||
|
enabled: false,
|
||||||
|
retentionDays: 7,
|
||||||
|
lastRun: null,
|
||||||
|
nextRun: null,
|
||||||
|
});
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO configs (id, user_id, name, is_active, github_config, gitea_config, include, exclude, schedule_config, created_at, updated_at)
|
INSERT INTO configs (id, user_id, name, is_active, github_config, gitea_config, include, exclude, schedule_config, cleanup_config, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
stmt.run(
|
stmt.run(
|
||||||
@@ -475,6 +509,7 @@ async function initializeDatabase() {
|
|||||||
include,
|
include,
|
||||||
exclude,
|
exclude,
|
||||||
scheduleConfig,
|
scheduleConfig,
|
||||||
|
cleanupConfig,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
Date.now()
|
Date.now()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from 'react';
|
|||||||
import { GitHubConfigForm } from './GitHubConfigForm';
|
import { GitHubConfigForm } from './GitHubConfigForm';
|
||||||
import { GiteaConfigForm } from './GiteaConfigForm';
|
import { GiteaConfigForm } from './GiteaConfigForm';
|
||||||
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
||||||
|
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
||||||
import type {
|
import type {
|
||||||
ConfigApiResponse,
|
ConfigApiResponse,
|
||||||
GiteaConfig,
|
GiteaConfig,
|
||||||
@@ -9,6 +10,7 @@ import type {
|
|||||||
SaveConfigApiRequest,
|
SaveConfigApiRequest,
|
||||||
SaveConfigApiResponse,
|
SaveConfigApiResponse,
|
||||||
ScheduleConfig,
|
ScheduleConfig,
|
||||||
|
DatabaseCleanupConfig,
|
||||||
} from '@/types/config';
|
} from '@/types/config';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
@@ -22,6 +24,7 @@ type ConfigState = {
|
|||||||
githubConfig: GitHubConfig;
|
githubConfig: GitHubConfig;
|
||||||
giteaConfig: GiteaConfig;
|
giteaConfig: GiteaConfig;
|
||||||
scheduleConfig: ScheduleConfig;
|
scheduleConfig: ScheduleConfig;
|
||||||
|
cleanupConfig: DatabaseCleanupConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ConfigTabs() {
|
export function ConfigTabs() {
|
||||||
@@ -48,13 +51,19 @@ export function ConfigTabs() {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
interval: 3600,
|
interval: 3600,
|
||||||
},
|
},
|
||||||
|
cleanupConfig: {
|
||||||
|
enabled: false,
|
||||||
|
retentionDays: 7,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const { user, refreshUser } = useAuth();
|
const { user, refreshUser } = useAuth();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
||||||
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
|
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
|
||||||
const [isAutoSaving, setIsAutoSaving] = useState<boolean>(false);
|
const [isAutoSavingSchedule, setIsAutoSavingSchedule] = useState<boolean>(false);
|
||||||
const autoSaveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState<boolean>(false);
|
||||||
|
const autoSaveScheduleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const isConfigFormValid = (): boolean => {
|
const isConfigFormValid = (): boolean => {
|
||||||
const { githubConfig, giteaConfig } = config;
|
const { githubConfig, giteaConfig } = config;
|
||||||
@@ -107,6 +116,7 @@ export function ConfigTabs() {
|
|||||||
githubConfig: config.githubConfig,
|
githubConfig: config.githubConfig,
|
||||||
giteaConfig: config.giteaConfig,
|
giteaConfig: config.giteaConfig,
|
||||||
scheduleConfig: config.scheduleConfig,
|
scheduleConfig: config.scheduleConfig,
|
||||||
|
cleanupConfig: config.cleanupConfig,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/config', {
|
const response = await fetch('/api/config', {
|
||||||
@@ -142,19 +152,20 @@ export function ConfigTabs() {
|
|||||||
if (!user?.id || !isConfigSaved) return; // Only auto-save if config was previously saved
|
if (!user?.id || !isConfigSaved) return; // Only auto-save if config was previously saved
|
||||||
|
|
||||||
// Clear any existing timeout
|
// Clear any existing timeout
|
||||||
if (autoSaveTimeoutRef.current) {
|
if (autoSaveScheduleTimeoutRef.current) {
|
||||||
clearTimeout(autoSaveTimeoutRef.current);
|
clearTimeout(autoSaveScheduleTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debounce the auto-save to prevent excessive API calls
|
// Debounce the auto-save to prevent excessive API calls
|
||||||
autoSaveTimeoutRef.current = setTimeout(async () => {
|
autoSaveScheduleTimeoutRef.current = setTimeout(async () => {
|
||||||
setIsAutoSaving(true);
|
setIsAutoSavingSchedule(true);
|
||||||
|
|
||||||
const reqPayload: SaveConfigApiRequest = {
|
const reqPayload: SaveConfigApiRequest = {
|
||||||
userId: user.id!,
|
userId: user.id!,
|
||||||
githubConfig: config.githubConfig,
|
githubConfig: config.githubConfig,
|
||||||
giteaConfig: config.giteaConfig,
|
giteaConfig: config.giteaConfig,
|
||||||
scheduleConfig: scheduleConfig,
|
scheduleConfig: scheduleConfig,
|
||||||
|
cleanupConfig: config.cleanupConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -184,16 +195,71 @@ export function ConfigTabs() {
|
|||||||
{ duration: 3000 }
|
{ duration: 3000 }
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsAutoSaving(false);
|
setIsAutoSavingSchedule(false);
|
||||||
}
|
}
|
||||||
}, 500); // 500ms debounce
|
}, 500); // 500ms debounce
|
||||||
}, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig]);
|
}, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig, config.cleanupConfig]);
|
||||||
|
|
||||||
// Cleanup timeout on unmount
|
// Auto-save function specifically for cleanup config changes
|
||||||
|
const autoSaveCleanupConfig = useCallback(async (cleanupConfig: DatabaseCleanupConfig) => {
|
||||||
|
if (!user?.id || !isConfigSaved) return; // Only auto-save if config was previously saved
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (autoSaveCleanupTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveCleanupTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce the auto-save to prevent excessive API calls
|
||||||
|
autoSaveCleanupTimeoutRef.current = setTimeout(async () => {
|
||||||
|
setIsAutoSavingCleanup(true);
|
||||||
|
|
||||||
|
const reqPayload: SaveConfigApiRequest = {
|
||||||
|
userId: user.id!,
|
||||||
|
githubConfig: config.githubConfig,
|
||||||
|
giteaConfig: config.giteaConfig,
|
||||||
|
scheduleConfig: config.scheduleConfig,
|
||||||
|
cleanupConfig: cleanupConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(reqPayload),
|
||||||
|
});
|
||||||
|
const result: SaveConfigApiResponse = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Silent success - no toast for auto-save
|
||||||
|
// Invalidate config cache so other components get fresh data
|
||||||
|
invalidateConfigCache();
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
|
{ duration: 3000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Auto-save error: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
{ duration: 3000 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsAutoSavingCleanup(false);
|
||||||
|
}
|
||||||
|
}, 500); // 500ms debounce
|
||||||
|
}, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig, config.scheduleConfig]);
|
||||||
|
|
||||||
|
// Cleanup timeouts on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (autoSaveTimeoutRef.current) {
|
if (autoSaveScheduleTimeoutRef.current) {
|
||||||
clearTimeout(autoSaveTimeoutRef.current);
|
clearTimeout(autoSaveScheduleTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (autoSaveCleanupTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveCleanupTimeoutRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -216,6 +282,8 @@ export function ConfigTabs() {
|
|||||||
response.giteaConfig || config.giteaConfig,
|
response.giteaConfig || config.giteaConfig,
|
||||||
scheduleConfig:
|
scheduleConfig:
|
||||||
response.scheduleConfig || config.scheduleConfig,
|
response.scheduleConfig || config.scheduleConfig,
|
||||||
|
cleanupConfig:
|
||||||
|
response.cleanupConfig || config.cleanupConfig,
|
||||||
});
|
});
|
||||||
if (response.id) setIsConfigSaved(true);
|
if (response.id) setIsConfigSaved(true);
|
||||||
}
|
}
|
||||||
@@ -273,11 +341,20 @@ export function ConfigTabs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg p-4">
|
<div className="flex gap-x-4">
|
||||||
<div className="space-y-4">
|
<div className="w-1/2 border rounded-lg p-4">
|
||||||
<Skeleton className="h-8 w-48" />
|
<div className="space-y-4">
|
||||||
<Skeleton className="h-16 w-full" />
|
<Skeleton className="h-8 w-48" />
|
||||||
<Skeleton className="h-8 w-32" />
|
<Skeleton className="h-16 w-full" />
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2 border rounded-lg p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
<Skeleton className="h-16 w-full" />
|
||||||
|
<Skeleton className="h-8 w-32" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -368,20 +445,40 @@ export function ConfigTabs() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ScheduleConfigForm
|
<div className="flex gap-x-4">
|
||||||
config={config.scheduleConfig}
|
<div className="w-1/2">
|
||||||
setConfig={update =>
|
<ScheduleConfigForm
|
||||||
setConfig(prev => ({
|
config={config.scheduleConfig}
|
||||||
...prev,
|
setConfig={update =>
|
||||||
scheduleConfig:
|
setConfig(prev => ({
|
||||||
typeof update === 'function'
|
...prev,
|
||||||
? update(prev.scheduleConfig)
|
scheduleConfig:
|
||||||
: update,
|
typeof update === 'function'
|
||||||
}))
|
? update(prev.scheduleConfig)
|
||||||
}
|
: update,
|
||||||
onAutoSave={autoSaveScheduleConfig}
|
}))
|
||||||
isAutoSaving={isAutoSaving}
|
}
|
||||||
/>
|
onAutoSave={autoSaveScheduleConfig}
|
||||||
|
isAutoSaving={isAutoSavingSchedule}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<DatabaseCleanupConfigForm
|
||||||
|
config={config.cleanupConfig}
|
||||||
|
setConfig={update =>
|
||||||
|
setConfig(prev => ({
|
||||||
|
...prev,
|
||||||
|
cleanupConfig:
|
||||||
|
typeof update === 'function'
|
||||||
|
? update(prev.cleanupConfig)
|
||||||
|
: update,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onAutoSave={autoSaveCleanupConfig}
|
||||||
|
isAutoSaving={isAutoSavingCleanup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
146
src/components/config/DatabaseCleanupConfigForm.tsx
Normal file
146
src/components/config/DatabaseCleanupConfigForm.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Checkbox } from "../ui/checkbox";
|
||||||
|
import type { DatabaseCleanupConfig } from "@/types/config";
|
||||||
|
import { formatDate } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../ui/select";
|
||||||
|
import { RefreshCw, Database } from "lucide-react";
|
||||||
|
|
||||||
|
interface DatabaseCleanupConfigFormProps {
|
||||||
|
config: DatabaseCleanupConfig;
|
||||||
|
setConfig: React.Dispatch<React.SetStateAction<DatabaseCleanupConfig>>;
|
||||||
|
onAutoSave?: (config: DatabaseCleanupConfig) => void;
|
||||||
|
isAutoSaving?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DatabaseCleanupConfigForm({
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
onAutoSave,
|
||||||
|
isAutoSaving = false,
|
||||||
|
}: DatabaseCleanupConfigFormProps) {
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
const { name, value, type } = e.target;
|
||||||
|
const newConfig = {
|
||||||
|
...config,
|
||||||
|
[name]:
|
||||||
|
type === "checkbox" ? (e.target as HTMLInputElement).checked : value,
|
||||||
|
};
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
|
// Trigger auto-save for cleanup config changes
|
||||||
|
if (onAutoSave) {
|
||||||
|
onAutoSave(newConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Predefined retention periods
|
||||||
|
const retentionOptions: { value: number; label: string }[] = [
|
||||||
|
{ value: 1, label: "1 day" },
|
||||||
|
{ value: 3, label: "3 days" },
|
||||||
|
{ value: 7, label: "7 days" },
|
||||||
|
{ value: 14, label: "14 days" },
|
||||||
|
{ value: 30, label: "30 days" },
|
||||||
|
{ value: 60, label: "60 days" },
|
||||||
|
{ value: 90, label: "90 days" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6 relative">
|
||||||
|
{isAutoSaving && (
|
||||||
|
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
|
||||||
|
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
|
||||||
|
<span className="text-xs">Auto-saving...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="cleanup-enabled"
|
||||||
|
name="enabled"
|
||||||
|
checked={config.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange({
|
||||||
|
target: {
|
||||||
|
name: "enabled",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: Boolean(checked),
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="cleanup-enabled"
|
||||||
|
className="select-none ml-2 block text-sm font-medium"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
Enable Automatic Database Cleanup
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.enabled && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
Retention Period
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
name="retentionDays"
|
||||||
|
value={String(config.retentionDays)}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleChange({
|
||||||
|
target: { name: "retentionDays", value },
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
||||||
|
<SelectValue placeholder="Select retention period" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
||||||
|
{retentionOptions.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value.toString()}
|
||||||
|
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Activities and events older than this period will be automatically deleted.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Last Run</label>
|
||||||
|
<div className="text-sm">
|
||||||
|
{config.lastRun ? formatDate(config.lastRun) : "Never"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.nextRun && config.enabled && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Next Run</label>
|
||||||
|
<div className="text-sm">{formatDate(config.nextRun)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -90,51 +90,53 @@ export function ScheduleConfigForm({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{config.enabled && (
|
||||||
<label
|
|
||||||
htmlFor="interval"
|
|
||||||
className="block text-sm font-medium mb-1.5"
|
|
||||||
>
|
|
||||||
Mirroring Interval
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
name="interval"
|
|
||||||
value={String(config.interval)}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleChange({
|
|
||||||
target: { name: "interval", value },
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
|
||||||
<SelectValue placeholder="Select interval" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
|
||||||
{intervals.map((interval) => (
|
|
||||||
<SelectItem
|
|
||||||
key={interval.value}
|
|
||||||
value={interval.value.toString()}
|
|
||||||
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
{interval.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
How often the mirroring process should run.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{config.lastRun && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Last Run</label>
|
<label
|
||||||
<div className="text-sm">{formatDate(config.lastRun)}</div>
|
htmlFor="interval"
|
||||||
|
className="block text-sm font-medium mb-1.5"
|
||||||
|
>
|
||||||
|
Mirroring Interval
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
name="interval"
|
||||||
|
value={String(config.interval)}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
handleChange({
|
||||||
|
target: { name: "interval", value },
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
||||||
|
<SelectValue placeholder="Select interval" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
||||||
|
{intervals.map((interval) => (
|
||||||
|
<SelectItem
|
||||||
|
key={interval.value}
|
||||||
|
value={interval.value.toString()}
|
||||||
|
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
||||||
|
>
|
||||||
|
{interval.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
How often the mirroring process should run.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Last Run</label>
|
||||||
|
<div className="text-sm">
|
||||||
|
{config.lastRun ? formatDate(config.lastRun) : "Never"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{config.nextRun && config.enabled && (
|
{config.nextRun && config.enabled && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Next Run</label>
|
<label className="block text-sm font-medium mb-1">Next Run</label>
|
||||||
|
|||||||
191
src/lib/cleanup-service.ts
Normal file
191
src/lib/cleanup-service.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Background cleanup service for automatic database maintenance
|
||||||
|
* This service runs periodically to clean up old events and mirror jobs
|
||||||
|
* based on user configuration settings
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db, configs, events, mirrorJobs } from "@/lib/db";
|
||||||
|
import { eq, lt, and } from "drizzle-orm";
|
||||||
|
|
||||||
|
interface CleanupResult {
|
||||||
|
userId: string;
|
||||||
|
eventsDeleted: number;
|
||||||
|
mirrorJobsDeleted: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old events and mirror jobs for a specific user
|
||||||
|
*/
|
||||||
|
async function cleanupForUser(userId: string, retentionDays: number): Promise<CleanupResult> {
|
||||||
|
try {
|
||||||
|
console.log(`Running cleanup for user ${userId} with ${retentionDays} days retention`);
|
||||||
|
|
||||||
|
// Calculate cutoff date
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
||||||
|
|
||||||
|
let eventsDeleted = 0;
|
||||||
|
let mirrorJobsDeleted = 0;
|
||||||
|
|
||||||
|
// Clean up old events
|
||||||
|
const eventsResult = await db
|
||||||
|
.delete(events)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(events.userId, userId),
|
||||||
|
lt(events.createdAt, cutoffDate)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
eventsDeleted = eventsResult.changes || 0;
|
||||||
|
|
||||||
|
// Clean up old mirror jobs (only completed ones)
|
||||||
|
const jobsResult = await db
|
||||||
|
.delete(mirrorJobs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(mirrorJobs.userId, userId),
|
||||||
|
eq(mirrorJobs.inProgress, false),
|
||||||
|
lt(mirrorJobs.timestamp, cutoffDate)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
mirrorJobsDeleted = jobsResult.changes || 0;
|
||||||
|
|
||||||
|
console.log(`Cleanup completed for user ${userId}: ${eventsDeleted} events, ${mirrorJobsDeleted} jobs deleted`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
eventsDeleted,
|
||||||
|
mirrorJobsDeleted,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error during cleanup for user ${userId}:`, error);
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
eventsDeleted: 0,
|
||||||
|
mirrorJobsDeleted: 0,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the cleanup configuration with last run time and calculate next run
|
||||||
|
*/
|
||||||
|
async function updateCleanupConfig(userId: string, cleanupConfig: any) {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const nextRun = new Date(now.getTime() + 24 * 60 * 60 * 1000); // Next day
|
||||||
|
|
||||||
|
const updatedConfig = {
|
||||||
|
...cleanupConfig,
|
||||||
|
lastRun: now,
|
||||||
|
nextRun: nextRun,
|
||||||
|
};
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(configs)
|
||||||
|
.set({
|
||||||
|
cleanupConfig: updatedConfig,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.where(eq(configs.userId, userId));
|
||||||
|
|
||||||
|
console.log(`Updated cleanup config for user ${userId}, next run: ${nextRun.toISOString()}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating cleanup config for user ${userId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run automatic cleanup for all users with cleanup enabled
|
||||||
|
*/
|
||||||
|
export async function runAutomaticCleanup(): Promise<CleanupResult[]> {
|
||||||
|
try {
|
||||||
|
console.log('Starting automatic cleanup service...');
|
||||||
|
|
||||||
|
// Get all users with cleanup enabled
|
||||||
|
const userConfigs = await db
|
||||||
|
.select()
|
||||||
|
.from(configs)
|
||||||
|
.where(eq(configs.isActive, true));
|
||||||
|
|
||||||
|
const results: CleanupResult[] = [];
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const config of userConfigs) {
|
||||||
|
try {
|
||||||
|
const cleanupConfig = config.cleanupConfig;
|
||||||
|
|
||||||
|
// Skip if cleanup is not enabled
|
||||||
|
if (!cleanupConfig?.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's time to run cleanup
|
||||||
|
const nextRun = cleanupConfig.nextRun ? new Date(cleanupConfig.nextRun) : null;
|
||||||
|
|
||||||
|
// If nextRun is null or in the past, run cleanup
|
||||||
|
if (!nextRun || now >= nextRun) {
|
||||||
|
const result = await cleanupForUser(config.userId, cleanupConfig.retentionDays || 7);
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
// Update the cleanup config with new run times
|
||||||
|
await updateCleanupConfig(config.userId, cleanupConfig);
|
||||||
|
} else {
|
||||||
|
console.log(`Skipping cleanup for user ${config.userId}, next run: ${nextRun.toISOString()}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing cleanup for user ${config.userId}:`, error);
|
||||||
|
results.push({
|
||||||
|
userId: config.userId,
|
||||||
|
eventsDeleted: 0,
|
||||||
|
mirrorJobsDeleted: 0,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Automatic cleanup completed. Processed ${results.length} users.`);
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in automatic cleanup service:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the cleanup service with periodic execution
|
||||||
|
* This should be called when the application starts
|
||||||
|
*/
|
||||||
|
export function startCleanupService() {
|
||||||
|
console.log('Starting background cleanup service...');
|
||||||
|
|
||||||
|
// Run cleanup every hour
|
||||||
|
const CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||||
|
|
||||||
|
// Run initial cleanup after 5 minutes to allow app to fully start
|
||||||
|
setTimeout(() => {
|
||||||
|
runAutomaticCleanup().catch(error => {
|
||||||
|
console.error('Error in initial cleanup run:', error);
|
||||||
|
});
|
||||||
|
}, 5 * 60 * 1000); // 5 minutes
|
||||||
|
|
||||||
|
// Set up periodic cleanup
|
||||||
|
setInterval(() => {
|
||||||
|
runAutomaticCleanup().catch(error => {
|
||||||
|
console.error('Error in periodic cleanup run:', error);
|
||||||
|
});
|
||||||
|
}, CLEANUP_INTERVAL);
|
||||||
|
|
||||||
|
console.log(`✅ Cleanup service started. Will run every ${CLEANUP_INTERVAL / 1000 / 60} minutes.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the cleanup service (for testing or shutdown)
|
||||||
|
*/
|
||||||
|
export function stopCleanupService() {
|
||||||
|
// Note: In a real implementation, you'd want to track the interval ID
|
||||||
|
// and clear it here. For now, this is a placeholder.
|
||||||
|
console.log('Cleanup service stop requested (not implemented)');
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ export const events = sqliteTable("events", {
|
|||||||
const githubSchema = configSchema.shape.githubConfig;
|
const githubSchema = configSchema.shape.githubConfig;
|
||||||
const giteaSchema = configSchema.shape.giteaConfig;
|
const giteaSchema = configSchema.shape.giteaConfig;
|
||||||
const scheduleSchema = configSchema.shape.scheduleConfig;
|
const scheduleSchema = configSchema.shape.scheduleConfig;
|
||||||
|
const cleanupSchema = configSchema.shape.cleanupConfig;
|
||||||
|
|
||||||
export const configs = sqliteTable("configs", {
|
export const configs = sqliteTable("configs", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
@@ -112,6 +113,10 @@ export const configs = sqliteTable("configs", {
|
|||||||
.$type<z.infer<typeof scheduleSchema>>()
|
.$type<z.infer<typeof scheduleSchema>>()
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
|
||||||
|
cleanupConfig: text("cleanup_config", { mode: "json" })
|
||||||
|
.$type<z.infer<typeof cleanupSchema>>()
|
||||||
|
.notNull(),
|
||||||
|
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(new Date()),
|
.default(new Date()),
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ export const configSchema = z.object({
|
|||||||
lastRun: z.date().optional(),
|
lastRun: z.date().optional(),
|
||||||
nextRun: z.date().optional(),
|
nextRun: z.date().optional(),
|
||||||
}),
|
}),
|
||||||
|
cleanupConfig: z.object({
|
||||||
|
enabled: z.boolean().default(false),
|
||||||
|
retentionDays: z.number().min(1).default(7), // in days
|
||||||
|
lastRun: z.date().optional(),
|
||||||
|
nextRun: z.date().optional(),
|
||||||
|
}),
|
||||||
createdAt: z.date().default(() => new Date()),
|
createdAt: z.date().default(() => new Date()),
|
||||||
updatedAt: z.date().default(() => new Date()),
|
updatedAt: z.date().default(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { defineMiddleware } from 'astro:middleware';
|
import { defineMiddleware } from 'astro:middleware';
|
||||||
import { initializeRecovery, hasJobsNeedingRecovery, getRecoveryStatus } from './lib/recovery';
|
import { initializeRecovery, hasJobsNeedingRecovery, getRecoveryStatus } from './lib/recovery';
|
||||||
|
import { startCleanupService } from './lib/cleanup-service';
|
||||||
|
|
||||||
// Flag to track if recovery has been initialized
|
// Flag to track if recovery has been initialized
|
||||||
let recoveryInitialized = false;
|
let recoveryInitialized = false;
|
||||||
let recoveryAttempted = false;
|
let recoveryAttempted = false;
|
||||||
|
let cleanupServiceStarted = false;
|
||||||
|
|
||||||
export const onRequest = defineMiddleware(async (context, next) => {
|
export const onRequest = defineMiddleware(async (context, next) => {
|
||||||
// Initialize recovery system only once when the server starts
|
// Initialize recovery system only once when the server starts
|
||||||
@@ -53,6 +55,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start cleanup service only once after recovery is complete
|
||||||
|
if (recoveryInitialized && !cleanupServiceStarted) {
|
||||||
|
try {
|
||||||
|
console.log('Starting automatic database cleanup service...');
|
||||||
|
startCleanupService();
|
||||||
|
cleanupServiceStarted = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start cleanup service:', error);
|
||||||
|
// Don't fail the request if cleanup service fails to start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Continue with the request
|
// Continue with the request
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|||||||
72
src/pages/api/cleanup/auto.ts
Normal file
72
src/pages/api/cleanup/auto.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* API endpoint to manually trigger automatic cleanup
|
||||||
|
* This is useful for testing and debugging the cleanup service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { runAutomaticCleanup } from '@/lib/cleanup-service';
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
console.log('Manual cleanup trigger requested');
|
||||||
|
|
||||||
|
// Run the automatic cleanup
|
||||||
|
const results = await runAutomaticCleanup();
|
||||||
|
|
||||||
|
// Calculate totals
|
||||||
|
const totalEventsDeleted = results.reduce((sum, result) => sum + result.eventsDeleted, 0);
|
||||||
|
const totalJobsDeleted = results.reduce((sum, result) => sum + result.mirrorJobsDeleted, 0);
|
||||||
|
const errors = results.filter(result => result.error);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Automatic cleanup completed',
|
||||||
|
results: {
|
||||||
|
usersProcessed: results.length,
|
||||||
|
totalEventsDeleted,
|
||||||
|
totalJobsDeleted,
|
||||||
|
errors: errors.length,
|
||||||
|
details: results,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in manual cleanup trigger:', error);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to run automatic cleanup',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'Use POST method to trigger cleanup',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 405,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,14 +6,14 @@ import { eq } from "drizzle-orm";
|
|||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { userId, githubConfig, giteaConfig, scheduleConfig } = body;
|
const { userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig } = body;
|
||||||
|
|
||||||
if (!userId || !githubConfig || !giteaConfig || !scheduleConfig) {
|
if (!userId || !githubConfig || !giteaConfig || !scheduleConfig || !cleanupConfig) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
"userId, githubConfig, giteaConfig, and scheduleConfig are required.",
|
"userId, githubConfig, giteaConfig, scheduleConfig, and cleanupConfig are required.",
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
@@ -64,6 +64,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
githubConfig,
|
githubConfig,
|
||||||
giteaConfig,
|
giteaConfig,
|
||||||
scheduleConfig,
|
scheduleConfig,
|
||||||
|
cleanupConfig,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(configs.id, existingConfig.id));
|
.where(eq(configs.id, existingConfig.id));
|
||||||
@@ -113,6 +114,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
include: [],
|
include: [],
|
||||||
exclude: [],
|
exclude: [],
|
||||||
scheduleConfig,
|
scheduleConfig,
|
||||||
|
cleanupConfig,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
@@ -197,6 +199,12 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
lastRun: null,
|
lastRun: null,
|
||||||
nextRun: null,
|
nextRun: null,
|
||||||
},
|
},
|
||||||
|
cleanupConfig: {
|
||||||
|
enabled: false,
|
||||||
|
retentionDays: 7,
|
||||||
|
lastRun: null,
|
||||||
|
nextRun: null,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ export interface ScheduleConfig {
|
|||||||
nextRun?: Date;
|
nextRun?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DatabaseCleanupConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
retentionDays: number;
|
||||||
|
lastRun?: Date;
|
||||||
|
nextRun?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitHubConfig {
|
export interface GitHubConfig {
|
||||||
username: string;
|
username: string;
|
||||||
token: string;
|
token: string;
|
||||||
@@ -34,6 +41,7 @@ export interface SaveConfigApiRequest {
|
|||||||
githubConfig: GitHubConfig;
|
githubConfig: GitHubConfig;
|
||||||
giteaConfig: GiteaConfig;
|
giteaConfig: GiteaConfig;
|
||||||
scheduleConfig: ScheduleConfig;
|
scheduleConfig: ScheduleConfig;
|
||||||
|
cleanupConfig: DatabaseCleanupConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SaveConfigApiResponse {
|
export interface SaveConfigApiResponse {
|
||||||
@@ -55,6 +63,7 @@ export interface ConfigApiResponse {
|
|||||||
githubConfig: GitHubConfig;
|
githubConfig: GitHubConfig;
|
||||||
giteaConfig: GiteaConfig;
|
giteaConfig: GiteaConfig;
|
||||||
scheduleConfig: ScheduleConfig;
|
scheduleConfig: ScheduleConfig;
|
||||||
|
cleanupConfig: DatabaseCleanupConfig;
|
||||||
include: string[];
|
include: string[];
|
||||||
exclude: string[];
|
exclude: string[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|||||||
Reference in New Issue
Block a user