feat: auto cleanup with retention period, storage limit, settings UI
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
<div class="tabs">
|
||||
<button :class="{ active: tab === 'videos' }" @click="tab = 'videos'">📹 Video Library</button>
|
||||
<button :class="{ active: tab === 'logs' }" @click="tab = 'logs'; fetchLogs()">📋 Download Logs</button>
|
||||
<button :class="{ active: tab === 'settings' }" @click="tab = 'settings'; fetchCleanup()">⚙️ Settings</button>
|
||||
<div class="stats" v-if="stats">📊 {{ stats.total_videos }} videos · {{ stats.total_size_human }}</div>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +100,83 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ── Settings ── -->
|
||||
<template v-if="tab === 'settings'">
|
||||
<div class="settings-card">
|
||||
<h3>🗑 Auto Cleanup</h3>
|
||||
<p class="settings-desc">Automatically delete downloaded videos based on age or disk usage.</p>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>Enable auto cleanup</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="cleanup.enabled" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>Retention period</label>
|
||||
<div class="retention-input">
|
||||
<input type="number" v-model.number="retentionValue" min="1" class="num-input" />
|
||||
<select v-model="retentionUnit" class="unit-select">
|
||||
<option value="hours">Hours</option>
|
||||
<option value="days">Days</option>
|
||||
<option value="weeks">Weeks</option>
|
||||
<option value="months">Months</option>
|
||||
</select>
|
||||
<span class="minutes-hint">= {{ cleanupMinutes.toLocaleString() }} min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<label>Storage limit</label>
|
||||
<div class="storage-limit-input">
|
||||
<input type="number" v-model.number="cleanup.storage_limit_pct" min="10" max="95" class="num-input" />
|
||||
<span>%</span>
|
||||
<span class="hint">of total disk</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disk usage bar -->
|
||||
<div class="disk-section" v-if="cleanupStatus">
|
||||
<div class="disk-header">
|
||||
<span>Disk Usage</span>
|
||||
<span :class="diskClass">{{ cleanupStatus.disk.used_pct }}%</span>
|
||||
</div>
|
||||
<div class="disk-bar">
|
||||
<div class="disk-fill" :style="{ width: cleanupStatus.disk.used_pct + '%' }" :class="diskClass"></div>
|
||||
<div class="disk-limit-line" :style="{ left: cleanup.storage_limit_pct + '%' }" title="Storage limit"></div>
|
||||
</div>
|
||||
<div class="disk-labels">
|
||||
<span>{{ humanSize(cleanupStatus.disk.used) }} used</span>
|
||||
<span>{{ humanSize(cleanupStatus.disk.free) }} free</span>
|
||||
<span>{{ humanSize(cleanupStatus.disk.total) }} total</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last run info -->
|
||||
<div class="last-run" v-if="cleanupStatus?.last_run">
|
||||
<span class="last-run-label">Last run:</span>
|
||||
{{ fmtTime(cleanupStatus.last_run) }}
|
||||
<span v-if="cleanupStatus.last_result?.time_deleted !== undefined" class="run-result">
|
||||
— deleted {{ cleanupStatus.last_result.time_deleted + (cleanupStatus.last_result.storage_deleted || 0) }} videos,
|
||||
freed {{ cleanupStatus.last_result.freed_mb }} MB
|
||||
</span>
|
||||
</div>
|
||||
<div class="last-run" v-else-if="cleanupStatus">Never run yet</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn-save" @click="saveCleanup" :disabled="saving">
|
||||
{{ saving ? '⏳ Saving...' : '💾 Save Settings' }}
|
||||
</button>
|
||||
<button class="btn-run" @click="runCleanup" :disabled="running">
|
||||
{{ running ? '⏳ Running...' : '🗑 Run Now' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="settingsMsg" :class="['settings-msg', settingsMsgType]">{{ settingsMsg }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Video Player Modal -->
|
||||
<div v-if="playing" class="modal" @click.self="playing = null">
|
||||
<div class="player-wrap">
|
||||
@@ -129,6 +207,75 @@ const playing = ref(null)
|
||||
const playUrl = ref('')
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize) || 1)
|
||||
|
||||
// ── Settings / Cleanup ──
|
||||
const cleanupStatus = ref(null)
|
||||
const cleanup = ref({ enabled: true, retention_minutes: 10080, storage_limit_pct: 80 })
|
||||
const retentionValue = ref(7)
|
||||
const retentionUnit = ref('days')
|
||||
const saving = ref(false)
|
||||
const running = ref(false)
|
||||
const settingsMsg = ref('')
|
||||
const settingsMsgType = ref('ok')
|
||||
|
||||
const cleanupMinutes = computed(() => {
|
||||
const m = { hours: 60, days: 1440, weeks: 10080, months: 43200 }
|
||||
return retentionValue.value * (m[retentionUnit.value] || 1440)
|
||||
})
|
||||
|
||||
const diskClass = computed(() => {
|
||||
const pct = cleanupStatus.value?.disk?.used_pct || 0
|
||||
if (pct >= 90) return 'disk-danger'
|
||||
if (pct >= 75) return 'disk-warn'
|
||||
return 'disk-ok'
|
||||
})
|
||||
|
||||
function minutesToUnit(min) {
|
||||
if (min % 43200 === 0) return { value: min / 43200, unit: 'months' }
|
||||
if (min % 10080 === 0) return { value: min / 10080, unit: 'weeks' }
|
||||
if (min % 1440 === 0) return { value: min / 1440, unit: 'days' }
|
||||
return { value: min / 60, unit: 'hours' }
|
||||
}
|
||||
|
||||
async function fetchCleanup() {
|
||||
try {
|
||||
const res = await axios.get('/api/admin/settings/cleanup', { headers: auth.getHeaders() })
|
||||
cleanupStatus.value = res.data
|
||||
cleanup.value = { ...res.data.config }
|
||||
const { value, unit } = minutesToUnit(res.data.config.retention_minutes)
|
||||
retentionValue.value = value
|
||||
retentionUnit.value = unit
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function saveCleanup() {
|
||||
saving.value = true; settingsMsg.value = ''
|
||||
try {
|
||||
cleanup.value.retention_minutes = cleanupMinutes.value
|
||||
const res = await axios.put('/api/admin/settings/cleanup', cleanup.value, { headers: auth.getHeaders() })
|
||||
cleanupStatus.value = res.data
|
||||
settingsMsg.value = '✅ Settings saved'
|
||||
settingsMsgType.value = 'ok'
|
||||
} catch { settingsMsg.value = '❌ Save failed'; settingsMsgType.value = 'err' }
|
||||
finally { saving.value = false; setTimeout(() => settingsMsg.value = '', 3000) }
|
||||
}
|
||||
|
||||
async function runCleanup() {
|
||||
running.value = true; settingsMsg.value = ''
|
||||
try {
|
||||
const res = await axios.post('/api/admin/cleanup/run', {}, { headers: auth.getHeaders() })
|
||||
const r = res.data
|
||||
if (r.skipped) {
|
||||
settingsMsg.value = '⚠️ Cleanup is disabled'
|
||||
settingsMsgType.value = 'warn'
|
||||
} else {
|
||||
settingsMsg.value = `✅ Done — deleted ${(r.time_deleted||0) + (r.storage_deleted||0)} videos, freed ${r.freed_mb} MB`
|
||||
settingsMsgType.value = 'ok'
|
||||
}
|
||||
await fetchCleanup()
|
||||
} catch { settingsMsg.value = '❌ Run failed'; settingsMsgType.value = 'err' }
|
||||
finally { running.value = false }
|
||||
}
|
||||
|
||||
// ── Logs ──
|
||||
const logs = ref([])
|
||||
const logSearch = ref('')
|
||||
@@ -325,6 +472,56 @@ onMounted(() => { fetchVideos(); fetchStats() })
|
||||
.plat-youtube { background: #c00; color: #fff; }
|
||||
.plat-twitter { background: #1da1f2; color: #fff; }
|
||||
|
||||
/* Settings */
|
||||
.settings-card { background: #1a1a2e; border: 1px solid #2a2a3e; border-radius: 12px; padding: 2rem; max-width: 680px; }
|
||||
.settings-card h3 { margin-bottom: 0.4rem; font-size: 1.15rem; }
|
||||
.settings-desc { color: #888; font-size: 0.9rem; margin-bottom: 1.8rem; }
|
||||
.setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.4rem; gap: 1rem; }
|
||||
.setting-row label:first-child { color: #ccc; font-size: 0.95rem; white-space: nowrap; }
|
||||
.retention-input, .storage-limit-input { display: flex; align-items: center; gap: 0.6rem; }
|
||||
.num-input { width: 80px; padding: 0.5rem 0.7rem; border: 1px solid #444; border-radius: 8px; background: #12122a; color: #fff; font-size: 0.95rem; text-align: center; }
|
||||
.unit-select { padding: 0.5rem 0.7rem; border: 1px solid #444; border-radius: 8px; background: #12122a; color: #fff; font-size: 0.95rem; }
|
||||
.minutes-hint { color: #666; font-size: 0.82rem; white-space: nowrap; }
|
||||
.hint { color: #666; font-size: 0.85rem; }
|
||||
|
||||
/* Toggle switch */
|
||||
.toggle { position: relative; display: inline-block; width: 48px; height: 26px; flex-shrink: 0; }
|
||||
.toggle input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; inset: 0; background: #444; border-radius: 26px; transition: 0.3s; }
|
||||
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 4px; bottom: 4px; background: #fff; border-radius: 50%; transition: 0.3s; }
|
||||
.toggle input:checked + .slider { background: #1da1f2; }
|
||||
.toggle input:checked + .slider:before { transform: translateX(22px); }
|
||||
|
||||
/* Disk bar */
|
||||
.disk-section { margin: 1.4rem 0; }
|
||||
.disk-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.9rem; color: #aaa; }
|
||||
.disk-bar { position: relative; height: 12px; background: #333; border-radius: 6px; overflow: visible; margin-bottom: 0.5rem; }
|
||||
.disk-fill { height: 100%; border-radius: 6px; transition: width 0.4s; }
|
||||
.disk-limit-line { position: absolute; top: -4px; bottom: -4px; width: 2px; background: #f39c12; border-radius: 2px; transform: translateX(-50%); }
|
||||
.disk-labels { display: flex; justify-content: space-between; font-size: 0.8rem; color: #666; }
|
||||
.disk-ok { color: #27ae60; } .disk-fill.disk-ok { background: #27ae60; }
|
||||
.disk-warn { color: #f39c12; } .disk-fill.disk-warn { background: #f39c12; }
|
||||
.disk-danger { color: #e74c3c; } .disk-fill.disk-danger { background: #e74c3c; }
|
||||
|
||||
/* Last run */
|
||||
.last-run { font-size: 0.88rem; color: #888; margin-bottom: 1.4rem; }
|
||||
.last-run-label { color: #666; margin-right: 0.3rem; }
|
||||
.run-result { color: #27ae60; }
|
||||
|
||||
/* Action buttons */
|
||||
.settings-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; }
|
||||
.btn-save, .btn-run {
|
||||
padding: 0.7rem 1.4rem; border: none; border-radius: 8px; cursor: pointer;
|
||||
font-size: 0.95rem; font-weight: 600; transition: opacity 0.2s;
|
||||
}
|
||||
.btn-save { background: #1da1f2; color: #fff; }
|
||||
.btn-run { background: #e74c3c; color: #fff; }
|
||||
.btn-save:disabled, .btn-run:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.settings-msg { margin-top: 0.8rem; font-size: 0.9rem; padding: 0.5rem 0; }
|
||||
.settings-msg.ok { color: #27ae60; }
|
||||
.settings-msg.warn { color: #f39c12; }
|
||||
.settings-msg.err { color: #e74c3c; }
|
||||
|
||||
.empty { text-align: center; color: #666; padding: 3rem; }
|
||||
.pagination { display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 1.5rem; }
|
||||
.pagination button { padding: 0.5rem 1rem; border: 1px solid #444; border-radius: 6px; background: transparent; color: #fff; cursor: pointer; }
|
||||
|
||||
Reference in New Issue
Block a user