Files
personal-ai-assistant/frontend/src/pages/admin/usage.tsx
dolgolyov.alexei d86d53f473 Phase 10: Per-User Rate Limits — messages + tokens, quota UI, admin usage
Backend:
- max_ai_messages_per_day + max_ai_tokens_per_day on User model (nullable, override)
- Migration 008: add columns + seed default settings (100 msgs, 500K tokens)
- usage_service: count today's messages + tokens, check quota, get limits
- GET /chats/quota returns usage vs limits + reset time
- POST /chats/{id}/messages checks quota before streaming (429 if exceeded)
- Admin user schemas expose both limit fields
- GET /admin/usage returns per-user daily message + token counts
- admin_user_service allows updating both limit fields

Frontend:
- Chat header shows "X/Y messages · XK/YK tokens" with red highlight at limit
- Quota refreshes every 30s via TanStack Query
- Admin usage page with table: user, messages today, tokens today
- Route + sidebar entry for admin usage
- English + Russian translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:44:51 +03:00

72 lines
2.4 KiB
TypeScript

import { Navigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { useAuthStore } from "@/stores/auth-store";
import api from "@/api/client";
import { cn } from "@/lib/utils";
interface UserUsage {
user_id: string;
username: string;
messages_today: number;
message_limit: number;
tokens_today: number;
token_limit: number;
}
export function AdminUsagePage() {
const { t } = useTranslation();
const user = useAuthStore((s) => s.user);
const { data } = useQuery({
queryKey: ["admin-usage"],
queryFn: async () => {
const { data } = await api.get<{ users: UserUsage[] }>("/admin/usage");
return data.users;
},
refetchInterval: 30000,
});
if (user?.role !== "admin") return <Navigate to="/" replace />;
const users = data || [];
return (
<div className="space-y-4">
<h1 className="text-2xl font-semibold">{t("admin_usage.title")}</h1>
<div className="rounded-lg border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="px-4 py-3 text-left font-medium">{t("admin_usage.user")}</th>
<th className="px-4 py-3 text-right font-medium">{t("admin_usage.messages")}</th>
<th className="px-4 py-3 text-right font-medium">{t("admin_usage.tokens")}</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.user_id} className="border-b last:border-0">
<td className="px-4 py-3">{u.username}</td>
<td className="px-4 py-3 text-right">
<span className={cn(u.messages_today >= u.message_limit && "text-destructive font-medium")}>
{u.messages_today} / {u.message_limit}
</span>
</td>
<td className="px-4 py-3 text-right">
<span className={cn(u.tokens_today >= u.token_limit && "text-destructive font-medium")}>
{Math.round(u.tokens_today / 1000)}K / {Math.round(u.token_limit / 1000)}K
</span>
</td>
</tr>
))}
{users.length === 0 && (
<tr><td colSpan={3} className="px-4 py-8 text-center text-muted-foreground">{t("admin_usage.no_data")}</td></tr>
)}
</tbody>
</table>
</div>
</div>
);
}