add proper status
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
// Static export for marketing site
|
||||
output: "export",
|
||||
// Use static export in production by default; keep dev server dynamic
|
||||
output: process.env.NEXT_OUTPUT ?? (process.env.NODE_ENV === "production" ? "export" : undefined),
|
||||
images: {
|
||||
// Required for static export if using images
|
||||
unoptimized: true,
|
||||
@@ -9,4 +9,3 @@ const config = {
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
|
92
apps/marketing/src/app/api/status/route.ts
Normal file
92
apps/marketing/src/app/api/status/route.ts
Normal 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" },
|
||||
}
|
||||
);
|
||||
}
|
@@ -4,67 +4,21 @@ import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
type StatusState = "operational" | "degraded" | "down" | "unknown";
|
||||
|
||||
// Best-effort fetcher for Uptime Kuma public status page JSON.
|
||||
// Falls back gracefully if the endpoint or CORS is not available.
|
||||
async function fetchUptimeStatus(baseUrl: string): Promise<StatusState> {
|
||||
const candidates = [
|
||||
"/api/status-page/heartbeat/default", // specific uptime-kuma status page slug
|
||||
];
|
||||
|
||||
for (const path of candidates) {
|
||||
// Fetch normalized status from the app's server API to bypass CORS.
|
||||
async function fetchUptimeStatus(): Promise<StatusState> {
|
||||
try {
|
||||
const res = await fetch(baseUrl.replace(/\/$/, "") + path, {
|
||||
// Always fetch from the browser; avoid caching too aggressively
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) continue;
|
||||
const res = await fetch("/api/status", { cache: "no-store" });
|
||||
if (!res.ok) return "unknown";
|
||||
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
|
||||
const s = (data?.status || "").toString().toLowerCase();
|
||||
console.log(data);
|
||||
if (s === "operational" || s === "degraded" || s === "down")
|
||||
return s as StatusState;
|
||||
return "unknown";
|
||||
} catch {
|
||||
// Try next candidate
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export function StatusBadge({
|
||||
baseUrl = "https://status.usesend.com",
|
||||
@@ -76,7 +30,8 @@ export function StatusBadge({
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const load = async () => {
|
||||
const s = await fetchUptimeStatus(baseUrl);
|
||||
const s = await fetchUptimeStatus();
|
||||
|
||||
if (mounted) setStatus(s);
|
||||
};
|
||||
load();
|
||||
@@ -85,7 +40,7 @@ export function StatusBadge({
|
||||
mounted = false;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [baseUrl]);
|
||||
}, []);
|
||||
|
||||
const dotClass = useMemo(() => {
|
||||
switch (status) {
|
||||
|
Reference in New Issue
Block a user