Files
FabledCurator/frontend/src/components/subscriptions/RecentArrivalsCard.vue
T
bvandeusen dc7fa6eae2
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 38s
CI / integration (push) Successful in 3m30s
feat(ia): wave 3 — Subscriptions landing answers 'what needs me, what came in?'
Daily-use reorder of the Subscriptions tab: needs-attention strip first
(FailingSourcesCard moves up from below the Downloads fold — a broken
subscription was invisible unless you went looking), then a new Recent
arrivals card (real downloads only, no-change scans filtered out, artist
links), then the source list. Both cards render nothing when there's nothing
to say.

Retry logic moves into the downloads store (retrySource / retryAllFailing) so
the needs-attention card and the Downloads maintenance menu share one
implementation — single-retry forces past cooldown, bulk keeps cooldown
enforcement, same tally shape. The card's Logs button deep-links into the
Downloads tab pre-filtered (?source_id now watched, not just read on mount).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
2026-07-02 17:52:00 -04:00

81 lines
2.4 KiB
Vue

<template>
<v-card v-if="arrivals.length" class="mb-4">
<CardHeading icon="mdi-new-box" title="Recent arrivals">
<v-spacer />
<v-btn
variant="text" size="small" rounded="pill"
to="/subscriptions?tab=downloads"
>
All downloads
<v-icon end size="small">mdi-arrow-right</v-icon>
</v-btn>
</CardHeading>
<v-card-text class="pt-0">
<div
v-for="ev in arrivals" :key="ev.id"
class="fc-arrival"
>
<router-link
v-if="ev.artist_slug"
:to="`/artist/${ev.artist_slug}`"
class="fc-arrival__artist"
>{{ ev.artist_name || ev.artist_slug }}</router-link>
<span v-else class="fc-arrival__artist">{{ ev.artist_name || '—' }}</span>
<span class="fc-arrival__meta">
{{ ev.platform }} ·
{{ ev.files_count ? `${ev.files_count} file(s)` : 'no new files' }}
· {{ formatRelative(ev.started_at) }}
</span>
</div>
</v-card-text>
</v-card>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import CardHeading from '../common/CardHeading.vue'
import { formatRelative } from '../../utils/date.js'
import { useApi } from '../../composables/useApi.js'
// "What came in?" — the other half of the landing tab's daily answer
// (2026-07-02). Own fetch, own state: the downloads store's event list is
// the Downloads tab's FILTERED feed; borrowing it would couple this card to
// whatever filter the operator left that tab on.
const api = useApi()
const arrivals = ref([])
let pollId = null
async function load() {
try {
const events = await api.get('/api/downloads', {
params: { status: 'ok', limit: 25 },
})
// Real arrivals only — scheduled scans that found nothing are noise here.
arrivals.value = events.filter((e) => (e.files_count || 0) > 0).slice(0, 6)
} catch { /* non-fatal — the card just hides */ }
}
onMounted(() => {
load()
pollId = setInterval(() => { if (!document.hidden) load() }, 60000)
})
onUnmounted(() => { if (pollId) clearInterval(pollId) })
</script>
<style scoped>
.fc-arrival {
display: flex; align-items: baseline; gap: 10px;
padding: 4px 0;
}
.fc-arrival__artist {
font-weight: 600; text-decoration: none;
color: rgb(var(--v-theme-accent));
}
.fc-arrival__artist:hover { text-decoration: underline; }
.fc-arrival__meta {
font-size: 12px;
color: rgb(var(--v-theme-on-surface-variant));
}
</style>