Files
smart-org/apps/web/src/app/(app)/admin/page.tsx
T
mjalmoudy94 717e53a7ce نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
2026-10-13 00:39:28 +03:30

123 lines
4.8 KiB
TypeScript

"use client";
import Link from "next/link";
import * as React from "react";
import type { AdminOrganization, AdminUsageRow, ReadinessResponse } from "@agy/contracts";
import { Badge, Card, CardHint, CardTitle, ErrorState, LoadingState, Stat } from "@agy/ui";
import { ApiError, api } from "@/lib/api";
/**
* Platform owner overview.
*
* Everything on this page is invisible to normal customers by design — the whole
* point of the AI Control Plane is that model identity never reaches a tenant.
*/
export default function AdminOverviewPage() {
const [readiness, setReadiness] = React.useState<ReadinessResponse | null>(null);
const [organizations, setOrganizations] = React.useState<AdminOrganization[]>([]);
const [usage, setUsage] = React.useState<AdminUsageRow[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const load = React.useCallback(async () => {
setLoading(true);
setError(null);
try {
const [nextReadiness, nextOrganizations, nextUsage] = await Promise.all([
api.readiness(),
api.adminOrganizations(),
api.adminUsage(),
]);
setReadiness(nextReadiness);
setOrganizations(nextOrganizations);
setUsage(nextUsage);
} catch (err) {
setError(err instanceof ApiError ? err.message : "دریافت وضعیت ناموفق بود.");
} finally {
setLoading(false);
}
}, []);
React.useEffect(() => {
void load();
}, [load]);
if (loading) return <LoadingState label="در حال دریافت وضعیت سامانه…" />;
if (error) return <ErrorState message={error} onRetry={load} />;
const totalBilled = usage.reduce((sum, row) => sum + row.total_price, 0);
return (
<div className="flex flex-col gap-5">
<header className="flex flex-col gap-1">
<h1 className="text-lg font-medium text-slate-100">کنسول مالک سامانه</h1>
<p className="text-sm text-slate-400">
مدیریت ارائه‌دهنده‌ها، مدل‌ها، سیاست مسیریابی و تعرفه‌ها.
</p>
</header>
<div className="grid gap-3 sm:grid-cols-3">
<Stat label="سازمان‌ها" value={organizations.length} />
<Stat label="مجموع اعضای سازمان‌ها" value={organizations.reduce((s, o) => s + o.member_count, 0)} />
<Stat
label="مجموع مصرف ثبت‌شده"
value={totalBilled.toFixed(2)}
hint="بر پایهٔ دفتر مصرف، به واحد پول داخلی سامانه."
/>
</div>
<Card>
<CardTitle>آداپتورهای فعال</CardTitle>
<CardHint>
این مقادیر از محیط خوانده می‌شوند. هر مقدار «mock» یعنی آن سرویس بیرونی هنوز
متصل نشده است.
</CardHint>
<div className="mt-3 flex flex-wrap gap-2">
{Object.entries(readiness?.checks.adapters ?? {}).map(([key, value]) => (
<Badge key={key} tone={value === "mock" ? "warning" : "success"}>
{key}: {value}
</Badge>
))}
</div>
</Card>
<div className="grid gap-4 lg:grid-cols-2">
{[
{
title: "ارائه‌دهنده‌ها و کلیدها",
hint: "ساخت ارائه‌دهنده و ذخیرهٔ کلید رمزنگاری‌شده. کلید خام هرگز نمایش داده نمی‌شود.",
path: "/admin/providers",
},
{
title: "مدل‌ها",
hint: "نام داخلی، توانایی‌ها و قیمت ورودی/خروجی — فقط برای مالک سامانه.",
path: "/admin/models",
},
{
title: "سیاست مسیریابی",
hint: "حالت‌های fast / cheap / balanced / reasoning / vision / coding و اجرای آزمایشی.",
path: "/admin/routing",
},
{
title: "تعرفه‌ها",
hint: "قیمت مشتری و برآورد هزینهٔ داخلی به تفکیک واحد مصرف.",
path: "/admin/rate-cards",
},
{
title: "سازمان‌ها",
hint: "فهرست همهٔ سازمان‌ها، تعداد اعضا و مصرف تجمیعی هرکدام.",
path: "/admin/organizations",
},
].map((item) => (
<Link key={item.path} href={item.path}>
<Card className="h-full transition-colors hover:border-accent/60">
<CardTitle>{item.title}</CardTitle>
<CardHint>{item.hint}</CardHint>
</Card>
</Link>
))}
</div>
</div>
);
}