feat: add session auto-cleanup settings and functionality

This commit is contained in:
Bohdan Triapitsyn
2025-12-20 02:06:00 +02:00
parent 79ed6abab7
commit 53c103a222
14 changed files with 571 additions and 5 deletions
@@ -0,0 +1,136 @@
import React from 'react';
import { toast } from 'sonner';
import { RiInformationLine } from '@remixicon/react';
import { NumberInput } from '@/components/ui/number-input';
import { ButtonSmall } from '@/components/ui/button-small';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useDeviceInfo } from '@/lib/device';
import { useUIStore } from '@/stores/useUIStore';
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
const MIN_DAYS = 1;
const MAX_DAYS = 365;
export const SessionRetentionSettings: React.FC = () => {
const { isMobile } = useDeviceInfo();
const autoDeleteEnabled = useUIStore((state) => state.autoDeleteEnabled);
const autoDeleteAfterDays = useUIStore((state) => state.autoDeleteAfterDays);
const setAutoDeleteEnabled = useUIStore((state) => state.setAutoDeleteEnabled);
const setAutoDeleteAfterDays = useUIStore((state) => state.setAutoDeleteAfterDays);
const [mobileDraftDays, setMobileDraftDays] = React.useState(String(autoDeleteAfterDays));
React.useEffect(() => {
setMobileDraftDays(String(autoDeleteAfterDays));
}, [autoDeleteAfterDays]);
const { candidates, isRunning, runCleanup, keepRecentCount } = useSessionAutoCleanup({ autoRun: false });
const pendingCount = candidates.length;
const handleRunCleanup = React.useCallback(async () => {
const result = await runCleanup({ force: true });
if (result.deletedIds.length === 0 && result.failedIds.length === 0) {
toast.message('No sessions eligible for deletion');
return;
}
if (result.deletedIds.length > 0) {
toast.success(`Deleted ${result.deletedIds.length} session${result.deletedIds.length === 1 ? '' : 's'}`);
}
if (result.failedIds.length > 0) {
toast.error(`Failed to delete ${result.failedIds.length} session${result.failedIds.length === 1 ? '' : 's'}`);
}
}, [runCleanup]);
return (
<div className="space-y-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="typography-ui-header font-semibold text-foreground">Session retention</h3>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
Automatically delete inactive sessions based on their last activity.<br />
You can also run a one-time cleanup without enabling auto-cleanup.<br />
Keeps the most recent 5 sessions, and never deletes shared sessions.
</TooltipContent>
</Tooltip>
</div>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
checked={autoDeleteEnabled}
onChange={(event) => setAutoDeleteEnabled(event.target.checked)}
/>
<span className="typography-ui-header font-semibold text-foreground">Enable auto-cleanup</span>
</label>
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-3">
{isMobile ? (
<input
type="number"
inputMode="numeric"
value={mobileDraftDays}
onChange={(event) => {
const nextValue = event.target.value;
setMobileDraftDays(nextValue);
if (nextValue.trim() === '') {
return;
}
const parsed = Number(nextValue);
if (!Number.isFinite(parsed)) {
return;
}
const clamped = Math.min(MAX_DAYS, Math.max(MIN_DAYS, Math.round(parsed)));
setAutoDeleteAfterDays(clamped);
}}
onBlur={() => {
if (mobileDraftDays.trim() === '') {
setMobileDraftDays(String(autoDeleteAfterDays));
return;
}
const parsed = Number(mobileDraftDays);
if (!Number.isFinite(parsed)) {
setMobileDraftDays(String(autoDeleteAfterDays));
return;
}
const clamped = Math.min(MAX_DAYS, Math.max(MIN_DAYS, Math.round(parsed)));
setAutoDeleteAfterDays(clamped);
setMobileDraftDays(String(clamped));
}}
aria-label="Retention period in days"
className="h-8 w-16 rounded-lg border border-border bg-background px-2 text-center typography-ui-label text-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/50"
/>
) : (
<NumberInput
value={autoDeleteAfterDays}
onValueChange={setAutoDeleteAfterDays}
min={MIN_DAYS}
max={MAX_DAYS}
step={1}
aria-label="Retention period in days"
/>
)}
<span className="typography-ui-label text-muted-foreground">days since last activity</span>
</div>
<ButtonSmall
type="button"
variant="outline"
onClick={handleRunCleanup}
disabled={isRunning}
>
{isRunning ? 'Cleaning up...' : 'Run cleanup now'}
</ButtonSmall>
</div>
<div className="typography-meta text-muted-foreground">
Eligible for deletion right now: {pendingCount}
</div>
</div>
);
};
@@ -1,6 +1,7 @@
import React from 'react';
import { AppearanceSettings } from './AppearanceSettings';
import { AboutSettings } from './AboutSettings';
import { SessionRetentionSettings } from './SessionRetentionSettings';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { useDeviceInfo } from '@/lib/device';
import { isWebRuntime } from '@/lib/desktop';
@@ -15,6 +16,9 @@ export const SettingsPage: React.FC = () => {
className="settings-page-body mx-auto max-w-3xl space-y-3 p-3 sm:space-y-6 sm:p-6"
>
<AppearanceSettings />
<div className="border-t border-border/40 pt-6">
<SessionRetentionSettings />
</div>
{showAbout && (
<div className="border-t border-border/40 pt-6">
<AboutSettings />