add proper status

This commit is contained in:
KM Koushik
2025-09-06 11:39:18 +10:00
parent 69470a4937
commit e00e75c511
3 changed files with 110 additions and 64 deletions

View File

@@ -1,7 +1,7 @@
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
// Static export for marketing site // Use static export in production by default; keep dev server dynamic
output: "export", output: process.env.NEXT_OUTPUT ?? (process.env.NODE_ENV === "production" ? "export" : undefined),
images: { images: {
// Required for static export if using images // Required for static export if using images
unoptimized: true, unoptimized: true,
@@ -9,4 +9,3 @@ const config = {
}; };
export default config; export default config;

View File

@@ -0,0 +1,92 @@
import type { NextRequest } from "next/server";
export const dynamic = "force-dynamic";
type StatusState = "operational" | "degraded" | "down" | "unknown";
function normalizeStatus(data: any): StatusState {
const overall = (data?.overallStatus || data?.status || "")
.toString()
.toLowerCase();
if (
overall.includes("up") ||
overall.includes("ok") ||
overall.includes("oper")
)
return "operational";
if (overall.includes("degrad") || overall.includes("partial"))
return "degraded";
if (
overall.includes("down") ||
overall.includes("outage") ||
overall.includes("incident")
)
return "down";
// Status page incidents (default endpoint)
const sp = (data?.statusPage || data?.status_page || {}) as any;
const incidentRaw = sp?.incident ?? sp?.incidents ?? data?.incident ?? data?.incidents;
if (incidentRaw) {
const incidents: any[] = Array.isArray(incidentRaw)
? incidentRaw
: [incidentRaw];
const hasActive = incidents.some((i) => {
const resolved =
i?.resolved === true ||
(typeof i?.status === "string" && i.status.toLowerCase().includes("resolv")) ||
!!i?.endAt ||
!!i?.end_at ||
!!i?.endTime ||
!!i?.end_time;
return !resolved;
});
if (hasActive) return "down";
}
// Heartbeat map shape: { [monitorId: string]: Heartbeat[] }
if (data?.heartbeatList && typeof data.heartbeatList === "object") {
const lists = Object.values<any>(data.heartbeatList);
const isAnyDown = lists.some(
(arr: any) => Array.isArray(arr) && arr.some((hb) => hb?.status === 0)
);
if (isAnyDown) return "down";
return "operational";
}
if (typeof data?.allUp === "boolean")
return data.allUp ? "operational" : "down";
return "unknown";
}
export async function GET(_req: NextRequest) {
const base = (
process.env.STATUS_BASE_URL || "https://status.usesend.com"
).replace(/\/$/, "");
const candidates = [
"/api/status-page/default",
"/api/status-page/heartbeat/default",
];
let status = "unknown";
for (const path of candidates) {
try {
const res = await fetch(`${base}${path}`, { cache: "no-store" });
if (!res.ok) continue;
const data = await res.json().catch(() => null);
if (!data) continue;
status = normalizeStatus(data);
if (status !== "unknown") break;
} catch {
// try next candidate
continue;
}
}
return Response.json(
{ status },
{
headers: { "Cache-Control": "s-maxage=30, stale-while-revalidate=300" },
}
);
}

View File

@@ -4,66 +4,20 @@ import { useEffect, useMemo, useState } from "react";
type StatusState = "operational" | "degraded" | "down" | "unknown"; type StatusState = "operational" | "degraded" | "down" | "unknown";
// Best-effort fetcher for Uptime Kuma public status page JSON. // Fetch normalized status from the app's server API to bypass CORS.
// Falls back gracefully if the endpoint or CORS is not available. async function fetchUptimeStatus(): Promise<StatusState> {
async function fetchUptimeStatus(baseUrl: string): Promise<StatusState> { try {
const candidates = [ const res = await fetch("/api/status", { cache: "no-store" });
"/api/status-page/heartbeat/default", // specific uptime-kuma status page slug if (!res.ok) return "unknown";
]; const data: any = await res.json().catch(() => null);
const s = (data?.status || "").toString().toLowerCase();
for (const path of candidates) { console.log(data);
try { if (s === "operational" || s === "degraded" || s === "down")
const res = await fetch(baseUrl.replace(/\/$/, "") + path, { return s as StatusState;
// Always fetch from the browser; avoid caching too aggressively return "unknown";
cache: "no-store", } catch {
}); return "unknown";
if (!res.ok) continue;
const data: any = await res.json().catch(() => null);
if (!data) continue;
// Heuristics across possible Kuma payloads
// 1) overallStatus or status fields
const overall = (data.overallStatus || data.status || "")
.toString()
.toLowerCase();
if (
overall.includes("up") ||
overall.includes("ok") ||
overall.includes("oper")
)
return "operational";
if (overall.includes("degrad") || overall.includes("partial"))
return "degraded";
if (
overall.includes("down") ||
overall.includes("outage") ||
overall.includes("incident")
)
return "down";
// 2) heartbeat style: if any monitor is down
if (Array.isArray(data.monitors) && Array.isArray(data.heartbeatList)) {
// If any lastHeartbeatStatus === 0 (down) => down
const isAnyDown = Object.values<any>(data.heartbeatList).some(
(arr: any[]) =>
Array.isArray(arr) && arr.some((hb) => hb?.status === 0)
);
if (isAnyDown) return "down";
return "operational";
}
// 3) Generic boolean hints
if (typeof data.allUp === "boolean")
return data.allUp ? "operational" : "down";
// Unknown but successful response
return "unknown";
} catch {
// Try next candidate
continue;
}
} }
return "unknown";
} }
export function StatusBadge({ export function StatusBadge({
@@ -76,7 +30,8 @@ export function StatusBadge({
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
const load = async () => { const load = async () => {
const s = await fetchUptimeStatus(baseUrl); const s = await fetchUptimeStatus();
if (mounted) setStatus(s); if (mounted) setStatus(s);
}; };
load(); load();
@@ -85,7 +40,7 @@ export function StatusBadge({
mounted = false; mounted = false;
clearInterval(id); clearInterval(id);
}; };
}, [baseUrl]); }, []);
const dotClass = useMemo(() => { const dotClass = useMemo(() => {
switch (status) { switch (status) {