Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/api/stellar/health/history/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* GET /api/stellar/health/history
*
* Returns the outage history log for audit/debugging purposes.
*/

import { NextResponse } from "next/server";
import { getOutageLog } from "@/lib/stellar/health-monitor";

export async function GET() {
const log = getOutageLog();

return NextResponse.json({
outages: log.map((event) => ({
id: event.id,
startedAt: event.startedAt,
endedAt: event.endedAt,
durationMs: event.durationMs,
status: event.status,
error: event.error,
recovered: event.recovered,
})),
summary: {
total: log.length,
recovered: log.filter((o) => o.recovered).length,
active: log.filter((o) => !o.recovered).length,
},
});
}
38 changes: 38 additions & 0 deletions app/api/stellar/health/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* API route tests for /api/stellar/health
*/

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { GET } from "./route";

describe("GET /api/stellar/health", () => {
beforeEach(() => {
// Reset environment
delete process.env.STELLAR_HORIZON_URL;
delete process.env.STELLAR_NETWORK;
delete process.env.STELLAR_SOURCE_SECRET;
});

it("should return JSON health status", async () => {
const request = new Request("http://localhost/api/stellar/health");
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data).toHaveProperty("status");
expect(data).toHaveProperty("latencyMs");
expect(data).toHaveProperty("checkedAt");
expect(data).toHaveProperty("outage");
expect(data).toHaveProperty("history");
});

it("should return SSE stream when Accept header is text/event-stream", async () => {
const request = new Request("http://localhost/api/stellar/health", {
headers: { Accept: "text/event-stream" },
});
const response = await GET(request);

expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/event-stream");
});
});
122 changes: 122 additions & 0 deletions app/api/stellar/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* GET /api/stellar/health
*
* Returns the current Stellar network health status.
* Supports Server-Sent Events (SSE) for real-time updates when
* the client requests streaming via Accept: text/event-stream.
*/

import { NextResponse } from "next/server";
import {
checkHealth,
subscribe,
getCurrentStatus,
getNetworkStatus,
getOutageLog,
startMonitoring,
getActiveOutage,
} from "@/lib/stellar/health-monitor";

// Ensure monitoring is running on the server
startMonitoring(30000);

export async function GET(request: Request) {
const accept = request.headers.get("accept") || "";

// SSE streaming for real-time updates
if (accept.includes("text/event-stream")) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
const send = (data: any) => {
try {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
} catch {
// Stream closed
}
};

// Send initial state
const current = getCurrentStatus();
if (current) {
send({
type: "health_update",
status: current.status,
latencyMs: current.latencyMs,
ledger: current.ledger,
protocolVersion: current.protocolVersion,
network: current.network,
checkedAt: current.checkedAt,
outage: getActiveOutage(),
});
}

// Subscribe to updates
const unsubscribe = subscribe((result) => {
send({
type: "health_update",
status: result.status,
latencyMs: result.latencyMs,
ledger: result.ledger,
protocolVersion: result.protocolVersion,
network: result.network,
checkedAt: result.checkedAt,
outage: getActiveOutage(),
});
});

// Keep alive
const keepAlive = setInterval(() => {
try {
controller.enqueue(encoder.encode(":keepalive\n\n"));
} catch {
clearInterval(keepAlive);
}
}, 15000);

// Cleanup on close
request.signal.addEventListener("abort", () => {
unsubscribe();
clearInterval(keepAlive);
});
},
});

return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}

// Standard JSON response
const result = getCurrentStatus() || (await checkHealth());
const activeOutage = getActiveOutage();

return NextResponse.json({
status: result.status,
latencyMs: result.latencyMs,
ledger: result.ledger,
protocolVersion: result.protocolVersion,
horizonUrl: result.horizonUrl,
network: result.network,
checkedAt: result.checkedAt,
error: result.error,
outage: activeOutage
? {
id: activeOutage.id,
startedAt: activeOutage.startedAt,
status: activeOutage.status,
error: activeOutage.error,
}
: null,
history: {
totalOutages: getOutageLog().length,
lastOutage: getOutageLog().slice(-1)[0] || null,
},
});
}
3 changes: 3 additions & 0 deletions app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
ScrollText,
Users,
} from "lucide-react";
import { StellarNetworkStatus } from "@/components/stellar-network-status";

type ChatPreview = {
id: string;
Expand Down Expand Up @@ -488,6 +489,8 @@ export default function ChatPage() {
<div className="min-h-screen bg-background flex flex-col">
<Header />

<StellarNetworkStatus variant="banner" />

<main className="flex-1 pt-24 pb-24 md:pb-8 px-3 sm:px-6">
<div className="mx-auto w-full max-w-7xl h-[min(84vh,820px)] rounded-3xl border border-border/70 bg-card/90 shadow-[0_24px_64px_-24px_rgba(0,0,0,0.35)] backdrop-blur-sm overflow-hidden">
<div className="h-full flex relative">
Expand Down
Loading
Loading