2025-10-26 20:23:26 +03:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="ru">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>Lichess Bot Users Monitor</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #D2B48C;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 400px 1fr;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
height: calc(100vh - 40px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #F5E6D3;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
border-radius: 15px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.users-panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gamers-panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h1 {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #5C4033;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #E8D5B7;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
font-size: 14px;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #5C4033;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats strong {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #8B6F47;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-box {
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-box input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 2px solid #e0e0e0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
transition: border-color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-box input:focus {
|
|
|
|
|
|
outline: none;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
border-color: #8B6F47;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.users-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
margin-bottom: 10px;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #E8D5B7;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item:hover {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #DDC9A3;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
transform: translateX(5px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #8B6F47;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
color: white;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
border-color: #6B5432;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .user-stats {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-name {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-info {
|
|
|
|
|
|
font-size: 12px;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #5C4033;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .user-info {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-stats {
|
|
|
|
|
|
font-size: 12px;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #8B6F47;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .user-stats {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-stats span {
|
|
|
|
|
|
padding: 3px 8px;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #F5E6D3;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .user-stats span {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gamers-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gamers-table th {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #E8D5B7;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
padding: 12px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
font-weight: 600;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #5C4033;
|
|
|
|
|
|
border-bottom: 2px solid #D2B48C;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gamers-table td {
|
|
|
|
|
|
padding: 12px;
|
2025-11-13 15:43:42 +03:00
|
|
|
|
border-bottom: 1px solid #D2B48C;
|
|
|
|
|
|
color: #5C4033;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gamers-table tr:hover {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #E8D5B7;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge {
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-success {
|
|
|
|
|
|
background: #d4edda;
|
|
|
|
|
|
color: #155724;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-secondary {
|
|
|
|
|
|
background: #e2e3e5;
|
|
|
|
|
|
color: #383d41;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-token {
|
|
|
|
|
|
background: #fff3cd;
|
|
|
|
|
|
color: #856404;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selected-user-info {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
background: #E8D5B7;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selected-user-info h2 {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #5C4033;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.selected-user-info p {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #5C4033;
|
2025-10-26 20:23:26 +03:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin: 5px 0;
|
|
|
|
|
|
}
|
2025-10-31 19:18:54 +03:00
|
|
|
|
|
|
|
|
|
|
.gamer-link {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #8B6F47 !important;
|
2025-10-31 19:18:54 +03:00
|
|
|
|
text-decoration: none !important;
|
|
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gamer-link:hover {
|
2025-11-13 15:43:42 +03:00
|
|
|
|
color: #6B5432 !important;
|
2025-10-31 19:18:54 +03:00
|
|
|
|
text-decoration: underline !important;
|
|
|
|
|
|
}
|
2025-11-18 14:05:39 +03:00
|
|
|
|
|
|
|
|
|
|
.user-link {
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-link:hover {
|
|
|
|
|
|
color: #6B5432;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .user-link {
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .user-link:hover {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.username-link {
|
|
|
|
|
|
color: #8B6F47;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.username-link:hover {
|
|
|
|
|
|
color: #6B5432;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .username-link {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-item.active .username-link:hover {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.95);
|
|
|
|
|
|
}
|
2025-10-26 20:23:26 +03:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<!-- Левая панель: Пользователи -->
|
|
|
|
|
|
<div class="panel users-panel">
|
2025-11-23 02:36:15 +03:00
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
|
|
|
|
<h1 style="margin: 0;">👥 Пользователи</h1>
|
|
|
|
|
|
<a href="/logout" style="padding: 8px 16px; background: #8B6F47; color: white; text-decoration: none; border-radius: 8px; font-size: 14px; transition: background 0.3s;" onmouseover="this.style.background='#6B5432'" onmouseout="this.style.background='#8B6F47'">Выход</a>
|
|
|
|
|
|
</div>
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
<div class="stats">
|
2025-11-18 14:43:38 +03:00
|
|
|
|
Всего пользователей: <strong id="total-users">0</strong> (сегодня: <strong id="users-today">0</strong>)<br>
|
2025-11-20 02:22:44 +03:00
|
|
|
|
👤 Пользователей без игроков: <strong id="users-without-gamers">0</strong> (<strong id="users-without-gamers-percent">0</strong>%)<br>
|
2025-11-20 13:45:50 +03:00
|
|
|
|
🇷🇺 Пользователей с русским языком: <strong id="users-with-ru-language">0</strong> (<strong id="users-ru-language-percent">0</strong>%)<br>
|
2025-11-18 14:43:38 +03:00
|
|
|
|
Кол-во игроков: <strong id="total-gamers">0</strong> (сегодня: <strong id="gamers-today">0</strong>)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-13 15:43:42 +03:00
|
|
|
|
<div class="stats" style="margin-top: 15px; padding: 15px; background: #E8D5B7; border-radius: 8px;">
|
2025-11-13 13:32:46 +03:00
|
|
|
|
<h3 style="margin-top: 0; margin-bottom: 10px; font-size: 16px;">📨 Счетчики сообщений</h3>
|
|
|
|
|
|
<div style="margin-bottom: 8px;">
|
|
|
|
|
|
Всего отправлено: <strong id="total-messages-all">0</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-bottom: 8px;">
|
|
|
|
|
|
Сегодня отправлено: <strong id="total-messages-today">0</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="message-stats-by-command" style="margin-top: 10px; font-size: 12px; color: #666;">
|
2025-11-13 15:27:10 +03:00
|
|
|
|
<div style="display: flex; gap: 20px;">
|
|
|
|
|
|
<div style="flex: 1;">
|
|
|
|
|
|
<div style="margin-bottom: 8px; font-weight: bold;">За все время:</div>
|
|
|
|
|
|
<div id="command-stats-all-time"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="flex: 1;">
|
|
|
|
|
|
<div style="margin-bottom: 8px; font-weight: bold;">За сегодня:</div>
|
|
|
|
|
|
<div id="command-stats-today"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-11-13 13:32:46 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-26 20:23:26 +03:00
|
|
|
|
<div class="search-box">
|
|
|
|
|
|
<input type="text" id="search-input" placeholder="Поиск по имени или никнейму...">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="users-list" id="users-list">
|
|
|
|
|
|
<div class="loading">Загрузка...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Правая панель: Игроки -->
|
|
|
|
|
|
<div class="panel gamers-panel">
|
|
|
|
|
|
<h1>🎮 Отслеживаемые игроки</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="selected-user-info" style="display: none;">
|
|
|
|
|
|
<div class="selected-user-info">
|
|
|
|
|
|
<h2 id="selected-user-name">Выберите пользователя</h2>
|
|
|
|
|
|
<p id="selected-user-username"></p>
|
|
|
|
|
|
<p id="selected-user-date"></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="gamers-table-container">
|
|
|
|
|
|
<div class="empty">Выберите пользователя для просмотра его игроков</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
let users = [];
|
|
|
|
|
|
let selectedUserId = null;
|
|
|
|
|
|
let filteredUsers = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузка пользователей
|
|
|
|
|
|
async function loadUsers() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/users');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
users = data.users;
|
2025-10-28 22:20:49 +03:00
|
|
|
|
document.getElementById('total-users').textContent = data.total_users;
|
2025-11-18 14:43:38 +03:00
|
|
|
|
document.getElementById('users-today').textContent = data.users_today || 0;
|
2025-11-20 02:22:44 +03:00
|
|
|
|
document.getElementById('users-without-gamers').textContent = data.users_without_gamers || 0;
|
|
|
|
|
|
document.getElementById('users-without-gamers-percent').textContent = data.users_without_gamers_percent || 0;
|
2025-11-20 13:45:50 +03:00
|
|
|
|
document.getElementById('users-with-ru-language').textContent = data.users_with_ru_language || 0;
|
|
|
|
|
|
document.getElementById('users-ru-language-percent').textContent = data.users_ru_language_percent || 0;
|
2025-10-28 22:20:49 +03:00
|
|
|
|
document.getElementById('total-gamers').textContent = data.total_gamers;
|
2025-11-18 14:43:38 +03:00
|
|
|
|
document.getElementById('gamers-today').textContent = data.gamers_today || 0;
|
2025-11-13 13:32:46 +03:00
|
|
|
|
|
|
|
|
|
|
// Update message counters
|
|
|
|
|
|
if (data.message_stats) {
|
|
|
|
|
|
document.getElementById('total-messages-all').textContent = data.message_stats.total_all_time || 0;
|
|
|
|
|
|
document.getElementById('total-messages-today').textContent = data.message_stats.total_today || 0;
|
|
|
|
|
|
|
2025-11-13 15:27:10 +03:00
|
|
|
|
// Render command stats in two columns
|
|
|
|
|
|
const commandStatsAllTime = document.getElementById('command-stats-all-time');
|
|
|
|
|
|
const commandStatsToday = document.getElementById('command-stats-today');
|
|
|
|
|
|
|
2025-11-13 13:32:46 +03:00
|
|
|
|
if (data.message_stats.by_command && Object.keys(data.message_stats.by_command).length > 0) {
|
2025-11-13 15:27:10 +03:00
|
|
|
|
// Filter out excluded commands (already filtered on server, but double-check)
|
2025-11-13 13:32:46 +03:00
|
|
|
|
const excludedCommands = ['start', 'lang', 'resetlang'];
|
|
|
|
|
|
const filteredCommands = Object.entries(data.message_stats.by_command)
|
2025-11-13 15:27:10 +03:00
|
|
|
|
.filter(([cmd]) => !excludedCommands.includes(cmd))
|
|
|
|
|
|
.sort((a, b) => a[0].localeCompare(b[0])); // Sort alphabetically
|
2025-11-13 13:32:46 +03:00
|
|
|
|
|
2025-11-13 15:27:10 +03:00
|
|
|
|
if (filteredCommands.length > 0) {
|
|
|
|
|
|
// Render "All time" column
|
|
|
|
|
|
commandStatsAllTime.innerHTML = filteredCommands
|
|
|
|
|
|
.map(([cmd, stats]) => {
|
|
|
|
|
|
return `<div style="margin-bottom: 3px;">
|
|
|
|
|
|
• ${cmd}: <strong>${stats.total}</strong>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
// Render "Today" column
|
|
|
|
|
|
commandStatsToday.innerHTML = filteredCommands
|
|
|
|
|
|
.map(([cmd, stats]) => {
|
|
|
|
|
|
return `<div style="margin-bottom: 3px;">
|
|
|
|
|
|
• ${cmd}: <strong>${stats.today}</strong>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
commandStatsAllTime.innerHTML = '<div style="color: #999;">Нет данных</div>';
|
|
|
|
|
|
commandStatsToday.innerHTML = '<div style="color: #999;">Нет данных</div>';
|
|
|
|
|
|
}
|
2025-11-13 13:32:46 +03:00
|
|
|
|
} else {
|
2025-11-13 15:27:10 +03:00
|
|
|
|
commandStatsAllTime.innerHTML = '<div style="color: #999;">Нет данных</div>';
|
|
|
|
|
|
commandStatsToday.innerHTML = '<div style="color: #999;">Нет данных</div>';
|
2025-11-13 13:32:46 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 20:23:26 +03:00
|
|
|
|
filteredUsers = users;
|
|
|
|
|
|
renderUsers();
|
|
|
|
|
|
|
|
|
|
|
|
// Выбираем первого пользователя по умолчанию
|
|
|
|
|
|
if (users.length > 0) {
|
|
|
|
|
|
selectUser(users[0].user_id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error loading users:', error);
|
|
|
|
|
|
document.getElementById('users-list').innerHTML = '<div class="empty">Ошибка загрузки данных</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Рендеринг пользователей
|
|
|
|
|
|
function renderUsers() {
|
|
|
|
|
|
const usersList = document.getElementById('users-list');
|
|
|
|
|
|
|
|
|
|
|
|
if (filteredUsers.length === 0) {
|
|
|
|
|
|
usersList.innerHTML = '<div class="empty">Пользователи не найдены</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 14:05:39 +03:00
|
|
|
|
usersList.innerHTML = filteredUsers.map(user => {
|
|
|
|
|
|
const userName = escapeHtml(user.first_name);
|
|
|
|
|
|
const username = user.username && user.username !== '-' ? escapeHtml(user.username) : null;
|
|
|
|
|
|
const telegramLink = username ? `https://t.me/${username}` : `tg://user?id=${user.user_id}`;
|
2025-11-20 13:45:50 +03:00
|
|
|
|
const botLanguage = user.bot_language || 'en';
|
|
|
|
|
|
const languageBadge = botLanguage === 'ru' ? '🇷🇺 ru' : '🇬🇧 en';
|
2025-11-18 14:05:39 +03:00
|
|
|
|
|
|
|
|
|
|
return `
|
2025-10-26 20:23:26 +03:00
|
|
|
|
<div class="user-item ${selectedUserId === user.user_id ? 'active' : ''}" onclick="selectUser(${user.user_id})">
|
2025-11-18 14:05:39 +03:00
|
|
|
|
<div class="user-name">
|
|
|
|
|
|
<a href="${telegramLink}" target="_blank" class="user-link" onclick="event.stopPropagation();">
|
|
|
|
|
|
${userName}
|
|
|
|
|
|
</a>
|
2025-11-20 13:45:50 +03:00
|
|
|
|
<span style="font-size: 12px; color: #666; margin-left: 5px;">${languageBadge}</span>
|
2025-11-18 14:05:39 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="user-info">
|
|
|
|
|
|
${username ?
|
|
|
|
|
|
`<a href="${telegramLink}" target="_blank" class="username-link" onclick="event.stopPropagation();">@${username}</a>` :
|
|
|
|
|
|
`<span>ID: ${user.user_id}</span>`
|
|
|
|
|
|
} • ID: ${user.user_id}
|
|
|
|
|
|
</div>
|
2025-10-26 20:23:26 +03:00
|
|
|
|
<div class="user-info">Добавлен: ${formatDate(user.created_at)}</div>
|
|
|
|
|
|
<div class="user-stats">
|
2025-11-18 14:05:39 +03:00
|
|
|
|
<span>📊 ${user.gamer_count} игроков</span>
|
2025-10-26 20:23:26 +03:00
|
|
|
|
<span>⏰ ${user.monitored_gamers} с уведомл.</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-11-18 14:05:39 +03:00
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
2025-10-26 20:23:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Выбор пользователя
|
|
|
|
|
|
async function selectUser(userId) {
|
|
|
|
|
|
selectedUserId = userId;
|
|
|
|
|
|
renderUsers();
|
|
|
|
|
|
|
|
|
|
|
|
// Находим выбранного пользователя
|
|
|
|
|
|
const user = users.find(u => u.user_id === userId);
|
|
|
|
|
|
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
document.getElementById('selected-user-info').style.display = 'block';
|
2025-11-18 14:05:39 +03:00
|
|
|
|
const username = user.username && user.username !== '-' ? user.username : null;
|
|
|
|
|
|
const telegramLink = username ? `https://t.me/${username}` : `tg://user?id=${user.user_id}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create name with link
|
|
|
|
|
|
const nameElement = document.getElementById('selected-user-name');
|
|
|
|
|
|
nameElement.innerHTML = `<a href="${telegramLink}" target="_blank" class="user-link" style="color: #5C4033; text-decoration: none;">${escapeHtml(user.first_name)}</a>`;
|
|
|
|
|
|
|
|
|
|
|
|
// Create username with link
|
|
|
|
|
|
const usernameElement = document.getElementById('selected-user-username');
|
2025-11-20 13:45:50 +03:00
|
|
|
|
const botLanguage = user.bot_language || 'en';
|
|
|
|
|
|
const languageBadge = botLanguage === 'ru' ? '🇷🇺 ru' : '🇬🇧 en';
|
2025-11-18 14:05:39 +03:00
|
|
|
|
if (username) {
|
2025-11-20 13:45:50 +03:00
|
|
|
|
usernameElement.innerHTML = `<a href="${telegramLink}" target="_blank" class="username-link" style="color: #8B6F47; text-decoration: none;">@${escapeHtml(username)}</a> • ID: ${user.user_id} • ${languageBadge}`;
|
2025-11-18 14:05:39 +03:00
|
|
|
|
} else {
|
2025-11-20 13:45:50 +03:00
|
|
|
|
usernameElement.textContent = `ID: ${user.user_id} • ${languageBadge}`;
|
2025-11-18 14:05:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 20:23:26 +03:00
|
|
|
|
document.getElementById('selected-user-date').textContent = `Добавлен: ${formatDate(user.created_at)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузка игроков
|
|
|
|
|
|
await loadGamers(userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузка игроков пользователя
|
|
|
|
|
|
async function loadGamers(userId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/users/${userId}/gamers`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
renderGamers(data.gamers);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.getElementById('gamers-table-container').innerHTML =
|
|
|
|
|
|
'<div class="empty">Ошибка загрузки игроков</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error loading gamers:', error);
|
|
|
|
|
|
document.getElementById('gamers-table-container').innerHTML =
|
|
|
|
|
|
'<div class="empty">Ошибка загрузки данных</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Рендеринг игроков
|
|
|
|
|
|
function renderGamers(gamers) {
|
|
|
|
|
|
const container = document.getElementById('gamers-table-container');
|
|
|
|
|
|
|
|
|
|
|
|
if (gamers.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<div class="empty">Нет отслеживаемых игроков</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const table = `
|
|
|
|
|
|
<table class="gamers-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>Игрок</th>
|
|
|
|
|
|
<th>Токен</th>
|
|
|
|
|
|
<th>Период уведомлений</th>
|
|
|
|
|
|
<th>Добавлен</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
${gamers.map(gamer => `
|
|
|
|
|
|
<tr>
|
2025-10-31 19:18:54 +03:00
|
|
|
|
<td><strong><a href="https://lichess.org/@/${gamer.username}" target="_blank" class="gamer-link">${escapeHtml(gamer.username)}</a></strong> (ID: ${gamer.id})</td>
|
2025-10-26 20:23:26 +03:00
|
|
|
|
<td>
|
|
|
|
|
|
<span class="badge badge-token">
|
|
|
|
|
|
${gamer.has_token ? '🔑 Есть' : '❌ Нет'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
${gamer.period_minutes > 0 ?
|
|
|
|
|
|
`⏰ ${gamer.period_minutes} мин` :
|
|
|
|
|
|
'<span style="color: #999;">—</span>'}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td style="color: #666;">${formatDate(gamer.created_at)}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = table;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Поиск
|
|
|
|
|
|
document.getElementById('search-input').addEventListener('input', function(e) {
|
|
|
|
|
|
const searchTerm = e.target.value.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
filteredUsers = users.filter(user => {
|
|
|
|
|
|
const name = (user.first_name || '').toLowerCase();
|
|
|
|
|
|
const username = (user.username || '').toLowerCase();
|
|
|
|
|
|
return name.includes(searchTerm) || username.includes(searchTerm);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
renderUsers();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Утилиты
|
|
|
|
|
|
function escapeHtml(text) {
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
|
div.textContent = text;
|
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDate(dateString) {
|
|
|
|
|
|
if (!dateString) return '—';
|
|
|
|
|
|
const date = new Date(dateString);
|
|
|
|
|
|
return date.toLocaleDateString('ru-RU') + ' ' + date.toLocaleTimeString('ru-RU', {
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Инициализация
|
|
|
|
|
|
loadUsers();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
|
2025-10-28 21:34:35 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|