diff --git a/backend/app/api/downloads.py b/backend/app/api/downloads.py
index ca8fe20..f42cc09 100644
--- a/backend/app/api/downloads.py
+++ b/backend/app/api/downloads.py
@@ -128,10 +128,12 @@ async def retry_download(download_id: int):
if download.status != DownloadStatus.FAILED:
return jsonify({"error": "Can only retry failed downloads"}), 400
- # Reset status to queued
+ # Reset status to queued and clear prior-run timestamps
download.status = DownloadStatus.QUEUED
download.error_type = None
download.error_message = None
+ download.started_at = None
+ download.completed_at = None
await session.commit()
# Queue Celery task to retry the download
diff --git a/backend/app/tasks/downloads.py b/backend/app/tasks/downloads.py
index 3cdfa87..c3f0b2e 100644
--- a/backend/app/tasks/downloads.py
+++ b/backend/app/tasks/downloads.py
@@ -265,6 +265,7 @@ async def _download_source_async(source_id: int, download_id: int = None) -> dic
download.status = DownloadStatus.RUNNING
download.started_at = utcnow()
+ download.completed_at = None
else:
logger.warning(f"Download {download_id} not found, creating new record")
download = None
diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue
index ab65890..8f8d1ed 100644
--- a/frontend/src/views/Dashboard.vue
+++ b/frontend/src/views/Dashboard.vue
@@ -317,7 +317,7 @@
{{ source.error_count }}
|
- {{ formatDate(source.last_check) }} |
+ {{ source.last_check ? formatRelativeTime(source.last_check) : 'Never' }} |
- {{ getSourceLabel(item.source_id) }}
+ {{ getSourceLabel(item) }}
@@ -175,7 +175,7 @@
- {{ formatDate(item.created_at) }}
+ {{ formatRelativeTime(item.created_at) }}
@@ -234,7 +234,7 @@
| Source |
- {{ getSourceLabel(selectedDownload.source_id) }} |
+ {{ getSourceLabel(selectedDownload) }} |
| URL |
@@ -444,14 +444,36 @@ function formatDate(dateStr) {
return new Date(dateStr).toLocaleString()
}
+function formatRelativeTime(dateStr) {
+ if (!dateStr) return '-'
+ const date = new Date(dateStr)
+ const now = new Date()
+ const diffMs = now - date
+ const diffMins = Math.floor(diffMs / 60000)
+ const diffHours = Math.floor(diffMs / 3600000)
+ const diffDays = Math.floor(diffMs / 86400000)
+
+ if (diffMins < 1) return 'Just now'
+ if (diffMins < 60) return `${diffMins} min${diffMins !== 1 ? 's' : ''} ago`
+ if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`
+ if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`
+ return date.toLocaleDateString()
+}
+
function formatDuration(download) {
- if (!download.started_at || !download.completed_at) return '-'
+ if (!download.started_at) return '-'
const start = new Date(download.started_at)
- const end = new Date(download.completed_at)
+ // Running rows elapse against "now"; terminal rows need a valid completed_at.
+ const end = download.status === 'running' || !download.completed_at
+ ? new Date()
+ : new Date(download.completed_at)
const seconds = Math.round((end - start) / 1000)
+ if (seconds < 0) return '-'
if (seconds < 60) return `${seconds}s`
const minutes = Math.floor(seconds / 60)
- return `${minutes}m ${seconds % 60}s`
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`
+ const hours = Math.floor(minutes / 60)
+ return `${hours}h ${minutes % 60}m`
}
function formatErrorType(errorType) {
@@ -465,7 +487,18 @@ function truncateUrl(url) {
return url.substring(0, 37) + '...'
}
-function getSourceLabel(sourceId) {
+function getSourceLabel(item) {
+ // Prefer the subscription_name/platform embedded in the download record —
+ // these survive source deletion and don't require the sources store to be populated.
+ if (item && typeof item === 'object') {
+ if (item.subscription_name && item.platform) {
+ return `${item.subscription_name}:${item.platform}`
+ }
+ if (item.platform) {
+ return `(unnamed):${item.platform}`
+ }
+ }
+ const sourceId = typeof item === 'number' ? item : item?.source_id
if (!sourceId) return 'N/A'
const source = sourcesStore.sources.find(s => s.id === sourceId)
if (source) {
|