diff --git a/backend/app/services/purge.py b/backend/app/services/purge.py index 0331904..549d00d 100644 --- a/backend/app/services/purge.py +++ b/backend/app/services/purge.py @@ -93,6 +93,20 @@ async def run_purge( await db.commit() + # Clean up orphaned sessions (sessions with no remaining jobs) + if purge_all: + orphan_cursor = await db.execute( + """ + DELETE FROM sessions + WHERE id NOT IN (SELECT DISTINCT session_id FROM jobs) + """ + ) + sessions_deleted = orphan_cursor.rowcount + await db.commit() + logger.info("Purge: removed %d orphaned sessions", sessions_deleted) + else: + sessions_deleted = 0 + # Count skipped active jobs for observability active_cursor = await db.execute( "SELECT COUNT(*) FROM jobs WHERE status IN ('queued', 'extracting', 'downloading')" @@ -105,6 +119,7 @@ async def run_purge( "files_deleted": files_deleted, "files_missing": files_missing, "active_skipped": active_skipped, + "sessions_deleted": sessions_deleted, "deleted_job_ids": deleted_job_ids, } diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue index 2ad52cf..ca673db 100644 --- a/frontend/src/components/AdminPanel.vue +++ b/frontend/src/components/AdminPanel.vue @@ -23,6 +23,9 @@ const defaultAudioFormat = ref('auto') const settingsSaved = ref(false) const privacyMode = ref(false) const privacyRetentionHours = ref(24) +const privacySaved = ref(false) +const purgeConfirming = ref(false) +let purgeConfirmTimer: ReturnType | null = null // Change password state const currentPassword = ref('') @@ -70,14 +73,35 @@ async function saveSettings() { welcome_message: welcomeMessage.value, default_video_format: defaultVideoFormat.value, default_audio_format: defaultAudioFormat.value, + }) + if (ok) { + await configStore.loadConfig() + settingsSaved.value = true + setTimeout(() => { settingsSaved.value = false }, 3000) + } +} + +async function savePrivacy() { + privacySaved.value = false + const ok = await store.updateSettings({ privacy_mode: privacyMode.value, privacy_retention_hours: privacyRetentionHours.value, }) if (ok) { - // Reload public config so main page picks up new defaults await configStore.loadConfig() - settingsSaved.value = true - setTimeout(() => { settingsSaved.value = false }, 3000) + privacySaved.value = true + setTimeout(() => { privacySaved.value = false }, 3000) + } +} + +function handlePurgeClick() { + if (purgeConfirming.value) { + purgeConfirming.value = false + if (purgeConfirmTimer) clearTimeout(purgeConfirmTimer) + store.triggerPurge() + } else { + purgeConfirming.value = true + purgeConfirmTimer = setTimeout(() => { purgeConfirming.value = false }, 3000) } } @@ -290,18 +314,11 @@ function formatFilesize(bytes: number | null): string {
- ✓ Saved
-

- Changes are applied immediately but reset on server restart. -


@@ -320,7 +337,7 @@ function formatFilesize(bytes: number | null): string {

Automatically purge download history, files, and session data - after the retention period. Changes are saved with the button above. + after the retention period.

@@ -340,24 +357,33 @@ function formatFilesize(bytes: number | null): string {
-
+
+ + ✓ Saved +
+ +

Immediately clear all completed and failed downloads — removes - database records and files from disk. Active downloads are never affected. + database records, files from disk, and orphaned sessions. + Active downloads are never affected.

-

Rows deleted: {{ store.purgeResult.rows_deleted }}

-

Files deleted: {{ store.purgeResult.files_deleted }}

-

Files already gone: {{ store.purgeResult.files_missing }}

-

Active jobs skipped: {{ store.purgeResult.active_skipped }}

+

{{ store.purgeResult.rows_deleted }} jobs removed

+

{{ store.purgeResult.files_deleted }} files deleted

+

{{ store.purgeResult.sessions_deleted }} sessions cleared

+

{{ store.purgeResult.active_skipped }} active jobs skipped

@@ -414,6 +440,10 @@ function formatFilesize(bytes: number | null): string { + +

+ All settings are applied immediately but reset on server restart. +

@@ -532,12 +562,18 @@ h3 { color: var(--color-bg); font-weight: 600; margin-top: var(--space-md); + min-width: 130px; + transition: background-color 0.15s; } .btn-purge:hover:not(:disabled) { background: var(--color-error); } +.btn-purge.btn-confirm { + background: var(--color-error); +} + .purge-result { margin-top: var(--space-md); padding: var(--space-md); diff --git a/frontend/src/stores/admin.ts b/frontend/src/stores/admin.ts index b5bb52d..fbd348c 100644 --- a/frontend/src/stores/admin.ts +++ b/frontend/src/stores/admin.ts @@ -23,6 +23,7 @@ interface PurgeResult { files_deleted: number files_missing: number active_skipped: number + sessions_deleted?: number } export const useAdminStore = defineStore('admin', () => {