Add packet cleanup

This commit is contained in:
Jack Kingsman
2026-01-13 12:49:27 -08:00
parent 3ea98e309c
commit 547a2adb94
14 changed files with 573 additions and 44 deletions
+5
View File
@@ -653,11 +653,16 @@ export function App() {
open={showConfig}
config={config}
appSettings={appSettings}
health={health}
onClose={() => setShowConfig(false)}
onSave={handleSaveConfig}
onSaveAppSettings={handleSaveAppSettings}
onSetPrivateKey={handleSetPrivateKey}
onReboot={handleReboot}
onHealthRefresh={async () => {
const data = await api.getHealth();
setHealth(data);
}}
/>
<Toaster position="top-right" />
+9
View File
@@ -4,7 +4,9 @@ import type {
Channel,
CommandResponse,
Contact,
DedupResult,
HealthStatus,
MaintenanceResult,
Message,
RadioConfig,
RadioConfigUpdate,
@@ -164,6 +166,13 @@ export const api = {
method: 'POST',
body: JSON.stringify(params),
}),
runMaintenance: (pruneUndecryptedDays: number) =>
fetchJson<MaintenanceResult>('/packets/maintenance', {
method: 'POST',
body: JSON.stringify({ prune_undecrypted_days: pruneUndecryptedDays }),
}),
deduplicatePackets: () =>
fetchJson<DedupResult>('/packets/dedup', { method: 'POST' }),
// Read State
markAllRead: () =>
+108 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import type { AppSettings, AppSettingsUpdate, RadioConfig, RadioConfigUpdate } from '../types';
import type { AppSettings, AppSettingsUpdate, HealthStatus, RadioConfig, RadioConfigUpdate } from '../types';
import {
Dialog,
DialogContent,
@@ -12,27 +12,33 @@ import { Label } from './ui/label';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Alert, AlertDescription } from './ui/alert';
import { toast } from './ui/sonner';
import { api } from '../api';
interface ConfigModalProps {
open: boolean;
config: RadioConfig | null;
appSettings: AppSettings | null;
health: HealthStatus | null;
onClose: () => void;
onSave: (update: RadioConfigUpdate) => Promise<void>;
onSaveAppSettings: (update: AppSettingsUpdate) => Promise<void>;
onSetPrivateKey: (key: string) => Promise<void>;
onReboot: () => Promise<void>;
onHealthRefresh: () => Promise<void>;
}
export function ConfigModal({
open,
config,
appSettings,
health,
onClose,
onSave,
onSaveAppSettings,
onSetPrivateKey,
onReboot,
onHealthRefresh,
}: ConfigModalProps) {
const [name, setName] = useState('');
const [lat, setLat] = useState('');
@@ -44,8 +50,11 @@ export function ConfigModal({
const [cr, setCr] = useState('');
const [privateKey, setPrivateKey] = useState('');
const [maxRadioContacts, setMaxRadioContacts] = useState('');
const [retentionDays, setRetentionDays] = useState('14');
const [loading, setLoading] = useState(false);
const [rebooting, setRebooting] = useState(false);
const [cleaning, setCleaning] = useState(false);
const [deduping, setDeduping] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
@@ -135,6 +144,58 @@ export function ConfigModal({
}
};
const handleCleanup = async () => {
const days = parseInt(retentionDays, 10);
if (isNaN(days) || days < 1) {
setError('Retention days must be at least 1');
return;
}
setError('');
setCleaning(true);
try {
const result = await api.runMaintenance(days);
toast.success('Database cleanup complete', {
description: `Deleted ${result.packets_deleted} old packet${result.packets_deleted === 1 ? '' : 's'}`,
});
// Refresh health to get updated database size
await onHealthRefresh();
} catch (err) {
console.error('Failed to run maintenance:', err);
toast.error('Database cleanup failed', {
description: err instanceof Error ? err.message : 'Unknown error',
});
} finally {
setCleaning(false);
}
};
const handleDedup = async () => {
setError('');
setDeduping(true);
try {
const result = await api.deduplicatePackets();
if (result.started) {
toast.success('Deduplication started', {
description: result.message,
});
} else {
toast.info('Deduplication', {
description: result.message,
});
}
} catch (err) {
console.error('Failed to start deduplication:', err);
toast.error('Deduplication failed', {
description: err instanceof Error ? err.message : 'Unknown error',
});
} finally {
setDeduping(false);
}
};
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
@@ -308,6 +369,52 @@ export function ConfigModal({
</Button>
</div>
<Separator className="my-4" />
<div className="space-y-3">
<Label>Database Maintenance</Label>
<p className="text-xs text-muted-foreground">
Current database size: <span className="font-medium">{health?.database_size_mb ?? '?'} MB</span>
</p>
<p className="text-xs text-muted-foreground">
Delete undecrypted packets older than the specified days. This helps manage storage
for packets that couldn't be decrypted (unknown channel keys).
</p>
<div className="flex gap-2 items-end">
<div className="space-y-1">
<Label htmlFor="retention-days" className="text-xs">Days to retain</Label>
<Input
id="retention-days"
type="number"
min="1"
max="365"
value={retentionDays}
onChange={(e) => setRetentionDays(e.target.value)}
className="w-20"
/>
</div>
<Button
variant="outline"
onClick={handleCleanup}
disabled={cleaning || loading}
>
{cleaning ? 'Cleaning...' : 'Cleanup'}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4">
Remove packets with duplicate payloads (same message received via different paths).
Runs in background and may take a long time.
</p>
<Button
variant="outline"
onClick={handleDedup}
disabled={deduping || loading}
>
{deduping ? 'Starting...' : 'Remove Duplicates'}
</Button>
</div>
{error && (
<div className="text-sm text-destructive">{error}</div>
)}
+12
View File
@@ -41,6 +41,18 @@ export interface HealthStatus {
status: string;
radio_connected: boolean;
serial_port: string | null;
database_size_mb: number;
}
export interface MaintenanceResult {
packets_deleted: number;
vacuumed: boolean;
}
export interface DedupResult {
started: boolean;
total_packets: number;
message: string;
}
export interface Contact {