Files
FabledCurator/frontend/src/components/subscriptions/SettingsTab.vue
T
bvandeusen 56970fb66d feat(credentials+downloads): real credential Verify button + live download-activity polling
Operator-flagged 2026-05-28, two asks.

**1. Credential Verify (was missing vs GS — and now actually verifies).**
GS's Verify was a stub (`TODO: implement actual verification` — just
stamped last_verified). FC does a real check, which matters given the
recent auth pain (subscribestar age cookie, HF host-only PHPSESSID):

- GalleryDLService.verify(url, platform, cookies_path, auth_token) runs
  gallery-dl in `--simulate --range 1-1` mode (no download) against the
  URL with the materialized credentials, then reuses _categorize_error:
  returncode 0 / NO_NEW_CONTENT → valid; AUTH_ERROR → invalid; other →
  inconclusive (reason surfaced). 45s timeout.
- POST /api/credentials/<platform>/verify picks an enabled Source for
  the platform to probe, runs verify, and on success stamps
  credential.last_verified (new CredentialService.mark_verified).
  Returns {valid: bool|null, reason, last_verified?}. valid=null means
  untestable (no credential, or no enabled source to point at).
- CredentialCard gains a Verify button (on credentialed cards) + a
  result chip (Verified ✓ / Failed / Untestable) and a toast with the
  reason. SettingsTab reloads on @verified so last_verified refreshes.

**2. Live download-activity feedback.** The Downloads tab was static —
no way to tell if downloads were succeeding without manually hitting
Refresh. It now auto-polls: stats every 4s, and the event list too
while anything is queued/running. Polling pauses when the tab is
backgrounded (document.hidden) and the list reload is skipped on idle
ticks to stay light. A pulsing "● live" indicator next to the stat
chips shows when auto-refresh is active (queued+running > 0); honors
prefers-reduced-motion.

Tests: verify endpoint — untestable with no credential, untestable with
no enabled source, valid+stamped on success (gallery-dl mocked), and
auth-failure reported without stamping.
2026-05-28 07:52:20 -04:00

198 lines
6.1 KiB
Vue

<template>
<div>
<ExtensionKeyBar class="mb-4" />
<v-alert v-if="credentialsStore.error" type="error" variant="tonal" closable class="mb-4">
{{ String(credentialsStore.error) }}
</v-alert>
<h3 class="text-h6 mb-3">Platform credentials</h3>
<v-row>
<v-col
v-for="p in platformsStore.list"
:key="p.key"
cols="12" md="6"
>
<CredentialCard
:platform="p"
:credential="credentialsStore.byPlatform.get(p.key) || null"
@replace="openUpload"
@remove="confirmRemove"
@verified="onSaved"
/>
</v-col>
</v-row>
<h3 class="text-h6 mb-3 mt-6">Downloader</h3>
<v-card variant="outlined">
<v-card-text v-if="importStore.settings">
<v-row>
<v-col cols="12" sm="6">
<v-text-field
v-model.number="dl.download_rate_limit_seconds"
label="Rate limit (seconds between requests)"
type="number" step="0.5" min="0"
density="compact" hide-details
@blur="saveDownloader"
/>
<div class="fc-help">gallery-dl extractor.sleep. Higher = slower but safer.</div>
</v-col>
<v-col cols="12" sm="6">
<v-switch
v-model="dl.download_validate_files"
label="Validate downloaded files (magic-byte check)"
density="compact" hide-details color="accent"
@change="saveDownloader"
/>
</v-col>
</v-row>
</v-card-text>
<v-card-text v-else>
<v-skeleton-loader type="paragraph" />
</v-card-text>
</v-card>
<h3 class="text-h6 mb-3 mt-6">Schedule defaults</h3>
<v-card variant="outlined">
<v-card-text v-if="importStore.settings">
<v-row>
<v-col cols="12" sm="4">
<v-text-field
v-model.number="dl.download_schedule_default_seconds"
label="Default check interval (seconds)"
type="number" :min="60" :max="86400"
density="compact" hide-details
@blur="saveDownloader"
/>
<div class="fc-help">
Used when a source has no per-source or per-artist override.
Default 28800 (8 hours).
</div>
</v-col>
<v-col cols="12" sm="4">
<v-text-field
v-model.number="dl.download_event_retention_days"
label="Event retention (days)"
type="number" :min="1" :max="3650"
density="compact" hide-details
@blur="saveDownloader"
/>
<div class="fc-help">
Completed download events older than this are deleted nightly.
Default 90.
</div>
</v-col>
<v-col cols="12" sm="4">
<v-text-field
v-model.number="dl.download_failure_warning_threshold"
label="Failure warning threshold"
type="number" :min="1" :max="100"
density="compact" hide-details
@blur="saveDownloader"
/>
<div class="fc-help">
Source row badge turns red after this many consecutive
failures. Sources are never auto-disabled. Default 5.
</div>
</v-col>
</v-row>
<v-alert v-if="importStore.settingsError" type="error" variant="tonal" class="mt-2" closable>
{{ importStore.settingsError }}
</v-alert>
</v-card-text>
<v-card-text v-else>
<v-skeleton-loader type="paragraph" />
</v-card-text>
</v-card>
<CredentialUploadDialog
v-model="showUpload"
:platform="uploadPlatform"
@saved="onSaved"
/>
<v-dialog v-model="removeConfirm.open" max-width="420">
<v-card>
<v-card-title>Delete {{ removeConfirm.platform?.name }} credential?</v-card-title>
<v-card-text>
The encrypted credential will be removed permanently. You'll need to
re-upload to use this platform again.
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="removeConfirm.open = false">Cancel</v-btn>
<v-btn color="error" variant="flat" @click="doRemove">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, ref, watch } from 'vue'
import { usePlatformsStore } from '../../stores/platforms.js'
import { useCredentialsStore } from '../../stores/credentials.js'
import { useImportStore } from '../../stores/import.js'
import ExtensionKeyBar from '../credentials/ExtensionKeyBar.vue'
import CredentialUploadDialog from '../credentials/CredentialUploadDialog.vue'
import CredentialCard from './CredentialCard.vue'
const platformsStore = usePlatformsStore()
const credentialsStore = useCredentialsStore()
const importStore = useImportStore()
const showUpload = ref(false)
const uploadPlatform = ref(null)
const removeConfirm = reactive({ open: false, platform: null })
const dl = reactive({
download_rate_limit_seconds: 3.0,
download_validate_files: true,
download_schedule_default_seconds: 28800,
download_event_retention_days: 90,
download_failure_warning_threshold: 5,
})
watch(() => importStore.settings, (s) => { if (s) Object.assign(dl, s) }, { immediate: true })
onMounted(async () => {
await Promise.all([
platformsStore.loadAll(),
credentialsStore.loadAll(),
importStore.loadSettings(),
])
})
function openUpload(platform) {
uploadPlatform.value = platform
showUpload.value = true
}
async function onSaved() {
await credentialsStore.loadAll()
}
function confirmRemove(platform) {
removeConfirm.platform = platform
removeConfirm.open = true
}
async function doRemove() {
await credentialsStore.remove(removeConfirm.platform.key)
removeConfirm.open = false
await credentialsStore.loadAll()
}
async function saveDownloader() {
await importStore.patchSettings({ ...dl })
}
</script>
<style scoped>
.fc-help {
font-size: 12px;
color: rgb(var(--v-theme-on-surface-variant));
margin-top: 2px;
}
</style>