Refine health checks and add polling animation
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user