import React, { useEffect, useMemo, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { Activity, Bot, CheckCircle, Clock, Database, KeyRound, MessageCircle, PlugZap, RefreshCw, Save, Server, ShieldCheck, Users, Wifi } from 'lucide-react'; import './styles.css'; const API_URL = import.meta.env.VITE_API_URL || 'https://api.stackworks.ru'; const REFRESH_MS = 15000; const emptyTelegramSettings = { botToken: '', hasBotToken: false, proxy: { enabled: false, type: 'socks5', host: '127.0.0.1', port: 9050, username: '', password: '', hasPassword: false } }; function formatDate(value) { if (!value) return '—'; const source = value instanceof Date ? value : new Date(String(value).replace(' ', 'T') + 'Z'); if (Number.isNaN(source.getTime())) return String(value); return new Intl.DateTimeFormat('ru-RU', { dateStyle: 'short', timeStyle: 'short' }).format(source); } function StatCard({ icon: Icon, label, value }) { return (

{label}

{value ?? 0}
); } function App() { const [health, setHealth] = useState(null); const [stats, setStats] = useState(null); const [messages, setMessages] = useState([]); const [telegramSettings, setTelegramSettings] = useState(emptyTelegramSettings); const [telegramBridge, setTelegramBridge] = useState(null); const [telegramDirty, setTelegramDirty] = useState(false); const [loading, setLoading] = useState(false); const [savingTelegram, setSavingTelegram] = useState(false); const [testingProxy, setTestingProxy] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [error, setError] = useState(''); const [telegramError, setTelegramError] = useState(''); const [telegramNotice, setTelegramNotice] = useState(''); const [proxyTestResult, setProxyTestResult] = useState(null); const healthOk = health?.ok === true; const statCards = useMemo(() => ([ { icon: ShieldCheck, label: 'Сайты', value: stats?.sites }, { icon: Users, label: 'Посетители', value: stats?.visitors }, { icon: MessageCircle, label: 'Диалоги', value: stats?.conversations }, { icon: Activity, label: 'Открытые', value: stats?.openConversations }, { icon: Bot, label: 'Сообщения', value: stats?.messages } ]), [stats]); function patchTelegramSettings(path, value) { setTelegramDirty(true); setTelegramSettings((current) => { if (path.startsWith('proxy.')) { const key = path.replace('proxy.', ''); return { ...current, proxy: { ...current.proxy, [key]: value } }; } return { ...current, [path]: value }; }); } async function loadTelegramSettings({ force = false } = {}) { if (telegramDirty && !force) return; setTelegramError(''); const response = await fetch(API_URL + '/api/admin/telegram/settings'); const data = await response.json(); if (!data.ok) throw new Error(data.error || 'Telegram settings load failed'); setTelegramSettings(data.settings || emptyTelegramSettings); setTelegramBridge(data.bridge || null); setTelegramDirty(false); } async function loadDashboard({ silent = false, includeTelegram = false } = {}) { if (!silent) setLoading(true); setError(''); try { const [healthResponse, statsResponse, messagesResponse] = await Promise.all([ fetch(API_URL + '/health'), fetch(API_URL + '/api/admin/stats'), fetch(API_URL + '/api/admin/messages?limit=25') ]); const [healthData, statsData, messagesData] = await Promise.all([ healthResponse.json(), statsResponse.json(), messagesResponse.json() ]); setHealth(healthData); setStats(statsData.stats || null); setMessages(Array.isArray(messagesData.messages) ? messagesData.messages : []); if (healthData.telegram) setTelegramBridge(healthData.telegram); if (statsData.telegram) setTelegramBridge(statsData.telegram); if (includeTelegram) { try { await loadTelegramSettings({ force: false }); } catch (telegramRequestError) { setTelegramError(telegramRequestError.message); } } setLastUpdate(new Date()); } catch (requestError) { setHealth({ ok: false, error: requestError.message }); setError(requestError.message); } finally { if (!silent) setLoading(false); } } async function saveTelegramSettings(event) { event.preventDefault(); setSavingTelegram(true); setTelegramNotice(''); setTelegramError(''); setProxyTestResult(null); try { const response = await fetch(API_URL + '/api/admin/telegram/settings', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ botToken: telegramSettings.botToken, proxy: { enabled: Boolean(telegramSettings.proxy.enabled), type: telegramSettings.proxy.type || 'socks5', host: telegramSettings.proxy.host || '', port: Number(telegramSettings.proxy.port || 9050), username: telegramSettings.proxy.username || '', password: telegramSettings.proxy.password || '' } }) }); const data = await response.json(); if (!data.ok) throw new Error(data.error || 'Save failed'); setTelegramSettings(data.settings || emptyTelegramSettings); setTelegramBridge(data.bridge || telegramBridge); setTelegramDirty(false); setTelegramNotice(data.message || 'Настройки Telegram/SOCKS5 сохранены.'); } catch (saveError) { setTelegramNotice(`Ошибка сохранения: ${saveError.message}`); } finally { setSavingTelegram(false); } } async function testProxy() { setTestingProxy(true); setTelegramNotice(''); setTelegramError(''); try { const response = await fetch(API_URL + '/api/admin/telegram/test-proxy', { method: 'POST' }); const data = await response.json(); setProxyTestResult(data); if (data.settings && !telegramDirty) setTelegramSettings(data.settings); if (data.bridge) setTelegramBridge(data.bridge); } catch (testError) { setProxyTestResult({ ok: false, status: 'error', message: testError.message }); } finally { setTestingProxy(false); } } async function restartBridge() { setTestingProxy(true); setTelegramNotice(''); setTelegramError(''); try { const response = await fetch(API_URL + '/api/admin/telegram/restart', { method: 'POST' }); const data = await response.json(); if (!data.ok) throw new Error(data.error || 'Bridge restart failed'); setTelegramBridge(data.bridge || null); setTelegramNotice(data.bridge?.running ? 'Telegram bridge перезапущен.' : `Bridge не запущен: ${data.bridge?.error || 'unknown error'}`); await loadDashboard({ silent: true, includeTelegram: true }); } catch (restartError) { setTelegramError(restartError.message); } finally { setTestingProxy(false); } } async function resetTelegramForm() { setTelegramDirty(false); setTelegramNotice(''); setTelegramError(''); setProxyTestResult(null); await loadTelegramSettings({ force: true }); } useEffect(() => { loadDashboard({ includeTelegram: true }); const timer = setInterval(() => loadDashboard({ silent: true, includeTelegram: false }), REFRESH_MS); return () => clearInterval(timer); }, []); return (

WSChat · operator console

Панель управления

Сообщения с сайта, статистика, Telegram-операторы и настройки прокси в одном месте.

API: {healthOk ? 'работает' : 'ошибка'} SOCKS5: {telegramSettings.proxy.enabled ? 'включен' : 'выключен'} bridge: {telegramBridge?.running ? 'online' : 'offline'} {telegramDirty ? ( есть несохранённые изменения ) : null} {lastUpdate ? `обновлено ${formatDate(lastUpdate)}` : 'ожидает данных'}
{error ?
Ошибка API: {error}
: null} {telegramError ?
Ошибка Telegram settings API: {telegramError}
: null}
{statCards.map((card) => )}

Telegram bridge

Настройки бота и SOCKS5

token {telegramSettings.hasBotToken ? 'есть' : 'не задан'}
API: {API_URL} DB: {health?.dbPath || 'неизвестно'} Bot: {telegramBridge?.username || 'неизвестно'} Error: {telegramBridge?.error || 'нет'}
{telegramDirty ? Сначала сохраните изменения, потом проверяйте прокси или перезапускайте bridge. : null}
{telegramNotice ?
{telegramNotice}
: null} {proxyTestResult ? (
{proxyTestResult.message || proxyTestResult.status}
) : null}

Inbox

Последние сообщения

{messages.length}
{messages.length === 0 ? (

Сообщений пока нет. После отправки из виджета они появятся здесь.

) : (
{messages.map((message) => (
{message.visitor_key || message.direction} {formatDate(message.created_at)}

{message.body}

{message.site_domain || message.site_name || message.site_id} {message.conversation_id}
))}
)}
); } createRoot(document.getElementById('root')).render();