fix(subs): stop the lock/reload on source actions + regroup the row buttons
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 24s
CI / integration (push) Successful in 3m2s

Lock/reload: every inline source action (check / backfill / recover / toggle /
remove) ended by refetching the WHOLE subscription list (store.loadAll /
refresh), which blocked the UI and re-rendered the table — collapsing the
expanded row, which read as "locks then resets." The action APIs already return
the updated source, so the store now patches that one row in place
(_patchSource / _dropSource); the post-action loadAll/refresh calls are gone.
Toggling enabled, starting/stopping a backfill, recovering, and removing are now
instant and leave the expansion intact.

Button regroup (operator-flagged: tiny, mis-clickable, not grouped by function):
- New shared SourceActions.vue used by desktop SourceRow + mobile SourceCard.
- Frequent actions stay as size="small" buttons: Check, Backfill/Stop.
- Low-frequency / destructive actions move into a labelled overflow (⋮) menu —
  Preview backfill, Recover dropped near-duplicates, Remove source — so they
  can't be fat-fingered, and the labels spell out recover-vs-backfill.
- Edit moves next to the source URL (its identity), out of the action cluster
  where it sat beside Remove.
- Single-source Remove now confirms (it had no guard before).

Tests: store patches/drops in place without dropping the cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 20:48:01 -04:00
parent c65da42593
commit b7d07324ee
6 changed files with 238 additions and 126 deletions
@@ -0,0 +1,91 @@
<template>
<!-- Shared per-source action cluster (desktop SourceRow + mobile SourceCard).
Frequent actions stay as buttons; low-frequency / destructive ones move
into a labelled overflow menu so they can't be mis-clicked, and the
labels spell out what each one does (recover vs backfill). -->
<div class="fc-src-actions">
<v-btn
icon size="small" variant="text" :loading="checking"
@click.stop="emit('check', source)"
>
<v-icon>mdi-play</v-icon>
<v-tooltip activator="parent" location="top">
Check now — fetch posts published since the last check
</v-tooltip>
</v-btn>
<v-btn
icon size="small" variant="text"
:color="running ? 'warning' : undefined"
@click.stop="emit('backfill', source)"
>
<v-icon>{{ running ? 'mdi-stop' : 'mdi-magnify-scan' }}</v-icon>
<v-tooltip activator="parent" location="top">
{{ running
? (recovering ? 'Stop recovery' : 'Stop backfill')
: 'Backfill one-time walk of the FULL post history until complete' }}
</v-tooltip>
</v-btn>
<v-menu location="bottom end">
<template #activator="{ props: menuProps }">
<v-btn icon size="small" variant="text" v-bind="menuProps" @click.stop>
<v-icon>mdi-dots-vertical</v-icon>
<v-tooltip activator="parent" location="top">More actions</v-tooltip>
</v-btn>
</template>
<v-list density="compact" min-width="260">
<v-list-item
v-if="isPatreon && !running"
prepend-icon="mdi-eye-outline"
@click="emit('preview', source)"
>
<v-list-item-title>Preview backfill</v-list-item-title>
<v-list-item-subtitle>
Count what a backfill would download — nothing is downloaded
</v-list-item-subtitle>
</v-list-item>
<v-list-item
v-if="isPatreon && !running"
prepend-icon="mdi-backup-restore"
@click="emit('recover', source)"
>
<v-list-item-title>Recover dropped near-duplicates</v-list-item-title>
<v-list-item-subtitle>
Re-fetch images previously dropped as near-dups and re-judge them
under the current similarity threshold
</v-list-item-subtitle>
</v-list-item>
<v-divider v-if="isPatreon && !running" />
<v-list-item
base-color="error"
prepend-icon="mdi-delete-outline"
@click="emit('remove', source)"
>
<v-list-item-title>Remove source</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
source: { type: Object, required: true },
checking: { type: Boolean, default: false },
})
const emit = defineEmits(['check', 'backfill', 'recover', 'preview', 'remove'])
const running = computed(() => props.source.backfill_state === 'running')
const recovering = computed(() => !!props.source.backfill_bypass_seen)
const isPatreon = computed(() => props.source.platform === 'patreon')
</script>
<style scoped>
.fc-src-actions {
display: inline-flex;
align-items: center;
gap: 2px;
}
</style>