feat(downloads): Logs button on failing-sources rollup rows
Operator-asked 2026-06-01: each row in the "X sources are failing" rollup needs a button to surface the last run's stdout/stderr/error without leaving the Downloads tab to find the matching event row. Wired: - `downloadsStore.loadLastForSource(sourceId)` two-steps via `GET /api/downloads?source_id=N&limit=1` (most recent event) then the existing `loadOne(id)` for the full detail payload. Returns null if no events exist (toast warns). - `FailingSourcesCard` row: new text button `Logs` with `mdi-text-box-search-outline`, per-row spinner via `logLoadingIds`, emits `view-logs` with the source record. - `DownloadsTab.onViewFailingLogs` is the handler — same `DownloadDetailModal` instance the event-row clicks use.
This commit is contained in:
@@ -58,6 +58,7 @@
|
|||||||
:retrying-all="retryingAll"
|
:retrying-all="retryingAll"
|
||||||
@retry="onRetrySource"
|
@retry="onRetrySource"
|
||||||
@retry-all="onRetryAll"
|
@retry-all="onRetryAll"
|
||||||
|
@view-logs="onViewFailingLogs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="store.loading && store.events.length === 0" class="fc-dl__loading">
|
<div v-if="store.loading && store.events.length === 0" class="fc-dl__loading">
|
||||||
@@ -364,6 +365,23 @@ watch(filterModel, async (m) => {
|
|||||||
async function openDetail(id) {
|
async function openDetail(id) {
|
||||||
await store.loadOne(id)
|
await store.loadOne(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onViewFailingLogs(source) {
|
||||||
|
// Find and open the most recent DownloadEvent for this source.
|
||||||
|
// Reuses the existing DownloadDetailModal — same stdout/stderr/error
|
||||||
|
// surface the row-click in the events feed shows.
|
||||||
|
try {
|
||||||
|
const ev = await store.loadLastForSource(source.id)
|
||||||
|
if (!ev) {
|
||||||
|
toast({
|
||||||
|
text: `No download events recorded for ${source.artist_name || source.platform} yet.`,
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast({ text: `Failed to load logs: ${e.message}`, type: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -29,6 +29,14 @@
|
|||||||
{{ s.last_error || 'no error message recorded' }}
|
{{ s.last_error || 'no error message recorded' }}
|
||||||
</span>
|
</span>
|
||||||
<v-spacer />
|
<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
|
<v-btn
|
||||||
size="x-small" variant="text" prepend-icon="mdi-refresh"
|
size="x-small" variant="text" prepend-icon="mdi-refresh"
|
||||||
:loading="retryingIds.has(s.id)"
|
:loading="retryingIds.has(s.id)"
|
||||||
@@ -52,9 +60,23 @@ defineProps({
|
|||||||
retryingIds: { type: Set, default: () => new Set() },
|
retryingIds: { type: Set, default: () => new Set() },
|
||||||
retryingAll: { type: Boolean, default: false },
|
retryingAll: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
defineEmits(['retry', 'retry-all'])
|
const emit = defineEmits(['retry', 'retry-all', 'view-logs'])
|
||||||
|
|
||||||
const open = ref(true)
|
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())
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -55,6 +55,21 @@ export const useDownloadsStore = defineStore('downloads', () => {
|
|||||||
return selected.value
|
return selected.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open the detail modal for the most recent DownloadEvent of a given
|
||||||
|
// source. Used by the failing-sources rollup's "Logs" button so the
|
||||||
|
// operator can troubleshoot without leaving the Downloads tab to find
|
||||||
|
// the row (operator-flagged 2026-06-01).
|
||||||
|
async function loadLastForSource(sourceId) {
|
||||||
|
const events = await api.get('/api/downloads', {
|
||||||
|
params: { source_id: sourceId, limit: 1 },
|
||||||
|
})
|
||||||
|
if (!events.length) {
|
||||||
|
selected.value = null
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return await loadOne(events[0].id)
|
||||||
|
}
|
||||||
|
|
||||||
async function applyFilter(patch) {
|
async function applyFilter(patch) {
|
||||||
filter.value = { ...filter.value, ...patch }
|
filter.value = { ...filter.value, ...patch }
|
||||||
await loadFirst()
|
await loadFirst()
|
||||||
@@ -98,7 +113,8 @@ export const useDownloadsStore = defineStore('downloads', () => {
|
|||||||
return {
|
return {
|
||||||
events, cursor, hasMore, filter, selected, loading, error, stats,
|
events, cursor, hasMore, filter, selected, loading, error, stats,
|
||||||
activity, failing, activeEvents,
|
activity, failing, activeEvents,
|
||||||
loadFirst, loadMore, loadOne, applyFilter, closeDetail, loadStats,
|
loadFirst, loadMore, loadOne, loadLastForSource, applyFilter,
|
||||||
|
closeDetail, loadStats,
|
||||||
loadActivity, loadFailing, loadActive, recoverStalled,
|
loadActivity, loadFailing, loadActive, recoverStalled,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user