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
bvandeusen 9ab94eb1f9 docs: add failure display implementation plan
4-task plan: backend default setting, Settings page input,
Dashboard script computeds, Dashboard template replacement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 23:01:09 -04:00

17 KiB
Raw Permalink Blame History

Failure Display 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: Introduce a configurable "failing source" threshold (default 5 consecutive failures) and surface it on the Dashboard as per-platform health bars plus a strict failing-source list.

Architecture: Four small changes. (1) Backend adds one default setting key. (2) Settings page adds one number input. (3) Dashboard script adds a settings fetch and three derived computeds. (4) Dashboard template replaces the "Sources Needing Attention" card body with platform bars + a filtered table, removing the old recent-failure-based list.

Tech Stack: Python/Quart + SQLAlchemy backend, Vue 3 + Vuetify 3 + Pinia frontend. No test infrastructure in the repo; verification is via npm run build, spec-file diffs, and visual inspection after the user deploys.

Spec: docs/superpowers/specs/2026-04-17-failure-display-design.md


File Structure

File Role Change
backend/app/models/setting.py DEFAULT_SETTINGS dict Add "dashboard.failure_threshold": 5
frontend/src/views/Settings.vue Application settings page Add Dashboard section with threshold input
frontend/src/views/Dashboard.vue Main dashboard page Add settings store + computeds; replace "Sources Needing Attention" card body; remove obsolete sourcesWithErrors; add scoped CSS

No new files. No store changes — useSettingsStore in frontend/src/stores/settings.js already exposes fetchSettings() and a reactive settings ref.


Task 1: Add default threshold to backend settings

Files:

  • Modify: backend/app/models/setting.py:39-46

  • Step 1: Add the new key to DEFAULT_SETTINGS

Open backend/app/models/setting.py and replace the DEFAULT_SETTINGS dict (currently lines 3946) with:

# Default settings that should exist
DEFAULT_SETTINGS = {
    "download.parallel_limit": 3,
    "download.rate_limit": 3.0,
    "download.retry_count": 3,
    "download.schedule_interval": 28800,  # 8 hours
    "notification.enabled": False,
    "notification.webhook_url": None,
    "dashboard.failure_threshold": 5,
}

Why this is enough: settings is a key-value table. GET /api/settings (in backend/app/api/settings.py) merges stored rows on top of DEFAULT_SETTINGS and returns the result. A brand-new key appears in the API response immediately; existing installations get the default until/unless the user saves a different value from the Settings page. No migration, no new endpoint.

  • Step 2: Commit
git add backend/app/models/setting.py
git commit -m "feat(settings): add dashboard.failure_threshold default (5)"

Task 2: Add threshold input to the Settings page

Files:

  • Modify: frontend/src/views/Settings.vue (template only)

  • Step 1: Insert a Dashboard section above the Notifications divider

In frontend/src/views/Settings.vue, locate the block that ends the Download Settings section and starts the Notifications section:

          </v-row>

          <v-divider class="my-6" />

          <!-- Notification Settings -->
          <h3 class="text-h6 mb-4">Notifications</h3>

Replace it with:

          </v-row>

          <v-divider class="my-6" />

          <!-- Dashboard Settings -->
          <h3 class="text-h6 mb-4">Dashboard</h3>
          <v-row>
            <v-col cols="12" md="6">
              <v-text-field
                v-model.number="settings['dashboard.failure_threshold']"
                label="Failing source threshold"
                type="number"
                min="1"
                max="20"
                hint="Consecutive download failures before a source is marked failing. One success resets the counter."
                persistent-hint
              />
            </v-col>
          </v-row>

          <v-divider class="my-6" />

          <!-- Notification Settings -->
          <h3 class="text-h6 mb-4">Notifications</h3>

Why this is enough: saveSettings() at Settings.vue:463 calls settingsStore.updateSettings(settings.value) which PATCHes every key in the object. A new v-model binding flows through without any handler changes.

  • Step 2: Verify the frontend still builds

Run from the repo root:

cd frontend && npm run build

Expected: build completes with no errors. A Vuetify warning about persistent-hint is non-fatal if it appears.

  • Step 3: Commit
git add frontend/src/views/Settings.vue
git commit -m "feat(settings-ui): expose dashboard failure threshold"

Task 3: Dashboard script — settings store, fetch, computeds

Files:

  • Modify: frontend/src/views/Dashboard.vue (<script setup> block only)

  • Step 1: Import the settings store

Find the existing imports at the top of <script setup> (around line 297303):

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useSourcesStore } from '../stores/sources'
import { useDownloadsStore } from '../stores/downloads'
import { useCredentialsStore } from '../stores/credentials'
import { useNotificationStore } from '../stores/notifications'
import { settingsApi, downloadsApi } from '../services/api'

Add one line for the settings store:

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useSourcesStore } from '../stores/sources'
import { useDownloadsStore } from '../stores/downloads'
import { useCredentialsStore } from '../stores/credentials'
import { useNotificationStore } from '../stores/notifications'
import { useSettingsStore } from '../stores/settings'
import { settingsApi, downloadsApi } from '../services/api'
  • Step 2: Instantiate the store

