feat(downloads): enrich modal with run stats, errors panel, copy, and log filter

Builds on 513b191. Consumes the new metadata fields in the Downloads
Details modal:

  • Run stats rows — exit code, downloaded/skipped/per-item-failures/
    warning counts, duration. Zero-value rows hide to reduce noise.
    Files Downloaded row prefers run_stats.downloaded_count with
    file_count fallback for pre-migration rows.
  • Errors & Warnings panel — new panel at the top, populated from
    metadata.stderr_errors_warnings and open-by-default when present.
  • Copy-to-clipboard buttons — small icon button on each log panel
    title, uses navigator.clipboard with snackbar confirmation.
  • Verbose log filter — substring filter on the stderr panel.

Graceful for existing rows: every new field is v-if-gated so sessions
saved before the backend change still render correctly with their
old-shape metadata.
This commit is contained in:
2026-04-19 14:25:17 -04:00
parent 513b191e47
commit 1f677b6862
+106 -5
View File
@@ -301,7 +301,29 @@
</tr>
<tr>
<td class="font-weight-bold">Files Downloaded</td>
<td>{{ selectedDownload.file_count }}</td>
<td>{{ runStats?.downloaded_count ?? selectedDownload.file_count }}</td>
</tr>
<tr v-if="runStats">
<td class="font-weight-bold">Skipped</td>
<td>{{ runStats.skipped_count }} <span class="text-grey text-caption">(already in archive)</span></td>
</tr>
<tr v-if="runStats && runStats.per_item_failures > 0">
<td class="font-weight-bold">Per-item Failures</td>
<td>{{ runStats.per_item_failures }} <span class="text-grey text-caption">(single items gallery-dl skipped over)</span></td>
</tr>
<tr v-if="runStats && runStats.warning_count > 0">
<td class="font-weight-bold">Warnings</td>
<td>{{ runStats.warning_count }}</td>
</tr>
<tr>
<td class="font-weight-bold">Duration</td>
<td>{{ formatDuration(selectedDownload) }}</td>
</tr>
<tr v-if="runStats">
<td class="font-weight-bold">Exit Code</td>
<td>
<code :class="runStats.exit_code === 0 ? 'text-success' : 'text-warning'">{{ runStats.exit_code }}</code>
</td>
</tr>
<tr>
<td class="font-weight-bold">Created</td>
@@ -326,17 +348,69 @@
</tbody>
</v-table>
<v-expansion-panels class="mt-4" v-if="selectedDownload.metadata">
<v-expansion-panels class="mt-4" v-model="openLogPanels" multiple v-if="selectedDownload.metadata">
<v-expansion-panel v-if="selectedDownload.metadata.stderr_errors_warnings">
<v-expansion-panel-title>
Errors &amp; Warnings
<v-btn
icon="mdi-content-copy"
size="x-small"
variant="text"
class="ml-2"
@click.stop="copyToClipboard(selectedDownload.metadata.stderr_errors_warnings, 'errors &amp; warnings')"
>
<v-icon size="small">mdi-content-copy</v-icon>
<v-tooltip activator="parent">Copy</v-tooltip>
</v-btn>
</v-expansion-panel-title>
<v-expansion-panel-text>
<pre class="text-caption text-error" style="white-space: pre-wrap; max-height: 300px; overflow: auto">{{ selectedDownload.metadata.stderr_errors_warnings }}</pre>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel v-if="selectedDownload.metadata.stdout">
<v-expansion-panel-title>Output Log</v-expansion-panel-title>
<v-expansion-panel-title>
Output Log (stdout)
<v-btn
icon="mdi-content-copy"
size="x-small"
variant="text"
class="ml-2"
@click.stop="copyToClipboard(selectedDownload.metadata.stdout, 'stdout')"
>
<v-icon size="small">mdi-content-copy</v-icon>
<v-tooltip activator="parent">Copy</v-tooltip>
</v-btn>
</v-expansion-panel-title>
<v-expansion-panel-text>
<pre class="text-caption" style="white-space: pre-wrap; max-height: 300px; overflow: auto">{{ selectedDownload.metadata.stdout }}</pre>
</v-expansion-panel-text>
</v-expansion-panel>
<v-expansion-panel v-if="selectedDownload.metadata.stderr">
<v-expansion-panel-title>Verbose Log (stderr)</v-expansion-panel-title>
<v-expansion-panel-title>
Verbose Log (stderr)
<v-btn
icon="mdi-content-copy"
size="x-small"
variant="text"
class="ml-2"
@click.stop="copyToClipboard(selectedDownload.metadata.stderr, 'verbose log')"
>
<v-icon size="small">mdi-content-copy</v-icon>
<v-tooltip activator="parent">Copy</v-tooltip>
</v-btn>
</v-expansion-panel-title>
<v-expansion-panel-text>
<pre class="text-caption" style="white-space: pre-wrap; max-height: 300px; overflow: auto">{{ selectedDownload.metadata.stderr }}</pre>
<v-text-field
v-model="verboseLogFilter"
density="compact"
variant="outlined"
hide-details
clearable
placeholder="Filter lines (substring match)"
prepend-inner-icon="mdi-magnify"
class="mb-2"
/>
<pre class="text-caption" style="white-space: pre-wrap; max-height: 300px; overflow: auto">{{ filteredVerboseLog || '(no matching lines)' }}</pre>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
@@ -379,6 +453,8 @@ const filterExcludeSuperseded = ref(true)
const detailsDialog = ref(false)
const selectedDownload = ref(null)
const retryingIds = ref([])
const verboseLogFilter = ref('')
const openLogPanels = ref([])
let refreshInterval = null
const statusOptions = [
@@ -595,9 +671,34 @@ function getSourceLabel(item) {
return `Source #${sourceId}`
}
const runStats = computed(() => selectedDownload.value?.metadata?.run_stats || null)
const filteredVerboseLog = computed(() => {
const stderr = selectedDownload.value?.metadata?.stderr || ''
const query = verboseLogFilter.value.trim().toLowerCase()
if (!query) return stderr
return stderr
.split('\n')
.filter(line => line.toLowerCase().includes(query))
.join('\n')
})
function showDetails(download) {
selectedDownload.value = download
detailsDialog.value = true
verboseLogFilter.value = ''
// Open the errors/warnings panel by default when there's anything worth seeing.
openLogPanels.value = download?.metadata?.stderr_errors_warnings ? [0] : []
}
async function copyToClipboard(text, label = 'content') {
if (!text) return
try {
await navigator.clipboard.writeText(text)
notifications.success(`Copied ${label} to clipboard`)
} catch (err) {
notifications.error(`Copy failed: ${err.message}`)
}
}
async function retryDownload(download) {