docs: add UI improvement pass implementation plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 17:51:00 -04:00
parent 728a8bb529
commit a25c332cda
@@ -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 48). 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 147, 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 1142 — 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 263)**
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 65142)**
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 595). 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 5664):
```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 7783):
```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