import React, { useEffect, useMemo, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { Activity, Bot, Clock, MessageCircle, RefreshCw, 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; function formatDate(value) { if (!value) return '—'; const date = new Date(value.replace(' ', 'T') + 'Z'); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat('ru-RU', { dateStyle: 'short', timeStyle: 'short' }).format(date); } 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 [loading, setLoading] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [error, setError] = useState(''); 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]); async function loadDashboard({ silent = 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 : []); setLastUpdate(new Date()); } catch (requestError) { setHealth({ ok: false, error: requestError.message }); setError(requestError.message); } finally { if (!silent) setLoading(false); } } useEffect(() => { loadDashboard(); const timer = setInterval(() => loadDashboard({ silent: true }), REFRESH_MS); return () => clearInterval(timer); }, []); return (

WSChat · operator console

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

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

API: {healthOk ? 'работает' : 'ошибка'} {lastUpdate ? `обновлено ${formatDate(lastUpdate.toISOString())}` : 'ожидает данных'}
{error ?
Ошибка API: {error}
: null}
{statCards.map((card) => )}

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();