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

8.7 KiB
Raw Blame History

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:

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:

<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

// 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

<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:

<v-chip class="ml-2" color="error" size="small" v-if="sourcesWithErrors.length">
  {{ sourcesWithErrors.length }}
</v-chip>

Swap the binding to totalFailingCount:

<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)

.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).