Find the block of store instantiations (around lines 305308):

const sourcesStore = useSourcesStore()
const downloadsStore = useDownloadsStore()
const credentialsStore = useCredentialsStore()
const notifications = useNotificationStore()

Add the settings store instance:

const sourcesStore = useSourcesStore()
const downloadsStore = useDownloadsStore()
const credentialsStore = useCredentialsStore()
const notifications = useNotificationStore()
const settingsStore = useSettingsStore()
  • Step 3: Fetch settings in loadDashboardData

Locate loadDashboardData() (around line 367). The current body is:

function loadDashboardData() {
  // Fire off all requests in parallel, don't block UI
  // Each section handles its own loading state
  sourcesStore.fetchSources().catch(e => console.error('Failed to fetch sources:', e))
  credentialsStore.fetchCredentials().catch(e => console.error('Failed to fetch credentials:', e))

  downloadsStore.fetchStats()
    .catch(e => console.error('Failed to fetch stats:', e))
    .finally(() => loadingStats.value = false)

  downloadsStore.fetchRecentActivity({ limit: 10 })
    .catch(e => console.error('Failed to fetch recent activity:', e))
    .finally(() => loadingActivity.value = false)

  fetchActiveDownloads()

  // Storage stats can be slow - load independently
  fetchStorageStats()
}

Add one line — the settingsStore.fetchSettings() call — right after the credentialsStore.fetchCredentials() line:

function loadDashboardData() {
  // Fire off all requests in parallel, don't block UI
  // Each section handles its own loading state
  sourcesStore.fetchSources().catch(e => console.error('Failed to fetch sources:', e))
  credentialsStore.fetchCredentials().catch(e => console.error('Failed to fetch credentials:', e))
  settingsStore.fetchSettings().catch(e => console.error('Failed to fetch settings:', e))

  downloadsStore.fetchStats()
    .catch(e => console.error('Failed to fetch stats:', e))
    .finally(() => loadingStats.value = false)

  downloadsStore.fetchRecentActivity({ limit: 10 })
    .catch(e => console.error('Failed to fetch recent activity:', e))
    .finally(() => loadingActivity.value = false)

  fetchActiveDownloads()

  // Storage stats can be slow - load independently
  fetchStorageStats()
}
  • Step 4: Add platform order constant, threshold computed, and two derived computeds

Find the sourcesWithErrors computed (currently lines 329347). Leave it alone for now — Task 4 removes it. Immediately above it, after the line const recentActivity = computed(() => downloadsStore.recentActivity), insert:

// Fixed display order for platform health bars
const PLATFORM_ORDER = [
  'patreon', 'subscribestar', 'hentaifoundry',
  'discord', 'pixiv', 'deviantart',
]

// Read threshold from settings, default 5 if unset
const failureThreshold = computed(() =>
  settingsStore.settings?.['dashboard.failure_threshold'] ?? 5
)

// Per-platform health for platforms with ≥1 enabled source
const platformHealth = computed(() => {
  const enabled = sourcesStore.sources.filter(s => s.enabled)
  return PLATFORM_ORDER
    .map(platform => {
      const sources = enabled.filter(s => s.platform === platform)
      if (sources.length === 0) return null
      const failing = sources.filter(
        s => (s.error_count || 0) >= failureThreshold.value
      ).length
      return { platform, total: sources.length, failing }
    })
    .filter(Boolean)
})

// Sources currently in failing state, most-recent-check first
const failingSources = computed(() => {
  return sourcesStore.sources
    .filter(s => s.enabled && (s.error_count || 0) >= failureThreshold.value)
    .sort((a, b) => new Date(b.last_check || 0) - new Date(a.last_check || 0))
})

const totalFailingCount = computed(() => failingSources.value.length)
  • Step 5: Verify the frontend still builds
cd frontend && npm run build

Expected: build completes with no errors.

  • Step 6: Commit
git add frontend/src/views/Dashboard.vue
git commit -m "feat(dashboard): add failure-threshold computeds backed by settings store"

Task 4: Dashboard template — replace card body, remove obsolete code, add CSS

This task rewrites the "Sources Needing Attention" card to show platform health bars plus the strict failing-source list, and deletes the now-dead sourcesWithErrors computed. Do the steps in order — the removal at the end relies on the template no longer referencing the old computed.

Files:

  • Modify: frontend/src/views/Dashboard.vue (template, script cleanup, scoped CSS)

  • Step 1: Replace the card header chip binding

Find the "Sources Needing Attention" card title (around lines 239244):

<v-card-title>
  Sources Needing Attention
  <v-chip class="ml-2" color="error" size="small" v-if="sourcesWithErrors.length">
    {{ sourcesWithErrors.length }}
  </v-chip>
</v-card-title>

Replace the chip binding so it reads from the new total:

<v-card-title>
  Sources Needing Attention
  <v-chip class="ml-2" color="error" size="small" v-if="totalFailingCount">
    {{ totalFailingCount }}
  </v-chip>
</v-card-title>
  • Step 2: Replace the card body

