ongoing polish in queue handling

This commit is contained in:
Bryan Van Deusen
2026-01-28 09:32:47 -05:00
parent c7a259a63b
commit 575e20f58c
9 changed files with 738 additions and 69 deletions
+1
View File
@@ -37,6 +37,7 @@ export const downloadsApi = {
retry: (id) => api.post(`/downloads/${id}/retry`),
stats: (params = {}) => api.get('/downloads/stats', { params }),
recentActivity: (params = {}) => api.get('/downloads/recent-activity', { params }),
resetOrphaned: (thresholdMinutes = 0) => api.post('/downloads/reset-orphaned', { threshold_minutes: thresholdMinutes }),
}
// Credentials API
+77 -32
View File
@@ -51,8 +51,11 @@
<v-icon>mdi-clock-outline</v-icon>
</v-avatar>
<div>
<div class="text-h4">{{ stats?.pending || 0 }}</div>
<div class="text-caption">Pending Downloads</div>
<div class="text-h4">{{ activeDownloadsCount }}</div>
<div class="text-caption">Active Downloads</div>
<div class="text-caption text-grey" v-if="stats?.queued || stats?.running">
{{ stats?.queued || 0 }} queued, {{ stats?.running || 0 }} running
</div>
</div>
</v-card-text>
</v-card>
@@ -81,6 +84,16 @@
>
Retry All Failed ({{ failedCount }})
</v-btn>
<v-btn
color="secondary"
variant="outlined"
prepend-icon="mdi-broom"
:loading="resettingOrphaned"
:disabled="!stuckRunningCount"
@click="resetOrphanedJobs"
>
Reset Stuck ({{ stuckRunningCount }})
</v-btn>
<v-spacer />
<div class="text-body-2 text-grey d-flex align-center">
<v-icon size="small" class="mr-1">mdi-harddisk</v-icon>
@@ -112,41 +125,43 @@
</v-row>
<v-row class="mt-4">
<!-- Running Downloads (auto-refresh) -->
<!-- Active Downloads (queued + running, auto-refresh) -->
<v-col cols="12" md="4">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="mr-2" :class="{ 'mdi-spin': runningDownloads.length }">
{{ runningDownloads.length ? 'mdi-loading' : 'mdi-download' }}
<v-icon class="mr-2" :class="{ 'mdi-spin': activeDownloads.length }">
{{ activeDownloads.length ? 'mdi-loading' : 'mdi-download' }}
</v-icon>
Running
<v-chip v-if="runningDownloads.length" size="small" color="info" class="ml-2">
{{ runningDownloads.length }}
Active
<v-chip v-if="activeDownloads.length" size="small" color="info" class="ml-2">
{{ activeDownloads.length }}
</v-chip>
<v-spacer />
<v-btn
v-if="runningDownloads.length"
v-if="activeDownloads.length"
icon
size="x-small"
variant="text"
@click="refreshRunning"
@click="refreshActive"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-list v-if="runningDownloads.length" density="compact">
<v-list-item v-for="dl in runningDownloads" :key="dl.id">
<v-list v-if="activeDownloads.length" density="compact">
<v-list-item v-for="dl in activeDownloads" :key="dl.id">
<template v-slot:prepend>
<v-icon :color="dl.status === 'running' ? 'info' : 'warning'" size="small">
{{ dl.status === 'running' ? 'mdi-loading mdi-spin' : 'mdi-clock-outline' }}
</v-icon>
</template>
<v-list-item-title class="text-body-2">
{{ truncate(dl.url, 30) }}
</v-list-item-title>
<v-list-item-subtitle>
<v-progress-linear
indeterminate
color="info"
height="4"
class="mt-1"
/>
<v-chip size="x-small" :color="dl.status === 'running' ? 'info' : 'warning'" variant="tonal">
{{ dl.status }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
</v-list>
@@ -276,7 +291,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useSourcesStore } from '../stores/sources'
import { useDownloadsStore } from '../stores/downloads'
import { useNotificationStore } from '../stores/notifications'
import { settingsApi } from '../services/api'
import { settingsApi, downloadsApi } from '../services/api'
const sourcesStore = useSourcesStore()
const downloadsStore = useDownloadsStore()
@@ -285,8 +300,9 @@ const notifications = useNotificationStore()
// State
const checkingAll = ref(false)
const retryingAll = ref(false)
const resettingOrphaned = ref(false)
const storageStats = ref(null)
const runningDownloads = ref([])
const activeDownloads = ref([])
const loadingStats = ref(true)
const loadingActivity = ref(true)
const loadingStorage = ref(true)
@@ -300,6 +316,11 @@ const sourcesWithErrors = computed(() =>
sourcesStore.sources.filter(s => s.error_count > 0).slice(0, 5)
)
const failedCount = computed(() => stats.value?.failed || 0)
const activeDownloadsCount = computed(() =>
(stats.value?.queued || 0) + (stats.value?.running || 0)
)
// Running jobs that might be stuck (shows count for UI, actual stuck detection happens server-side)
const stuckRunningCount = computed(() => stats.value?.running || 0)
onMounted(() => {
// Load data in background - DON'T await, let UI render immediately
@@ -325,18 +346,25 @@ function loadDashboardData() {
.catch(e => console.error('Failed to fetch recent activity:', e))
.finally(() => loadingActivity.value = false)
fetchRunningDownloads()
fetchActiveDownloads()
// Storage stats can be slow - load independently
fetchStorageStats()
}
async function fetchRunningDownloads() {
async function fetchActiveDownloads() {
try {
const response = await downloadsStore.fetchDownloads({ status: 'running', per_page: 20 })
runningDownloads.value = response.items.filter(d => d.status === 'running')
// Fetch both queued and running downloads
const [queuedResponse, runningResponse] = await Promise.all([
downloadsStore.fetchDownloads({ status: 'queued', per_page: 20 }),
downloadsStore.fetchDownloads({ status: 'running', per_page: 20 }),
])
// Combine and sort: running first, then queued
const running = runningResponse.items.filter(d => d.status === 'running')
const queued = queuedResponse.items.filter(d => d.status === 'queued')
activeDownloads.value = [...running, ...queued]
} catch (e) {
console.error('Failed to fetch running downloads:', e)
console.error('Failed to fetch active downloads:', e)
}
}
@@ -368,10 +396,10 @@ async function refreshStorageStats() {
}
function startAutoRefresh() {
// Refresh running downloads every 5 seconds when there are active downloads
// Refresh active downloads every 5 seconds when there are active downloads
refreshInterval = setInterval(async () => {
if (runningDownloads.value.length > 0 || stats.value?.pending > 0) {
await fetchRunningDownloads()
if (activeDownloads.value.length > 0 || activeDownloadsCount.value > 0) {
await fetchActiveDownloads()
await downloadsStore.fetchStats()
await downloadsStore.fetchRecentActivity({ limit: 10 })
}
@@ -385,8 +413,8 @@ function stopAutoRefresh() {
}
}
async function refreshRunning() {
await fetchRunningDownloads()
async function refreshActive() {
await fetchActiveDownloads()
}
async function checkAllSources() {
@@ -403,7 +431,8 @@ async function checkAllSources() {
}
}
notifications.success(`Queued checks for ${queued} sources`)
await fetchRunningDownloads()
await fetchActiveDownloads()
await downloadsStore.fetchStats()
} catch (error) {
notifications.error(`Failed to check sources: ${error.message}`)
} finally {
@@ -427,7 +456,7 @@ async function retryAllFailed() {
}
notifications.success(`Queued retry for ${retried} downloads`)
await downloadsStore.fetchStats()
await fetchRunningDownloads()
await fetchActiveDownloads()
} catch (error) {
notifications.error(`Failed to retry downloads: ${error.message}`)
} finally {
@@ -435,11 +464,26 @@ async function retryAllFailed() {
}
}
async function resetOrphanedJobs() {
resettingOrphaned.value = true
try {
const response = await downloadsApi.resetOrphaned(0) // 0 = reset all running
notifications.success(response.data.message || `Reset ${response.data.reset_count} stuck jobs`)
await downloadsStore.fetchStats()
await fetchActiveDownloads()
} catch (error) {
notifications.error(`Failed to reset stuck jobs: ${error.message}`)
} finally {
resettingOrphaned.value = false
}
}
function getStatusIcon(status) {
const icons = {
completed: 'mdi-check-circle',
failed: 'mdi-alert-circle',
running: 'mdi-loading mdi-spin',
queued: 'mdi-clock-outline',
pending: 'mdi-clock-outline',
skipped: 'mdi-skip-next',
}
@@ -451,6 +495,7 @@ function getStatusColor(status) {
completed: 'success',
failed: 'error',
running: 'info',
queued: 'warning',
pending: 'warning',
skipped: 'grey',
}
+14 -2
View File
@@ -18,7 +18,7 @@
type="number"
min="1"
max="10"
hint="Maximum number of concurrent downloads (1-10)"
hint="Maximum concurrent downloads (1-10). Requires container restart to take effect."
persistent-hint
/>
</v-col>
@@ -54,7 +54,7 @@
label="Schedule Interval (seconds)"
type="number"
min="300"
hint="How often to check for new content (minimum 5 minutes)"
:hint="scheduleIntervalHint"
persistent-hint
/>
</v-col>
@@ -419,6 +419,18 @@ const sortedAllPackages = computed(() => {
return sorted
})
const scheduleIntervalHint = computed(() => {
const seconds = settings.value['download.schedule_interval'] || 3600
if (seconds < 60) return `Check every ${seconds} seconds`
if (seconds < 3600) return `Check every ${Math.round(seconds / 60)} minutes`
if (seconds < 86400) {
const hours = seconds / 3600
return `Check every ${hours % 1 === 0 ? hours : hours.toFixed(1)} hours`
}
const days = seconds / 86400
return `Check every ${days % 1 === 0 ? days : days.toFixed(1)} days`
})
onMounted(() => {
// Don't block UI - load data in background
loadAll()