feat(dashboards): 1600px max-width, richer Downloads filters, needs-attention + sticky headers

Operator-flagged 2026-05-28: the Subscriptions/Downloads dashboards were
full-bleed and thin on filtering. Chosen via AskUserQuestion.

**Layout**
- SubscriptionsView capped at a centered max-width 1600px (covers all
  three subtabs) so rows aren't a mile wide on ultrawide monitors.
- Sticky control headers on both tabs (top: 48px, below the top nav,
  opaque bg) so filters/stat-chips stay reachable while scrolling a
  long list.

**Downloads filters (all four requested)**
- Stat chips are now clickable filters: click Queued/Running/Completed/
  Failed/Skipped to filter the list to that status; the active chip is
  outlined + elevated; re-click clears.
- Free-text search box over the loaded events (artist / platform /
  error substring).
- Artist filter: the filter popover's numeric "Source ID" field is
  replaced with an artist autocomplete (sources.autocompleteArtist →
  artist_id, which /api/downloads already supports). Pill shows the
  artist name.
- "Show no-change scans" toggle (default OFF): hides status=ok/skipped
  rows with 0 files (the scheduled scans that found nothing) so real
  downloads + failures stand out.

**Subscriptions**
- "Needs attention" quick-filter chip: one click to show only artists
  with sources that have errors OR have never been checked; chip shows
  the count and disables the status dropdown while active.

Frontend-only — backend filter params (status/artist_id/date) and the
/api/downloads endpoint already supported everything.
This commit is contained in:
2026-05-28 08:10:04 -04:00
parent 73520b7cc3
commit a459d21a65
5 changed files with 184 additions and 32 deletions
@@ -7,10 +7,22 @@
<v-btn variant="outlined" prepend-icon="mdi-account-plus" @click="showArtistDialog = true">
New artist
</v-btn>
<v-chip
:color="needsAttention ? 'error' : undefined"
:variant="needsAttention ? 'flat' : 'outlined'"
prepend-icon="mdi-alert-circle-outline"
@click="needsAttention = !needsAttention"
>
Needs attention
<v-chip v-if="attentionCount" size="x-small" class="ms-2" variant="tonal">
{{ attentionCount }}
</v-chip>
</v-chip>
<v-spacer />
<v-select
v-model="statusFilter"
:items="STATUS_OPTIONS"
:disabled="needsAttention"
density="compact" variant="outlined" hide-details
style="max-width: 180px"
/>
@@ -218,6 +230,7 @@ const importStore = useImportStore()
const search = ref('')
const statusFilter = ref('all')
const needsAttention = ref(false)
const expanded = ref([])
const selected = ref([])
const showSourceDialog = ref(false)
@@ -277,12 +290,23 @@ const groups = computed(() => {
})
})
// A group "needs attention" if any of its sources has accumulated
// failures OR has never been checked — the ones worth acting on.
function groupNeedsAttention(g) {
return groupMatchesStatus(g, 'errors') || groupMatchesStatus(g, 'stale')
}
const attentionCount = computed(
() => groups.value.filter(groupNeedsAttention).length,
)
const filteredGroups = computed(() => {
let arr = groups.value
if (artistFilter.value != null) {
arr = arr.filter((g) => g.artist.id === artistFilter.value)
}
if (statusFilter.value !== 'all') {
if (needsAttention.value) {
arr = arr.filter(groupNeedsAttention)
} else if (statusFilter.value !== 'all') {
arr = arr.filter((g) => groupMatchesStatus(g, statusFilter.value))
}
const q = search.value?.trim().toLowerCase()
@@ -478,9 +502,17 @@ async function bulkDelete() {
<style scoped>
.fc-subs__bar {
display: flex; gap: 0.75rem; align-items: center;
padding-bottom: 1rem;
flex-wrap: wrap;
/* Sticky below the top nav so the filter/search controls stay
reachable while scrolling a long subscription list. Opaque bg so
table rows don't bleed through. Operator-flagged 2026-05-28. */
position: sticky;
top: 48px;
z-index: 3;
background: rgb(var(--v-theme-background));
padding: 8px 0 1rem;
}
.fc-subs__bar .v-chip { cursor: pointer; }
.fc-subs__loading, .fc-subs__empty {
display: flex; justify-content: center; padding: 2rem;
color: rgb(var(--v-theme-on-surface-variant));