Locate the v-card-text that follows that title (currently lines 245290 — the block starting with <v-card-text> and containing the <v-table v-if="sourcesWithErrors.length"> and its v-else empty state, ending with </v-card-text>).

Replace the entire v-card-text block with:

<v-card-text>
  <!-- Platform Health bars -->
  <div class="mb-4">
    <div class="text-caption text-medium-emphasis mb-2">Platform Health</div>
    <div
      v-for="row in platformHealth"
      :key="row.platform"
      class="d-flex align-center mb-2"
    >
      <v-icon :color="getPlatformColor(row.platform)" size="small" class="mr-2">
        {{ getPlatformIcon(row.platform) }}
      </v-icon>
      <div class="text-body-2 platform-label">{{ row.platform }}</div>
      <v-progress-linear
        :model-value="row.failing > 0 ? (row.failing / row.total) * 100 : 100"
        :color="row.failing > 0 ? 'error' : 'success'"
        height="12"
        rounded
        class="mx-3 flex-grow-1"
      />
      <div class="text-caption text-medium-emphasis count-label">
        {{ row.failing }}/{{ row.total }}
      </div>
    </div>
    <div
      v-if="!platformHealth.length"
      class="text-caption text-medium-emphasis text-center py-2"
    >
      No enabled sources
    </div>
  </div>

  <v-divider class="my-3" />

  <!-- Failing Sources list -->
  <v-table v-if="failingSources.length" density="compact">
    <thead>
      <tr>
        <th>Source</th>
        <th>Consecutive failures</th>
        <th>Last check</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="source in failingSources" :key="source.id">
        <td>{{ getSourceLabel(source) }}</td>
        <td>
          <v-chip color="error" size="small">{{ source.error_count }}</v-chip>
        </td>
        <td>{{ formatDate(source.last_check) }}</td>
        <td>
          <v-btn
            size="small"
            variant="text"
            color="primary"
            @click="retrySource(source)"
          >
            Retry
          </v-btn>
          <v-btn size="small" variant="text" to="/subscriptions">
            View
            <v-tooltip activator="parent">View in Subscriptions</v-tooltip>
          </v-btn>
        </td>
      </tr>
    </tbody>
  </v-table>
  <div v-else class="text-caption text-medium-emphasis text-center py-3">
    No sources in failing state
  </div>
</v-card-text>

Notes:

  • The extra "No enabled sources" line inside the Platform Health block is a defensive empty state for the edge case where the user has deleted every source. Normally platformHealth has at least one entry.

  • getPlatformColor, getPlatformIcon, getSourceLabel, formatDate, and retrySource are all existing functions in the same component — no new helpers needed.

  • source.subscription_name is included in the Source.to_dict() payload (see backend/app/models/source.py:66), so getSourceLabel works on the store's raw source objects.

  • Step 3: Delete the obsolete sourcesWithErrors computed

In <script setup>, remove the entire sourcesWithErrors computed (previously lines 329347). After removal, this block:

const stats = computed(() => downloadsStore.stats)
const recentActivity = computed(() => downloadsStore.recentActivity)

// Fixed display order for platform health bars
const PLATFORM_ORDER = [
  'patreon', 'subscribestar', 'hentaifoundry',
  'discord', 'pixiv', 'deviantart',
]

should sit directly after the platform-health computeds you added in Task 3, with nothing between recentActivity and PLATFORM_ORDER except a blank line. Verify no sourcesWithErrors references remain:

grep -n "sourcesWithErrors" frontend/src/views/Dashboard.vue

Expected: no output.

  • Step 4: Add scoped CSS for label alignment

Find the existing <style scoped> block at the bottom of Dashboard.vue (currently lines 601611, containing .download-running). Append two rules inside that block so it becomes:

<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; }
}

.platform-label {
  min-width: 110px;
  text-transform: capitalize;
}

.count-label {
  min-width: 50px;
  text-align: right;
}
</style>
  • Step 5: Verify the frontend builds
cd frontend && npm run build

Expected: build completes with no errors. Any warning about the removed sourcesWithErrors reference means Step 3 missed a spot — re-check the template.

  • Step 6: Commit
git add frontend/src/views/Dashboard.vue
git commit -m "feat(dashboard): replace recent-failures list with platform health bars + strict failing-source table"

Verification after all tasks

The user deploys separately. After deploy:

  1. Visit /settings — confirm the Dashboard section shows a "Failing source threshold" input defaulting to 5.
  2. Change the threshold, Save, reload — confirm the value persists.
  3. Visit / — confirm the "Sources Needing Attention" card shows:
    • One bar per platform with ≥1 enabled source, in the fixed order.
    • Green full bars for platforms with no failing sources.
    • Proportional red fill on platforms where some sources have error_count >= threshold.
    • A table listing sources in failing state (if any), sorted by last_check DESC.
    • "No sources in failing state" caption when the list is empty.
  4. Lower the threshold on the Settings page — confirm additional sources appear in the failing list when their error_count crosses the new (lower) line.

No automated test verification is available. The build passing and the Dashboard rendering without console errors is the bar.