BillTracker/client/components/admin/CleanupPanel.jsx

194 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}