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:
@@ -1,20 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="fc-dl__top">
|
||||
<DownloadStatChips :stats="store.stats" />
|
||||
<span v-if="liveActive" class="fc-dl__live" title="Auto-refreshing while downloads are active">
|
||||
<span class="fc-dl__live-dot" />
|
||||
live
|
||||
</span>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" icon @click="refresh">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
<v-tooltip activator="parent" location="top">Refresh</v-tooltip>
|
||||
</v-btn>
|
||||
<MaintenanceMenu @refresh="refresh" />
|
||||
</div>
|
||||
<div class="fc-dl__sticky">
|
||||
<div class="fc-dl__top">
|
||||
<DownloadStatChips
|
||||
:stats="store.stats"
|
||||
:active-status="filterModel.status"
|
||||
@select="onStatusChipSelect"
|
||||
/>
|
||||
<span v-if="liveActive" class="fc-dl__live" title="Auto-refreshing while downloads are active">
|
||||
<span class="fc-dl__live-dot" />
|
||||
live
|
||||
</span>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" icon @click="refresh">
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
<v-tooltip activator="parent" location="top">Refresh</v-tooltip>
|
||||
</v-btn>
|
||||
<MaintenanceMenu @refresh="refresh" />
|
||||
</div>
|
||||
|
||||
<DownloadsFilterPopover v-model="filterModel" class="fc-dl__filter" />
|
||||
<div class="fc-dl__controls">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
density="compact" variant="outlined" hide-details clearable
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search artist / platform / error"
|
||||
style="max-width: 340px"
|
||||
/>
|
||||
<DownloadsFilterPopover v-model="filterModel" />
|
||||
<v-switch
|
||||
v-model="showNoChange"
|
||||
label="Show no-change scans"
|
||||
density="compact" hide-details color="accent"
|
||||
class="fc-dl__nochange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-alert v-if="store.error" type="error" variant="tonal" closable class="my-4">
|
||||
{{ String(store.error) }}
|
||||
@@ -92,6 +113,17 @@ const route = useRoute()
|
||||
const store = useDownloadsStore()
|
||||
const filterModel = ref({ ...store.filter })
|
||||
|
||||
// Free-text search (client-side over the loaded events) + a toggle to
|
||||
// hide "no-change" scheduled scans (status=ok/skipped with 0 files) so
|
||||
// real downloads + failures stand out. Operator-flagged 2026-05-28.
|
||||
const search = ref('')
|
||||
const showNoChange = ref(false)
|
||||
|
||||
// Clicking a stat chip toggles a status filter (re-click clears it).
|
||||
function onStatusChipSelect(statusKey) {
|
||||
filterModel.value = { ...filterModel.value, status: statusKey }
|
||||
}
|
||||
|
||||
// Each group's collapsed state persists across refreshes for the
|
||||
// lifetime of the SubscriptionsView (operator-friendly default: all
|
||||
// expanded; collapse what you don't care about right now).
|
||||
@@ -158,6 +190,21 @@ const filteredEvents = computed(() => {
|
||||
const toTs = new Date(to).getTime() + 24 * 3600 * 1000 - 1
|
||||
arr = arr.filter((e) => new Date(e.started_at).getTime() <= toTs)
|
||||
}
|
||||
// Hide no-change scheduled scans (succeeded/skipped, downloaded
|
||||
// nothing) unless the operator opts to show them.
|
||||
if (!showNoChange.value) {
|
||||
arr = arr.filter(
|
||||
(e) => !((e.status === 'ok' || e.status === 'skipped') && !(e.files_count > 0)),
|
||||
)
|
||||
}
|
||||
const q = search.value?.trim().toLowerCase()
|
||||
if (q) {
|
||||
arr = arr.filter((e) =>
|
||||
(e.artist_name || '').toLowerCase().includes(q)
|
||||
|| (e.platform || '').toLowerCase().includes(q)
|
||||
|| (e.error || '').toLowerCase().includes(q),
|
||||
)
|
||||
}
|
||||
return arr
|
||||
})
|
||||
|
||||
@@ -207,6 +254,7 @@ watch(filterModel, async (m) => {
|
||||
await store.applyFilter({
|
||||
status: m.status,
|
||||
source_id: m.source_id || null,
|
||||
artist_id: m.artist_id || null,
|
||||
from_date: m.from_date,
|
||||
to_date: m.to_date,
|
||||
})
|
||||
@@ -219,12 +267,27 @@ async function openDetail(id) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Sticky control header: stat chips + search + filter + toggle stay
|
||||
pinned below the top nav while the event feed scrolls. Opaque
|
||||
background so rows don't bleed through. */
|
||||
.fc-dl__sticky {
|
||||
position: sticky;
|
||||
top: 48px;
|
||||
z-index: 3;
|
||||
background: rgb(var(--v-theme-background));
|
||||
padding: 8px 0 10px;
|
||||
margin-bottom: 4px;
|
||||
border-bottom: 1px solid rgb(var(--v-theme-on-surface-variant) / 0.12);
|
||||
}
|
||||
.fc-dl__top {
|
||||
display: flex; gap: 8px; align-items: center;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fc-dl__filter { margin-bottom: 12px; }
|
||||
.fc-dl__controls {
|
||||
display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
|
||||
}
|
||||
.fc-dl__nochange { flex: 0 0 auto; }
|
||||
.fc-dl__live {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
|
||||
|
||||
Reference in New Issue
Block a user