BillTracker/client/components/admin/CleanupPanel.jsx

194 lines
7.5 KiB
React
Raw Normal View History

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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
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>
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<Badge className="bg-emerald-500/15 text-emerald-400 border-emerald-500/20 shrink-0 cursor-default">Auto</Badge>
</TooltipTrigger>
<TooltipContent>Cleanup runs automatically at 6:00 AM daily</TooltipContent>
</Tooltip>
</TooltipProvider>
</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, 172)">
<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, 303650)">
<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>
);
}