Files
FabledCurator/frontend/src/components/subscriptions/FailingSourcesCard.vue
T
bvandeusen f05aaa707b
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Failing after 26s
CI / intimp (push) Successful in 3m46s
CI / intapi (push) Failing after 8m10s
CI / intcore (push) Failing after 9m42s
fix(audit-g5d): surface ErrorType taxonomy on FailingSourcesCard
Alembic 0032 adds Source.error_type (varchar(32), indexed).
_update_source_health stamps it alongside last_error on status='error'
and clears it on 'ok'. SourceRecord/to_dict exposes it.

FailingSourcesCard renders a colored chip next to the consecutive-
failures count, with a tooltip explaining the suggested operator
action. Color reflects intent:
  - warning (yellow) — operator action needed (auth_error)
  - info (blue)      — backend-paced (rate_limited / timeout /
                       network_error / partial / tier_limited)
  - error (red)      — likely terminal without intervention
                       (not_found / access_denied / validation_failed /
                        unsupported_url / http_error / unknown_error)

Audit 2026-06-02: the backend computed 13 ErrorType categories but
only the free-text last_error reached the operator. Bulk-triage by
class ("all auth_error → rotate cookies", "12 rate_limited → just
wait") required opening Logs per row.
2026-06-02 17:54:44 -04:00

171 lines
5.9 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>
<v-card v-if="sources.length" variant="tonal" color="error" class="fc-fail mb-4">
<div class="fc-fail__head" @click="open = !open">
<v-icon size="small">mdi-alert-circle</v-icon>
<span class="fc-fail__title">
{{ sources.length }}
{{ sources.length === 1 ? 'source is' : 'sources are' }} failing
</span>
<v-spacer />
<v-btn
size="small" variant="text" prepend-icon="mdi-refresh"
:loading="retryingAll"
@click.stop="$emit('retry-all', sources)"
>
Retry all
</v-btn>
<v-icon size="small">{{ open ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</div>
<v-expand-transition>
<div v-if="open" class="fc-fail__body">
<div v-for="s in sources" :key="s.id" class="fc-fail__row">
<PlatformChip :platform="s.platform" size="x-small" />
<span class="fc-fail__artist">{{ s.artist_name || `#${s.artist_id}` }}</span>
<v-chip size="x-small" color="error" variant="flat" label class="fc-fail__count">
{{ s.consecutive_failures }}× failed
</v-chip>
<v-chip
v-if="s.error_type"
size="x-small" variant="outlined" label
:color="errorTypeColor(s.error_type)"
class="fc-fail__class"
:title="errorTypeHint(s.error_type)"
>
{{ s.error_type }}
</v-chip>
<span class="fc-fail__err" :title="s.last_error || ''">
{{ s.last_error || 'no error message recorded' }}
</span>
<v-spacer />
<v-btn
size="x-small" variant="text" prepend-icon="mdi-text-box-search-outline"
:loading="logLoadingIds.has(s.id)"
@click="onViewLogs(s)"
title="Show the most recent download event's stdout/stderr/error"
>
Logs
</v-btn>
<v-btn
size="x-small" variant="text" prepend-icon="mdi-refresh"
:loading="retryingIds.has(s.id)"
@click="$emit('retry', s)"
>
Retry
</v-btn>
</div>
</div>
</v-expand-transition>
</v-card>
</template>
<script setup>
import { ref } from 'vue'
import PlatformChip from './PlatformChip.vue'
defineProps({
// source records from /api/sources?failing=true
sources: { type: Array, default: () => [] },
retryingIds: { type: Set, default: () => new Set() },
retryingAll: { type: Boolean, default: false },
})
const emit = defineEmits(['retry', 'retry-all', 'view-logs'])
const open = ref(true)
// Per-row loading flag so the spinner lives on the row whose Logs
// button was clicked, not on every row.
const logLoadingIds = ref(new Set())
// Audit 2026-06-02: surface the ErrorType taxonomy as a colored chip
// next to the consecutive-failures count so operators can bulk-triage
// by error class. Color reflects "what to do next":
// warning (yellow) — auth/cookie issue: operator should rotate
// info (blue) — backend-paced (cooldown / rate limit / timeout)
// error (red) — likely terminal without operator intervention
const ERROR_TYPE_COLOR = {
auth_error: 'warning',
rate_limited: 'info',
timeout: 'info',
network_error: 'info',
not_found: 'error',
access_denied: 'error',
validation_failed: 'error',
unsupported_url: 'error',
http_error: 'error',
unknown_error: 'error',
partial: 'info',
tier_limited: 'info',
no_new_content: 'info',
}
const ERROR_TYPE_HINT = {
auth_error: 'Cookies likely expired — re-upload in Credentials.',
rate_limited: 'Platform-wide cooldown active. Will retry after it expires.',
timeout: 'Subprocess exceeded its time budget. Often retries cleanly.',
network_error: 'Transient network issue. Will retry on next tick.',
not_found: 'URL 404 — creator may have renamed or deleted.',
access_denied: 'Subscription tier may not grant this content.',
validation_failed: 'Downloaded files were quarantined by the validator.',
http_error: 'Generic HTTP error — see Logs.',
unsupported_url: 'gallery-dl does not support this URL pattern.',
unknown_error: 'Could not classify — see Logs.',
}
function errorTypeColor(t) { return ERROR_TYPE_COLOR[t] || 'error' }
function errorTypeHint(t) { return ERROR_TYPE_HINT[t] || '' }
async function onViewLogs(s) {
if (logLoadingIds.value.has(s.id)) return
logLoadingIds.value = new Set(logLoadingIds.value).add(s.id)
try {
await emit('view-logs', s)
} finally {
const next = new Set(logLoadingIds.value)
next.delete(s.id)
logLoadingIds.value = next
}
}
</script>
<style scoped>
.fc-fail { border-radius: 8px; }
.fc-fail__head {
display: flex; align-items: center; gap: 8px;
padding: 10px 14px;
cursor: pointer;
user-select: none;
}
.fc-fail__title { font-weight: 600; }
.fc-fail__body {
padding: 0 14px 10px;
display: flex; flex-direction: column;
/* Borders, not gap, so the row separators are visible inside the
* tonal error card (where surface tints get washed out and gap is
* just empty space). */
}
.fc-fail__row {
display: flex; align-items: center; gap: 10px;
padding: 8px 10px;
border-radius: 4px;
/* Hover-darken gives the eye a horizontal track from the artist
* name on the left to the Logs/Retry buttons on the right.
* Bottom border replaces the previous-too-subtle zebra striping —
* inside the tonal error card, surface-tint contrast was negligible.
* Operator-flagged 2026-06-01 (twice). */
border-bottom: 1px solid rgb(255 255 255 / 0.08);
transition: background 80ms ease;
}
.fc-fail__row:last-child {
border-bottom: none;
}
.fc-fail__row:hover {
background: rgb(0 0 0 / 0.25);
}
.fc-fail__artist { font-weight: 600; white-space: nowrap; }
.fc-fail__count { flex: 0 0 auto; }
.fc-fail__class { flex: 0 0 auto; }
.fc-fail__err {
color: rgb(var(--v-theme-on-surface-variant));
font-size: 0.8rem;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
max-width: 46ch;
}
</style>