diff --git a/app/admin/(protected)/drafts/page.js b/app/admin/(protected)/drafts/page.js
new file mode 100644
index 0000000..27fab42
--- /dev/null
+++ b/app/admin/(protected)/drafts/page.js
@@ -0,0 +1,337 @@
+'use client';
+import { useState, useEffect, useCallback } from 'react';
+import { CheckCircle, RefreshCw, Pencil, Clock, ImageIcon, ChevronDown, ChevronUp, X, Save } from 'lucide-react';
+
+export const metadata = { title: 'Черновики' };
+
+const COVER_STYLES = [
+ { id: 'tech-photo', name: 'Tech Photo — реальное железо' },
+ { id: '3d-device', name: '3D Device — рендер устройства' },
+ { id: 'code-screen', name: 'Code Screen — экран с кодом' },
+ { id: 'data-flow', name: 'Data Flow — абстракция данных' },
+ { id: 'ai-neural', name: 'AI Neural — нейросетевая эстетика' },
+ { id: 'cinematic-tech', name: 'Cinematic — кинематографичный' },
+];
+
+const CATEGORY_LABELS = {
+ 'ai-tools': '🤖 AI Tools',
+ 'cybersec': '🔒 Cybersec',
+ 'automation': '⚡ Automation',
+ 'ai-dev': '💻 AI Dev',
+};
+
+function DraftCard({ draft, onApproved, onCoverRegenerated }) {
+ const [loading, setLoading] = useState(false);
+ const [regenLoading, setRegenLoading] = useState(false);
+ const [expanded, setExpanded] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const [editTitle, setEditTitle] = useState(draft.title);
+ const [editExcerpt, setEditExcerpt] = useState(draft.excerpt || '');
+ const [saveLoading, setSaveLoading] = useState(false);
+ const [selectedStyle, setSelectedStyle] = useState('');
+ const [coverUrl, setCoverUrl] = useState(draft.cover_url);
+ const [msg, setMsg] = useState('');
+
+ const engineUrl = process.env.NEXT_PUBLIC_ENGINE_URL || '';
+
+ async function approve() {
+ setLoading(true);
+ setMsg('');
+ try {
+ const r = await fetch(`${engineUrl}/api/drafts/${draft.id}/approve`, {
+ method: 'PATCH',
+ headers: { 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' },
+ });
+ const d = await r.json();
+ if (d.ok) {
+ const slot = d.scheduled_at
+ ? new Date(d.scheduled_at).toLocaleTimeString('ru', { hour: '2-digit', minute: '2-digit' })
+ : null;
+ setMsg(slot ? `✅ Одобрено, выйдет в ${slot}` : '✅ Одобрено');
+ setTimeout(() => onApproved(draft.id), 1200);
+ } else {
+ setMsg('❌ ' + (d.error || 'Ошибка'));
+ }
+ } catch { setMsg('❌ Ошибка сети'); }
+ setLoading(false);
+ }
+
+ async function regenCover() {
+ setRegenLoading(true);
+ setMsg('');
+ try {
+ const r = await fetch(`${engineUrl}/api/drafts/${draft.id}/regenerate-cover`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '',
+ },
+ body: JSON.stringify({ style: selectedStyle || undefined }),
+ });
+ const d = await r.json();
+ if (d.ok && d.cover_url) {
+ setCoverUrl(d.cover_url);
+ setMsg('🎨 Обложка обновлена');
+ onCoverRegenerated(draft.id, d.cover_url);
+ } else {
+ setMsg('❌ ' + (d.error || 'Ошибка генерации'));
+ }
+ } catch { setMsg('❌ Ошибка сети'); }
+ setRegenLoading(false);
+ }
+
+ async function saveEdit() {
+ setSaveLoading(true);
+ try {
+ const r = await fetch(`${engineUrl}/api/drafts/${draft.id}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '',
+ },
+ body: JSON.stringify({ title: editTitle, excerpt: editExcerpt }),
+ });
+ const d = await r.json();
+ if (d.ok) { setEditing(false); setMsg('💾 Сохранено'); }
+ else setMsg('❌ ' + (d.error || 'Ошибка'));
+ } catch { setMsg('❌ Ошибка сети'); }
+ setSaveLoading(false);
+ }
+
+ const coverSrc = coverUrl
+ ? (coverUrl.startsWith('http') ? coverUrl : `${engineUrl}${coverUrl}`)
+ : null;
+
+ return (
+
+ {/* Обложка + основная инфо */}
+
+
+ {coverSrc
+ ?

+ :
+ }
+
+
+
+
+ {CATEGORY_LABELS[draft.category] || draft.category}
+
+
+ {draft.reading_time} мин
+
+
+ {editing ? (
+
setEditTitle(e.target.value)}
+ className="w-full font-semibold text-sm border border-neutral-300 dark:border-neutral-700 rounded px-2 py-1 bg-transparent mb-1"
+ />
+ ) : (
+
+ {editTitle}
+
+ )}
+
+
+
+ {/* Анонс */}
+
+
+ {/* Сообщение */}
+ {msg && (
+
{msg}
+ )}
+
+ {/* Перегенерация обложки — разворачиваемый блок */}
+
+
+
+ {expanded && (
+
+
+
+
+ )}
+
+
+ {/* Кнопки действий */}
+
+ {editing ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
+
+export default function AdminDraftsPage() {
+ const [drafts, setDrafts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [approveAllLoading, setApproveAllLoading] = useState(false);
+ const [msg, setMsg] = useState('');
+
+ const engineUrl = typeof window !== 'undefined'
+ ? (process.env.NEXT_PUBLIC_ENGINE_URL || '')
+ : '';
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const r = await fetch(`${engineUrl}/api/drafts`, {
+ headers: { 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' },
+ cache: 'no-store',
+ });
+ const d = await r.json();
+ setDrafts(Array.isArray(d) ? d : []);
+ } catch { setDrafts([]); }
+ setLoading(false);
+ }, [engineUrl]);
+
+ useEffect(() => { load(); }, [load]);
+
+ function handleApproved(id) {
+ setDrafts(prev => prev.filter(d => d.id !== id));
+ }
+
+ function handleCoverRegenerated(id, coverUrl) {
+ setDrafts(prev => prev.map(d => d.id === id ? { ...d, cover_url: coverUrl } : d));
+ }
+
+ async function approveAll() {
+ setApproveAllLoading(true);
+ setMsg('');
+ try {
+ const r = await fetch(`${engineUrl}/api/drafts/approve-all`, {
+ method: 'POST',
+ headers: { 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' },
+ });
+ const d = await r.json();
+ if (d.ok) { setMsg('✅ Все черновики одобрены'); load(); }
+ else setMsg('❌ ' + (d.error || 'Ошибка'));
+ } catch { setMsg('❌ Ошибка сети'); }
+ setApproveAllLoading(false);
+ }
+
+ return (
+
+
+
+
Черновики
+
+ Статьи ждут проверки. Если не одобришь вручную — автоматически опубликуются в 07:00 МСК.
+
+
+
+
+ {drafts.length > 0 && (
+
+ )}
+
+
+
+ {msg && (
+
+ {msg}
+
+ )}
+
+ {loading ? (
+
Загружаю черновики...
+ ) : drafts.length === 0 ? (
+
+
+
Черновиков нет
+
Следующие появятся после autogen
+
+ ) : (
+
+ {drafts.map(draft => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/admin/AdminNav.js b/components/admin/AdminNav.js
index 0af0594..7fcbae4 100644
--- a/components/admin/AdminNav.js
+++ b/components/admin/AdminNav.js
@@ -1,11 +1,12 @@
'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
-import { LayoutDashboard, FileText, Radio, Zap, Settings, LogOut, ExternalLink, MessageCircle } from 'lucide-react';
+import { LayoutDashboard, FileText, Radio, Zap, Settings, LogOut, ExternalLink, MessageCircle, Clock } from 'lucide-react';
const NAV = [
{ href: '/admin', label: 'Дашборд', icon: LayoutDashboard, exact: true },
{ href: '/admin/articles', label: 'Статьи', icon: FileText },
+ { href: '/admin/drafts', label: 'Черновики', icon: Clock },
{ href: '/admin/channels', label: 'Каналы', icon: Radio },
{ href: '/admin/autogen', label: 'Автогенерация', icon: Zap },
{ href: '/admin/notes', label: 'Заметки', icon: MessageCircle },