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 (
+
+ );
+ })}
+
+
+ )}
+
+ {/* Статистика + фильтр */}
+
+
+ {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') ? 'текстовой генерации' : 'изображений'} — возможны проблемы у провайдера
+
+ )}
+
+ )}
+
+ );
+ })}
+
+ >)}
+
+ );
+}