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

484 lines
17 KiB
Markdown
Raw Permalink 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.
# 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.