Revert Vercel dashboard tracker, add HF Space HTML pipeline tracker
Browse filesThe pipeline tracker belongs on the HF Space status page (where you
monitor runs), not on the Vercel frontend (where donors see the demo).
HF Space root page now shows:
- Live progress bar with step name while pipeline runs (auto-refreshes 5s)
- Last run result (status, zones, triggers, duration) when idle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- frontend/src/lib/api.ts +0 -23
- frontend/src/pages/Dashboard.tsx +1 -41
- frontend/tsconfig.tsbuildinfo +1 -1
- src/api.py +47 -0
- tests/eval_results/heat_predictor_eval.json +12 -12
frontend/src/lib/api.ts
CHANGED
|
@@ -313,29 +313,6 @@ export function usePipelineStats() {
|
|
| 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'],
|
|
|
|
| 313 |
})
|
| 314 |
}
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
export function useEnrolled() {
|
| 317 |
return useQuery<EnrolledResponse>({
|
| 318 |
queryKey: ['enrolled'],
|
frontend/src/pages/Dashboard.tsx
CHANGED
|
@@ -4,23 +4,13 @@ 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
|
| 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,36 +33,6 @@ export default function Dashboard() {
|
|
| 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">
|
|
|
|
| 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 |
<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">
|
frontend/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/LoadingState.tsx","./src/components/MetricCard.tsx","./src/components/Sidebar.tsx","./src/components/StatusBadge.tsx","./src/lib/api.ts","./src/lib/tour.ts","./src/pages/Dashboard.tsx","./src/pages/Notifications.tsx","./src/pages/Pipeline.tsx","./src/pages/ProgramDesigner.tsx","./src/pages/Zones.tsx"],"version":"5.6.3"}
|
|
|
|
| 1 |
+
{"root":["./src/App.tsx","./src/main.tsx","./src/regionConfig.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/LoadingState.tsx","./src/components/MetricCard.tsx","./src/components/Sidebar.tsx","./src/components/StatusBadge.tsx","./src/lib/api.ts","./src/lib/tour.ts","./src/pages/Dashboard.tsx","./src/pages/Notifications.tsx","./src/pages/Pipeline.tsx","./src/pages/ProgramDesigner.tsx","./src/pages/Zones.tsx"],"version":"5.6.3"}
|
src/api.py
CHANGED
|
@@ -884,6 +884,44 @@ def _start_scheduler():
|
|
| 884 |
|
| 885 |
# ββ Status page (lightweight, no React build needed) βββββββββββββββββββββ
|
| 886 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 887 |
@app.get("/", response_class=HTMLResponse)
|
| 888 |
async def status_page():
|
| 889 |
"""Simple status dashboard for the HF Space operator."""
|
|
@@ -914,6 +952,7 @@ async def status_page():
|
|
| 914 |
<head>
|
| 915 |
<meta charset="utf-8">
|
| 916 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
| 917 |
<title>Heat Risk Engine β API Status</title>
|
| 918 |
<style>
|
| 919 |
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
@@ -936,12 +975,20 @@ async def status_page():
|
|
| 936 |
.link {{ color: #d4a019; text-decoration: none; font-weight: 600; }}
|
| 937 |
.link:hover {{ text-decoration: underline; }}
|
| 938 |
.footer {{ margin-top: 32px; padding-top: 16px; border-top: 1px solid #e0dcd5; font-size: 0.75rem; color: #aaa; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 939 |
</style>
|
| 940 |
</head>
|
| 941 |
<body>
|
| 942 |
<h1>Heat Risk Engine <span class="status">Running</span></h1>
|
| 943 |
<p class="subtitle">API backend for the Climate Risk Engine β <a class="link" href="https://climate-risk-engine.vercel.app" target="_blank">Open Frontend</a></p>
|
| 944 |
|
|
|
|
|
|
|
| 945 |
<div class="grid">
|
| 946 |
<div class="card">
|
| 947 |
<div class="label">Zones</div>
|
|
|
|
| 884 |
|
| 885 |
# ββ Status page (lightweight, no React build needed) βββββββββββββββββββββ
|
| 886 |
|
| 887 |
+
_STEP_NAMES = {
|
| 888 |
+
"ingest": "Collecting climate data",
|
| 889 |
+
"heal": "Fixing data issues",
|
| 890 |
+
"downscale": "Adjusting for urban heat",
|
| 891 |
+
"predict": "Forecasting heat danger",
|
| 892 |
+
"explain": "Writing alert messages",
|
| 893 |
+
"notify": "Notifying workers",
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
def _pipeline_tracker_html() -> str:
|
| 898 |
+
"""Generate HTML for the pipeline tracker banner."""
|
| 899 |
+
ps = _pipeline_status
|
| 900 |
+
if ps["running"] and ps.get("current_step"):
|
| 901 |
+
step = ps["current_step"]
|
| 902 |
+
idx = ps.get("current_step_index", 0)
|
| 903 |
+
total = ps.get("total_steps", 6)
|
| 904 |
+
label = _STEP_NAMES.get(step, step)
|
| 905 |
+
pct = int((idx / total) * 100) if total else 0
|
| 906 |
+
return f"""
|
| 907 |
+
<div class="pipeline-banner">
|
| 908 |
+
<div class="step-label">Pipeline running β step {idx}/{total}: {label}</div>
|
| 909 |
+
<div class="bar-bg"><div class="bar-fill" style="width:{pct}%"></div></div>
|
| 910 |
+
</div>"""
|
| 911 |
+
elif ps.get("last_result"):
|
| 912 |
+
r = ps["last_result"]
|
| 913 |
+
status = r.get("status", "unknown")
|
| 914 |
+
dur = r.get("duration_s", 0)
|
| 915 |
+
zones = r.get("zones_processed", 0)
|
| 916 |
+
triggers = r.get("triggers_found", 0)
|
| 917 |
+
color = "#2a9d8f" if status == "ok" else "#e63946"
|
| 918 |
+
return f"""
|
| 919 |
+
<div class="pipeline-done" style="border-color:{color}22">
|
| 920 |
+
<div class="label" style="color:{color}">Last run: {status.upper()} β {zones} zones, {triggers} triggers, {dur:.0f}s</div>
|
| 921 |
+
</div>"""
|
| 922 |
+
return ""
|
| 923 |
+
|
| 924 |
+
|
| 925 |
@app.get("/", response_class=HTMLResponse)
|
| 926 |
async def status_page():
|
| 927 |
"""Simple status dashboard for the HF Space operator."""
|
|
|
|
| 952 |
<head>
|
| 953 |
<meta charset="utf-8">
|
| 954 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 955 |
+
<meta http-equiv="refresh" content="5">
|
| 956 |
<title>Heat Risk Engine β API Status</title>
|
| 957 |
<style>
|
| 958 |
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
|
|
| 975 |
.link {{ color: #d4a019; text-decoration: none; font-weight: 600; }}
|
| 976 |
.link:hover {{ text-decoration: underline; }}
|
| 977 |
.footer {{ margin-top: 32px; padding-top: 16px; border-top: 1px solid #e0dcd5; font-size: 0.75rem; color: #aaa; }}
|
| 978 |
+
.pipeline-banner {{ background: linear-gradient(135deg, rgba(21,101,192,0.06), rgba(42,157,143,0.04)); border: 1px solid rgba(21,101,192,0.15); border-radius: 8px; padding: 14px 16px; margin-bottom: 20px; }}
|
| 979 |
+
.pipeline-banner .step-label {{ font-size: 0.85rem; font-weight: 600; color: #1565C0; }}
|
| 980 |
+
.pipeline-banner .bar-bg {{ width: 100%; height: 6px; background: #e0dcd5; border-radius: 3px; margin-top: 8px; overflow: hidden; }}
|
| 981 |
+
.pipeline-banner .bar-fill {{ height: 100%; background: #1565C0; border-radius: 3px; transition: width 0.5s; }}
|
| 982 |
+
.pipeline-done {{ background: rgba(42,157,143,0.06); border: 1px solid rgba(42,157,143,0.15); border-radius: 8px; padding: 14px 16px; margin-bottom: 20px; }}
|
| 983 |
+
.pipeline-done .label {{ font-size: 0.85rem; font-weight: 600; color: #2a9d8f; }}
|
| 984 |
</style>
|
| 985 |
</head>
|
| 986 |
<body>
|
| 987 |
<h1>Heat Risk Engine <span class="status">Running</span></h1>
|
| 988 |
<p class="subtitle">API backend for the Climate Risk Engine β <a class="link" href="https://climate-risk-engine.vercel.app" target="_blank">Open Frontend</a></p>
|
| 989 |
|
| 990 |
+
{_pipeline_tracker_html()}
|
| 991 |
+
|
| 992 |
<div class="grid">
|
| 993 |
<div class="card">
|
| 994 |
<div class="label">Zones</div>
|
tests/eval_results/heat_predictor_eval.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
| 1 |
{
|
| 2 |
"NBO-KIB": {
|
| 3 |
"city": "Nairobi",
|
| 4 |
-
"auroc": 0.
|
| 5 |
-
"precision": 0.
|
| 6 |
-
"recall": 0.
|
| 7 |
"n_samples": 328,
|
| 8 |
"positive_rate": 0.351
|
| 9 |
},
|
| 10 |
"DAR-JAN": {
|
| 11 |
"city": "Dar es Salaam",
|
| 12 |
-
"auroc": 0.
|
| 13 |
-
"precision": 0.
|
| 14 |
-
"recall": 0.
|
| 15 |
"n_samples": 328,
|
| 16 |
"positive_rate": 0.351
|
| 17 |
},
|
| 18 |
"KLA-BWA": {
|
| 19 |
"city": "Kampala",
|
| 20 |
-
"auroc": 0.
|
| 21 |
-
"precision": 0.
|
| 22 |
-
"recall": 0.
|
| 23 |
"n_samples": 328,
|
| 24 |
"positive_rate": 0.351
|
| 25 |
},
|
| 26 |
"KGL-NYA": {
|
| 27 |
"city": "Kigali",
|
| 28 |
-
"auroc": 0.
|
| 29 |
-
"precision": 0.
|
| 30 |
-
"recall": 0.
|
| 31 |
"n_samples": 328,
|
| 32 |
"positive_rate": 0.351
|
| 33 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"NBO-KIB": {
|
| 3 |
"city": "Nairobi",
|
| 4 |
+
"auroc": 0.868,
|
| 5 |
+
"precision": 0.808,
|
| 6 |
+
"recall": 0.548,
|
| 7 |
"n_samples": 328,
|
| 8 |
"positive_rate": 0.351
|
| 9 |
},
|
| 10 |
"DAR-JAN": {
|
| 11 |
"city": "Dar es Salaam",
|
| 12 |
+
"auroc": 0.138,
|
| 13 |
+
"precision": 0.029,
|
| 14 |
+
"recall": 0.009,
|
| 15 |
"n_samples": 328,
|
| 16 |
"positive_rate": 0.351
|
| 17 |
},
|
| 18 |
"KLA-BWA": {
|
| 19 |
"city": "Kampala",
|
| 20 |
+
"auroc": 0.322,
|
| 21 |
+
"precision": 0.303,
|
| 22 |
+
"recall": 0.235,
|
| 23 |
"n_samples": 328,
|
| 24 |
"positive_rate": 0.351
|
| 25 |
},
|
| 26 |
"KGL-NYA": {
|
| 27 |
"city": "Kigali",
|
| 28 |
+
"auroc": 0.795,
|
| 29 |
+
"precision": 0.726,
|
| 30 |
+
"recall": 0.6,
|
| 31 |
"n_samples": 328,
|
| 32 |
"positive_rate": 0.351
|
| 33 |
}
|