feat(subscriptions): dry-run backfill preview — B4 preview (plan #708)
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

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>
This commit is contained in:
2026-06-06 12:18:08 -04:00
parent cd43439401
commit e82c2ee57b
10 changed files with 368 additions and 2 deletions
@@ -176,6 +176,7 @@
@check="onCheck"
@backfill="onBackfill"
@recover="onRecover"
@preview="onPreview"
/>
<tr v-if="item.sources.length === 0">
<td colspan="8" class="fc-subs__sources-empty">
@@ -253,6 +254,7 @@
@check="onCheck"
@backfill="onBackfill"
@recover="onRecover"
@preview="onPreview"
/>
<div v-if="item.sources.length === 0" class="fc-subs__sources-empty">
No sources yet. Tap + to add one.
@@ -268,6 +270,11 @@
@saved="onSourceSaved"
/>
<ArtistCreateDialog v-model="showArtistDialog" @created="onArtistCreated" />
<PreviewDialog
v-model="showPreviewDialog"
:source="previewTarget"
@backfill="onPreviewBackfill"
/>
</div>
</template>
@@ -284,6 +291,7 @@ import SourceCard from './SourceCard.vue'
import SourceHealthDot from './SourceHealthDot.vue'
import SourceFormDialog from './SourceFormDialog.vue'
import ArtistCreateDialog from './ArtistCreateDialog.vue'
import PreviewDialog from './PreviewDialog.vue'
import PlatformChip from './PlatformChip.vue'
import SchedulerStatusBar from './SchedulerStatusBar.vue'
import { formatRelative } from '../../utils/date.js'
@@ -334,6 +342,8 @@ const showSourceDialog = ref(false)
const editingSource = ref(null)
const editingArtist = ref(null)
const showArtistDialog = ref(false)
const showPreviewDialog = ref(false)
const previewTarget = ref(null)
const artistFilter = computed(() => {
const raw = route.query.artist_id
@@ -572,6 +582,18 @@ async function onRecover(source) {
}
}
// Plan #708 B4: open the dry-run preview dialog (it self-fetches on open).
function onPreview(source) {
previewTarget.value = source
showPreviewDialog.value = true
}
// "Start backfill" from inside the preview dialog → arm it + close.
async function onPreviewBackfill(source) {
showPreviewDialog.value = false
await onBackfill(source)
}
async function checkAll(group) {
let ok = 0
let conflict = 0