From d2a61a91fe16795070ccfa0763fd8428651d15cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Fri, 8 May 2026 21:18:18 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=81=D0=B0=D0=B9=D1=82=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin-panel/src/main.jsx | 415 ++++++++++++++++++++------------------- 1 file changed, 209 insertions(+), 206 deletions(-) diff --git a/admin-panel/src/main.jsx b/admin-panel/src/main.jsx index 7871876..27f4af3 100644 --- a/admin-panel/src/main.jsx +++ b/admin-panel/src/main.jsx @@ -5,8 +5,11 @@ import { Bot, CheckCircle, Clock, + Copy, Database, + Globe, KeyRound, + Link2, MessageCircle, PlugZap, RefreshCw, @@ -19,6 +22,8 @@ import { import './styles.css'; const API_URL = import.meta.env.VITE_API_URL || 'https://api.stackworks.ru'; +const WIDGET_URL = import.meta.env.VITE_WIDGET_URL || 'https://widget.stackworks.ru/widget.js'; +const WS_URL = import.meta.env.VITE_WS_URL || 'wss://api.stackworks.ru'; const REFRESH_MS = 15000; const emptyTelegramSettings = { @@ -39,10 +44,11 @@ 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); + return new Intl.DateTimeFormat('ru-RU', { dateStyle: 'short', timeStyle: 'short' }).format(source); +} + +function makeEmbedCode(widgetKey) { + return ``; } function StatCard({ icon: Icon, label, value }) { @@ -61,16 +67,21 @@ function App() { const [health, setHealth] = useState(null); const [stats, setStats] = useState(null); const [messages, setMessages] = useState([]); + const [sites, setSites] = useState([]); + const [operators, setOperators] = useState([]); + const [newSite, setNewSite] = useState({ name: '', domain: '' }); 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 [savingSite, setSavingSite] = 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 [adminNotice, setAdminNotice] = useState(''); const [proxyTestResult, setProxyTestResult] = useState(null); const healthOk = health?.ok === true; @@ -88,49 +99,52 @@ function App() { setTelegramSettings((current) => { if (path.startsWith('proxy.')) { const key = path.replace('proxy.', ''); - return { - ...current, - proxy: { - ...current.proxy, - [key]: value - } - }; + return { ...current, proxy: { ...current.proxy, [key]: value } }; } - - return { - ...current, - [path]: value - }; + return { ...current, [path]: value }; }); } + async function requestJson(path, options = {}) { + const response = await fetch(API_URL + path, { + ...options, + headers: { + 'content-type': 'application/json', + ...(options.headers || {}) + } + }); + const data = await response.json(); + if (!data.ok) throw new Error(data.error || `Request failed: ${path}`); + return data; + } + 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'); + const data = await requestJson('/api/admin/telegram/settings'); setTelegramSettings(data.settings || emptyTelegramSettings); setTelegramBridge(data.bridge || null); setTelegramDirty(false); } + async function loadSitesAndOperators() { + const [sitesData, operatorsData] = await Promise.all([ + requestJson('/api/admin/sites'), + requestJson('/api/admin/operators') + ]); + setSites(Array.isArray(sitesData.sites) ? sitesData.sites : []); + setOperators(Array.isArray(operatorsData.operators) ? operatorsData.operators : []); + } + 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() + fetch(API_URL + '/health').then((r) => r.json()), + requestJson('/api/admin/stats'), + requestJson('/api/admin/messages?limit=25') ]); setHealth(healthData); @@ -139,12 +153,11 @@ function App() { if (healthData.telegram) setTelegramBridge(healthData.telegram); if (statsData.telegram) setTelegramBridge(statsData.telegram); + await loadSitesAndOperators(); + if (includeTelegram) { - try { - await loadTelegramSettings({ force: false }); - } catch (telegramRequestError) { - setTelegramError(telegramRequestError.message); - } + try { await loadTelegramSettings({ force: false }); } + catch (telegramRequestError) { setTelegramError(telegramRequestError.message); } } setLastUpdate(new Date()); @@ -156,6 +169,59 @@ function App() { } } + async function createSite(event) { + event.preventDefault(); + setSavingSite(true); + setAdminNotice(''); + + try { + await requestJson('/api/admin/sites', { + method: 'POST', + body: JSON.stringify(newSite) + }); + setNewSite({ name: '', domain: '' }); + setAdminNotice('Сайт добавлен. Embed-код появился в списке ниже.'); + await loadDashboard({ silent: true }); + } catch (siteError) { + setAdminNotice(`Ошибка добавления сайта: ${siteError.message}`); + } finally { + setSavingSite(false); + } + } + + async function bindOperator(siteId, operatorId) { + setAdminNotice(''); + try { + await requestJson('/api/admin/site-operators', { + method: 'POST', + body: JSON.stringify({ siteId, operatorId }) + }); + setAdminNotice('Оператор привязан к сайту.'); + await loadSitesAndOperators(); + } catch (bindError) { + setAdminNotice(`Ошибка привязки: ${bindError.message}`); + } + } + + async function unbindOperator(siteId, operatorId) { + setAdminNotice(''); + try { + await requestJson('/api/admin/site-operators', { + method: 'DELETE', + body: JSON.stringify({ siteId, operatorId }) + }); + setAdminNotice('Оператор отвязан от сайта.'); + await loadSitesAndOperators(); + } catch (bindError) { + setAdminNotice(`Ошибка отвязки: ${bindError.message}`); + } + } + + async function copyText(text) { + await navigator.clipboard.writeText(text); + setAdminNotice('Embed-код скопирован.'); + } + async function saveTelegramSettings(event) { event.preventDefault(); setSavingTelegram(true); @@ -164,9 +230,8 @@ function App() { setProxyTestResult(null); try { - const response = await fetch(API_URL + '/api/admin/telegram/settings', { + const data = await requestJson('/api/admin/telegram/settings', { method: 'POST', - headers: { 'content-type': 'application/json' }, body: JSON.stringify({ botToken: telegramSettings.botToken, proxy: { @@ -179,9 +244,6 @@ function App() { } }) }); - - 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); @@ -197,12 +259,8 @@ function App() { setTestingProxy(true); setTelegramNotice(''); setTelegramError(''); - try { - const response = await fetch(API_URL + '/api/admin/telegram/test-proxy', { - method: 'POST' - }); - const data = await response.json(); + const data = await requestJson('/api/admin/telegram/test-proxy', { method: 'POST' }); setProxyTestResult(data); if (data.settings && !telegramDirty) setTelegramSettings(data.settings); if (data.bridge) setTelegramBridge(data.bridge); @@ -217,13 +275,8 @@ function App() { 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'); + const data = await requestJson('/api/admin/telegram/restart', { method: 'POST' }); setTelegramBridge(data.bridge || null); setTelegramNotice(data.bridge?.running ? 'Telegram bridge перезапущен.' : `Bridge не запущен: ${data.bridge?.error || 'unknown error'}`); await loadDashboard({ silent: true, includeTelegram: true }); @@ -254,40 +307,107 @@ function App() {

WSChat · operator console

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

-

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

+

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

- - API: {healthOk ? 'работает' : 'ошибка'} - - - SOCKS5: {telegramSettings.proxy.enabled ? 'включен' : 'выключен'} - - - bridge: {telegramBridge?.running ? 'online' : 'offline'} - - {telegramDirty ? ( - - есть несохранённые изменения - - ) : null} - - {lastUpdate ? `обновлено ${formatDate(lastUpdate)}` : 'ожидает данных'} - + 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} + {adminNotice ?
{adminNotice}
: null}
{statCards.map((card) => )}
+
+
+
+

Sites & operators

+

Сайты, embed-код и Telegram-операторы

+
+ {sites.length} +
+ +
+ + +
+ +
+
+ +
+ {sites.map((site) => { + const embed = makeEmbedCode(site.widget_key); + const linkedOperatorIds = new Set( + operators + .filter((operator) => String(operator.sites || '').split(', ').includes(site.domain)) + .map((operator) => operator.id) + ); + + return ( +
+
+
+ {site.name} + {site.domain} +
+ {site.is_active ? 'active' : 'off'} +
+ +
+ {site.widget_key} + {site.visitors_count || 0} visitors + {site.conversations_count || 0} dialogs + {site.operators_count || 0} operators +
+ +
{embed}
+ + +
+

Привязка операторов

+ {operators.length === 0 ?

Операторов пока нет. Напишите /start Telegram-боту.

: null} + {operators.map((operator) => { + const linked = linkedOperatorIds.has(operator.id); + return ( + + ); + })} +
+
+ ); + })} +
+
+
@@ -295,9 +415,7 @@ function App() {

Telegram bridge

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

- - token {telegramSettings.hasBotToken ? 'есть' : 'не задан'} - + token {telegramSettings.hasBotToken ? 'есть' : 'не задан'}
@@ -310,157 +428,42 @@ function App() {
- - - - - - - - - - - - + + + + +
- - - + + +
- {telegramDirty ? Сначала сохраните изменения, потом проверяйте прокси или перезапускайте bridge. : null}
{telegramNotice ?
{telegramNotice}
: null} - {proxyTestResult ? ( -
- {proxyTestResult.message || proxyTestResult.status} -
- ) : 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} -
-
- ))} -
+

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}
)}
)}
-