Files
FabledCurator/frontend/src/components/subscriptions/SourceHealthDot.vue
T
bvandeusenandClaude Opus 4.7 eebc8e2413 refactor(dry-F2): centralize shared UI primitives (relative-time, toast, download-status)
- utils/date.js: add formatRelative(iso, {future,nullText}); migrate 6 sites
  (SourceRow, SubscriptionsTab, SourceHealthDot, SchedulerStatusBar +
  thin adapters in BackupRunsTable/SystemActivityTab for their '—' null text).
  PostCard (30d->absolute) and CredentialCard (mo/y buckets) intentionally
  keep bespoke formatters.
- utils/toast.js: toast(opts) wraps the globalThis.window?.__fcToast?.(...)
  incantation; migrate 63 call sites across 24 files.
- utils/downloadStatus.js: single source for the download-event status enum
  -> label/color/icon; collapse the 3 duplicate maps (DownloadStatChips,
  DownloadsFilterPopover, DownloadEventRow).

Net -33 lines. Platform metadata was already centralized in platformColor.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:20:07 -04:00

80 lines
2.3 KiB
Vue

<template>
<v-tooltip location="top" open-delay="200">
<template #activator="{ props: tipProps }">
<span
v-bind="tipProps"
:class="['fc-health-dot', `fc-health-dot--${level}`]"
:aria-label="ariaLabel"
/>
</template>
<div class="fc-health-tip">
<div>Last checked: {{ lastCheckedText }}</div>
<div v-if="nextCheckText">Next check: {{ nextCheckText }}</div>
<div v-if="(source.consecutive_failures || 0) > 0">
Failures: {{ source.consecutive_failures }}
</div>
<div v-if="source.last_error" class="fc-health-tip__err">
{{ truncatedError }}
</div>
</div>
</v-tooltip>
</template>
<script setup>
import { computed } from 'vue'
import { formatRelative } from '../../utils/date.js'
const props = defineProps({
source: { type: Object, required: true },
warningThreshold: { type: Number, default: 5 },
})
const level = computed(() => {
if (!props.source.last_checked_at) return 'unchecked'
const f = props.source.consecutive_failures || 0
if (f === 0) return 'healthy'
if (f < props.warningThreshold) return 'warning'
return 'critical'
})
const ariaLabel = computed(() => `source health: ${level.value}`)
const lastCheckedText = computed(() => formatRelative(props.source.last_checked_at))
const nextCheckText = computed(() =>
props.source.next_check_at
? formatRelative(props.source.next_check_at, { future: true })
: null,
)
const truncatedError = computed(() => {
const e = props.source.last_error || ''
return e.length > 120 ? e.slice(0, 117) + '…' : e
})
</script>
<style scoped>
.fc-health-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.fc-health-dot--unchecked { background-color: rgb(var(--v-theme-on-surface-variant)); opacity: 0.5; }
.fc-health-dot--healthy { background-color: rgb(var(--v-theme-success, 76 175 80)); }
.fc-health-dot--warning { background-color: rgb(var(--v-theme-warning, 255 167 38)); }
.fc-health-dot--critical { background-color: rgb(var(--v-theme-error, 244 67 54)); }
.fc-health-tip {
font-size: 0.85rem;
line-height: 1.4;
}
.fc-health-tip__err {
margin-top: 0.25rem;
color: rgb(var(--v-theme-error, 244 67 54));
white-space: pre-wrap;
max-width: 32rem;
}
</style>