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
2026-03-19 17:51:00 -04:00

24 KiB
Raw Permalink Blame History

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
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
cd frontend && npm run build

Expected: exits 0, no errors. Warnings about unused variables are OK.

  • Step 3: Visual check
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
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:

<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
cd frontend && npm run build

Expected: exits 0.

  • Step 3: Visual check
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

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:

<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:

<!-- 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
cd frontend && npm run build

Expected: exits 0.

  • Step 4: Visual check
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

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):

<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:

<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:

<div v-else class="text-center text-grey py-4">
  No recent activity (failures or new downloads)
</div>

Replace with:

<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:

<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:

<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:

<v-list-item v-for="dl in activeDownloads" :key="dl.id">

Replace with:

<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:

<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
cd frontend && npm run build

Expected: exits 0.

  • Step 7: Visual check
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

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):

<v-table density="compact" class="ml-8 bg-grey-lighten-4">

Replace with:

<v-table density="compact" class="ml-8 bg-surface border-t">
  • Step 2: Verify build passes
cd frontend && npm run build

Expected: exits 0.

  • Step 3: Visual check in dark mode
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
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)):

const filterExcludeSuperseded = ref(true)
  • Step 2: Add filterExcludeSuperseded to the watch

Find:

watch([filterStatus, filterSourceId, filterFromDate, filterToDate], () => {
  loadDownloads()
})

Replace with:

watch([filterStatus, filterSourceId, filterFromDate, filterToDate, filterExcludeSuperseded], () => {
  loadDownloads()
})
  • Step 3: Update loadDownloads() to pass the param

Find inside loadDownloads():

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:

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:

<!-- 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
cd frontend && npm run build

Expected: exits 0.

  • Step 6: Visual check
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
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):

<v-card>

Replace with:

<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):

<v-alert
  v-else
  type="info"
  variant="tonal"
  density="compact"
  class="mb-4"
>
  No credentials stored for {{ info.name }}
</v-alert>

Replace with:

<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):

<v-btn
  variant="text"
  color="primary"
  @click="openUploadDialog(platform, info)"
>
  {{ getCredential(platform) ? 'Update' : 'Add' }} Credentials
</v-btn>

Replace with:

<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
cd frontend && npm run build

Expected: exits 0.

  • Step 5: Visual check
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
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
cd frontend && npm run build

Expected: exits 0, dist/ produced.

  • Visual smoke check across all pages
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