154 lines
5.7 KiB
TypeScript
154 lines
5.7 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { fetchClient } from "@/lib/api-client";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
interface Status {
|
|
uptimeSeconds: number;
|
|
memory: Record<string, number>;
|
|
versions: Record<string, string>;
|
|
counts: { vendors: number; orders: number; products: number; chats: number };
|
|
}
|
|
|
|
function formatDuration(seconds: number) {
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
return `${h}h ${m}m ${s}s`;
|
|
}
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (bytes === 0) return '0 Bytes';
|
|
if (bytes < 0) return '0 Bytes'; // Handle negative values
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.max(0, Math.floor(Math.log(bytes) / Math.log(k))); // Ensure i is at least 0
|
|
// Clamp i to valid array index
|
|
const clampedI = Math.min(i, sizes.length - 1);
|
|
return Math.round(bytes / Math.pow(k, clampedI) * 100) / 100 + ' ' + sizes[clampedI];
|
|
}
|
|
|
|
export default function SystemStatusCard() {
|
|
const [data, setData] = useState<Status | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [showDebug, setShowDebug] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
(async () => {
|
|
try {
|
|
const res = await fetchClient<Status>("/admin/system-status");
|
|
if (mounted) setData(res);
|
|
} catch (e: any) {
|
|
if (mounted) setError(e?.message || "Failed to load status");
|
|
}
|
|
})();
|
|
return () => { mounted = false; };
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h2 className="font-medium">System status</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">Uptime, versions, environment</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400">OK</span>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowDebug(!showDebug)}
|
|
>
|
|
{showDebug ? 'Hide' : 'Show'} Debug
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && <p className="text-sm text-muted-foreground mt-3">{error}</p>}
|
|
|
|
{data && (
|
|
<div className="mt-3 grid grid-cols-2 gap-3 text-sm">
|
|
<div className="space-y-1">
|
|
<div className="text-muted-foreground">Uptime</div>
|
|
<div>{formatDuration(data.uptimeSeconds)}</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-muted-foreground">Node</div>
|
|
<div>{data.versions?.node}</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-muted-foreground">Vendors</div>
|
|
<div>{data.counts?.vendors}</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-muted-foreground">Orders</div>
|
|
<div>{data.counts?.orders}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showDebug && data && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Debug: Raw System Status Data</CardTitle>
|
|
<CardDescription>Complete system status response from backend</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4 text-xs font-mono">
|
|
<div>
|
|
<div className="font-semibold mb-2">Memory Usage:</div>
|
|
<div className="pl-4 space-y-1">
|
|
<div>RSS (Resident Set Size): {formatBytes(data.memory?.rss || 0)}</div>
|
|
<div>Heap Total: {formatBytes(data.memory?.heapTotal || 0)}</div>
|
|
<div>Heap Used: {formatBytes(data.memory?.heapUsed || 0)}</div>
|
|
<div>External: {formatBytes(data.memory?.external || 0)}</div>
|
|
<div>Array Buffers: {formatBytes(data.memory?.arrayBuffers || 0)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="font-semibold mb-2">Versions:</div>
|
|
<div className="pl-4 space-y-1">
|
|
{Object.entries(data.versions || {}).map(([key, value]) => (
|
|
<div key={key}>{key}: {value}</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="font-semibold mb-2">Counts:</div>
|
|
<div className="pl-4 space-y-1">
|
|
<div>Vendors: {data.counts?.vendors || 0}</div>
|
|
<div>Orders: {data.counts?.orders || 0}</div>
|
|
<div>Products: {data.counts?.products || 0}</div>
|
|
<div>Chats: {data.counts?.chats || 0}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="font-semibold mb-2">Uptime:</div>
|
|
<div className="pl-4">
|
|
{data.uptimeSeconds} seconds ({formatDuration(data.uptimeSeconds)})
|
|
</div>
|
|
</div>
|
|
|
|
<details className="mt-4">
|
|
<summary className="font-semibold cursor-pointer">Full JSON Response</summary>
|
|
<pre className="mt-2 bg-muted p-4 rounded overflow-auto max-h-96 text-[10px]">
|
|
{JSON.stringify(data, null, 2)}
|
|
</pre>
|
|
</details>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|