Bankbot / frontend /src /app /status /page.tsx
mohsin-devs's picture
fix: resolve AI assistant page client crash by converting to HTTP streaming fallback, passing env vars to supervisord, and add payments feature
d581b14
Raw
History Blame Contribute Delete
15.6 kB
"use client";
import { useState, useEffect, useCallback } from "react";
import { motion } from "framer-motion";
import {
Activity, Database, Cpu, Wifi, Shield, RefreshCw,
TrendingUp, AlertTriangle, CheckCircle, Clock
} from "lucide-react";
interface Metrics {
uptime_seconds: number;
requests: { total: number; errors: number; auth_failures: number; error_rate_pct: number };
websocket: { total_connects: number; reconnects: number };
ai: {
fallbacks: number;
by_provider: Record<string, { calls: number; errors: number; avg_latency_ms: number; p95_latency_ms: number | null }>;
};
cache: { hits: number; misses: number; hit_ratio_pct: number };
route_timings: Record<string, { calls: number; avg_ms: number; max_ms: number }>;
recent_errors: Array<{ ts: string; path: string; status: number; detail: string }>;
}
interface ApiStatus {
ai_backend: string;
ai_available: boolean;
db_type: string;
cache_type: string;
version: string;
}
const API = process.env.NEXT_PUBLIC_API_URL || "";
function formatUptime(s: number) {
if (s < 60) return `${s}s`;
if (s < 3600) return `${Math.floor(s / 60)}m ${Math.floor(s % 60)}s`;
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
}
function StatusDot({ ok }: { ok: boolean }) {
return (
<span className={`inline-block h-2 w-2 rounded-full ${ok ? "bg-emerald-400" : "bg-red-400"} ${ok ? "animate-pulse" : ""}`} />
);
}
function MetricCard({ label, value, sub, icon: Icon, color = "text-zinc-400" }: {
label: string; value: string | number; sub?: string;
icon: React.ElementType; color?: string;
}) {
return (
<motion.div
whileHover={{ scale: 1.02 }}
className="glass-card p-5"
>
<div className="flex items-start justify-between mb-3">
<p className="text-xs text-zinc-500 uppercase tracking-wider">{label}</p>
<Icon className={`h-4 w-4 ${color}`} />
</div>
<p className="text-2xl font-bold text-white">{value}</p>
{sub && <p className="text-xs text-zinc-500 mt-1">{sub}</p>}
</motion.div>
);
}
const containerVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.06 } },
};
const itemVariants = {
hidden: { opacity: 0, y: 14 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4 } },
};
export default function StatusPage() {
const [metrics, setMetrics] = useState<Metrics | null>(null);
const [status, setStatus] = useState<ApiStatus | null>(null);
const [loading, setLoading] = useState(true);
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
const [backendOnline, setBackendOnline] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const [m, s] = await Promise.all([
fetch(`${API}/api/metrics`).then((r) => r.json()),
fetch(`${API}/api/status`).then((r) => r.json()),
]);
setMetrics(m);
setStatus(s);
setBackendOnline(true);
setLastRefresh(new Date());
} catch {
setBackendOnline(false);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load();
const interval = setInterval(load, 15000); // auto-refresh every 15s
return () => clearInterval(interval);
}, [load]);
const aiProviders = metrics ? Object.entries(metrics.ai.by_provider) : [];
const topRoutes = metrics
? Object.entries(metrics.route_timings).sort((a, b) => b[1].avg_ms - a[1].avg_ms).slice(0, 6)
: [];
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="flex flex-col gap-6"
>
{/* Header */}
<motion.div variants={itemVariants} className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">System Status</h1>
<p className="text-zinc-400 mt-1 text-sm">
Live observability — auto-refreshes every 15s
{lastRefresh && (
<span className="ml-2 text-zinc-600">
· Last updated {lastRefresh.toLocaleTimeString()}
</span>
)}
</p>
</div>
<button
onClick={load}
className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors"
>
<RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />
Refresh
</button>
</motion.div>
{/* Overall health banner */}
<motion.div
variants={itemVariants}
className={`flex items-center gap-3 rounded-2xl border px-5 py-3 ${
backendOnline
? "border-emerald-500/20 bg-emerald-500/5"
: "border-red-500/20 bg-red-500/5"
}`}
>
{backendOnline ? (
<CheckCircle className="h-4 w-4 text-emerald-400 flex-shrink-0" />
) : (
<AlertTriangle className="h-4 w-4 text-red-400 flex-shrink-0" />
)}
<div>
<p className={`text-sm font-semibold ${backendOnline ? "text-emerald-400" : "text-red-400"}`}>
{backendOnline ? "All Systems Operational" : "Backend Offline"}
</p>
<p className="text-xs text-zinc-500 mt-0.5">
{backendOnline
? `API v${status?.version || "2.0.0"} · ${status?.db_type?.toUpperCase()} · ${status?.cache_type?.toUpperCase()} cache · Uptime ${formatUptime(metrics?.uptime_seconds || 0)}`
: "Cannot reach backend. Start with: uvicorn app.main:app --port 8000"}
</p>
</div>
</motion.div>
{/* System info cards */}
{status && (
<motion.div variants={itemVariants} className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{[
{
label: "AI Backend",
value: status.ai_backend.toUpperCase(),
sub: status.ai_available ? "Available" : "Unavailable",
icon: Cpu,
color: status.ai_available ? "text-emerald-400" : "text-red-400",
},
{
label: "Database",
value: status.db_type.toUpperCase(),
sub: "Connected",
icon: Database,
color: "text-blue-400",
},
{
label: "Cache",
value: status.cache_type.toUpperCase(),
sub: metrics ? `${metrics.cache.hit_ratio_pct}% hit rate` : "—",
icon: Activity,
color: "text-purple-400",
},
{
label: "Uptime",
value: formatUptime(metrics?.uptime_seconds || 0),
sub: "Since last restart",
icon: Clock,
color: "text-amber-400",
},
].map((c) => (
<MetricCard key={c.label} {...c} />
))}
</motion.div>
)}
{/* Request metrics */}
{metrics && (
<motion.div variants={itemVariants} className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<MetricCard
label="Total Requests"
value={metrics.requests.total.toLocaleString()}
sub="Since startup"
icon={TrendingUp}
color="text-emerald-400"
/>
<MetricCard
label="Error Rate"
value={`${metrics.requests.error_rate_pct}%`}
sub={`${metrics.requests.errors} errors`}
icon={AlertTriangle}
color={metrics.requests.error_rate_pct > 5 ? "text-red-400" : "text-emerald-400"}
/>
<MetricCard
label="Auth Failures"
value={metrics.requests.auth_failures}
sub="401 responses"
icon={Shield}
color={metrics.requests.auth_failures > 10 ? "text-amber-400" : "text-zinc-400"}
/>
<MetricCard
label="WebSocket Connects"
value={metrics.websocket.total_connects}
sub={`${metrics.websocket.reconnects} reconnects`}
icon={Wifi}
color="text-cyan-400"
/>
</motion.div>
)}
<div className="grid gap-6 lg:grid-cols-2">
{/* AI Provider Health */}
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="flex items-center gap-2 mb-5">
<Cpu className="h-4 w-4 text-emerald-400" />
<h2 className="text-sm font-semibold text-white">AI Provider Health</h2>
</div>
{aiProviders.length === 0 ? (
<div className="flex flex-col items-center gap-2 py-8 text-center">
<Cpu className="h-8 w-8 text-zinc-700" />
<p className="text-sm text-zinc-500">No AI calls recorded yet</p>
<p className="text-xs text-zinc-600">Use the chat or briefing endpoints to generate AI calls</p>
</div>
) : (
<div className="space-y-4">
{aiProviders.map(([provider, data]) => {
const errorRate = data.calls > 0 ? (data.errors / data.calls) * 100 : 0;
return (
<div key={provider} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<StatusDot ok={errorRate < 10} />
<span className="text-sm font-medium text-white capitalize">{provider}</span>
</div>
<div className="flex items-center gap-4 text-xs text-zinc-400">
<span>{data.calls} calls</span>
<span className={errorRate > 10 ? "text-red-400" : "text-emerald-400"}>
{errorRate.toFixed(1)}% errors
</span>
<span>{data.avg_latency_ms}ms avg</span>
</div>
</div>
<div className="h-1.5 w-full rounded-full bg-white/5 overflow-hidden">
<div
className="h-full rounded-full bg-emerald-400 transition-all duration-500"
style={{ width: `${Math.min(100 - errorRate, 100)}%` }}
/>
</div>
</div>
);
})}
<div className="pt-2 border-t border-white/8">
<p className="text-xs text-zinc-500">
AI fallbacks: <span className="text-white font-medium">{metrics?.ai.fallbacks || 0}</span>
</p>
</div>
</div>
)}
</motion.div>
{/* Cache Performance */}
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="flex items-center gap-2 mb-5">
<Activity className="h-4 w-4 text-purple-400" />
<h2 className="text-sm font-semibold text-white">Cache Performance</h2>
</div>
{metrics && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-zinc-400">Hit Ratio</span>
<span className={`text-sm font-bold ${metrics.cache.hit_ratio_pct > 60 ? "text-emerald-400" : "text-amber-400"}`}>
{metrics.cache.hit_ratio_pct}%
</span>
</div>
<div className="h-3 w-full rounded-full bg-white/5 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${metrics.cache.hit_ratio_pct}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="h-full rounded-full bg-gradient-to-r from-purple-500 to-blue-500"
/>
</div>
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="rounded-xl bg-white/5 p-3 text-center">
<p className="text-lg font-bold text-emerald-400">{metrics.cache.hits}</p>
<p className="text-xs text-zinc-500 mt-0.5">Cache Hits</p>
</div>
<div className="rounded-xl bg-white/5 p-3 text-center">
<p className="text-lg font-bold text-zinc-400">{metrics.cache.misses}</p>
<p className="text-xs text-zinc-500 mt-0.5">Cache Misses</p>
</div>
</div>
</div>
)}
</motion.div>
</div>
{/* Route Timings */}
{topRoutes.length > 0 && (
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="flex items-center gap-2 mb-5">
<Clock className="h-4 w-4 text-amber-400" />
<h2 className="text-sm font-semibold text-white">Route Performance</h2>
<span className="text-xs text-zinc-500 ml-auto">Sorted by avg response time</span>
</div>
<div className="space-y-2">
{topRoutes.map(([path, data]) => {
const isSlow = data.avg_ms > 500;
const barWidth = Math.min((data.avg_ms / 2000) * 100, 100);
return (
<div key={path} className="flex items-center gap-4">
<code className="text-xs text-zinc-400 w-56 truncate flex-shrink-0">{path}</code>
<div className="flex-1 h-1.5 rounded-full bg-white/5 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${isSlow ? "bg-amber-400" : "bg-emerald-400"}`}
style={{ width: `${barWidth}%` }}
/>
</div>
<div className="flex items-center gap-3 text-xs text-zinc-400 w-40 flex-shrink-0 text-right">
<span className={`font-medium ${isSlow ? "text-amber-400" : "text-emerald-400"}`}>
{data.avg_ms}ms avg
</span>
<span className="text-zinc-600">{data.max_ms}ms max</span>
<span className="text-zinc-600">{data.calls} calls</span>
</div>
</div>
);
})}
</div>
</motion.div>
)}
{/* Recent Errors */}
{metrics && metrics.recent_errors.length > 0 && (
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="flex items-center gap-2 mb-5">
<AlertTriangle className="h-4 w-4 text-red-400" />
<h2 className="text-sm font-semibold text-white">Recent Errors</h2>
<span className="text-xs text-zinc-500 ml-auto">Last 10</span>
</div>
<div className="space-y-2">
{metrics.recent_errors.map((err, i) => (
<div key={i} className="flex items-center gap-3 rounded-xl bg-red-500/5 border border-red-500/10 px-3 py-2">
<span className={`text-xs font-bold px-1.5 py-0.5 rounded ${
err.status >= 500 ? "bg-red-500/20 text-red-400" : "bg-amber-500/20 text-amber-400"
}`}>
{err.status}
</span>
<code className="text-xs text-zinc-400 flex-1 truncate">{err.path}</code>
<span className="text-xs text-zinc-600 flex-shrink-0">
{new Date(err.ts).toLocaleTimeString()}
</span>
</div>
))}
</div>
</motion.div>
)}
</motion.div>
);
}