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
GallerySubscriber/docs/superpowers/specs/2026-04-17-failure-display-design.md
T
bvandeusen 38bade0320 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 <noreply@anthropic.com>
2026-04-17 22:56:23 -04:00

239 lines
8.7 KiB
Markdown
Raw 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 — 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
<v-divider class="my-6" />
<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>
```
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
<v-card-text>
<!-- Section 1: 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-divider class="my-3" />
<!-- Section 2: 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-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>
```
#### Header chip
The card title already has:
```vue
<v-chip class="ml-2" color="error" size="small" v-if="sourcesWithErrors.length">
{{ sourcesWithErrors.length }}
</v-chip>
```
Swap the binding to `totalFailingCount`:
```vue
<v-chip class="ml-2" color="error" size="small" v-if="totalFailingCount">
{{ totalFailingCount }}
</v-chip>
```
#### 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` (120, 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).