LichessStatTgWeb/LichessWebView/templates/index.html
2025-11-16 23:36:57 +03:00

515 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
background: #D2B48C;
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 {
background: #F5E6D3;
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 {
color: #5C4033;
margin-bottom: 20px;
font-size: 24px;
}
.stats {
background: #E8D5B7;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
font-size: 14px;
color: #5C4033;
}
.stats strong {
color: #8B6F47;
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;
border-color: #8B6F47;
}
.users-list {
flex: 1;
}
.user-item {
padding: 15px;
margin-bottom: 10px;
background: #E8D5B7;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
border: 2px solid transparent;
}
.user-item:hover {
background: #DDC9A3;
transform: translateX(5px);
}
.user-item.active {
background: #8B6F47;
color: white;
border-color: #6B5432;
}
.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;
color: #5C4033;
margin-bottom: 5px;
}
.user-item.active .user-info {
color: rgba(255, 255, 255, 0.7);
}
.user-stats {
font-size: 12px;
color: #8B6F47;
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;
background: #F5E6D3;
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 {
background: #E8D5B7;
padding: 12px;
text-align: left;
font-weight: 600;
color: #5C4033;
border-bottom: 2px solid #D2B48C;
}
.gamers-table td {
padding: 12px;
border-bottom: 1px solid #D2B48C;
color: #5C4033;
}
.gamers-table tr:hover {
background: #E8D5B7;
}
.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 {
background: #E8D5B7;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
}
.selected-user-info h2 {
color: #5C4033;
font-size: 18px;
margin-bottom: 10px;
}
.selected-user-info p {
color: #5C4033;
font-size: 14px;
margin: 5px 0;
}
.gamer-link {
color: #8B6F47 !important;
text-decoration: none !important;
transition: color 0.3s;
}
.gamer-link:hover {
color: #6B5432 !important;
text-decoration: underline !important;
}
</style>
</head>
<body>
<div class="container">
<!-- Левая панель: Пользователи -->
<div class="panel users-panel">
<h1>👥 Пользователи</h1>
<div class="stats">
Всего пользователей: <strong id="total-users">0</strong><br>
Кол-во игроков: <strong id="total-gamers">0</strong>
</div>
<div class="stats" style="margin-top: 15px; padding: 15px; background: #E8D5B7; border-radius: 8px;">
<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;">
<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>
</div>
</div>
<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;
document.getElementById('total-users').textContent = data.total_users;
document.getElementById('total-gamers').textContent = data.total_gamers;
// 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;
// Render command stats in two columns
const commandStatsAllTime = document.getElementById('command-stats-all-time');
const commandStatsToday = document.getElementById('command-stats-today');
if (data.message_stats.by_command && Object.keys(data.message_stats.by_command).length > 0) {
// Filter out excluded commands (already filtered on server, but double-check)
const excludedCommands = ['start', 'lang', 'resetlang'];
const filteredCommands = Object.entries(data.message_stats.by_command)
.filter(([cmd]) => !excludedCommands.includes(cmd))
.sort((a, b) => a[0].localeCompare(b[0])); // Sort alphabetically
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>';
}
} else {
commandStatsAllTime.innerHTML = '<div style="color: #999;">Нет данных</div>';
commandStatsToday.innerHTML = '<div style="color: #999;">Нет данных</div>';
}
}
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;
}
usersList.innerHTML = filteredUsers.map(user => `
<div class="user-item ${selectedUserId === user.user_id ? 'active' : ''}" onclick="selectUser(${user.user_id})">
<div class="user-name">${escapeHtml(user.first_name)}</div>
<div class="user-info">@${escapeHtml(user.username)} • ID: ${user.user_id}</div>
<div class="user-info">Добавлен: ${formatDate(user.created_at)}</div>
<div class="user-stats">
<span>📊 ${user.gamer_count} игроков</span>
<span>⏰ ${user.monitored_gamers} с уведомл.</span>
</div>
</div>
`).join('');
}
// Выбор пользователя
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';
document.getElementById('selected-user-name').textContent = user.first_name;
document.getElementById('selected-user-username').textContent = `@${user.username} • ID: ${user.user_id}`;
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>
<td><strong><a href="https://lichess.org/@/${gamer.username}" target="_blank" class="gamer-link">${escapeHtml(gamer.username)}</a></strong> (ID: ${gamer.id})</td>
<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>