docs: add UI improvement pass implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,790 @@
|
||||
# UI Improvement Pass Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the default Vuetify blue palette with a cool slate/navy monitoring-dashboard theme, lift the nav branding, add a WebSocket status indicator, improve Dashboard layout, fix a dark-mode bug in Subscriptions, and add per-page UX improvements to Downloads and Credentials.
|
||||
|
||||
**Architecture:** All changes are purely frontend (Vue 3 + Vuetify 3). No backend changes, no new API endpoints. Each task touches one file and can be built/verified independently with `npm run build` from the `frontend/` directory.
|
||||
|
||||
**Tech Stack:** Vue 3.4, Vuetify 3.5, Pinia, Vite 5 — all in `frontend/src/`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | What changes |
|
||||
|------|-------------|
|
||||
| `frontend/src/plugins/vuetify.js` | Full palette replacement; component defaults update |
|
||||
| `frontend/src/App.vue` | Nav brand header; nav item rounding/density; app bar border + WS indicator |
|
||||
| `frontend/src/views/Dashboard.vue` | Number-forward stat cards; action bar split; running pulse animation; consistent empty states |
|
||||
| `frontend/src/views/Subscriptions.vue` | Replace `bg-grey-lighten-4` with `bg-surface` on expanded rows |
|
||||
| `frontend/src/views/Downloads.vue` | Add `exclude_superseded` filter toggle below existing filters |
|
||||
| `frontend/src/views/Credentials.vue` | Configured vs unconfigured card visual treatment |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Color System & Component Defaults
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/plugins/vuetify.js` (full replacement)
|
||||
|
||||
**Context:** The current file sets identical colours for dark and light themes (both use `#1976D2` blue). We replace both themes entirely and add a `VChip` default. The existing `VCard: { elevation: 2 }` and `VBtn: { variant: 'flat' }` defaults are updated, not added from scratch.
|
||||
|
||||
- [ ] **Step 1: Replace `frontend/src/plugins/vuetify.js` with the new palette**
|
||||
|
||||
```js
|
||||
import 'vuetify/styles'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
|
||||
export default createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: 'dark',
|
||||
themes: {
|
||||
dark: {
|
||||
dark: true,
|
||||
colors: {
|
||||
background: '#0F172A',
|
||||
surface: '#1E293B',
|
||||
primary: '#60A5FA',
|
||||
secondary: '#94A3B8',
|
||||
error: '#F87171',
|
||||
warning: '#FBBF24',
|
||||
success: '#34D399',
|
||||
info: '#38BDF8',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
dark: false,
|
||||
colors: {
|
||||
background: '#F1F5F9',
|
||||
surface: '#FFFFFF',
|
||||
primary: '#2563EB',
|
||||
secondary: '#475569',
|
||||
error: '#EF4444',
|
||||
warning: '#F59E0B',
|
||||
success: '#10B981',
|
||||
info: '#0EA5E9',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
VCard: {
|
||||
elevation: 0,
|
||||
border: true,
|
||||
},
|
||||
VBtn: {
|
||||
variant: 'flat',
|
||||
},
|
||||
VChip: {
|
||||
rounded: 'md',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0, no errors. Warnings about unused variables are OK.
|
||||
|
||||
- [ ] **Step 3: Visual check**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
Open the app. Cards should now have a border instead of a shadow. Background should be deep navy in dark mode, cool light grey in light mode. Primary colour is a steel blue (`#60A5FA`), not the old Material blue.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/plugins/vuetify.js
|
||||
git commit -m "feat(ui): replace palette with slate/navy monitoring dashboard theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Nav Drawer Brand Header & App Bar
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/App.vue`
|
||||
|
||||
**Context:** Current `App.vue` uses a plain `v-list-item` for the brand block (lines 4–8). The nav list uses `density="compact"` (line 10) with no rounding or active-color. The app bar has `elevation="1"`. The `wsStore` is already imported and called (line 59, `wsStore.init()`), so `wsStore.isConnected` is available in the template without any new imports.
|
||||
|
||||
- [ ] **Step 1: Replace the entire `<template>` block in `App.vue`**
|
||||
|
||||
Replace the template (lines 1–47, inclusive of the closing `</template>` tag) with:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<v-app>
|
||||
<v-navigation-drawer v-model="drawer" app>
|
||||
<!-- Brand header -->
|
||||
<div class="pa-4 bg-surface">
|
||||
<div class="d-flex align-center ga-3">
|
||||
<v-icon color="primary" size="32">mdi-download-network</v-icon>
|
||||
<div>
|
||||
<div class="font-weight-bold text-subtitle-1">GallerySubscriber</div>
|
||||
<div class="text-caption text-medium-emphasis">Download Manager</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider />
|
||||
|
||||
<v-list nav density="comfortable">
|
||||
<v-list-item
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
:prepend-icon="item.icon"
|
||||
:title="item.title"
|
||||
rounded="lg"
|
||||
active-color="primary"
|
||||
/>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar app elevation="0" border="b">
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer" />
|
||||
<v-toolbar-title>{{ currentPageTitle }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
|
||||
<!-- WebSocket status indicator -->
|
||||
<v-btn icon variant="text" style="cursor: default" :ripple="false">
|
||||
<v-icon
|
||||
:color="wsStore.isConnected ? 'success' : 'error'"
|
||||
size="10"
|
||||
>mdi-circle</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
{{ wsStore.isConnected ? 'Real-time updates active' : 'Disconnected — real-time updates paused' }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="toggleTheme">
|
||||
<v-icon>{{ isDark ? 'mdi-weather-sunny' : 'mdi-weather-night' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<v-container fluid>
|
||||
<router-view />
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-snackbar
|
||||
v-model="notification.show"
|
||||
:color="notification.color"
|
||||
:timeout="3000"
|
||||
>
|
||||
{{ notification.message }}
|
||||
<template v-slot:actions>
|
||||
<v-btn variant="text" @click="notification.show = false">Close</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
```
|
||||
|
||||
The `<script setup>` block is unchanged — do not touch it.
|
||||
|
||||
- [ ] **Step 2: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 3: Visual check**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
- Nav drawer shows the icon + "GallerySubscriber" / "Download Manager" brand block with a surface-coloured background
|
||||
- Active nav item has a tinted pill highlight
|
||||
- App bar is flat with a bottom border line
|
||||
- A small coloured dot appears before the theme toggle (green when WS connected, red when not)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/App.vue
|
||||
git commit -m "feat(ui): lift nav branding and add WebSocket status indicator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Dashboard Stat Cards & Action Bar
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/Dashboard.vue` (lines 1–142 — the stat cards row and quick actions row)
|
||||
|
||||
**Context:** The stat cards currently use a `v-avatar` side-by-side layout with `text-h4` numbers. The action bar has all buttons, credential chips, and storage stats crammed into a single `v-card-text` with a `d-flex`. We replace both sections while leaving the rest of the file (active downloads, recent activity, sources needing attention) untouched in this task.
|
||||
|
||||
- [ ] **Step 1: Replace the stat cards row (lines 2–63)**
|
||||
|
||||
Replace from `<v-row>` (the first row, stat cards) through its closing `</v-row>` with:
|
||||
|
||||
```html
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text style="border-left: 4px solid rgb(var(--v-theme-primary)); padding-left: 20px">
|
||||
<div class="text-h3 font-weight-bold">{{ sourcesStore.sources.length }}</div>
|
||||
<div class="text-caption text-medium-emphasis">Total Sources</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text style="border-left: 4px solid rgb(var(--v-theme-success)); padding-left: 20px">
|
||||
<div class="text-h3 font-weight-bold">{{ stats?.completed || 0 }}</div>
|
||||
<div class="text-caption text-medium-emphasis">Completed Downloads</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text style="border-left: 4px solid rgb(var(--v-theme-error)); padding-left: 20px">
|
||||
<div class="text-h3 font-weight-bold">{{ stats?.active_failed || 0 }}</div>
|
||||
<div class="text-caption text-medium-emphasis">Failed Downloads</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text style="border-left: 4px solid rgb(var(--v-theme-warning)); padding-left: 20px">
|
||||
<div class="text-h3 font-weight-bold">{{ activeDownloadsCount }}</div>
|
||||
<div class="text-caption text-medium-emphasis">Active Downloads</div>
|
||||
<div class="text-caption text-medium-emphasis" v-if="stats?.queued || stats?.running">
|
||||
{{ stats?.queued || 0 }} queued · {{ stats?.running || 0 }} running
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the Quick Actions row (lines 65–142)**
|
||||
|
||||
Replace from `<!-- Quick Actions -->` comment through its closing `</v-row>` with:
|
||||
|
||||
```html
|
||||
<!-- Quick Actions & Status -->
|
||||
<v-row class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<!-- Row 1: Action buttons -->
|
||||
<v-card-text class="d-flex align-center ga-3 flex-wrap pb-3">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-refresh"
|
||||
:loading="checkingAll"
|
||||
@click="checkAllSources"
|
||||
>
|
||||
Check All Sources
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="warning"
|
||||
prepend-icon="mdi-replay"
|
||||
:loading="retryingAll"
|
||||
:disabled="!failedCount"
|
||||
@click="retryAllFailed"
|
||||
>
|
||||
Retry All Failed ({{ failedCount }})
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-broom"
|
||||
:loading="resettingOrphaned"
|
||||
:disabled="!stuckRunningCount"
|
||||
@click="resetOrphanedJobs"
|
||||
>
|
||||
Reset Stuck ({{ stuckRunningCount }})
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- Row 2: Status info -->
|
||||
<v-card-text class="d-flex align-center ga-2 flex-wrap pt-3">
|
||||
<v-icon size="small">mdi-key</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">Auth:</span>
|
||||
<v-chip
|
||||
v-for="platform in supportedPlatforms"
|
||||
:key="platform"
|
||||
:color="credentialsStore.hasPlatformCredentials(platform) ? 'success' : 'grey'"
|
||||
:variant="credentialsStore.hasPlatformCredentials(platform) ? 'tonal' : 'outlined'"
|
||||
size="x-small"
|
||||
:to="credentialsStore.hasPlatformCredentials(platform) ? undefined : '/credentials'"
|
||||
>
|
||||
<v-icon start size="x-small">{{ getPlatformIcon(platform) }}</v-icon>
|
||||
{{ platform }}
|
||||
</v-chip>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
|
||||
<div class="text-body-2 text-medium-emphasis d-flex align-center">
|
||||
<v-icon size="small" class="mr-1">mdi-harddisk</v-icon>
|
||||
Storage: {{ storageStats?.total_size_formatted || 'Calculating...' }}
|
||||
<span v-if="storageStats?.file_count" class="ml-2">
|
||||
({{ storageStats.file_count.toLocaleString() }} files)
|
||||
</span>
|
||||
<v-tooltip v-if="storageStats?.calculated_at" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="x-small" class="ml-2">mdi-information-outline</v-icon>
|
||||
</template>
|
||||
<span>Updated: {{ formatDate(storageStats.calculated_at) }}</span>
|
||||
</v-tooltip>
|
||||
<v-btn
|
||||
icon
|
||||
size="x-small"
|
||||
variant="text"
|
||||
class="ml-1"
|
||||
:loading="refreshingStorage"
|
||||
@click="refreshStorageStats"
|
||||
title="Refresh storage stats"
|
||||
>
|
||||
<v-icon size="small">mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 4: Visual check**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
- Stat cards show large bold numbers with coloured left-border stripes; no avatar blobs
|
||||
- Action bar has two distinct rows separated by a divider
|
||||
- Credential chips and storage stats sit on the lower row, not crowded with action buttons
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Dashboard.vue
|
||||
git commit -m "feat(ui): number-forward stat cards and split action bar on Dashboard"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Dashboard Empty States & Running Pulse
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/Dashboard.vue` (three empty state blocks + `<style>` section)
|
||||
|
||||
**Context:** Dashboard.vue has three panels with empty states: Active Downloads (around line 185), Recent Activity (around line 233), and Sources Needing Attention (around line 294). Each currently has a slightly different empty state treatment. A `<style scoped>` block does not yet exist in this file — add one.
|
||||
|
||||
- [ ] **Step 1: Update the Active Downloads empty state**
|
||||
|
||||
Find the block (after Task 3, around line 185 in the active downloads panel):
|
||||
```html
|
||||
<div v-else class="text-center text-grey py-4">
|
||||
<v-icon size="32" class="mb-2">mdi-check-circle-outline</v-icon>
|
||||
<div>No active downloads</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="48" color="secondary" :style="{ opacity: 0.5 }">mdi-check-circle-outline</v-icon>
|
||||
<div class="text-body-1 mt-2">No active downloads</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">All sources are idle</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the Recent Activity empty state**
|
||||
|
||||
Find:
|
||||
```html
|
||||
<div v-else class="text-center text-grey py-4">
|
||||
No recent activity (failures or new downloads)
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="48" color="secondary" :style="{ opacity: 0.5 }">mdi-history</v-icon>
|
||||
<div class="text-body-1 mt-2">No recent activity</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">Failures and new downloads appear here</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the Sources Needing Attention empty state**
|
||||
|
||||
Find:
|
||||
```html
|
||||
<div v-else class="text-center text-grey py-4">
|
||||
<v-icon size="48" class="mb-2">mdi-check-circle-outline</v-icon>
|
||||
<div>All sources are healthy!</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="48" color="secondary" :style="{ opacity: 0.5 }">mdi-check-all</v-icon>
|
||||
<div class="text-body-1 mt-2">All sources healthy</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">No failures in the last 7 days</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add running-item pulse class to the active downloads list item**
|
||||
|
||||
Find the `v-list-item` inside the active downloads panel:
|
||||
```html
|
||||
<v-list-item v-for="dl in activeDownloads" :key="dl.id">
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<v-list-item
|
||||
v-for="dl in activeDownloads"
|
||||
:key="dl.id"
|
||||
:class="{ 'download-running': dl.status === 'running' }"
|
||||
>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add a `<style scoped>` block at the bottom of the file**
|
||||
|
||||
Dashboard.vue currently has no `<style>` block. Add after the closing `</script>` tag:
|
||||
|
||||
```html
|
||||
<style scoped>
|
||||
.download-running {
|
||||
border-left: 3px solid rgb(var(--v-theme-primary));
|
||||
animation: pulse-border 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 7: Visual check**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
- All three empty states have consistent centered icon + body text + caption layout
|
||||
- If a download is running, its list item has a pulsing left border
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Dashboard.vue
|
||||
git commit -m "feat(ui): consistent empty states and running pulse on Dashboard"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Subscriptions Dark Mode Fix
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/Subscriptions.vue` (one line change)
|
||||
|
||||
**Context:** Line 157 in Subscriptions.vue has `class="ml-8 bg-grey-lighten-4"` on the inner expanded-row table. `bg-grey-lighten-4` is a hardcoded Vuetify grey utility class that does not adapt to dark mode — it forces a light background in both themes, making text unreadable in dark mode. Replace with `bg-surface`, which resolves to the current theme's surface colour.
|
||||
|
||||
- [ ] **Step 1: Fix the expanded row background class**
|
||||
|
||||
Find (around line 157):
|
||||
```html
|
||||
<v-table density="compact" class="ml-8 bg-grey-lighten-4">
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<v-table density="compact" class="ml-8 bg-surface border-t">
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 3: Visual check in dark mode**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
Go to Subscriptions, expand a subscription row. The inner source table should have the same dark background as the rest of the UI, not a forced light grey.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Subscriptions.vue
|
||||
git commit -m "fix(ui): use bg-surface on expanded subscription rows for dark mode compat"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Downloads — Exclude Superseded Filter
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/Downloads.vue`
|
||||
|
||||
**Context:** The Downloads page has a filter row with four `md="3"` columns (Status, Source, From Date, To Date) filling the full 12 columns. The existing `loadDownloads()` function already accepts arbitrary params and passes them to `downloadsStore.fetchDownloads()`, which supports `exclude_superseded`. We add a new `v-row` below the existing filter row.
|
||||
|
||||
- [ ] **Step 1: Add the `filterExcludeSuperseded` ref in `<script setup>`**
|
||||
|
||||
In the script, after the existing filter refs (around line 306, after `const filterToDate = ref(null)`):
|
||||
|
||||
```js
|
||||
const filterExcludeSuperseded = ref(true)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `filterExcludeSuperseded` to the watch**
|
||||
|
||||
Find:
|
||||
```js
|
||||
watch([filterStatus, filterSourceId, filterFromDate, filterToDate], () => {
|
||||
loadDownloads()
|
||||
})
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```js
|
||||
watch([filterStatus, filterSourceId, filterFromDate, filterToDate, filterExcludeSuperseded], () => {
|
||||
loadDownloads()
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `loadDownloads()` to pass the param**
|
||||
|
||||
Find inside `loadDownloads()`:
|
||||
```js
|
||||
async function loadDownloads() {
|
||||
const params = {}
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
if (filterSourceId.value) params.source_id = filterSourceId.value
|
||||
if (filterFromDate.value) params.from_date = filterFromDate.value
|
||||
if (filterToDate.value) params.to_date = filterToDate.value
|
||||
await downloadsStore.fetchDownloads(params)
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```js
|
||||
async function loadDownloads() {
|
||||
const params = {}
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
if (filterSourceId.value) params.source_id = filterSourceId.value
|
||||
if (filterFromDate.value) params.from_date = filterFromDate.value
|
||||
if (filterToDate.value) params.to_date = filterToDate.value
|
||||
if (filterExcludeSuperseded.value) params.exclude_superseded = true
|
||||
await downloadsStore.fetchDownloads(params)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the filter toggle row in the template**
|
||||
|
||||
In the template, find the closing `</v-row>` of the existing filter row (around line 113 — the row containing the four filter columns). Add a new row directly after it:
|
||||
|
||||
```html
|
||||
<!-- Superseded filter -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12">
|
||||
<v-switch
|
||||
v-model="filterExcludeSuperseded"
|
||||
label="Hide superseded failures"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 6: Visual check**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
Go to Downloads. A toggle "Hide superseded failures" should appear below the four filter dropdowns, defaulting to on. Toggling it off should show additional failed downloads (if any superseded ones exist in the DB).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Downloads.vue
|
||||
git commit -m "feat(ui): add exclude superseded failures filter toggle to Downloads"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Credentials Visual States
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/views/Credentials.vue`
|
||||
|
||||
**Context:** The Credentials page loops over `platforms` and renders one `v-card` per platform (lines 5–95). Cards for platforms without credentials currently show a `v-alert type="info"` block. We give configured cards a solid border and unconfigured cards a dashed border plus a more prominent CTA button.
|
||||
|
||||
The global `VCard: { border: true }` default (set in Task 1) will add a border to all cards. For unconfigured cards we override this with `:border="false"` and an inline dashed style.
|
||||
|
||||
- [ ] **Step 1: Update the `v-card` opening tag to apply conditional border styling**
|
||||
|
||||
Find (around line 11 — the card inside the `v-for` loop, not the dialog cards later in the file):
|
||||
```html
|
||||
<v-card>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<v-card
|
||||
:border="!!getCredential(platform)"
|
||||
:style="!getCredential(platform)
|
||||
? { border: '2px dashed var(--v-border-color)' }
|
||||
: { borderColor: 'rgb(var(--v-theme-success))' }"
|
||||
>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the `v-alert` (unconfigured state) with an empty-state block**
|
||||
|
||||
Find (around lines 56–64):
|
||||
```html
|
||||
<v-alert
|
||||
v-else
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mb-4"
|
||||
>
|
||||
No credentials stored for {{ info.name }}
|
||||
</v-alert>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<div v-else class="text-center py-4 mb-4">
|
||||
<v-icon size="40" color="secondary" :style="{ opacity: 0.5 }">mdi-key-remove</v-icon>
|
||||
<div class="text-body-2 mt-2">No credentials configured</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">Add credentials to enable downloads</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Promote the "Add Credentials" button to `variant="flat"` for unconfigured cards**
|
||||
|
||||
Find (around lines 77–83):
|
||||
```html
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="openUploadDialog(platform, info)"
|
||||
>
|
||||
{{ getCredential(platform) ? 'Update' : 'Add' }} Credentials
|
||||
</v-btn>
|
||||
```
|
||||
|
||||
Replace with:
|
||||
```html
|
||||
<v-btn
|
||||
:variant="getCredential(platform) ? 'text' : 'flat'"
|
||||
color="primary"
|
||||
@click="openUploadDialog(platform, info)"
|
||||
>
|
||||
{{ getCredential(platform) ? 'Update' : 'Add' }} Credentials
|
||||
</v-btn>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify build passes**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 5: Visual check**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
Go to Credentials. Configured platform cards should have a solid border. Unconfigured cards should have a dashed border, a centered key icon with "No credentials configured" text, and a prominent solid-blue "Add Credentials" button.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/Credentials.vue
|
||||
git commit -m "feat(ui): configured vs unconfigured visual treatment on Credentials cards"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final Verification
|
||||
|
||||
After all 7 tasks are committed:
|
||||
|
||||
- [ ] **Full build clean**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Expected: exits 0, dist/ produced.
|
||||
|
||||
- [ ] **Visual smoke check across all pages**
|
||||
|
||||
```bash
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
Visit each page in dark mode, then toggle to light mode and verify:
|
||||
- Dashboard: slate-navy background, number-forward stat cards, split action bar, consistent empty states
|
||||
- Subscriptions: expanded rows have dark background in dark mode
|
||||
- Downloads: "Hide superseded failures" toggle present and functional
|
||||
- Credentials: dashed borders on unconfigured platforms
|
||||
- Nav: branded header with icon, pill-highlight active items, WS status dot in app bar
|
||||
- Settings, Logs: benefit from new palette automatically, no structural changes needed
|
||||
Reference in New Issue
Block a user