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>
This commit is contained in:
2026-04-17 23:01:09 -04:00
parent 38bade0320
commit 9ab94eb1f9
@@ -0,0 +1,483 @@
# 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:
```python
# 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**
```bash
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:
```vue
</v-row>
<v-divider class="my-6" />
<!-- Notification Settings -->
<h3 class="text-h6 mb-4">Notifications</h3>
```
Replace it with:
```vue
</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:
```bash
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**
```bash
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):
```js
<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:
```js
<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):
```js
const sourcesStore = useSourcesStore()
const downloadsStore = useDownloadsStore()
const credentialsStore = useCredentialsStore()
const notifications = useNotificationStore()
```
Add the settings store instance:
```js
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:
```js
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:
```js
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:
```js
// 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**
```bash
cd frontend && npm run build
```
Expected: build completes with no errors.
- [ ] **Step 6: Commit**
```bash
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):
```vue
<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:
```vue
<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:
```vue
<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:
```js
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:
```bash
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:
```vue
<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**
```bash
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**
```bash
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.