مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
123 lines
4.8 KiB
TypeScript
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>
|
|
);
|
|
}
|