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>
17 KiB
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 39–46) 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 297–303):
<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 305–308):
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 329–347). 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 239–244):
<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 245–290 — 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
platformHealthhas at least one entry. -
getPlatformColor,getPlatformIcon,getSourceLabel,formatDate, andretrySourceare all existing functions in the same component — no new helpers needed. -
source.subscription_nameis included in theSource.to_dict()payload (seebackend/app/models/source.py:66), sogetSourceLabelworks on the store's raw source objects. -
Step 3: Delete the obsolete
sourcesWithErrorscomputed
In <script setup>, remove the entire sourcesWithErrors computed (previously lines 329–347). 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 601–611, 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:
- Visit
/settings— confirm the Dashboard section shows a "Failing source threshold" input defaulting to 5. - Change the threshold, Save, reload — confirm the value persists.
- 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_checkDESC. - "No sources in failing state" caption when the list is empty.
- Lower the threshold on the Settings page — confirm additional sources appear in the failing list when their
error_countcrosses the new (lower) line.
No automated test verification is available. The build passing and the Dashboard rendering without console errors is the bar.