Files
FabledCurator/frontend/src/components/subscriptions/PreviewDialog.vue
T
bvandeusen e82c2ee57b
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 24s
CI / integration (push) Successful in 3m1s
CI / frontend-build (push) Successful in 5m13s
feat(subscriptions): dry-run backfill preview — B4 preview (plan #708)
Owning the walk lets an operator gauge "is this source worth a backfill?" before
arming one. ingest_core.Ingester.preview walks the first few feed pages and
counts media NOT already in the seen/dead ledgers, downloading nothing
(read-only). download_backends.preview_source resolves the campaign id + runs it
(native-only, mirrors verify_source_credential / run_download); POST
/api/sources/{id}/preview returns {total_new, posts_scanned, has_more, sample[]}
(409 on auth/drift/unresolvable, 400 for gallery-dl platforms). PatreonClient
gains post_meta(post) for the sample's title/date.

UI: a Patreon-only Preview button (mdi-eye-outline) on SourceRow + SourceCard
opens PreviewDialog — self-fetches with loading / error / empty / result states
and a "Start backfill" shortcut. Store action previewSource.

Tests: preview counts new media without downloading + samples only posts with
new items; page_limit caps the walk + flags has_more.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:18:08 -04:00

114 lines
3.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- Plan #708 B4: dry-run preview what a backfill WOULD download, without
downloading. Self-fetches on open; loading / error / empty / result. -->
<v-dialog
:model-value="modelValue" max-width="520"
@update:model-value="$emit('update:modelValue', $event)"
>
<v-card v-if="source">
<v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-eye-outline</v-icon>
Preview · {{ source.artist_name }}
</v-card-title>
<v-card-subtitle class="fc-preview__url">{{ source.url }}</v-card-subtitle>
<v-card-text>
<div v-if="loading" class="text-center py-8">
<v-progress-circular indeterminate color="accent" />
<div class="text-caption mt-3 text-medium-emphasis">Walking the feed</div>
</div>
<v-alert
v-else-if="error" type="error" variant="tonal" density="comfortable"
>{{ error }}</v-alert>
<div v-else-if="result">
<div class="text-h5">
{{ result.total_new }} new item{{ result.total_new === 1 ? '' : 's' }}
</div>
<div class="text-body-2 text-medium-emphasis mb-3">
in the {{ result.posts_scanned }} most recent
post{{ result.posts_scanned === 1 ? '' : 's' }}<span
v-if="result.has_more"> · more pages not scanned</span>
</div>
<div
v-if="result.total_new === 0"
class="text-body-2 text-medium-emphasis"
>Youre caught up nothing new to download.</div>
<v-list v-else density="compact" class="fc-preview__list" bg-color="transparent">
<v-list-item v-for="(row, i) in result.sample" :key="i" class="px-0">
<template #prepend>
<v-chip
size="x-small" color="info" variant="tonal" label class="mr-3"
>+{{ row.new }}</v-chip>
</template>
<v-list-item-title class="text-body-2">{{ row.title }}</v-list-item-title>
<v-list-item-subtitle v-if="row.date" class="text-caption">
{{ formatRelative(row.date) }}
</v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="$emit('update:modelValue', false)">Close</v-btn>
<v-btn
v-if="result && result.total_new > 0"
color="accent" variant="flat"
@click="$emit('backfill', source)"
>Start backfill</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useSourcesStore } from '../../stores/sources.js'
import { formatRelative } from '../../utils/date.js'
const props = defineProps({
modelValue: { type: Boolean, default: false },
source: { type: Object, default: null },
})
defineEmits(['update:modelValue', 'backfill'])
const store = useSourcesStore()
const loading = ref(false)
const error = ref('')
const result = ref(null)
watch(
() => props.modelValue,
async (open) => {
if (!open || !props.source) return
loading.value = true
error.value = ''
result.value = null
try {
result.value = await store.previewSource(props.source.id)
} catch (e) {
error.value = e?.body?.detail || e?.message || 'Preview failed'
} finally {
loading.value = false
}
},
)
</script>
<style scoped>
.fc-preview__url {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fc-preview__list {
max-height: 320px;
overflow-y: auto;
}
</style>