From 38bade032089257caa5aa92a5841098cdd033f5e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 17 Apr 2026 22:56:23 -0400 Subject: [PATCH] docs: add failure display design spec Defines "failing source" as error_count >= configurable threshold (default 5), adds per-platform health bars and a strict failing-source list to the Dashboard, surfaces the threshold on the Settings page. Co-Authored-By: Claude Opus 4.7 --- .../2026-04-17-failure-display-design.md | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-17-failure-display-design.md diff --git a/docs/superpowers/specs/2026-04-17-failure-display-design.md b/docs/superpowers/specs/2026-04-17-failure-display-design.md new file mode 100644 index 0000000..8ad8dd0 --- /dev/null +++ b/docs/superpowers/specs/2026-04-17-failure-display-design.md @@ -0,0 +1,238 @@ +# 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).