From 11f07f350145108b64360f0f3f904d2be1c57b8c Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sat, 21 Feb 2026 00:10:29 -0800 Subject: [PATCH] Add endpoint for deleting raw packets of decrypted messages --- app/repository.py | 7 ++++ app/routers/packets.py | 36 +++++++++++++------ frontend/src/api.ts | 11 ++++-- frontend/src/components/SettingsModal.tsx | 43 ++++++++++++++++++++++- frontend/src/test/settingsModal.test.tsx | 17 +++++++++ tests/test_api.py | 34 ++++++++++++++++++ 6 files changed, 135 insertions(+), 13 deletions(-) diff --git a/app/repository.py b/app/repository.py index b090b69..4b1b39d 100644 --- a/app/repository.py +++ b/app/repository.py @@ -801,6 +801,13 @@ class RawPacketRepository: await db.conn.commit() return cursor.rowcount + @staticmethod + async def purge_linked_to_messages() -> int: + """Delete raw packets that are already linked to a stored message.""" + cursor = await db.conn.execute("DELETE FROM raw_packets WHERE message_id IS NOT NULL") + await db.conn.commit() + return cursor.rowcount + @staticmethod async def get_undecrypted_text_messages() -> list[tuple[int, bytes, int]]: """Get all undecrypted TEXT_MESSAGE packets as (id, data, timestamp) tuples. diff --git a/app/routers/packets.py b/app/routers/packets.py index 554f5f3..049ebb1 100644 --- a/app/routers/packets.py +++ b/app/routers/packets.py @@ -236,8 +236,12 @@ async def decrypt_historical_packets( class MaintenanceRequest(BaseModel): - prune_undecrypted_days: int = Field( - ge=1, description="Delete undecrypted packets older than this many days" + prune_undecrypted_days: int | None = Field( + default=None, ge=1, description="Delete undecrypted packets older than this many days" + ) + purge_linked_raw_packets: bool = Field( + default=False, + description="Delete raw packets already linked to a stored message", ) @@ -249,18 +253,30 @@ class MaintenanceResult(BaseModel): @router.post("/maintenance", response_model=MaintenanceResult) async def run_maintenance(request: MaintenanceRequest) -> MaintenanceResult: """ - Clean up old undecrypted packets and reclaim disk space. + Run packet maintenance tasks and reclaim disk space. - - Deletes undecrypted packets older than the specified number of days + - Optionally deletes undecrypted packets older than the specified number of days + - Optionally deletes raw packets already linked to stored messages - Runs VACUUM to reclaim disk space """ - logger.info( - "Running maintenance: pruning packets older than %d days", request.prune_undecrypted_days - ) + deleted = 0 - # Prune old undecrypted packets - deleted = await RawPacketRepository.prune_old_undecrypted(request.prune_undecrypted_days) - logger.info("Deleted %d old undecrypted packets", deleted) + if request.prune_undecrypted_days is not None: + logger.info( + "Running maintenance: pruning undecrypted packets older than %d days", + request.prune_undecrypted_days, + ) + pruned_undecrypted = await RawPacketRepository.prune_old_undecrypted( + request.prune_undecrypted_days + ) + deleted += pruned_undecrypted + logger.info("Deleted %d old undecrypted packets", pruned_undecrypted) + + if request.purge_linked_raw_packets: + logger.info("Running maintenance: purging raw packets linked to stored messages") + purged_linked = await RawPacketRepository.purge_linked_to_messages() + deleted += purged_linked + logger.info("Deleted %d linked raw packets", purged_linked) # Run VACUUM to reclaim space on a dedicated connection async with aiosqlite.connect(db.db_path) as vacuum_conn: diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 2b94af7..dd77764 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -183,10 +183,17 @@ export const api = { method: 'POST', body: JSON.stringify(params), }), - runMaintenance: (pruneUndecryptedDays: number) => + runMaintenance: (options: { pruneUndecryptedDays?: number; purgeLinkedRawPackets?: boolean }) => fetchJson('/packets/maintenance', { method: 'POST', - body: JSON.stringify({ prune_undecrypted_days: pruneUndecryptedDays }), + body: JSON.stringify({ + ...(options.pruneUndecryptedDays !== undefined && { + prune_undecrypted_days: options.pruneUndecryptedDays, + }), + ...(options.purgeLinkedRawPackets !== undefined && { + purge_linked_raw_packets: options.purgeLinkedRawPackets, + }), + }), }), // Read State diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 792bdd4..0e8b87c 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -145,6 +145,7 @@ export function SettingsModal(props: SettingsModalProps) { // Database maintenance state const [retentionDays, setRetentionDays] = useState('14'); const [cleaning, setCleaning] = useState(false); + const [purgingDecryptedRaw, setPurgingDecryptedRaw] = useState(false); const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false); const [reopenLastConversation, setReopenLastConversation] = useState( getReopenLastConversationEnabled @@ -509,7 +510,7 @@ export function SettingsModal(props: SettingsModalProps) { setCleaning(true); try { - const result = await api.runMaintenance(days); + const result = await api.runMaintenance({ pruneUndecryptedDays: days }); toast.success('Database cleanup complete', { description: `Deleted ${result.packets_deleted} old packet${result.packets_deleted === 1 ? '' : 's'}`, }); @@ -524,6 +525,25 @@ export function SettingsModal(props: SettingsModalProps) { } }; + const handlePurgeDecryptedRawPackets = async () => { + setPurgingDecryptedRaw(true); + + try { + const result = await api.runMaintenance({ purgeLinkedRawPackets: true }); + toast.success('Decrypted raw packets purged', { + description: `Deleted ${result.packets_deleted} raw packet${result.packets_deleted === 1 ? '' : 's'}`, + }); + await onHealthRefresh(); + } catch (err) { + console.error('Failed to purge decrypted raw packets:', err); + toast.error('Failed to purge decrypted raw packets', { + description: err instanceof Error ? err.message : 'Unknown error', + }); + } finally { + setPurgingDecryptedRaw(false); + } + }; + const handleSaveDatabaseSettings = async () => { setBusySection('database'); setSectionError(null); @@ -1046,6 +1066,27 @@ export function SettingsModal(props: SettingsModalProps) { +
+ +

+ Deletes raw packet bytes for messages already stored in chat history. No displayed + message data is lost. Only use this if you do not plan to run manual analytics or + parsing on original packet bytes. +

+ +
+ + +