diff --git a/app/admin/(protected)/settings/SettingsForm.js b/app/admin/(protected)/settings/SettingsForm.js index 683c4be..7f6a2d7 100644 --- a/app/admin/(protected)/settings/SettingsForm.js +++ b/app/admin/(protected)/settings/SettingsForm.js @@ -1,155 +1,235 @@ 'use client'; -import { useState } from 'react'; -import { Check, Eye, EyeOff, Loader2, Save, AlertCircle } from 'lucide-react'; +import { useState, useMemo } from 'react'; +import { Save, Eye, EyeOff } from 'lucide-react'; -const CATEGORY_LABELS = { - telegram: { title: 'Telegram', hint: 'Прокси к Bot API. Нужен на серверах в РФ.' }, - engine: { title: 'Engine', hint: 'Системные настройки engine (Telegram-прокси и т.п.).' }, - general: { title: 'Общие', hint: '' }, +// ── Категории и человекочитаемые названия ────────────────────────────────── +const CATEGORIES = [ + { + id: 'general', + label: 'Основные', + keys: ['APP_PUBLIC_URL', 'ENGINE_PUBLIC_URL', 'MAINTENANCE_MODE', 'MAINTENANCE_MESSAGE'], + }, + { + id: 'ai-text', + label: 'AI — Текст', + keys: ['AI_TEXT_BASE_URL', 'AI_TEXT_API_KEY', 'AI_TEXT_MODEL_ARTICLE', 'AI_TEXT_MODEL_POST', 'AI_TEXT_MODEL_TOPICS', 'AI_USD_RUB_RATE', 'AI_PROVIDER_MARKUP'], + }, + { + id: 'ai-image', + label: 'AI — Картинки', + keys: ['ROUTERAI_BASE_URL', 'ROUTERAI_API_KEY', 'ROUTERAI_IMAGE_MODEL', 'AI_IMAGE_BASE_URL', 'AI_IMAGE_API_KEY', 'AI_IMAGE_MODEL', 'AI_IMAGE_MODEL_VIA_RESPONSES', 'AI_IMAGE_FALLBACK_BASE_URL', 'AI_IMAGE_FALLBACK_API_KEY'], + }, + { + id: 'telegram', + label: 'Telegram', + keys: ['TELEGRAM_API_BASE'], + }, + { + id: 'photo-search', + label: 'Поиск фото', + keys: ['PHOTO_SEARCH_PROVIDER', 'YANDEX_SEARCH_API_KEY', 'YANDEX_SEARCH_FOLDER_ID', 'YANDEX_SEARCH_DAILY_LIMIT'], + }, + { + id: 'defaults', + label: 'Каналы (умолчания)', + keys: ['DEFAULT_POST_LANGUAGE', 'DEFAULT_POST_LENGTH', 'DEFAULT_POST_STYLE', 'DEFAULT_POST_GOAL', 'DEFAULT_IMAGE_ENABLED', 'DEFAULT_EMOJI_ENABLED', 'DEFAULT_HASHTAGS_IN_POST', 'DEFAULT_AI_STYLE_PROMPT', 'DEFAULT_AUTO_DRAFT_COUNT', 'DEFAULT_AUTO_DRAFT_TIME', 'AUTO_DRAFT_DEFAULT_COUNT', 'AUTO_DRAFT_DEFAULT_TIME'], + }, + { + id: 'smtp', + label: 'Email (SMTP)', + keys: ['SMTP_ENABLED', 'SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM'], + }, + { + id: 'payments', + label: 'ЮKassa', + keys: ['YUKASSA_SHOP_ID', 'YUKASSA_SECRET', 'YUKASSA_RETURN_URL'], + }, +]; + +const KEY_LABELS = { + APP_PUBLIC_URL: 'URL приложения', + ENGINE_PUBLIC_URL: 'URL движка', + MAINTENANCE_MODE: 'Режим обслуживания', + MAINTENANCE_MESSAGE: 'Сообщение при обслуживании', + AI_TEXT_BASE_URL: 'Base URL провайдера', + AI_TEXT_API_KEY: 'API ключ', + AI_TEXT_MODEL_ARTICLE: 'Модель для статей', + AI_TEXT_MODEL_POST: 'Модель для постов', + AI_TEXT_MODEL_TOPICS: 'Модель для тем', + AI_USD_RUB_RATE: 'Курс USD → RUB', + AI_PROVIDER_MARKUP: 'Коэффициент наценки', + ROUTERAI_BASE_URL: 'RouterAI Base URL', + ROUTERAI_API_KEY: 'RouterAI API ключ', + ROUTERAI_IMAGE_MODEL: 'Модель картинок', + AI_IMAGE_BASE_URL: 'Fallback Base URL', + AI_IMAGE_API_KEY: 'Fallback API ключ', + AI_IMAGE_MODEL: 'Fallback модель', + AI_IMAGE_MODEL_VIA_RESPONSES: 'Fallback модель (responses)', + AI_IMAGE_FALLBACK_BASE_URL: 'Fallback Base URL 2', + AI_IMAGE_FALLBACK_API_KEY: 'Fallback API ключ 2', + TELEGRAM_API_BASE: 'Telegram Bot API Base URL', + PHOTO_SEARCH_PROVIDER: 'Провайдер поиска фото', + YANDEX_SEARCH_API_KEY: 'Yandex API ключ', + YANDEX_SEARCH_FOLDER_ID: 'Yandex Folder ID', + YANDEX_SEARCH_DAILY_LIMIT: 'Дневной лимит запросов', + DEFAULT_POST_LANGUAGE: 'Язык постов', + DEFAULT_POST_LENGTH: 'Длина поста', + DEFAULT_POST_STYLE: 'Стиль', + DEFAULT_POST_GOAL: 'Цель поста', + DEFAULT_IMAGE_ENABLED: 'Генерировать картинку', + DEFAULT_EMOJI_ENABLED: 'Использовать эмодзи', + DEFAULT_HASHTAGS_IN_POST: 'Добавлять хештеги', + DEFAULT_AI_STYLE_PROMPT: 'Базовый стиль (промпт)', + DEFAULT_AUTO_DRAFT_COUNT: 'Черновиков в день', + DEFAULT_AUTO_DRAFT_TIME: 'Время генерации черновиков', + AUTO_DRAFT_DEFAULT_COUNT: 'Черновиков по умолчанию', + AUTO_DRAFT_DEFAULT_TIME: 'Время по умолчанию', + SMTP_ENABLED: 'Email включён', + SMTP_HOST: 'SMTP сервер', + SMTP_PORT: 'SMTP порт', + SMTP_USER: 'SMTP логин', + SMTP_PASS: 'SMTP пароль', + SMTP_FROM: 'Email отправителя', + YUKASSA_SHOP_ID: 'ID магазина', + YUKASSA_SECRET: 'Секретный ключ', + YUKASSA_RETURN_URL: 'URL возврата', }; -// Категории, которые здесь НЕ показываются: они управляются из app.zeropost.ru/system. -const HIDDEN_CATEGORIES = new Set([ - 'photo_search', +const SECRET_KEYS = new Set([ + 'AI_TEXT_API_KEY', 'AI_IMAGE_API_KEY', 'AI_IMAGE_FALLBACK_API_KEY', + 'ROUTERAI_API_KEY', 'YUKASSA_SECRET', 'SMTP_PASS', 'YANDEX_SEARCH_API_KEY', ]); -export default function SettingsForm({ initial }) { - const filtered = (initial || []).filter(s => !HIDDEN_CATEGORIES.has(s.category)); - // Группируем настройки по категориям - const byCategory = filtered.reduce((acc, s) => { - (acc[s.category] ||= []).push(s); - return acc; - }, {}); - const order = ['engine', 'telegram', 'general']; - const categories = [...order, ...Object.keys(byCategory).filter(c => !order.includes(c))]; - - return ( -
- {categories.filter(c => byCategory[c]).map(cat => { - const info = CATEGORY_LABELS[cat] || { title: cat, hint: '' }; - return ( -
-
-

{info.title}

- {info.hint &&

{info.hint}

} -
-
- {byCategory[cat].map(s => )} -
-
- ); - })} -

- Системные настройки внешних сервисов (поиск фото и т.п.) управляются из{' '} - - app.zeropost.ru/system - . -

-
- ); -} - -function SettingRow({ setting }) { - const [value, setValue] = useState(setting.value ?? ''); - const [saving, setSaving] = useState(false); - const [saved, setSaved] = useState(false); - const [error, setError] = useState(null); - const [reveal, setReveal] = useState(false); - - const original = setting.value ?? ''; - const dirty = value !== original; +function SettingRow({ setting, engineUrl }) { + const [val, setVal] = useState(setting.value || ''); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + const [error, setError] = useState(''); + const [showSecret, setShowSecret] = useState(false); + const isSecret = SECRET_KEYS.has(setting.key); + const label = KEY_LABELS[setting.key] || setting.key; async function save() { - setSaving(true); - setError(null); - setSaved(false); + setSaving(true); setSaved(false); setError(''); try { - const r = await fetch(`/admin/api/settings/${encodeURIComponent(setting.key)}`, { + const r = await fetch(`${engineUrl}/api/settings/${setting.key}`, { method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ value }), + headers: { 'Content-Type': 'application/json', 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' }, + body: JSON.stringify({ value: val }), }); - if (!r.ok) { - const d = await r.json().catch(() => ({})); - throw new Error(d.error || `HTTP ${r.status}`); - } - setSaved(true); - setTimeout(() => setSaved(false), 2000); - // Обновляем "оригинал" чтобы dirty стал false - setting.value = value; - } catch (e) { - setError(e.message); - } finally { - setSaving(false); - } + const d = await r.json(); + if (d.ok || d.key) { setSaved(true); setTimeout(() => setSaved(false), 2000); } + else setError(d.error || 'Ошибка'); + } catch { setError('Сеть'); } + setSaving(false); } - const isSecret = setting.is_secret; - const inputType = isSecret && !reveal ? 'password' : 'text'; - const isLong = (value?.length ?? 0) > 80; - return ( -
-
- - {setting.key} - - {isSecret && ( - - secret - - )} -
- {setting.description && ( -

{setting.description}

- )} -
- {isLong ? ( -