feat: dashboard operations intelligence — analytics cards + signal breakdown
Deploy to VPS / deploy (push) Has been cancelled

Add real-time operational metrics to the HQ dashboard:

- Signals This Month: count with trend % vs previous 30 days
- Pending Actions: highlights when tickets need attention (rose border)
- Email Delivery: success rate percentage + sent/failed counts
- B2B Clients: approved vs total registered

Plus a Signal Breakdown panel showing all-time distribution by type
(Orders, Diagnostics, Consultations, B2B Access) with proportional
bars. All queries run in parallel via Promise.all for minimal latency.
Wrapped in try/catch so the dashboard never breaks if DB is slow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 15:02:06 -05:00
parent 9c1e0cce01
commit f6c3b89e08
+138 -4
View File
@@ -18,15 +18,65 @@ import {
Server, Server,
Image as ImageIcon, Image as ImageIcon,
Settings as SettingsIcon, Settings as SettingsIcon,
TrendingUp,
TrendingDown,
Minus,
MailCheck,
AlertCircle,
UserCheck,
Package,
Sparkles,
Stethoscope,
KeyRound,
} from "lucide-react"; } from "lucide-react";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { logoutAdmin } from "@/app/hq-command/login/actions"; import { logoutAdmin } from "@/app/hq-command/login/actions";
export const revalidate = 0; export const revalidate = 0;
export default async function DashboardPage() { export default async function DashboardPage() {
const nodesCount = await prisma.globalNode.count({ where: { isActive: true } }); const now = new Date();
const appsCount = await prisma.application.count({ where: { isActive: true } }); const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const sixtyDaysAgo = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000);
let nodesCount = 0, appsCount = 0;
let signalsPending = 0, signalsThisMonth = 0, signalsLastMonth = 0;
let signalsOrders = 0, signalsDiag = 0, signalsConsult = 0, signalsAccess = 0;
let emailSent = 0, emailFailed = 0;
let clientsTotal = 0, clientsApproved = 0;
try {
[
nodesCount, appsCount,
signalsPending, signalsThisMonth, signalsLastMonth,
signalsOrders, signalsDiag, signalsConsult, signalsAccess,
emailSent, emailFailed,
clientsTotal, clientsApproved,
] = await Promise.all([
prisma.globalNode.count({ where: { isActive: true } }),
prisma.application.count({ where: { isActive: true } }),
prisma.operationsSignal.count({ where: { status: "PENDING" } }),
prisma.operationsSignal.count({ where: { createdAt: { gte: thirtyDaysAgo } } }),
prisma.operationsSignal.count({ where: { createdAt: { gte: sixtyDaysAgo, lt: thirtyDaysAgo } } }),
prisma.operationsSignal.count({ where: { type: "ORDER" } }),
prisma.operationsSignal.count({ where: { type: "DIAGNOSTIC" } }),
prisma.operationsSignal.count({ where: { type: "CONSULTATION" } }),
prisma.operationsSignal.count({ where: { type: "ACCESS_REQUEST" } }),
prisma.operationsSignal.count({ where: { emailSentAt: { not: null } } }),
prisma.operationsSignal.count({ where: { emailError: { not: null }, emailSentAt: null } }),
prisma.clientUser.count(),
prisma.clientUser.count({ where: { isApproved: true } }),
]);
} catch (e) {
console.error("[dashboard] Analytics query failed:", e);
}
const signalsTotal = signalsOrders + signalsDiag + signalsConsult + signalsAccess;
const emailTotal = emailSent + emailFailed;
const emailRate = emailTotal > 0 ? Math.round((emailSent / emailTotal) * 100) : 100;
const monthTrend = signalsLastMonth > 0
? Math.round(((signalsThisMonth - signalsLastMonth) / signalsLastMonth) * 100)
: signalsThisMonth > 0 ? 100 : 0;
const modules = [ const modules = [
{ {
@@ -184,6 +234,90 @@ export default async function DashboardPage() {
</div> </div>
</div> </div>
{/* ── Operations Intelligence ─────────────────────────────────────── */}
<div className="mb-6">
<span className="text-[10px] uppercase tracking-widest text-[#86868B] font-semibold">Operations Intelligence</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{/* Signals This Month */}
<div className="bg-[#111] border border-white/5 p-5 rounded-2xl shadow-lg">
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Signals · 30d</span>
<div className="flex items-end justify-between">
<span className="text-2xl font-medium text-white">{signalsThisMonth}</span>
{monthTrend !== 0 ? (
<span className={`flex items-center gap-1 text-xs font-medium ${monthTrend > 0 ? "text-emerald-400" : "text-rose-400"}`}>
{monthTrend > 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
{monthTrend > 0 ? "+" : ""}{monthTrend}%
</span>
) : (
<span className="flex items-center gap-1 text-xs text-[#86868B]"><Minus size={14} /> flat</span>
)}
</div>
</div>
{/* Pending Actions */}
<div className={`bg-[#111] border p-5 rounded-2xl shadow-lg ${signalsPending > 0 ? "border-rose-500/30" : "border-white/5"}`}>
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Pending Actions</span>
<div className="flex items-end justify-between">
<span className={`text-2xl font-medium ${signalsPending > 0 ? "text-rose-400" : "text-emerald-400"}`}>{signalsPending}</span>
{signalsPending > 0 ? <AlertCircle size={18} className="text-rose-400/50" /> : <Activity size={18} className="text-emerald-400/30" />}
</div>
</div>
{/* Email Delivery */}
<div className="bg-[#111] border border-white/5 p-5 rounded-2xl shadow-lg">
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Email Delivery</span>
<div className="flex items-end justify-between">
<span className="text-2xl font-medium text-white">{emailRate}%</span>
<div className="flex items-center gap-2 text-[10px] text-[#86868B]">
<span className="flex items-center gap-1"><MailCheck size={12} className="text-emerald-400" />{emailSent}</span>
{emailFailed > 0 && <span className="text-rose-400">{emailFailed} fail</span>}
</div>
</div>
</div>
{/* B2B Clients */}
<div className="bg-[#111] border border-white/5 p-5 rounded-2xl shadow-lg">
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">B2B Clients</span>
<div className="flex items-end justify-between">
<span className="text-2xl font-medium text-white">{clientsApproved}<span className="text-sm text-[#86868B] font-normal">/{clientsTotal}</span></span>
<UserCheck size={18} className="text-emerald-400/30" />
</div>
</div>
</div>
{/* Signal Type Breakdown */}
{signalsTotal > 0 && (
<div className="bg-[#111] border border-white/5 rounded-2xl p-5 mb-12 shadow-lg">
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-4">Signal Breakdown · All Time</span>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{ label: "Orders", count: signalsOrders, icon: Package, color: "text-amber-400", bg: "bg-amber-500/10", bar: "bg-amber-500" },
{ label: "Diagnostics", count: signalsDiag, icon: Stethoscope, color: "text-rose-400", bg: "bg-rose-500/10", bar: "bg-rose-500" },
{ label: "Consultations", count: signalsConsult, icon: Sparkles, color: "text-[#00F0FF]", bg: "bg-[#00F0FF]/10", bar: "bg-[#00F0FF]" },
{ label: "B2B Access", count: signalsAccess, icon: KeyRound, color: "text-emerald-400", bg: "bg-emerald-500/10", bar: "bg-emerald-500" },
].map(s => {
const pct = signalsTotal > 0 ? (s.count / signalsTotal) * 100 : 0;
return (
<div key={s.label} className={`${s.bg} rounded-xl p-4 flex flex-col gap-3`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<s.icon size={14} className={s.color} />
<span className={`text-[10px] uppercase tracking-widest font-bold ${s.color}`}>{s.label}</span>
</div>
<span className="text-lg font-medium text-white">{s.count}</span>
</div>
<div className="h-1.5 bg-black/30 rounded-full overflow-hidden">
<div className={`h-full ${s.bar} rounded-full transition-all duration-700`} style={{ width: `${pct}%` }} />
</div>
</div>
);
})}
</div>
</div>
)}
<div className="mb-6"> <div className="mb-6">
<span className="text-[10px] uppercase tracking-widest text-[#86868B] font-semibold">Core Modules</span> <span className="text-[10px] uppercase tracking-widest text-[#86868B] font-semibold">Core Modules</span>
</div> </div>