diff --git a/app/admin/(protected)/autogen/page.js b/app/admin/(protected)/autogen/page.js
new file mode 100644
index 0000000..7803d18
--- /dev/null
+++ b/app/admin/(protected)/autogen/page.js
@@ -0,0 +1,28 @@
+import { requireAdminAuth } from '@/lib/adminAuth';
+import AutogenPanel from '@/components/admin/AutogenPanel';
+
+export const dynamic = 'force-dynamic';
+export const metadata = { title: 'Автогенерация' };
+
+const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
+const ENGINE_SECRET = process.env.ENGINE_SECRET || 'zeropost_internal_2026';
+
+async function engineCall(path) {
+ const res = await fetch(`${ENGINE_URL}${path}`, {
+ headers: { 'x-internal-secret': ENGINE_SECRET },
+ cache: 'no-store',
+ });
+ if (!res.ok) return null;
+ return res.json();
+}
+
+export default async function AutogenPage() {
+ await requireAdminAuth();
+ const [status, queue, topics] = await Promise.all([
+ engineCall('/api/autogen/status'),
+ engineCall('/api/autogen/queue'),
+ engineCall('/api/autogen/topics'),
+ ]);
+
+ return ;
+}
diff --git a/app/admin/api/autogen/queue/[id]/route.js b/app/admin/api/autogen/queue/[id]/route.js
new file mode 100644
index 0000000..5721a3c
--- /dev/null
+++ b/app/admin/api/autogen/queue/[id]/route.js
@@ -0,0 +1,10 @@
+import { NextResponse } from 'next/server';
+import { checkAdminAuth } from '@/lib/adminAuth';
+const E = process.env.ENGINE_URL||'http://127.0.0.1:3030';
+const S = process.env.ENGINE_SECRET||'zeropost_internal_2026';
+export async function DELETE(req, { params }) {
+ if (!(await checkAdminAuth())) return NextResponse.json({error:'Unauthorized'},{status:401});
+ const { id } = await params;
+ await fetch(`${E}/api/autogen/queue/${id}`,{method:'DELETE',headers:{'x-internal-secret':S}});
+ return NextResponse.json({ok:true});
+}
diff --git a/app/admin/api/autogen/queue/route.js b/app/admin/api/autogen/queue/route.js
new file mode 100644
index 0000000..463d943
--- /dev/null
+++ b/app/admin/api/autogen/queue/route.js
@@ -0,0 +1,11 @@
+import { NextResponse } from 'next/server';
+import { checkAdminAuth } from '@/lib/adminAuth';
+const E = process.env.ENGINE_URL||'http://127.0.0.1:3030';
+const S = process.env.ENGINE_SECRET||'zeropost_internal_2026';
+const h = { 'Content-Type':'application/json','x-internal-secret':S };
+export async function POST(req) {
+ if (!(await checkAdminAuth())) return NextResponse.json({error:'Unauthorized'},{status:401});
+ const body = await req.json();
+ const res = await fetch(`${E}/api/autogen/queue`,{method:'POST',headers:h,body:JSON.stringify(body)});
+ return NextResponse.json(await res.json());
+}
diff --git a/app/admin/api/autogen/run/route.js b/app/admin/api/autogen/run/route.js
new file mode 100644
index 0000000..3da75e2
--- /dev/null
+++ b/app/admin/api/autogen/run/route.js
@@ -0,0 +1,13 @@
+import { NextResponse } from 'next/server';
+import { checkAdminAuth } from '@/lib/adminAuth';
+
+const E = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
+const S = process.env.ENGINE_SECRET || 'zeropost_internal_2026';
+const h = { 'Content-Type': 'application/json', 'x-internal-secret': S };
+
+export async function POST(req) {
+ if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ const body = await req.json();
+ const res = await fetch(`${E}/api/autogen/run`, { method: 'POST', headers: h, body: JSON.stringify(body) });
+ return NextResponse.json(await res.json());
+}
diff --git a/app/admin/api/autogen/settings/route.js b/app/admin/api/autogen/settings/route.js
new file mode 100644
index 0000000..488ee80
--- /dev/null
+++ b/app/admin/api/autogen/settings/route.js
@@ -0,0 +1,11 @@
+import { NextResponse } from 'next/server';
+import { checkAdminAuth } from '@/lib/adminAuth';
+const E = process.env.ENGINE_URL||'http://127.0.0.1:3030';
+const S = process.env.ENGINE_SECRET||'zeropost_internal_2026';
+const h = { 'Content-Type':'application/json','x-internal-secret':S };
+export async function PATCH(req) {
+ if (!(await checkAdminAuth())) return NextResponse.json({error:'Unauthorized'},{status:401});
+ const { category, ...data } = await req.json();
+ const res = await fetch(`${E}/api/autogen/settings/${category}`,{method:'PATCH',headers:h,body:JSON.stringify(data)});
+ return NextResponse.json(await res.json());
+}
diff --git a/components/admin/AdminNav.js b/components/admin/AdminNav.js
index 9adf547..1189193 100644
--- a/components/admin/AdminNav.js
+++ b/components/admin/AdminNav.js
@@ -1,12 +1,13 @@
'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
-import { LayoutDashboard, FileText, Radio, LogOut, ExternalLink } from 'lucide-react';
+import { LayoutDashboard, FileText, Radio, Zap, LogOut, ExternalLink } from 'lucide-react';
const NAV = [
{ href: '/admin', label: 'Дашборд', icon: LayoutDashboard, exact: true },
{ href: '/admin/articles', label: 'Статьи', icon: FileText },
{ href: '/admin/channels', label: 'Каналы', icon: Radio },
+ { href: '/admin/autogen', label: 'Автогенерация', icon: Zap },
];
export default function AdminNav() {
diff --git a/components/admin/AutogenPanel.js b/components/admin/AutogenPanel.js
new file mode 100644
index 0000000..423e3d3
--- /dev/null
+++ b/components/admin/AutogenPanel.js
@@ -0,0 +1,309 @@
+'use client';
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { Play, Plus, Trash2, RefreshCw, Clock, CheckCircle, XCircle, Zap } from 'lucide-react';
+
+const CAT_LABELS = {
+ 'ai-tools': { name: 'ИИ-инструменты', icon: '🤖', color: 'emerald' },
+ 'cybersec': { name: 'Кибербезопасность', icon: '🔒', color: 'red' },
+ 'automation': { name: 'Автоматизация', icon: '⚡', color: 'amber' },
+ 'ai-dev': { name: 'Разработка с ИИ', icon: '💻', color: 'blue' },
+};
+
+const COLOR = {
+ emerald: 'bg-emerald-50 dark:bg-emerald-950 border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300',
+ red: 'bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800 text-red-700 dark:text-red-300',
+ amber: 'bg-amber-50 dark:bg-amber-950 border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-300',
+ blue: 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300',
+};
+
+export default function AutogenPanel({ status, queue, topics }) {
+ const router = useRouter();
+ const [running, setRunning] = useState({});
+ const [toast, setToast] = useState(null);
+ const [newTopic, setNewTopic] = useState('');
+ const [newCat, setNewCat] = useState('ai-tools');
+ const [addingTopic, setAddingTopic] = useState(false);
+ const [showAddForm, setShowAddForm] = useState(false);
+
+ function showToast(msg, type = 'success') {
+ setToast({ msg, type });
+ setTimeout(() => setToast(null), 4000);
+ }
+
+ async function runCategory(category) {
+ setRunning(r => ({ ...r, [category]: true }));
+ try {
+ const res = await fetch('/admin/api/autogen/run', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category }),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ showToast(`Генерация запущена для "${CAT_LABELS[category]?.name}". Статья появится через ~2 мин.`);
+ } catch (e) {
+ showToast(e.message, 'error');
+ } finally {
+ setTimeout(() => {
+ setRunning(r => ({ ...r, [category]: false }));
+ router.refresh();
+ }, 5000);
+ }
+ }
+
+ async function runAll() {
+ setRunning({ all: true });
+ try {
+ const res = await fetch('/admin/api/autogen/run', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({}),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ showToast('Запущена генерация для всех активных категорий');
+ } catch (e) {
+ showToast(e.message, 'error');
+ } finally {
+ setTimeout(() => { setRunning({}); router.refresh(); }, 8000);
+ }
+ }
+
+ async function toggleCategory(category, enabled) {
+ await fetch('/admin/api/autogen/settings', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category, enabled }),
+ });
+ router.refresh();
+ }
+
+ async function updatePerDay(category, per_day) {
+ await fetch('/admin/api/autogen/settings', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category, per_day: parseInt(per_day) }),
+ });
+ router.refresh();
+ }
+
+ async function addTopic() {
+ if (!newTopic.trim()) return;
+ setAddingTopic(true);
+ try {
+ const res = await fetch('/admin/api/autogen/queue', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category: newCat, topic: newTopic.trim(), priority: 8 }),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ showToast('Тема добавлена в очередь');
+ setNewTopic('');
+ setShowAddForm(false);
+ router.refresh();
+ } catch (e) {
+ showToast(e.message, 'error');
+ } finally {
+ setAddingTopic(false);
+ }
+ }
+
+ async function removeTopic(id) {
+ await fetch(`/admin/api/autogen/queue/${id}`, { method: 'DELETE' });
+ router.refresh();
+ }
+
+ const pendingQueue = queue.filter(q => q.status === 'pending');
+ const doneQueue = queue.filter(q => q.status === 'done').slice(0, 5);
+
+ return (
+
+ {/* Toast */}
+ {toast && (
+
+ {toast.msg}
+
+ )}
+
+ {/* Шапка */}
+
+
+
Автогенерация
+
Автоматическое создание статей по расписанию
+
+
+
+
+ {/* Категории */}
+
+ {status.map(s => {
+ const cat = CAT_LABELS[s.category] || { name: s.category, icon: '📝', color: 'emerald' };
+ const colorCls = COLOR[cat.color] || COLOR.emerald;
+ const isRunning = running[s.category] || running.all;
+ return (
+
+
+
+
{cat.icon}
+
+
{cat.name}
+
{s.article_count || 0} статей · {s.queue_count || 0} в очереди
+
+
+ {/* Вкл/выкл */}
+
+
+
+ {/* Расписание */}
+
+
+ Статей в день:
+
+ {s.next_run_at && (
+
+ след.: {new Date(s.next_run_at).toLocaleString('ru-RU', { day:'numeric', month:'short', hour:'2-digit', minute:'2-digit' })}
+
+ )}
+
+
+ {/* Кнопка запуска */}
+
+
+ );
+ })}
+
+
+ {/* Очередь тем */}
+
+
+
+ Очередь тем ({pendingQueue.length})
+
+
+
+
+ {/* Форма добавления */}
+ {showAddForm && (
+
+
+
+ setNewTopic(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && addTopic()}
+ placeholder="Тема статьи..."
+ className="flex-1 px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500"
+ autoFocus
+ />
+
+
+
+ )}
+
+ {/* Список очереди */}
+
+ {pendingQueue.length === 0 && !showAddForm && (
+
+ Очередь пуста — темы берутся из банка автоматически
+
+ )}
+ {pendingQueue.map(item => {
+ const cat = CAT_LABELS[item.category];
+ return (
+
+
{cat?.icon || '📝'}
+
+
{item.topic}
+
{cat?.name} · приоритет {item.priority}
+
+
+
+ );
+ })}
+
+
+
+ {/* Банк тем */}
+
+
+
Банк тем
+
Темы из которых AI выбирает автоматически когда очередь пуста
+
+
+ {Object.entries(topics).map(([catSlug, topicList]) => {
+ const cat = CAT_LABELS[catSlug];
+ return (
+
+
+ {cat?.icon}
+ {cat?.name}
+
+
+ {topicList.map((t, i) => (
+
+ ))}
+
+
+ );
+ })}
+
+
+
+ );
+}