mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 02:53:58 -06:00
Settings flow rework, purge sessions, confirmation gate
Settings flow: - Each section has its own Save button — no ambiguous shared button - Appearance & Defaults: Save covers welcome message + output formats - Privacy & Data: Save covers privacy toggle + retention hours - Security: Change Password button is self-contained - Bottom note clarifies all settings reset on server restart Purge improvements: - Now clears orphaned sessions (sessions with no remaining jobs) - 'Sure?' confirmation gate on manual purge (3s timeout revert) - Purge result shows sessions_deleted count - Cleaner result display: 'X jobs removed' instead of 'Rows deleted: X' SSE broker: - publish_all() broadcasts to all sessions (used for purge)
This commit is contained in:
parent
dd60505f5a
commit
fe45fdce50
3 changed files with 73 additions and 21 deletions
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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 {
|
|||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button
|
||||
@click="saveSettings"
|
||||
:disabled="store.isLoading"
|
||||
class="btn-save"
|
||||
>
|
||||
{{ store.isLoading ? 'Saving…' : 'Save Settings' }}
|
||||
<button @click="saveSettings" :disabled="store.isLoading" class="btn-save">
|
||||
{{ store.isLoading ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
<span v-if="settingsSaved" class="save-confirm">✓ Saved</span>
|
||||
</div>
|
||||
<p class="field-hint" style="margin-top: var(--space-sm);">
|
||||
Changes are applied immediately but reset on server restart.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr class="settings-divider" />
|
||||
|
|
@ -320,7 +337,7 @@ function formatFilesize(bytes: number | null): string {
|
|||
</label>
|
||||
<p class="field-hint">
|
||||
Automatically purge download history, files, and session data
|
||||
after the retention period. Changes are saved with the button above.
|
||||
after the retention period.
|
||||
</p>
|
||||
<div v-if="privacyMode" class="retention-setting">
|
||||
<label class="retention-label">Retention period</label>
|
||||
|
|
@ -340,24 +357,33 @@ function formatFilesize(bytes: number | null): string {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-field" style="margin-top: var(--space-md);">
|
||||
<div class="settings-actions">
|
||||
<button @click="savePrivacy" :disabled="store.isLoading" class="btn-save">
|
||||
{{ store.isLoading ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
<span v-if="privacySaved" class="save-confirm">✓ Saved</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-field" style="margin-top: var(--space-lg);">
|
||||
<label>Manual Purge</label>
|
||||
<p class="field-hint">
|
||||
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.
|
||||
</p>
|
||||
<button
|
||||
@click="store.triggerPurge()"
|
||||
@click="handlePurgeClick"
|
||||
:disabled="store.isLoading"
|
||||
class="btn-purge"
|
||||
:class="{ 'btn-confirm': purgeConfirming }"
|
||||
>
|
||||
{{ store.isLoading ? 'Purging…' : 'Run Purge Now' }}
|
||||
{{ store.isLoading ? 'Purging…' : purgeConfirming ? 'Sure?' : 'Run Purge Now' }}
|
||||
</button>
|
||||
<div v-if="store.purgeResult" class="purge-result">
|
||||
<p>Rows deleted: {{ store.purgeResult.rows_deleted }}</p>
|
||||
<p>Files deleted: {{ store.purgeResult.files_deleted }}</p>
|
||||
<p>Files already gone: {{ store.purgeResult.files_missing }}</p>
|
||||
<p>Active jobs skipped: {{ store.purgeResult.active_skipped }}</p>
|
||||
<p>{{ store.purgeResult.rows_deleted }} jobs removed</p>
|
||||
<p>{{ store.purgeResult.files_deleted }} files deleted</p>
|
||||
<p v-if="store.purgeResult.sessions_deleted">{{ store.purgeResult.sessions_deleted }} sessions cleared</p>
|
||||
<p v-if="store.purgeResult.active_skipped">{{ store.purgeResult.active_skipped }} active jobs skipped</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -414,6 +440,10 @@ function formatFilesize(bytes: number | null): string {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="field-hint" style="margin-top: var(--space-lg);">
|
||||
All settings are applied immediately but reset on server restart.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ interface PurgeResult {
|
|||
files_deleted: number
|
||||
files_missing: number
|
||||
active_skipped: number
|
||||
sessions_deleted?: number
|
||||
}
|
||||
|
||||
export const useAdminStore = defineStore('admin', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue