This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
GallerySubscriber/docs/superpowers/plans/2026-03-19-ui-improvement-pass.md
T
2026-03-19 17:51:00 -04:00

791 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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