feat(ui): show relative timestamps on Dashboard and Downloads tables
Converts the Source Health last-check column and the Downloads created-at
column from locale date strings to human-relative times ("6 hours ago").
The absolute timestamp is preserved as a native browser tooltip so full
precision is still one hover away; detail modals keep the absolute form.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -317,7 +317,7 @@
|
||||
<td>
|
||||
<v-chip color="error" size="small">{{ source.error_count }}</v-chip>
|
||||
</td>
|
||||
<td>{{ formatDate(source.last_check) }}</td>
|
||||
<td><span :title="formatDate(source.last_check)">{{ source.last_check ? formatRelativeTime(source.last_check) : 'Never' }}</span></td>
|
||||
<td>
|
||||
<v-btn
|
||||
size="small"
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.source="{ item }">
|
||||
<span class="text-caption">{{ getSourceLabel(item.source_id) }}</span>
|
||||
<span class="text-caption">{{ getSourceLabel(item) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.url="{ item }">
|
||||
@@ -175,7 +175,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.created_at="{ item }">
|
||||
{{ formatDate(item.created_at) }}
|
||||
<span :title="formatDate(item.created_at)">{{ formatRelativeTime(item.created_at) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.duration="{ item }">
|
||||
@@ -234,7 +234,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-weight-bold" width="150">Source</td>
|
||||
<td>{{ getSourceLabel(selectedDownload.source_id) }}</td>
|
||||
<td>{{ getSourceLabel(selectedDownload) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-weight-bold">URL</td>
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user