186 lines
7.1 KiB
JavaScript
186 lines
7.1 KiB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { Play, Wrench } from 'lucide-react';
|
||
import { toast } from 'sonner';
|
||
import { api } from '@/api';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||
import { FieldRow, Toggle, formatDateTime } from './adminShared';
|
||
|
||
export default function CleanupPanel() {
|
||
const [status, setStatus] = useState(null);
|
||
const [form, setForm] = useState({
|
||
import_sessions_enabled: true,
|
||
temp_exports_enabled: true,
|
||
temp_export_max_age_hours: 2,
|
||
backup_partials_enabled: true,
|
||
import_history_enabled: false,
|
||
import_history_max_age_days: 365,
|
||
});
|
||
const [saving, setSaving] = useState(false);
|
||
const [running, setRunning] = useState(false);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
const load = useCallback(async () => {
|
||
try {
|
||
const data = await api.adminCleanup();
|
||
setStatus(data);
|
||
if (data.settings) setForm(data.settings);
|
||
} catch (err) {
|
||
toast.error(err.message || 'Failed to load cleanup settings.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => { load(); }, [load]);
|
||
|
||
const set = (k, v) => setForm(prev => ({ ...prev, [k]: v }));
|
||
|
||
async function handleSave() {
|
||
setSaving(true);
|
||
try {
|
||
const next = await api.saveAdminCleanup(form);
|
||
if (next) setForm(next);
|
||
toast.success('Cleanup settings saved.');
|
||
} catch (err) {
|
||
toast.error(err.message || 'Failed to save cleanup settings.');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
}
|
||
|
||
async function handleRunNow() {
|
||
setRunning(true);
|
||
try {
|
||
const result = await api.runAdminCleanup();
|
||
setStatus(prev => ({
|
||
...prev,
|
||
last_run_at: result.ran_at,
|
||
last_result: result.tasks,
|
||
}));
|
||
toast.success('Cleanup tasks completed.');
|
||
} catch (err) {
|
||
toast.error(err.message || 'Cleanup run failed.');
|
||
} finally {
|
||
setRunning(false);
|
||
}
|
||
}
|
||
|
||
function resultLine(label, task, countKey) {
|
||
if (!task || task[countKey] == null) return null;
|
||
return `${label}: ${task[countKey]}`;
|
||
}
|
||
|
||
const resultLines = status?.last_result ? [
|
||
resultLine('Import sessions pruned', status.last_result.import_sessions, 'pruned'),
|
||
resultLine('Temp export files removed', status.last_result.temp_export_files, 'removed'),
|
||
resultLine('Backup partials removed', status.last_result.backup_partials, 'removed'),
|
||
resultLine('Import history rows pruned', status.last_result.import_history, 'pruned'),
|
||
].filter(Boolean) : [];
|
||
|
||
if (loading) {
|
||
return (
|
||
<Card>
|
||
<CardContent className="py-8 text-center text-muted-foreground text-sm">
|
||
Loading cleanup settings…
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Card>
|
||
<CardHeader className="pb-4">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div className="flex items-center gap-3">
|
||
<Wrench className="h-5 w-5 text-muted-foreground" />
|
||
<div>
|
||
<CardTitle>Cleanup / Maintenance</CardTitle>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
Automatic daily cleanup: expired import sessions, stale export temp files, orphaned backup partials. Runs at 6:00 AM.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Badge className="bg-emerald-500/15 text-emerald-400 border-emerald-500/20 shrink-0">Auto</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<CardContent className="space-y-6">
|
||
<div className="grid gap-3 md:grid-cols-2">
|
||
<div className="rounded-lg border border-border bg-muted/20 p-3">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Last Run</p>
|
||
<p className="text-sm font-medium mt-1">{status?.last_run_at ? formatDateTime(status.last_run_at) : '—'}</p>
|
||
</div>
|
||
<div className="rounded-lg border border-border bg-muted/20 p-3">
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Last Result</p>
|
||
{resultLines.length > 0 ? (
|
||
<ul className="mt-1 space-y-0.5">
|
||
{resultLines.map(line => (
|
||
<li key={line} className="text-xs text-muted-foreground">{line}</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<p className="text-sm font-medium mt-1 text-muted-foreground/60">No runs recorded yet</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<p className="text-sm font-medium">Task Settings</p>
|
||
{[
|
||
['import_sessions_enabled', 'Prune expired import sessions (24h TTL)'],
|
||
['temp_exports_enabled', 'Remove stale SQLite export temp files'],
|
||
['backup_partials_enabled', 'Remove orphaned backup .partial / .upload files'],
|
||
].map(([key, label]) => (
|
||
<FieldRow key={key} label={label}>
|
||
<Toggle checked={!!form[key]} onChange={v => set(key, v)} label={label} />
|
||
</FieldRow>
|
||
))}
|
||
|
||
<FieldRow label="Temp file max age (hours, 1–72)">
|
||
<Input
|
||
type="number" min="1" max="72"
|
||
value={form.temp_export_max_age_hours}
|
||
onChange={e => set('temp_export_max_age_hours', parseInt(e.target.value, 10) || 2)}
|
||
disabled={!form.temp_exports_enabled}
|
||
className="max-w-[120px] h-8 text-sm"
|
||
/>
|
||
</FieldRow>
|
||
|
||
<FieldRow label="Trim import history rows (off by default)">
|
||
<Toggle checked={!!form.import_history_enabled} onChange={v => set('import_history_enabled', v)} label="Trim import history rows" />
|
||
</FieldRow>
|
||
|
||
{form.import_history_enabled && (
|
||
<>
|
||
<FieldRow label="Import history max age (days, 30–3650)">
|
||
<Input
|
||
type="number" min="30" max="3650"
|
||
value={form.import_history_max_age_days}
|
||
onChange={e => set('import_history_max_age_days', parseInt(e.target.value, 10) || 365)}
|
||
className="max-w-[120px] h-8 text-sm"
|
||
/>
|
||
</FieldRow>
|
||
<div className="rounded-lg border border-amber-500/25 bg-amber-500/10 px-4 py-3 text-sm text-amber-600 dark:text-amber-400">
|
||
<strong>Warning:</strong> Import history trimming permanently deletes audit records older than {form.import_history_max_age_days} days. This cannot be undone.
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-border">
|
||
<Button onClick={handleSave} disabled={saving || running}>
|
||
{saving ? 'Saving…' : 'Save Settings'}
|
||
</Button>
|
||
<Button variant="outline" onClick={handleRunNow} disabled={saving || running}>
|
||
<Play className="h-4 w-4" />
|
||
{running ? 'Running…' : 'Run Cleanup Now'}
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|