Refine health checks and add polling animation

This commit is contained in:
2025-11-27 11:43:35 +01:00
parent c841e27c30
commit 25d80f5723
18 changed files with 499 additions and 219 deletions

View File

@@ -27,6 +27,38 @@ body {
padding: 40px 24px 80px;
}
.starfall {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0;
background-image: radial-gradient(2px 2px at 10% 20%, #d6e9ff, transparent),
radial-gradient(2px 2px at 30% 0%, #c7ddff, transparent),
radial-gradient(2px 2px at 60% 10%, #c0f2ff, transparent),
radial-gradient(2px 2px at 80% 25%, #f3d5ff, transparent),
radial-gradient(2px 2px at 50% 5%, #bfe2ff, transparent),
radial-gradient(2px 2px at 15% 5%, #d6e9ff, transparent);
background-size: 200px 200px, 260px 260px, 220px 220px, 240px 240px,
210px 210px, 230px 230px;
z-index: 3;
}
.starfall.active {
opacity: 1;
animation: starfall-move 1.4s ease-out forwards;
}
@keyframes starfall-move {
0% {
transform: translateY(-30px);
opacity: 0.9;
}
100% {
transform: translateY(80px);
opacity: 0;
}
}
.nebula {
position: absolute;
inset: 0;
@@ -138,6 +170,36 @@ body {
color: #cdd5f5;
}
.pulse {
position: relative;
overflow: hidden;
}
.pulse::after {
content: "";
position: absolute;
inset: -2px;
border-radius: 999px;
background: radial-gradient(
circle at 20% 20%,
rgba(124, 211, 255, 0.35),
transparent 50%
);
animation: ping 0.9s ease-out;
pointer-events: none;
}
@keyframes ping {
0% {
opacity: 0.75;
transform: scale(0.98);
}
100% {
opacity: 0;
transform: scale(1.25);
}
}
.status-pill {
display: inline-flex;
align-items: center;

View File

@@ -3,15 +3,17 @@
import Image from "next/image";
import { useEffect, useMemo, useState } from "react";
type HealthResponse = {
type HealthSummary = {
status: string;
service: string;
environment: string;
version?: string;
serviceId?: string;
description?: string;
checks: Record<string, { status: string; output?: string }>;
};
export default function Home() {
const [health, setHealth] = useState<HealthResponse | null>(null);
const [ready, setReady] = useState<HealthResponse | null>(null);
const [health, setHealth] = useState<HealthSummary | null>(null);
const [ready, setReady] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [readyError, setReadyError] = useState<string | null>(null);
const [attemptedHealthUrl, setAttemptedHealthUrl] = useState<string | null>(
@@ -21,6 +23,8 @@ export default function Home() {
null
);
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
const [isPolling, setIsPolling] = useState(false);
const [starsActive, setStarsActive] = useState(false);
const apiBase = useMemo(
() => process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000",
@@ -30,38 +34,48 @@ export default function Home() {
useEffect(() => {
async function fetchHealth() {
try {
const url = `${apiBase}/healthz`;
const url = `${apiBase}/health`;
setAttemptedHealthUrl(url);
const res = await fetch(url);
if (!res.ok) throw new Error(`Health check failed: ${res.status}`);
const payload = (await res.json()) as HealthResponse;
const payload = (await res.json()) as HealthSummary;
setHealth(payload);
setError(null);
return true;
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
setError(message);
setHealth(null);
return false;
}
}
async function fetchReady() {
try {
const url = `${apiBase}/readyz`;
const url = `${apiBase}/health/ready`;
setAttemptedReadyUrl(url);
const res = await fetch(url);
if (!res.ok) throw new Error(`Ready check failed: ${res.status}`);
const payload = (await res.json()) as HealthResponse;
const payload = await res.text();
setReady(payload);
setReadyError(null);
return true;
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
setReadyError(message);
setReady(null);
return false;
}
}
const run = async () => {
await Promise.allSettled([fetchHealth(), fetchReady()]);
setIsPolling(true);
const [healthOk, readyOk] = await Promise.all([fetchHealth(), fetchReady()]);
setLastUpdated(new Date().toLocaleTimeString());
if (healthOk && readyOk) {
setStarsActive(true);
setTimeout(() => setStarsActive(false), 1400);
}
setTimeout(() => setIsPolling(false), 900);
};
run();
@@ -75,12 +89,13 @@ export default function Home() {
: "status-pill bad";
const readyClass =
ready && ready.status === "ok" && !readyError
ready && !readyError
? "status-pill ok"
: "status-pill bad";
return (
<main className="page">
<div className={`starfall ${starsActive ? "active" : ""}`} aria-hidden="true" />
<div className="nebula" aria-hidden="true" />
<header className="topbar">
<div className="brand">
@@ -96,7 +111,9 @@ export default function Home() {
</div>
avaaz.ai status
</div>
<div className="pill subtle">Polling every 10s</div>
<div className={`pill subtle ${isPolling ? "pulse" : ""}`}>
Polling every 10s
</div>
</header>
<section className="hero">
@@ -109,10 +126,10 @@ export default function Home() {
</p>
<div className="chip-row">
<div className={badgeClass}>
Health: {health && !error ? "ok" : "unhealthy"}
Health: {health && !error ? health.status : "unhealthy"}
</div>
<div className={readyClass}>
Ready: {ready && !readyError ? "ok" : "not ready"}
Ready: {ready && !readyError ? ready : "not ready"}
</div>
{lastUpdated && (
<div className="pill subtle">Updated {lastUpdated}</div>
@@ -124,28 +141,26 @@ export default function Home() {
<section className="grid">
<article className="card glass">
<div className="card-header">
<span className="pill subtle">/healthz</span>
<span className="pill subtle">/health</span>
<span className={badgeClass}>
{health && !error ? "ok" : "unhealthy"}
{health && !error ? health.status : "unhealthy"}
</span>
</div>
<p className="muted">
{attemptedHealthUrl || `${apiBase}/healthz`}
{attemptedHealthUrl || `${apiBase}/health`}
</p>
{health ? (
<div className="meta">
<div>
<p className="meta-label">Service</p>
<p className="meta-value">{health.service}</p>
</div>
<div>
<p className="meta-label">Environment</p>
<p className="meta-value">{health.environment}</p>
</div>
<div>
<p className="meta-label">Status</p>
<p className="meta-value">{health.status}</p>
</div>
{Object.entries(health.checks || {}).map(([key, val]) => (
<div key={key}>
<p className="meta-label">{key}</p>
<p className="meta-value">{val.status}</p>
</div>
))}
</div>
) : (
<p className="muted">{error || "Waiting for response..."}</p>
@@ -154,27 +169,19 @@ export default function Home() {
<article className="card glass">
<div className="card-header">
<span className="pill subtle">/readyz</span>
<span className="pill subtle">/health/ready</span>
<span className={readyClass}>
{ready && !readyError ? "ok" : "not ready"}
{ready && !readyError ? ready : "not ready"}
</span>
</div>
<p className="muted">
{attemptedReadyUrl || `${apiBase}/readyz`}
{attemptedReadyUrl || `${apiBase}/health/ready`}
</p>
{ready ? (
<div className="meta">
<div>
<p className="meta-label">Service</p>
<p className="meta-value">{ready.service}</p>
</div>
<div>
<p className="meta-label">Environment</p>
<p className="meta-value">{ready.environment}</p>
</div>
<div>
<p className="meta-label">Status</p>
<p className="meta-value">{ready.status}</p>
<p className="meta-value">{ready}</p>
</div>
</div>
) : (