eb811e11f6
Two corrections from operator review: 1. Reuse the existing 'Raw stdout' panel instead of a bespoke structured UI section — the native ingester now writes a per-post line into the run stdout (parity with gallery-dl's per-file stdout), so the per-post handling shows in the panel the operator already uses. 2. DRY: stop re-reading post['attributes'] inline in ingest_core. write_post_record now returns a PostRecordOutcome (path, post_type, title, body_chars) — mirroring the download_post -> MediaOutcome contract — and the downloader owns the read; ingest_core only formats the outcome into the log line. Reverts the post_diagnostics metadata field + DownloadDetailModal 'Post capture' section added earlier. Per-post line: 'post <id> [<post_type>] body: N chars' (+ ' — EMPTY' when 0), so an empty body is self-explanatory by post_type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
7.2 KiB
Vue
189 lines
7.2 KiB
Vue
<template>
|
|
<v-dialog :model-value="!!event" max-width="900" @update:model-value="onClose">
|
|
<v-card v-if="event">
|
|
<v-card-title class="d-flex align-center" style="gap: 0.5rem">
|
|
<v-chip size="small" :color="statusColor">{{ event.status }}</v-chip>
|
|
<span>{{ event.platform || '—' }} · {{ event.artist_name || '—' }}</span>
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<p class="text-caption" style="opacity: 0.75">
|
|
{{ formatDateTime(event.started_at) }} → {{ event.finished_at ? formatDateTime(event.finished_at) : '(running)' }}
|
|
({{ fmtDuration(event.summary?.duration_seconds) }})
|
|
</p>
|
|
|
|
<h3 class="text-subtitle-2 mt-3">Run stats</h3>
|
|
<div class="fc-dl-grid">
|
|
<div><span class="fc-dl-grid__label">Downloaded</span><span>{{ stats.downloaded_count ?? 0 }}</span></div>
|
|
<div><span class="fc-dl-grid__label">Skipped</span><span>{{ stats.skipped_count ?? 0 }}</span></div>
|
|
<div><span class="fc-dl-grid__label">Per-item failures</span><span>{{ stats.per_item_failures ?? 0 }}</span></div>
|
|
<div><span class="fc-dl-grid__label">Warnings</span><span>{{ stats.warning_count ?? 0 }}</span></div>
|
|
<div><span class="fc-dl-grid__label">Tier-gated</span><span>{{ stats.tier_gated_count ?? 0 }}</span></div>
|
|
<div><span class="fc-dl-grid__label">Quarantined</span><span>{{ stats.quarantined_count ?? 0 }}</span></div>
|
|
</div>
|
|
|
|
<h3 class="text-subtitle-2 mt-4">Import summary</h3>
|
|
<div class="fc-dl-grid">
|
|
<div><span class="fc-dl-grid__label">Attached</span><span>{{ summary.attached ?? 0 }}</span></div>
|
|
<div><span class="fc-dl-grid__label">Skipped (dedup)</span><span>{{ summary.skipped ?? 0 }}</span></div>
|
|
<div><span class="fc-dl-grid__label">Errors</span><span>{{ summary.errors ?? 0 }}</span></div>
|
|
</div>
|
|
|
|
<template v-if="quarantinedPaths.length">
|
|
<h3 class="text-subtitle-2 mt-4">Quarantined paths</h3>
|
|
<ul class="fc-dl-quar">
|
|
<li v-for="p in quarantinedPaths" :key="p"><code>{{ p }}</code></li>
|
|
</ul>
|
|
</template>
|
|
|
|
<template v-if="event.error">
|
|
<div class="fc-dl-blockhead mt-4">
|
|
<h3 class="text-subtitle-2">Error</h3>
|
|
<v-btn
|
|
size="x-small" variant="text" prepend-icon="mdi-content-copy"
|
|
@click="onCopy('Error', event.error)"
|
|
>Copy</v-btn>
|
|
</div>
|
|
<pre class="fc-dl-pre">{{ event.error }}</pre>
|
|
</template>
|
|
|
|
<template v-if="errorsWarnings">
|
|
<div class="fc-dl-blockhead mt-4">
|
|
<h3 class="text-subtitle-2">Errors & warnings</h3>
|
|
<v-btn
|
|
size="x-small" variant="text" prepend-icon="mdi-content-copy"
|
|
@click="onCopy('Errors & warnings', errorsWarnings)"
|
|
>Copy</v-btn>
|
|
</div>
|
|
<pre class="fc-dl-pre">{{ errorsWarnings }}</pre>
|
|
</template>
|
|
|
|
<v-expansion-panels class="mt-4">
|
|
<v-expansion-panel>
|
|
<v-expansion-panel-title>
|
|
<span>Raw stdout</span>
|
|
<v-spacer />
|
|
<v-btn
|
|
size="x-small" variant="text" prepend-icon="mdi-content-copy"
|
|
class="me-2"
|
|
@click.stop="onCopy('stdout', event.metadata?.stdout || '')"
|
|
>Copy</v-btn>
|
|
</v-expansion-panel-title>
|
|
<v-expansion-panel-text>
|
|
<pre class="fc-dl-pre">{{ event.metadata?.stdout || '(empty)' }}</pre>
|
|
</v-expansion-panel-text>
|
|
</v-expansion-panel>
|
|
<v-expansion-panel>
|
|
<v-expansion-panel-title>
|
|
<span>Raw stderr</span>
|
|
<v-spacer />
|
|
<v-btn
|
|
size="x-small" variant="text" prepend-icon="mdi-content-copy"
|
|
class="me-2"
|
|
@click.stop="onCopy('stderr', event.metadata?.stderr || '')"
|
|
>Copy</v-btn>
|
|
</v-expansion-panel-title>
|
|
<v-expansion-panel-text>
|
|
<pre class="fc-dl-pre">{{ event.metadata?.stderr || '(empty)' }}</pre>
|
|
</v-expansion-panel-text>
|
|
</v-expansion-panel>
|
|
</v-expansion-panels>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-btn
|
|
variant="text" prepend-icon="mdi-content-copy"
|
|
@click="onCopy('All diagnostics', allDiagnostics)"
|
|
>Copy all diagnostics</v-btn>
|
|
<v-spacer />
|
|
<v-btn variant="text" @click="onClose(false)">Close</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { toast } from '../../utils/toast.js'
|
|
import { computed } from 'vue'
|
|
|
|
import { copyText } from '../../utils/clipboard.js'
|
|
import { formatDateTime } from '../../utils/date.js'
|
|
|
|
const props = defineProps({ event: { type: Object, default: null } })
|
|
const emit = defineEmits(['close'])
|
|
|
|
const stats = computed(() => props.event?.metadata?.run_stats || {})
|
|
const summary = computed(() => props.event?.metadata?.import_summary || {})
|
|
const quarantinedPaths = computed(() => props.event?.metadata?.quarantined_paths || [])
|
|
const errorsWarnings = computed(() => props.event?.metadata?.stderr_errors_warnings || '')
|
|
|
|
// One combined block for "research the issue elsewhere" — header line +
|
|
// error + full stdout/stderr. Built lazily from the current event.
|
|
const allDiagnostics = computed(() => {
|
|
const e = props.event
|
|
if (!e) return ''
|
|
const md = e.metadata || {}
|
|
return [
|
|
`event #${e.id} · ${e.platform || '—'} · ${e.artist_name || '—'}`,
|
|
`status: ${e.status}`,
|
|
`started: ${e.started_at} finished: ${e.finished_at || '(running)'}`,
|
|
e.error ? `\n--- error ---\n${e.error}` : '',
|
|
errorsWarnings.value ? `\n--- errors & warnings ---\n${errorsWarnings.value}` : '',
|
|
`\n--- stdout ---\n${md.stdout || '(empty)'}`,
|
|
`\n--- stderr ---\n${md.stderr || '(empty)'}`,
|
|
].filter(Boolean).join('\n')
|
|
})
|
|
|
|
async function onCopy(label, text) {
|
|
try {
|
|
await copyText(text || '')
|
|
toast({ text: `${label} copied`, type: 'success' })
|
|
} catch (e) {
|
|
toast({ text: `Copy failed: ${e.message}`, type: 'error' })
|
|
}
|
|
}
|
|
|
|
const statusColor = computed(() => ({
|
|
ok: 'success', error: 'error', running: 'info',
|
|
pending: 'secondary', skipped: 'warning',
|
|
}[props.event?.status] || undefined))
|
|
|
|
function fmtDuration(sec) {
|
|
if (sec == null) return '—'
|
|
if (sec < 60) return `${sec.toFixed(1)}s`
|
|
const m = Math.floor(sec / 60), s = Math.floor(sec % 60)
|
|
return `${m}m ${s}s`
|
|
}
|
|
|
|
function onClose() {
|
|
emit('close')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-dl-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 0.5rem 1rem;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.fc-dl-grid > div { display: flex; justify-content: space-between; }
|
|
.fc-dl-grid__label {
|
|
color: rgb(var(--v-theme-on-surface-variant));
|
|
}
|
|
.fc-dl-pre {
|
|
background: rgb(var(--v-theme-surface));
|
|
border: 1px solid rgb(var(--v-theme-on-surface-variant) / 0.18);
|
|
padding: 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.8rem;
|
|
max-height: 400px;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
.fc-dl-quar { padding-left: 1.5rem; font-size: 0.85rem; }
|
|
.fc-dl-blockhead {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
gap: 0.5rem;
|
|
}
|
|
</style>
|