feat(subscriptions): smarter-backfill UI — Start/Stop + state badge
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 11s
CI / frontend-build (push) Successful in 18s
CI / integration (push) Successful in 2m58s

Plan #693 (frontend). Backend landed in 96fffaf.

- sources store: setBackfill(runs) → startBackfill/stopBackfill ({action}).
- SubscriptionsTab: the deep-scan window.prompt for N runs becomes a
  Start/Stop toggle keyed on source.backfill_state.
- SourceRow + SourceCard: the 'backfill (N×)' chip becomes a state badge —
  Backfilling (chunk N) / Backfilled / Stalled — and the scan button
  toggles between Backfill (mdi-magnify-scan) and Stop (mdi-stop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 15:06:30 -04:00
parent 96fffaff64
commit 618dafde85
4 changed files with 55 additions and 39 deletions
@@ -25,9 +25,17 @@
size="x-small" color="error" variant="tonal" label size="x-small" color="error" variant="tonal" label
>{{ source.consecutive_failures }} err</v-chip> >{{ source.consecutive_failures }} err</v-chip>
<v-chip <v-chip
v-else-if="(source.backfill_runs_remaining || 0) > 0" v-else-if="source.backfill_state === 'running'"
size="x-small" color="info" variant="tonal" label size="x-small" color="info" variant="tonal" label
>backfill ({{ source.backfill_runs_remaining }}×)</v-chip> >Backfilling{{ source.backfill_chunks ? ` (${source.backfill_chunks})` : '' }}</v-chip>
<v-chip
v-else-if="source.backfill_state === 'complete'"
size="x-small" color="success" variant="tonal" label
>Backfilled</v-chip>
<v-chip
v-else-if="source.backfill_state === 'stalled'"
size="x-small" color="warning" variant="tonal" label
>Stalled</v-chip>
</div> </div>
<div class="fc-source-card__actions"> <div class="fc-source-card__actions">
@@ -40,11 +48,13 @@
</v-btn> </v-btn>
<v-btn <v-btn
size="x-small" variant="text" size="x-small" variant="text"
:disabled="(source.backfill_runs_remaining || 0) > 0" :color="source.backfill_state === 'running' ? 'warning' : undefined"
@click.stop="$emit('backfill', source)" @click.stop="$emit('backfill', source)"
> >
<v-icon>mdi-magnify-scan</v-icon> <v-icon>{{ source.backfill_state === 'running' ? 'mdi-stop' : 'mdi-magnify-scan' }}</v-icon>
<v-tooltip activator="parent" location="top">Deep scan</v-tooltip> <v-tooltip activator="parent" location="top">
{{ source.backfill_state === 'running' ? 'Stop backfill' : 'Backfill full history' }}
</v-tooltip>
</v-btn> </v-btn>
<v-btn size="x-small" variant="text" @click.stop="$emit('edit', source)"> <v-btn size="x-small" variant="text" @click.stop="$emit('edit', source)">
<v-icon>mdi-pencil</v-icon> <v-icon>mdi-pencil</v-icon>
@@ -32,11 +32,17 @@
size="x-small" color="error" variant="tonal" label size="x-small" color="error" variant="tonal" label
>{{ source.consecutive_failures }}</v-chip> >{{ source.consecutive_failures }}</v-chip>
<v-chip <v-chip
v-else-if="(source.backfill_runs_remaining || 0) > 0" v-else-if="source.backfill_state === 'running'"
size="x-small" color="info" variant="tonal" label size="x-small" color="info" variant="tonal" label
> >Backfilling{{ source.backfill_chunks ? ` (${source.backfill_chunks})` : '' }}</v-chip>
backfill ({{ source.backfill_runs_remaining }}×) <v-chip
</v-chip> v-else-if="source.backfill_state === 'complete'"
size="x-small" color="success" variant="tonal" label
>Backfilled</v-chip>
<v-chip
v-else-if="source.backfill_state === 'stalled'"
size="x-small" color="warning" variant="tonal" label
>Stalled</v-chip>
<span v-else class="fc-source-row__zero">0</span> <span v-else class="fc-source-row__zero">0</span>
</td> </td>
<td class="fc-source-row__actions"> <td class="fc-source-row__actions">
@@ -49,13 +55,15 @@
<v-tooltip activator="parent" location="top">Check now</v-tooltip> <v-tooltip activator="parent" location="top">Check now</v-tooltip>
</v-btn> </v-btn>
<v-btn <v-btn
icon="mdi-magnify-scan" size="x-small" variant="text" size="x-small" variant="text"
:disabled="(source.backfill_runs_remaining || 0) > 0" :color="source.backfill_state === 'running' ? 'warning' : undefined"
@click.stop="$emit('backfill', source)" @click.stop="$emit('backfill', source)"
> >
<v-icon>mdi-magnify-scan</v-icon> <v-icon>{{ source.backfill_state === 'running' ? 'mdi-stop' : 'mdi-magnify-scan' }}</v-icon>
<v-tooltip activator="parent" location="top"> <v-tooltip activator="parent" location="top">
Deep scan walk full history for next few runs {{ source.backfill_state === 'running'
? 'Stop backfill'
: 'Backfill — walk the full post history until complete' }}
</v-tooltip> </v-tooltip>
</v-btn> </v-btn>
<v-btn <v-btn
@@ -532,31 +532,23 @@ async function onCheck(source) {
} }
} }
// Plan #544: arm a source for backfill mode (gallery-dl walks the full // Plan #693: toggle a run-until-done backfill. Starting walks the full post
// post history) for the next N download runs. Default 3 — enough budget // history in time-boxed chunks across ticks until it reaches the bottom (the
// to finish a deep creator without re-prompting the operator across // row badge tracks progress / completion); stopping cancels back to tick mode.
// timeout boundaries. The chip on the row reflects the remaining count.
async function onBackfill(source) { async function onBackfill(source) {
const raw = globalThis.window?.prompt( const running = source.backfill_state === 'running'
`Deep scan "${source.artist_name} (${source.platform})" — walk full history for the next how many download runs? (110, default 3)`,
'3',
)
if (raw == null) return
const runs = parseInt(raw, 10)
if (!Number.isFinite(runs) || runs < 1 || runs > 10) {
toast({ text: 'Deep scan: runs must be 110', type: 'error' })
return
}
try { try {
await store.setBackfill(source.id, runs, source.artist_id) if (running) {
toast({ await store.stopBackfill(source.id, source.artist_id)
text: `Deep scan armed for ${runs} run${runs === 1 ? '' : 's'}`, toast({ text: `Backfill stopped for ${source.artist_name}`, type: 'success' })
type: 'success', } else {
}) await store.startBackfill(source.id, source.artist_id)
toast({ text: `Backfill started for ${source.artist_name}`, type: 'success' })
}
await store.loadAll() await store.loadAll()
} catch (e) { } catch (e) {
toast({ toast({
text: `Deep scan failed: ${e?.detail || e?.message || e}`, text: `Backfill ${running ? 'stop' : 'start'} failed: ${e?.detail || e?.message || e}`,
type: 'error', type: 'error',
}) })
} }
+12 -6
View File
@@ -85,11 +85,16 @@ export const useSourcesStore = defineStore('sources', () => {
} }
} }
// Plan #544: arm a source for backfill mode. The next `runs` download // Plan #693: start/stop a run-until-done backfill. 'start' walks the full
// runs (default 3) walk gallery-dl's full post history instead of // post history in time-boxed chunks until it reaches the bottom (then the
// exiting early at the first contiguous archived block. // source shows backfill_state 'complete'); 'stop' cancels back to tick mode.
async function setBackfill(id, runs = 3, artistIdHint = null) { async function startBackfill(id, artistIdHint = null) {
const body = await api.post(`/api/sources/${id}/backfill`, { body: { runs } }) const body = await api.post(`/api/sources/${id}/backfill`, { body: { action: 'start' } })
_invalidate(artistIdHint ?? body.artist_id)
return body
}
async function stopBackfill(id, artistIdHint = null) {
const body = await api.post(`/api/sources/${id}/backfill`, { body: { action: 'stop' } })
_invalidate(artistIdHint ?? body.artist_id) _invalidate(artistIdHint ?? body.artist_id)
return body return body
} }
@@ -120,7 +125,8 @@ export const useSourcesStore = defineStore('sources', () => {
loadAll, loadForArtist, loadAll, loadForArtist,
create, update, remove, create, update, remove,
checkNow, checkNow,
setBackfill, startBackfill,
stopBackfill,
findOrCreateArtist, autocompleteArtist, findOrCreateArtist, autocompleteArtist,
loadScheduleStatus, loadScheduleStatus,
sourcesByArtistGrouped, sourcesByArtistGrouped,