Files
FabledCurator/frontend/src/components/subscriptions/SchedulerStatusBar.vue
T
bvandeusenandClaude Opus 5 3f8306f705
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 5s
Build images / build-ml (push) Successful in 9s
Build images / build-agent (push) Successful in 10s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m14s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m9s
fix: two exact-shape assertions pinned the old schedule-status payload
B3 added failing_sources / no_access_sources to schedule-status and
broke test_schedule_status_shape and test_summary_returns_rollup_shape,
both of which assert the payload's EXACT key set. My own tests passed;
these two did not, and integration caught it.

A rule 90 miss: I grepped for the predicates I changed and not for
consumers of the response shape. The shape is the thing I actually
changed.

Both keys added to the assertions rather than loosening them to a
subset check — an exact-set assertion is what catches a key being
renamed out from under a consumer, which is precisely the value these
two tests just demonstrated.

The other readers (PipelineStatusChip, SchedulerStatusBar) pull
individual keys, so they were unaffected. SchedulerStatusBar's prop
comment documented the old shape and is corrected here; a comment that
lies about a contract is worth the same as a doc that does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LNXXULQDjVZmbuNa2G9mD9
2026-09-09 23:01:08 -04:00

107 lines
3.6 KiB
Vue

<template>
<div v-if="status" class="fc-sched" :class="`fc-sched--${health}`">
<span class="fc-sched__dot" />
<span class="fc-sched__label">Scheduler</span>
<span class="fc-sched__sep">·</span>
<span class="fc-sched__item">
<v-icon size="x-small">mdi-clock-outline</v-icon>
last ran {{ lastRanLabel }}
</span>
<span class="fc-sched__sep">·</span>
<span class="fc-sched__item">
<v-icon size="x-small">mdi-calendar-clock</v-icon>
{{ nextLabel }}
</span>
<span class="fc-sched__sep">·</span>
<span class="fc-sched__item">
{{ status.auto_sources }} on schedule
</span>
<v-tooltip activator="parent" location="bottom">
{{ tooltip }}
</v-tooltip>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { formatRelative } from '../../utils/date.js'
const props = defineProps({
// { last_tick_at, next_due_at, due_now, auto_sources,
// failing_sources, no_access_sources, platform_cooldowns } | null
// This bar renders the scheduling half; the ingestion counts are read by the
// front door's FeedStatusRibbon (#387 B3) off the same payload.
status: { type: Object, default: null },
})
// The Beat tick fires every 60s; if the last stamp is older than ~3
// minutes the scheduler (Beat) is almost certainly down.
const STALE_MS = 180_000
const tickAgeMs = computed(() => {
const t = props.status?.last_tick_at
if (!t) return null
return Date.now() - new Date(t).getTime()
})
const health = computed(() => {
if (tickAgeMs.value == null) return 'unknown'
return tickAgeMs.value <= STALE_MS ? 'ok' : 'stale'
})
const lastRanLabel = computed(() =>
formatRelative(props.status?.last_tick_at, { nullText: 'never' }),
)
const nextLabel = computed(() => {
const due = props.status?.due_now || 0
if (due > 0) return `${due} due now`
const n = props.status?.next_due_at
if (!n) return 'nothing scheduled'
const diff = new Date(n).getTime() - Date.now()
if (diff <= 0) return 'check due'
const s = Math.floor(diff / 1000)
if (s < 60) return `next in ${s}s`
if (s < 3600) return `next in ${Math.floor(s / 60)}m`
if (s < 86400) return `next in ${Math.floor(s / 3600)}h`
return `next in ${Math.floor(s / 86400)}d`
})
const tooltip = computed(() => {
if (health.value === 'unknown') return 'The scheduler has not ticked yet (or the worker just started).'
if (health.value === 'stale') return 'The scheduler tick is overdue — check that Celery Beat is running.'
return 'Celery Beat is firing the periodic source-check tick on schedule.'
})
</script>
<style scoped>
.fc-sched {
display: inline-flex; align-items: center; gap: 8px;
flex-wrap: wrap;
font-size: 0.78rem;
color: rgb(var(--v-theme-on-surface-variant));
padding: 4px 10px;
border-radius: 6px;
background: rgb(var(--v-theme-on-surface) / 0.04);
}
.fc-sched__dot {
width: 8px; height: 8px; border-radius: 50%;
flex: 0 0 auto;
background: rgb(var(--v-theme-on-surface-variant));
}
.fc-sched--ok .fc-sched__dot { background: rgb(var(--v-theme-success)); }
.fc-sched--stale .fc-sched__dot { background: rgb(var(--v-theme-error)); }
.fc-sched--unknown .fc-sched__dot { background: rgb(var(--v-theme-on-surface-variant)); }
.fc-sched--stale { color: rgb(var(--v-theme-error)); }
.fc-sched__label {
font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
color: rgb(var(--v-theme-on-surface));
}
.fc-sched--stale .fc-sched__label { color: rgb(var(--v-theme-error)); }
.fc-sched__sep { opacity: 0.4; }
.fc-sched__item {
display: inline-flex; align-items: center; gap: 3px;
white-space: nowrap;
}
</style>