diff --git a/app/api/admin/logs/route.js b/app/api/admin/logs/route.js new file mode 100644 index 0000000..5f85c55 --- /dev/null +++ b/app/api/admin/logs/route.js @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +export async function GET(req) { + const user = await requireUser(); + if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + const { searchParams } = new URL(req.url); + const res = await fetch( + `${ENGINE_URL}/api/admin/logs?${searchParams}`, + { + headers: { 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(user.id) }, + cache: 'no-store', + } + ); + return NextResponse.json(await res.json()); +} diff --git a/components/AdminPanel.js b/components/AdminPanel.js index 406827d..3c399c2 100644 --- a/components/AdminPanel.js +++ b/components/AdminPanel.js @@ -1,11 +1,12 @@ 'use client'; import { useState } from 'react'; -import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag } from 'lucide-react'; +import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag, AlertTriangle } from 'lucide-react'; import Link from 'next/link'; import AdminBilling from './admin/AdminBilling'; import AdminUsers from './admin/AdminUsers'; import AdminPromos from './admin/AdminPromos'; import AdminQueue from './admin/AdminQueue'; +import AdminLogs from './admin/AdminLogs'; // ────────────────────────────────────────────────────────────── // Sidebar navigation @@ -17,6 +18,7 @@ const SECTIONS = [ { id: 'payments', label: 'ЮKassa', icon: CreditCard, desc: 'Ключи для приёма оплат' }, { id: 'spending', label: 'Расходы AI', icon: TrendingUp, desc: 'aiprimetech + routerai' }, { id: 'queue', label: 'Очередь', icon: Zap, desc: 'Задачи генерации, ошибки' }, + { id: 'logs', label: 'Логи ошибок', icon: AlertTriangle, desc: 'Последние сбои и проблемы' }, { id: 'plans', label: 'Тарифы', icon: BarChart3, desc: 'Планы, кредиты, операции' }, { id: 'promos', label: 'Промокоды', icon: Tag, desc: 'Коды для кредитов и скидок' }, { id: 'billing', label: 'Пользователи', icon: Users, desc: 'Балансы и кредиты' }, @@ -68,6 +70,7 @@ export default function AdminPanel({ initialSection = 'settings' }) { {section === 'payments' && } {section === 'spending' && } {section === 'queue' && } + {section === 'logs' && } {section === 'plans' && } {section === 'promos' && } {section === 'billing' && } diff --git a/components/admin/AdminLogs.js b/components/admin/AdminLogs.js new file mode 100644 index 0000000..2eafe22 --- /dev/null +++ b/components/admin/AdminLogs.js @@ -0,0 +1,223 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { RefreshCw, Loader2, AlertTriangle, Cpu, Send, ChevronDown, ChevronUp } from 'lucide-react'; + +const SOURCE_CONFIG = { + generation: { icon: '⚙️', label: 'Генерация', color: 'text-purple-400', bg: 'bg-purple-500/10' }, + ai_provider:{ icon: '🤖', label: 'AI провайдер', color: 'text-blue-400', bg: 'bg-blue-500/10' }, + publish: { icon: '📤', label: 'Публикация', color: 'text-orange-400', bg: 'bg-orange-500/10' }, +}; + +// Категоризируем ошибку по тексту +function classifyError(msg) { + if (!msg) return { type: 'unknown', label: 'Неизвестно', color: 'text-gray-400' }; + const m = msg.toLowerCase(); + if (m.includes('timeout')) return { type: 'timeout', label: 'Таймаут', color: 'text-yellow-400' }; + if (m.includes('rate limit')) return { type: 'ratelimit',label: 'Rate limit', color: 'text-orange-400' }; + if (m.includes('not supported')) return { type: 'model', label: 'Модель', color: 'text-red-400' }; + if (m.includes('empty response')) return { type: 'empty', label: 'Пустой ответ', color: 'text-red-400' }; + if (m.includes('network') || m.includes('connect')) return { type: 'network', label: 'Сеть', color: 'text-orange-400' }; + if (m.includes('auth') || m.includes('key') || m.includes('401')) return { type: 'auth', label: 'Авторизация', color: 'text-red-400' }; + return { type: 'other', label: 'Другое', color: 'text-gray-400' }; +} + +function timeAgo(s) { + const diff = Date.now() - new Date(s); + if (diff < 60000) return Math.floor(diff / 1000) + 'с назад'; + if (diff < 3600000) return Math.floor(diff / 60000) + 'м назад'; + if (diff < 86400000)return Math.floor(diff / 3600000) + 'ч назад'; + return new Date(s).toLocaleString('ru-RU', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' }); +} + +export default function AdminLogs() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState('all'); // all | generation | ai_provider | publish + const [expanded, setExpanded] = useState(null); + + async function load() { + setLoading(true); + try { + const res = await fetch('/api/admin/logs?limit=100').then(r => r.json()); + setData(res); + } catch {} + setLoading(false); + } + + useEffect(() => { load(); }, []); + + const errors = (data?.errors || []).filter(e => + filter === 'all' || e.source === filter + ); + + const counts = (data?.errors || []).reduce((acc, e) => { + acc[e.source] = (acc[e.source] || 0) + 1; + return acc; + }, {}); + + return ( +
+
+

Логи ошибок

+ +
+ + {loading && !data && ( +
+ )} + + {data && (<> + {/* Топ ошибок */} + {data.topErrors?.length > 0 && ( +
+

Частые ошибки

+
+ {data.topErrors.map((e, i) => { + const cls = classifyError(e.msg); + const pct = Math.round((e.cnt / data.total) * 100); + return ( +
+
{e.cnt}×
+
+
{e.msg}
+
+
+
+
+ {cls.label} +
+
+
+ ); + })} +
+
+ )} + + {/* Статистика + фильтр */} +
+ + {Object.entries(SOURCE_CONFIG).map(([k, cfg]) => ( + + ))} +
+ + {/* Список */} + {errors.length === 0 && ( +
+ +
Ошибок не найдено 🎉
+
+ )} + +
+ {errors.map((err, i) => { + const src = SOURCE_CONFIG[err.source] || SOURCE_CONFIG.generation; + const cls = classifyError(err.message); + const isOpen = expanded === i; + const shortMsg = err.message?.split('\n')[0]?.slice(0, 100) || 'Unknown error'; + + return ( +
+ + + {isOpen && ( +
+
+
+ ID: + {err.entity_id} +
+
+ Источник: + {err.source} +
+
+ Операция: + {err.operation} +
+ {err.context && ( +
+ Контекст: + {err.context} +
+ )} +
+ Ошибка: + {err.message} +
+
+ Время: + + {new Date(err.created_at).toLocaleString('ru-RU')} + +
+
+ Тип ошибки: + {cls.label} +
+
+ + {/* Действие для ошибок модели */} + {cls.type === 'model' && err.source === 'ai_provider' && ( +
+ 💡 Проверь настройку AI_IMAGE_MODEL_VIA_RESPONSES в{' '} + Настройках API +
+ )} + {cls.type === 'timeout' && ( +
+ 💡 Таймаут {err.operation?.includes('chat') ? 'текстовой генерации' : 'изображений'} — возможны проблемы у провайдера +
+ )} +
+ )} +
+ ); + })} +
+ )} +
+ ); +}