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() {
{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}
-
-
- ))}
-
+
+ {messages.length === 0 ? Сообщений пока нет. После отправки из виджета они появятся здесь.
: (
+ {messages.map((message) =>
{message.visitor_key || message.direction}{formatDate(message.created_at)}
{message.body}
)}
)}
-