Add live pipeline tracker to Dashboard
Browse filesShows a blue progress banner with step name and progress bar while
pipeline is running. Polls /api/pipeline/status every 5s. Disappears
when pipeline completes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- frontend/src/lib/api.ts +23 -0
- frontend/src/pages/Dashboard.tsx +41 -1
frontend/src/lib/api.ts
CHANGED
|
@@ -313,6 +313,29 @@ export function usePipelineStats() {
|
|
| 313 |
})
|
| 314 |
}
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
export function useEnrolled() {
|
| 317 |
return useQuery<EnrolledResponse>({
|
| 318 |
queryKey: ['enrolled'],
|
|
|
|
| 313 |
})
|
| 314 |
}
|
| 315 |
|
| 316 |
+
export interface PipelineStatus {
|
| 317 |
+
running: boolean
|
| 318 |
+
current_step: string | null
|
| 319 |
+
current_step_index: number
|
| 320 |
+
total_steps: number
|
| 321 |
+
last_result: {
|
| 322 |
+
run_id: string
|
| 323 |
+
status: string
|
| 324 |
+
zones_processed: number
|
| 325 |
+
triggers_found: number
|
| 326 |
+
duration_s: number
|
| 327 |
+
} | null
|
| 328 |
+
last_run: string | null
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
export function usePipelineStatus() {
|
| 332 |
+
return useQuery<PipelineStatus>({
|
| 333 |
+
queryKey: ['pipeline-status'],
|
| 334 |
+
queryFn: () => fetchJson('/api/pipeline/status'),
|
| 335 |
+
refetchInterval: 5000, // poll every 5s while running
|
| 336 |
+
})
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
export function useEnrolled() {
|
| 340 |
return useQuery<EnrolledResponse>({
|
| 341 |
queryKey: ['enrolled'],
|
frontend/src/pages/Dashboard.tsx
CHANGED
|
@@ -4,13 +4,23 @@ import { Satellite, Thermometer, SlidersHorizontal, ChevronDown, ChevronRight, A
|
|
| 4 |
import MetricCard from '../components/MetricCard'
|
| 5 |
import StatusBadge from '../components/StatusBadge'
|
| 6 |
import { LoadingSpinner, ErrorState } from '../components/LoadingState'
|
| 7 |
-
import { usePipelineStats, usePipelineRuns, useTriggers } from '../lib/api'
|
| 8 |
import { REGION } from '../regionConfig'
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
export default function Dashboard() {
|
| 11 |
const stats = usePipelineStats()
|
| 12 |
const runs = usePipelineRuns()
|
| 13 |
const triggers = useTriggers()
|
|
|
|
| 14 |
const [showRuns, setShowRuns] = useState(false)
|
| 15 |
|
| 16 |
if (stats.isLoading) return <LoadingSpinner />
|
|
@@ -33,6 +43,36 @@ export default function Dashboard() {
|
|
| 33 |
<p className="page-caption">{REGION.heroCaption}</p>
|
| 34 |
</div>
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
{/* Danger banner — shows when there are active triggers */}
|
| 37 |
{activeTriggers.length > 0 && (
|
| 38 |
<div className="danger-banner">
|
|
|
|
| 4 |
import MetricCard from '../components/MetricCard'
|
| 5 |
import StatusBadge from '../components/StatusBadge'
|
| 6 |
import { LoadingSpinner, ErrorState } from '../components/LoadingState'
|
| 7 |
+
import { usePipelineStats, usePipelineRuns, useTriggers, usePipelineStatus } from '../lib/api'
|
| 8 |
import { REGION } from '../regionConfig'
|
| 9 |
|
| 10 |
+
const STEP_LABELS: Record<string, string> = {
|
| 11 |
+
ingest: 'Collecting climate data',
|
| 12 |
+
heal: 'Fixing data issues',
|
| 13 |
+
downscale: 'Adjusting for urban heat',
|
| 14 |
+
predict: 'Forecasting heat danger',
|
| 15 |
+
explain: 'Writing alert messages',
|
| 16 |
+
notify: 'Notifying workers',
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
export default function Dashboard() {
|
| 20 |
const stats = usePipelineStats()
|
| 21 |
const runs = usePipelineRuns()
|
| 22 |
const triggers = useTriggers()
|
| 23 |
+
const pipelineStatus = usePipelineStatus()
|
| 24 |
const [showRuns, setShowRuns] = useState(false)
|
| 25 |
|
| 26 |
if (stats.isLoading) return <LoadingSpinner />
|
|
|
|
| 43 |
<p className="page-caption">{REGION.heroCaption}</p>
|
| 44 |
</div>
|
| 45 |
|
| 46 |
+
{/* Pipeline running banner */}
|
| 47 |
+
{pipelineStatus.data?.running && (
|
| 48 |
+
<div className="mb-6 rounded-[10px] flex items-center gap-4" style={{
|
| 49 |
+
background: 'linear-gradient(135deg, rgba(21,101,192,0.06) 0%, rgba(42,157,143,0.04) 100%)',
|
| 50 |
+
border: '1px solid rgba(21,101,192,0.15)',
|
| 51 |
+
padding: '14px 20px',
|
| 52 |
+
}}>
|
| 53 |
+
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{
|
| 54 |
+
background: '#1565C0',
|
| 55 |
+
boxShadow: '0 0 0 3px rgba(21,101,192,0.2)',
|
| 56 |
+
animation: 'pulse 2s infinite',
|
| 57 |
+
}} />
|
| 58 |
+
<div className="flex-1">
|
| 59 |
+
<div className="text-sm font-sans font-medium" style={{ color: '#1565C0' }}>
|
| 60 |
+
Pipeline running — step {pipelineStatus.data.current_step_index}/{pipelineStatus.data.total_steps}:{' '}
|
| 61 |
+
{STEP_LABELS[pipelineStatus.data.current_step ?? ''] ?? pipelineStatus.data.current_step}
|
| 62 |
+
</div>
|
| 63 |
+
<div className="mt-1.5 w-full h-1.5 rounded-full bg-warm-border overflow-hidden">
|
| 64 |
+
<div
|
| 65 |
+
className="h-full rounded-full transition-all duration-500"
|
| 66 |
+
style={{
|
| 67 |
+
width: `${(pipelineStatus.data.current_step_index / pipelineStatus.data.total_steps) * 100}%`,
|
| 68 |
+
background: '#1565C0',
|
| 69 |
+
}}
|
| 70 |
+
/>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
)}
|
| 75 |
+
|
| 76 |
{/* Danger banner — shows when there are active triggers */}
|
| 77 |
{activeTriggers.length > 0 && (
|
| 78 |
<div className="danger-banner">
|