admin: Slice 3c AI-health dashboard UI
- useAiHealth hook + AiHealthOverview type (60s refetch). - /admin/ai-health: per-pipeline sections (try-on, metadata, instagram, apgs) with status pills, throughput KPIs, and recent-failure lists. - sidebar "سلامت AI" nav link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c1118f70d6
commit
b3189db773
@ -365,6 +365,62 @@ export interface AdminCollectionRow {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/* ---------------------- AI pipelines health (3c) ---------------------- */
|
||||
|
||||
interface FailureRow {
|
||||
id: string;
|
||||
error: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
productId?: string;
|
||||
sellerId?: string;
|
||||
}
|
||||
|
||||
export interface AiHealthOverview {
|
||||
generatedAt: string;
|
||||
tryon: {
|
||||
total: number;
|
||||
byStatus: Record<string, number>;
|
||||
last24h: number;
|
||||
last7d: number;
|
||||
recentFailures: FailureRow[];
|
||||
};
|
||||
metadata: {
|
||||
total: number;
|
||||
byStatus: Record<string, number>;
|
||||
last24h: number;
|
||||
last7d: number;
|
||||
recentFailures: FailureRow[];
|
||||
};
|
||||
instagram: {
|
||||
accountsTotal: number;
|
||||
accountsActive: number;
|
||||
postsTotal: number;
|
||||
postsPendingAi: number;
|
||||
postsConverted: number;
|
||||
importedLast7d: number;
|
||||
recentImports: { account: string; date: string; importedCount: number }[];
|
||||
};
|
||||
apgs: {
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
last24h: number;
|
||||
last7d: number;
|
||||
recentFailures: FailureRow[];
|
||||
};
|
||||
}
|
||||
|
||||
export function useAiHealth() {
|
||||
const token = useAuthToken();
|
||||
return useQuery({
|
||||
queryKey: ["admin", "ai-health"],
|
||||
enabled: !!token,
|
||||
refetchInterval: 60_000,
|
||||
queryFn: () => adminGet<AiHealthOverview>("ai-health/overview/", token as string),
|
||||
});
|
||||
}
|
||||
|
||||
export const useAdminDiscounts = (params: AdminListParams) =>
|
||||
useAdminList<AdminDiscountRow>("discounts", params);
|
||||
export const useAdminCollections = (params: AdminListParams) =>
|
||||
|
||||
182
app/routes/admin.ai-health.tsx
Normal file
182
app/routes/admin.ai-health.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { Loader2, Sparkles, FileText, Instagram, Bot } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { useAiHealth, type AiHealthOverview } from "~/requestHandler/use-admin-hooks";
|
||||
import { KpiCard } from "~/components/admin/KpiCard";
|
||||
import { AdminPageTitle, faNum, faDate } from "~/components/admin/DataTable";
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
pending: "در انتظار",
|
||||
processing: "در حال پردازش",
|
||||
success: "موفق",
|
||||
ready: "آماده",
|
||||
failed: "ناموفق",
|
||||
};
|
||||
|
||||
const STATUS_TONE: Record<string, string> = {
|
||||
pending: "bg-yellow-500/10 text-yellow-600",
|
||||
processing: "bg-VITROWN_BLUE/10 text-VITROWN_BLUE",
|
||||
success: "bg-GREEN/10 text-GREEN",
|
||||
ready: "bg-GREEN/10 text-GREEN",
|
||||
failed: "bg-RED/10 text-RED",
|
||||
};
|
||||
|
||||
function StatusPills({ byStatus }: { byStatus: Record<string, number> }) {
|
||||
const entries = Object.entries(byStatus);
|
||||
if (entries.length === 0) return <p className="text-GRAY text-[12px]">دادهای نیست</p>;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{entries.map(([k, v]) => (
|
||||
<span
|
||||
key={k}
|
||||
className={`text-[12px] font-semibold px-2.5 py-1 rounded-full ${
|
||||
STATUS_TONE[k] || "bg-WHITE3 text-BLACK2"
|
||||
}`}
|
||||
>
|
||||
{STATUS_LABEL[k] || k}: {faNum(v)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Failures({
|
||||
rows,
|
||||
dateKey,
|
||||
}: {
|
||||
rows: AiHealthOverview["tryon"]["recentFailures"];
|
||||
dateKey: "createdAt" | "updatedAt";
|
||||
}) {
|
||||
if (rows.length === 0)
|
||||
return <p className="text-GREEN text-[12px] mt-2">خطای اخیری ثبت نشده ✓</p>;
|
||||
return (
|
||||
<div className="mt-3 flex flex-col gap-1.5">
|
||||
<p className="text-[12px] font-semibold text-RED">خطاهای اخیر</p>
|
||||
{rows.map((r) => (
|
||||
<div key={r.id} className="rounded-xl border border-RED/20 bg-RED/5 p-2.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-GRAY text-[10.5px] tabular-nums" dir="ltr">
|
||||
{r.id.slice(0, 8)}
|
||||
</span>
|
||||
<span className="text-GRAY text-[10.5px] whitespace-nowrap">
|
||||
{faDate(r[dateKey])}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11.5px] text-BLACK2 mt-1 break-words" dir="ltr">
|
||||
{r.error || "—"}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
icon: Icon,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="rounded-2xl border border-WHITE3 bg-WHITE p-4 sm:p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="h-9 w-9 rounded-xl bg-WHITE2 grid place-items-center">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<h2 className="font-extrabold text-[15px]">{title}</h2>
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminAiHealth() {
|
||||
const { data, isLoading, isError } = useAiHealth();
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="flex min-h-[40vh] items-center justify-center">
|
||||
<Loader2 className="h-7 w-7 animate-spin text-GRAY" />
|
||||
</div>
|
||||
);
|
||||
if (isError || !data)
|
||||
return <p className="text-RED text-[14px]">خطا در بارگذاری وضعیت سرویسهای هوش مصنوعی</p>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPageTitle>سلامت سرویسهای هوش مصنوعی</AdminPageTitle>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Section title="پرو مجازی" icon={Sparkles}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<KpiCard label="کل" value={faNum(data.tryon.total)} />
|
||||
<KpiCard label="۲۴ ساعت" value={faNum(data.tryon.last24h)} />
|
||||
<KpiCard label="۷ روز" value={faNum(data.tryon.last7d)} />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<StatusPills byStatus={data.tryon.byStatus} />
|
||||
</div>
|
||||
<Failures rows={data.tryon.recentFailures} dateKey="createdAt" />
|
||||
</Section>
|
||||
|
||||
<Section title="متادیتای محصولات" icon={FileText}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<KpiCard label="کل" value={faNum(data.metadata.total)} />
|
||||
<KpiCard label="۲۴ ساعت" value={faNum(data.metadata.last24h)} />
|
||||
<KpiCard label="۷ روز" value={faNum(data.metadata.last7d)} />
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<StatusPills byStatus={data.metadata.byStatus} />
|
||||
</div>
|
||||
<Failures rows={data.metadata.recentFailures} dateKey="updatedAt" />
|
||||
</Section>
|
||||
|
||||
<Section title="ایمپورت اینستاگرام" icon={Instagram}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<KpiCard
|
||||
label="اکانتها"
|
||||
value={faNum(data.instagram.accountsTotal)}
|
||||
sub={`${faNum(data.instagram.accountsActive)} فعال`}
|
||||
/>
|
||||
<KpiCard
|
||||
label="در صف AI"
|
||||
value={faNum(data.instagram.postsPendingAi)}
|
||||
accent={data.instagram.postsPendingAi > 0 ? "red" : "default"}
|
||||
/>
|
||||
<KpiCard label="تبدیلشده" value={faNum(data.instagram.postsConverted)} accent="green" />
|
||||
</div>
|
||||
<p className="text-[12px] text-GRAY mt-3">
|
||||
ایمپورت ۷ روز اخیر: <b className="tabular-nums text-BLACK2">{faNum(data.instagram.importedLast7d)}</b>
|
||||
</p>
|
||||
{data.instagram.recentImports.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-1">
|
||||
{data.instagram.recentImports.map((im, i) => (
|
||||
<div key={i} className="flex items-center justify-between text-[12px] border-b border-WHITE3 py-1.5 last:border-0">
|
||||
<span className="tabular-nums" dir="ltr">@{im.account}</span>
|
||||
<span className="text-GRAY">
|
||||
{faNum(im.importedCount)} — {faDate(im.date)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title="تولید محصول با هوش مصنوعی" icon={Bot}>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<KpiCard label="کل" value={faNum(data.apgs.total)} />
|
||||
<KpiCard label="موفق" value={faNum(data.apgs.success)} accent="green" />
|
||||
<KpiCard
|
||||
label="ناموفق"
|
||||
value={faNum(data.apgs.failed)}
|
||||
accent={data.apgs.failed > 0 ? "red" : "default"}
|
||||
/>
|
||||
</div>
|
||||
<Failures rows={data.apgs.recentFailures} dateKey="createdAt" />
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,7 @@ import {
|
||||
Ticket,
|
||||
LayoutGrid,
|
||||
Landmark,
|
||||
Activity,
|
||||
ScrollText,
|
||||
ArrowRight,
|
||||
ShieldCheck,
|
||||
@ -33,6 +34,7 @@ const NAV: NavItem[] = [
|
||||
{ label: "تخفیفها", to: "/admin/discounts", icon: Ticket },
|
||||
{ label: "مجموعهها", to: "/admin/collections", icon: LayoutGrid },
|
||||
{ label: "دفتر مالی", to: "/admin/ledger", icon: Landmark },
|
||||
{ label: "سلامت AI", to: "/admin/ai-health", icon: Activity },
|
||||
{ label: "گزارش فعالیت", to: "/admin/audit", icon: ScrollText },
|
||||
];
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user