# Failure Display — Design Spec **Date:** 2026-04-17 **Scope:** Define a single "failing source" state and surface it on the Dashboard as per-platform health bars plus a filtered failing-source list. Make the threshold configurable via Settings. --- ## Motivation The user is seeing a large number of source failures and needs a way to spot which platforms are affected at a glance. Today, the Dashboard's "Sources Needing Attention" card shows any source with a failed download in the last 7 days — a transient, noisy definition. A single bad run puts a source on the list; a later success does nothing to clear it. This spec introduces a stricter, self-clearing definition: a source is *failing* when it has N consecutive failures (default 5), and a single success resets the counter. That semantic already exists in the data model — `Source.error_count` increments on failure and is set to 0 on success — so this is a presentation change with one small settings addition, not a schema change. --- ## Design ### Backend **File:** `backend/app/models/setting.py` Add one entry to `DEFAULT_SETTINGS`: ```python DEFAULT_SETTINGS = { # ... existing keys unchanged ... "dashboard.failure_threshold": 5, } ``` No migration. The `settings` table is a key-value store and `get_setting_value(session, key, default)` already falls back through `DEFAULT_SETTINGS`. No new endpoint — the existing `GET /api/settings` returns it in the merged `settings` dict. ### Settings page — `frontend/src/views/Settings.vue` Add a new section above the Notifications divider: ```vue

Dashboard

``` The existing save handler in `Settings.vue` already patches any key present in the `settings` object, so no handler changes are needed. ### Dashboard — `frontend/src/views/Dashboard.vue` Replace the body of the existing "Sources Needing Attention" card. The card title, outer shell, and card actions remain unchanged. The inner `v-card-text` is rewritten. #### New computeds ```js // Read threshold from settings store, default 5 if unset const settingsStore = useSettingsStore() const failureThreshold = computed(() => settingsStore.settings?.['dashboard.failure_threshold'] ?? 5 ) // Fixed platform display order const PLATFORM_ORDER = [ 'patreon', 'subscribestar', 'hentaifoundry', 'discord', 'pixiv', 'deviantart', ] // Per-platform health: { platform, total, failing } 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 // hide zero-enabled platforms const failing = sources.filter(s => (s.error_count || 0) >= failureThreshold.value).length return { platform, total: sources.length, failing } }) .filter(Boolean) }) // Sources currently in a 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) ``` `useSettingsStore` already exists at `frontend/src/stores/settings.js` with `fetchSettings()` and a reactive `settings` ref. `Dashboard.vue` must call `settingsStore.fetchSettings()` in `loadDashboardData` (alongside the existing `fetchSources`/`fetchStats`/etc calls) so `settings` is populated before the threshold is read. #### Template — replace `v-card-text` body ```vue
Platform Health
{{ getPlatformIcon(row.platform) }}
{{ row.platform }}
{{ row.failing }}/{{ row.total }}
Source Consecutive failures Last check Actions {{ getSourceLabel(source) }} {{ source.error_count }} {{ formatDate(source.last_check) }} Retry View
No sources in failing state
``` #### Header chip The card title already has: ```vue {{ sourcesWithErrors.length }} ``` Swap the binding to `totalFailingCount`: ```vue {{ totalFailingCount }} ``` #### Removed code - `sourcesWithErrors` computed (derived from recent activity) is no longer used. Delete it. - The existing `v-else` empty state block ("All sources healthy" big icon) is removed — its role is now played by the always-visible bars plus the small inline "No sources in failing state" line. #### Minor CSS (scoped block) ```css .platform-label { min-width: 110px; text-transform: capitalize; } .count-label { min-width: 50px; text-align: right; } ``` --- ## Data flow 1. `Dashboard.vue` on mount fetches sources (already happens), recent activity (already happens), and settings (new — one call to `settingsStore.fetchSettings()`). 2. `platformHealth` and `failingSources` are purely derived from `sourcesStore.sources` + `failureThreshold`. 3. When a download completes or fails, the existing `download.completed` / `download.failed` websocket handlers already trigger `sourcesStore.fetchSources()` through `source.updated`. Re-render happens via Vue reactivity. No polling changes; no new backend work beyond the one default value. --- ## Visual behavior - **All platforms healthy:** each bar is 100% green. - **One platform has failures:** that bar shows red fill proportional to `failing / total` against a neutral track; the others stay green. - **Platform has zero enabled sources:** row omitted entirely. - **Header chip:** visible only when `totalFailingCount > 0`, shows that count. - **Failing sources list:** rendered when non-empty; otherwise inline "No sources in failing state" caption sits directly under the bars. --- ## Files changed | File | Change | |---|---| | `backend/app/models/setting.py` | Add `"dashboard.failure_threshold": 5` to `DEFAULT_SETTINGS` | | `frontend/src/views/Settings.vue` | Add Dashboard section with threshold `v-text-field` (1–20, default 5) | | `frontend/src/views/Dashboard.vue` | Replace "Sources Needing Attention" card body; add `platformHealth`, `failingSources`, `totalFailingCount`, `failureThreshold` computeds; fetch settings on mount; remove `sourcesWithErrors` computed and the old empty state; small scoped CSS | --- ## Non-goals - No changes to `Source.error_count` increment/reset behavior — the logic in `backend/app/tasks/downloads.py` already has the correct semantics. - No alembic migration (the settings table is key-value; new keys arrive via `DEFAULT_SETTINGS`). - No new API endpoints. - No click-through navigation on platform rows — bars are pure display. - No changes to `Subscriptions.vue` or `Downloads.vue`. - No per-subscription health view (only per-platform).