Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LogUserRequest Lite</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --font-body: 'Inter', system-ui, sans-serif; --font-mono: 'JetBrains Mono', monospace; --text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --r-md: 8px; --r-lg: 12px; --r-xl: 16px; }
|
||||
[data-theme="dark"] { --bg: #0c0e12; --surface: #13161d; --surface-2: #1a1e28; --surface-3: #222733; --border: #2a3040; --text: #e2e6ef; --text-muted: #7c849a; --text-faint: #4d5568; --accent: #3ec9d1; --accent-dim: rgba(62, 201, 209, 0.12); --ok: #4ade80; --err: #f87171; --shadow-sm: 0 1px 2px rgba(0,0,0,0.3); --modal-overlay: rgba(0,0,0,0.7); --chart-grid: #2a3040; }
|
||||
[data-theme="light"] { --bg: #f4f5f8; --surface: #ffffff; --surface-2: #f0f1f5; --surface-3: #e8eaf0; --border: #d4d8e3; --text: #1a1e28; --text-muted: #6b7280; --text-faint: #9ca3af; --accent: #0d9499; --accent-dim: rgba(13, 148, 153, 0.1); --ok: #16a34a; --err: #dc2626; --shadow-sm: 0 1px 2px rgba(0,0,0,0.06); --modal-overlay: rgba(0,0,0,0.5); --chart-grid: #d4d8e3; }
|
||||
html, body { height: 100%; }
|
||||
body { font-family: var(--font-body); font-size: var(--text-base); background: var(--bg); color: var(--text); line-height: 1.5; -webkit-font-smoothing: antialiased; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
|
||||
.header-left { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
||||
.logo { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 1.2rem; color: var(--text); }
|
||||
.logo svg { width: 28px; height: 28px; }
|
||||
.logo small { font-weight: 400; color: var(--text-muted); margin-left: 4px; }
|
||||
.actions { display: flex; gap: 8px; }
|
||||
.btn { font: inherit; font-size: var(--text-xs); font-weight: 600; padding: 8px 16px; border-radius: var(--r-md); border: 1px solid var(--accent); background: var(--accent-dim); color: var(--accent); cursor: pointer; transition: all 0.15s; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.btn:hover { background: rgba(62, 201, 209, 0.22); transform: translateY(-1px); }
|
||||
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.btn-primary:hover { background: var(--accent); opacity: 0.85; }
|
||||
.btn-premium { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); margin-left: 8px; }
|
||||
.btn-premium:hover { background: rgba(62, 201, 209, 0.22); }
|
||||
.btn-premium svg { width: 16px; height: 16px; }
|
||||
.btn-ghost { border-color: var(--border); background: var(--surface-2); color: var(--text-muted); }
|
||||
.btn-ghost:hover { color: var(--text); background: var(--surface-3); }
|
||||
.btn-icon { width: 34px; height: 34px; padding: 0; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2); color: var(--text-muted); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.btn-icon:hover { color: var(--text); background: var(--surface-3); }
|
||||
.btn-icon svg { width: 16px; height: 16px; }
|
||||
.filter-bar { display: flex; gap: 10px; margin-bottom: 24px; flex-wrap: wrap; }
|
||||
.filter-bar input { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-md); padding: 10px 16px; color: var(--text); font-size: 0.9rem; flex: 1; min-width: 200px; }
|
||||
.filter-bar input:focus { outline: none; border-color: var(--accent); }
|
||||
|
||||
.stats-block { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-xl); padding: 20px; margin-bottom: 24px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||
.stat-card { background: var(--surface-2); border-radius: var(--r-lg); padding: 16px; text-align: center; border: 1px solid rgba(255,255,255,0.06); }
|
||||
.stat-value { font-size: 2rem; font-weight: 700; color: var(--accent); line-height: 1.2; }
|
||||
.stat-label { font-size: var(--text-xs); color: var(--text-muted); margin-top: 6px; }
|
||||
.top-section { margin-bottom: 20px; }
|
||||
.top-section h3 { font-size: 0.9rem; font-weight: 600; margin-bottom: 12px; color: var(--text-muted); }
|
||||
.scroll-grid { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.grid-item { background: var(--surface-2); border: 1px solid rgba(255,255,255,0.06); border-radius: var(--r-lg); padding: 8px 14px; min-width: 100px; display: inline-flex; align-items: center; gap: 8px; }
|
||||
.grid-rank { font-size: var(--text-xs); font-weight: 700; color: var(--text-faint); background: var(--surface-3); width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; }
|
||||
.grid-name { font-size: var(--text-sm); font-weight: 500; }
|
||||
.grid-count { font-size: var(--text-xs); color: var(--text-muted); background: var(--surface-3); padding: 2px 8px; border-radius: 6px; }
|
||||
.requests-list { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-xl); overflow: hidden; }
|
||||
.request-card { background: var(--surface-2); border-bottom: 1px solid var(--border); padding: 16px 20px; }
|
||||
.request-card:last-child { border-bottom: none; }
|
||||
.request-header { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 8px; font-size: 0.8rem; }
|
||||
.request-time { color: var(--text-faint); }
|
||||
.request-uid { color: var(--accent); font-weight: 600; }
|
||||
.request-ip { color: var(--text-muted); }
|
||||
.request-uri { font-family: var(--font-mono); font-size: 0.75rem; word-break: break-all; color: var(--text-muted); margin-top: 6px; }
|
||||
.loading, .empty { text-align: center; padding: 40px; color: var(--text-faint); }
|
||||
|
||||
/* Модальное окно */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--modal-overlay); display: flex; align-items: flex-start; justify-content: center; z-index: 1000; padding: 20px; padding-top: 60px; backdrop-filter: blur(4px); overflow-y: auto; }
|
||||
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-xl); max-width: 950px; width: 100%; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); animation: modalSlide 0.2s ease; max-height: calc(100vh - 80px); overflow-y: auto; }
|
||||
@keyframes modalSlide { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--surface); z-index: 5; }
|
||||
.modal-header h2 { font-size: 1.25rem; font-weight: 700; display: flex; align-items: center; gap: 8px; }
|
||||
.modal-header h2 svg { width: 24px; height: 24px; color: var(--accent); }
|
||||
.modal-close { width: 32px; height: 32px; border-radius: var(--r-md); border: 1px solid var(--border); background: var(--surface-2); color: var(--text-muted); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
||||
.modal-close:hover { color: var(--text); background: var(--surface-3); }
|
||||
.modal-body { padding: 24px; }
|
||||
|
||||
/* График в модалке */
|
||||
.lite-chart-block { background: var(--surface-2); border-radius: var(--r-lg); padding: 20px; margin-bottom: 32px; }
|
||||
.lite-chart-header { margin-bottom: 16px; }
|
||||
.lite-chart-header h3 { font-size: 1rem; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px; }
|
||||
.lite-chart-header h3 svg { width: 20px; height: 20px; }
|
||||
.lite-chart-container { position: relative; height: 200px; }
|
||||
.lite-scroll-hint { display: flex; justify-content: center; margin-top: 12px; }
|
||||
.lite-scroll-arrow { color: var(--text-faint); font-size: var(--text-xs); display: flex; flex-direction: column; align-items: center; gap: 4px; opacity: 0.7; }
|
||||
.lite-scroll-arrow svg { width: 20px; height: 20px; animation: bounce 2s infinite; }
|
||||
@keyframes bounce { 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(6px); } 60% { transform: translateY(3px); } }
|
||||
|
||||
.premium-section { margin-top: 8px; }
|
||||
.premium-header { display: flex; align-items: center; gap: 8px; margin-bottom: 24px; }
|
||||
.premium-header h3 { font-size: 1.1rem; font-weight: 700; color: var(--accent); display: flex; align-items: center; gap: 8px; }
|
||||
.premium-header svg { width: 24px; height: 24px; }
|
||||
|
||||
.comparison-table { width: 100%; border-collapse: collapse; margin: 0 0 24px; font-size: var(--text-sm); }
|
||||
.comparison-table th { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--border); color: var(--text-muted); font-weight: 500; font-size: var(--text-xs); }
|
||||
.comparison-table td { padding: 8px; border-bottom: 1px solid var(--border); text-align: left; }
|
||||
.comparison-table tr:last-child td { border-bottom: none; }
|
||||
.comparison-table .feature-col { font-weight: 500; color: var(--text); }
|
||||
.comparison-table .lite-col { color: var(--text-muted); }
|
||||
.comparison-table .premium-col { color: var(--ok); }
|
||||
.comparison-table .both-col { color: var(--ok); }
|
||||
|
||||
.video-container { position: relative; padding-bottom: 56.25%; height: 0; margin: 24px 0; border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--border); }
|
||||
.video-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0; }
|
||||
|
||||
.telegram-section { background: var(--accent-dim); border-radius: var(--r-lg); padding: 24px; text-align: center; border: 1px solid var(--accent); }
|
||||
.telegram-section h3 { color: var(--accent); margin-bottom: 8px; }
|
||||
.telegram-section p { color: var(--text-muted); margin-bottom: 20px; }
|
||||
.telegram-link { display: inline-flex; align-items: center; gap: 8px; background: var(--accent); color: #fff; padding: 12px 24px; border-radius: var(--r-lg); text-decoration: none; font-weight: 600; transition: all 0.15s; }
|
||||
.telegram-link:hover { opacity: 0.85; transform: translateY(-2px); }
|
||||
.telegram-link svg { width: 20px; height: 20px; fill: #fff; }
|
||||
|
||||
.hidden { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 28 28" fill="none" aria-hidden="true">
|
||||
<rect x="2" y="2" width="24" height="24" rx="6" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
<rect x="7" y="16" width="3" height="6" rx="1" fill="var(--accent)" />
|
||||
<rect x="12.5" y="11" width="3" height="11" rx="1" fill="var(--accent)" opacity="0.7" />
|
||||
<rect x="18" y="6" width="3" height="16" rx="1" fill="var(--accent)" opacity="0.45" />
|
||||
</svg>
|
||||
<span>LogUserRequest Lite</span>
|
||||
</div>
|
||||
<button class="btn btn-premium" id="premiumBtn">
|
||||
|
||||
Premium
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn-icon" id="themeBtn" title="Сменить тему (Alt+T)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-ghost" id="logoutBtn">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<input type="text" id="uidInput" placeholder="UID пользователя (оставьте пустым для всех)">
|
||||
<button class="btn btn-primary" onclick="applyFilter()">Показать</button>
|
||||
<button class="btn btn-ghost" onclick="resetFilter()">Сбросить</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-block" id="statsBlock" style="display:none;">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card"><div class="stat-value" id="statToday">0</div><div class="stat-label">Запросов сегодня</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statMonth">0</div><div class="stat-label">За месяц</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statDevices">0</div><div class="stat-label">Устройств</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statIps">0</div><div class="stat-label">IP адресов</div></div>
|
||||
</div>
|
||||
<div class="top-section">
|
||||
<h3>👥 Топ пользователей</h3>
|
||||
<div class="scroll-grid" id="topUsers"></div>
|
||||
</div>
|
||||
<div class="top-section">
|
||||
<h3>⚡ Топ балансиров</h3>
|
||||
<div class="scroll-grid" id="topBalancers"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="requests-list" id="requestsList">
|
||||
<div class="loading">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно Premium -->
|
||||
<div class="modal-overlay hidden" id="premiumModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2>
|
||||
<svg viewBox="0 0 28 28" fill="none" aria-hidden="true">
|
||||
<rect x="2" y="2" width="24" height="24" rx="6" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
<rect x="7" y="16" width="3" height="6" rx="1" fill="currentColor" />
|
||||
<rect x="12.5" y="11" width="3" height="11" rx="1" fill="currentColor" opacity="0.7" />
|
||||
<rect x="18" y="6" width="3" height="16" rx="1" fill="currentColor" opacity="0.45" />
|
||||
</svg>
|
||||
LogUserRequest Lite
|
||||
</h2>
|
||||
<button class="modal-close" id="closeModalBtn">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Блок Lite-версии с графиком -->
|
||||
<div class="lite-chart-block">
|
||||
<div class="lite-chart-header">
|
||||
<h3>
|
||||
<svg viewBox="0 0 28 28" fill="none" aria-hidden="true" width="20" height="20">
|
||||
<rect x="2" y="2" width="24" height="24" rx="6" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
<rect x="7" y="16" width="3" height="6" rx="1" fill="currentColor" />
|
||||
<rect x="12.5" y="11" width="3" height="11" rx="1" fill="currentColor" opacity="0.7" />
|
||||
<rect x="18" y="6" width="3" height="16" rx="1" fill="currentColor" opacity="0.45" />
|
||||
</svg>
|
||||
Активность за 3 дня
|
||||
</h3>
|
||||
</div>
|
||||
<div class="lite-chart-container">
|
||||
<canvas id="premiumChart"></canvas>
|
||||
</div>
|
||||
<div class="lite-scroll-hint">
|
||||
<span class="lite-scroll-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 5v14M19 12l-7 7-7-7"/>
|
||||
</svg>
|
||||
Прокрутите чтобы увидеть Premium-возможности
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Разделитель / пустое пространство -->
|
||||
<div style="height: 400px;"></div>
|
||||
|
||||
<!-- Premium секция -->
|
||||
<div class="premium-section">
|
||||
<div class="premium-header">
|
||||
<svg viewBox="0 0 28 28" fill="none" aria-hidden="true">
|
||||
<rect x="2" y="2" width="24" height="24" rx="6" stroke="var(--accent)" stroke-width="1.5" fill="none" />
|
||||
<rect x="7" y="16" width="3" height="6" rx="1" fill="var(--accent)" />
|
||||
<rect x="12.5" y="11" width="3" height="11" rx="1" fill="var(--accent)" opacity="0.7" />
|
||||
<rect x="18" y="6" width="3" height="16" rx="1" fill="var(--accent)" opacity="0.45" />
|
||||
</svg>
|
||||
<h3>LogUserRequest Premium</h3>
|
||||
</div>
|
||||
|
||||
<table class="comparison-table">
|
||||
<thead>
|
||||
<tr><th>Функция</th><th>Lite-версия</th><th>Premium-версия</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class="feature-col">Логирование запросов</td><td class="both-col">✅ Все запросы</td><td class="both-col">✅ Все запросы</td></tr>
|
||||
<tr><td class="feature-col">Фильтрация служебных путей</td><td class="both-col">✅ Хардкод-список</td><td class="premium-col">✅ Гибкий UI + regex + IP</td></tr>
|
||||
<tr><td class="feature-col">Авторизация</td><td class="both-col">✅ Сессии + rate-limit</td><td class="both-col">✅ Сессии + rate-limit</td></tr>
|
||||
<tr><td class="feature-col">База данных</td><td class="both-col">✅ SQLite</td><td class="premium-col">✅ SQLite с индексами</td></tr>
|
||||
<tr><td class="feature-col">Очистка старых записей</td><td class="both-col">✅ По дням (logDay)</td><td class="premium-col">✅ По дням + по размеру БД</td></tr>
|
||||
<tr><td class="feature-col">Статистика</td><td class="both-col">✅ Топы пользователей и балансиров</td><td class="premium-col">✅ Топы + графики + модальные окна</td></tr>
|
||||
<tr><td class="feature-col">Тёмная/светлая тема</td><td class="both-col">✅ Есть</td><td class="both-col">✅ Есть</td></tr>
|
||||
<tr><td class="feature-col">Мобильная адаптация</td><td class="both-col">✅ Есть</td><td class="both-col">✅ Есть</td></tr>
|
||||
<tr><td class="feature-col">Поиск по UID</td><td class="both-col">✅ Поле ввода</td><td class="premium-col">✅ Поле ввода + модальное окно</td></tr>
|
||||
<tr><td class="feature-col">Дашборд</td><td class="lite-col">❌ Простой список</td><td class="premium-col">✅ Полноценный SPA с графиками</td></tr>
|
||||
<tr><td class="feature-col">Дедупликация перебора балансиров</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ First + Last (окно 30 сек)</td></tr>
|
||||
<tr><td class="feature-col">Фильтр по дате</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ На дашборде</td></tr>
|
||||
<tr><td class="feature-col">Активность в реальном времени</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ Live-обновления</td></tr>
|
||||
<tr><td class="feature-col">Экспорт данных</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ CSV/JSON (ZIP)</td></tr>
|
||||
<tr><td class="feature-col">Массовое удаление</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ С гибкими фильтрами</td></tr>
|
||||
<tr><td class="feature-col">Архивация</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ По дате + по размеру БД</td></tr>
|
||||
<tr><td class="feature-col">Telegram-уведомления</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ Мониторинг + архив + ошибки</td></tr>
|
||||
<tr><td class="feature-col">Health-check</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ API для мониторинга</td></tr>
|
||||
<tr><td class="feature-col">Настройка фильтров через UI</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ Модальное окно + авто-сохранение</td></tr>
|
||||
<tr><td class="feature-col">Защита от ReDoS</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ Кэширование regex + таймаут</td></tr>
|
||||
<tr><td class="feature-col">Очередь с защитой от переполнения</td><td class="lite-col">❌ Нет</td><td class="premium-col">✅ До 50000 записей</td></tr>
|
||||
<tr><td class="feature-col">Потребление памяти</td><td class="both-col">~20-30 МБ</td><td class="both-col">~50-70 МБ</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Видео-обзор -->
|
||||
<div class="video-container">
|
||||
<iframe src="https://drive.google.com/file/d/1VCgOGJIVy4CVjYV1wL47t_5Vb5dbs-Gg/preview" allow="autoplay; fullscreen" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<!-- Telegram -->
|
||||
<div class="telegram-section">
|
||||
<h3>💬 Заинтересовал Premium?</h3>
|
||||
<p>Напишите автору в Telegram — отвечу на вопросы и дам доступ</p>
|
||||
<a href="https://t.me/Viacheslav_Sh1" target="_blank" class="telegram-link">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.562 8.161c-.18 1.897-.962 6.502-1.359 8.627-.168.9-.5 1.201-.82 1.23-.697.064-1.226-.46-1.901-.903-1.056-.692-1.653-1.123-2.678-1.799-1.185-.781-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.015-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.479.329-.913.489-1.302.481-.428-.009-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.015 3.333-1.386 4.025-1.627 4.476-1.635.099-.002.321.023.465.141.145.118.185.276.204.388.018.112.013.23.01.356z"/>
|
||||
</svg>
|
||||
@Viacheslav_Sh1
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const THEME_KEY = 'loguser-theme';
|
||||
function setTheme(dark) {
|
||||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||
try { localStorage.setItem(THEME_KEY, dark ? 'dark' : 'light'); } catch(e) {}
|
||||
}
|
||||
const saved = localStorage.getItem(THEME_KEY);
|
||||
setTheme(saved === 'dark' || (saved === null && window.matchMedia('(prefers-color-scheme: dark)').matches));
|
||||
document.getElementById('themeBtn').addEventListener('click', () => {
|
||||
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.altKey && (e.key === 't' || e.key === 'T')) {
|
||||
e.preventDefault();
|
||||
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
let currentUid = '';
|
||||
let skip = 0;
|
||||
const take = 50;
|
||||
let loading = false;
|
||||
let allLoaded = false;
|
||||
let premiumChart = null;
|
||||
|
||||
// Модальное окно
|
||||
const modal = document.getElementById('premiumModal');
|
||||
const premiumBtn = document.getElementById('premiumBtn');
|
||||
const closeBtn = document.getElementById('closeModalBtn');
|
||||
|
||||
function openModal() {
|
||||
modal.classList.remove('hidden');
|
||||
loadPremiumChart();
|
||||
}
|
||||
|
||||
premiumBtn.addEventListener('click', openModal);
|
||||
closeBtn.addEventListener('click', () => modal.classList.add('hidden'));
|
||||
modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.add('hidden'); });
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') modal.classList.add('hidden'); });
|
||||
|
||||
async function apiCall(path) {
|
||||
const res = await fetch('/lite/logrequest' + path, { credentials: 'include' });
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
window.location.href = '/lite/logrequest/auth';
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
const json = await res.json();
|
||||
return json.success ? json.data : json;
|
||||
}
|
||||
|
||||
async function loadPremiumChart() {
|
||||
if (premiumChart) return;
|
||||
|
||||
try {
|
||||
const result = await apiCall('/premium-chart?days=3');
|
||||
|
||||
const ctx = document.getElementById('premiumChart').getContext('2d');
|
||||
const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#3ec9d1';
|
||||
const gridColor = getComputedStyle(document.documentElement).getPropertyValue('--chart-grid').trim() || '#2a3040';
|
||||
|
||||
let labels, values;
|
||||
|
||||
if (result && result.data && result.data.length > 0) {
|
||||
labels = result.data.map(d => new Date(d.date).toLocaleDateString('ru', { day: 'numeric', month: 'short' }));
|
||||
values = result.data.map(d => d.count);
|
||||
} else {
|
||||
labels = ['Нет данных'];
|
||||
values = [0];
|
||||
}
|
||||
|
||||
premiumChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Запросы',
|
||||
data: values,
|
||||
borderColor: accentColor,
|
||||
backgroundColor: accentColor + '20',
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointBackgroundColor: accentColor
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { color: gridColor } },
|
||||
x: { grid: { display: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch(e) {
|
||||
console.error('Chart error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const url = currentUid ? `/stats?uid=${encodeURIComponent(currentUid)}` : '/stats';
|
||||
const stats = await apiCall(url);
|
||||
if (!stats) return;
|
||||
|
||||
document.getElementById('statsBlock').style.display = 'block';
|
||||
document.getElementById('statToday').innerText = stats.today || 0;
|
||||
document.getElementById('statMonth').innerText = stats.month || 0;
|
||||
document.getElementById('statDevices').innerText = stats.uniqueUserAgent || 0;
|
||||
document.getElementById('statIps').innerText = stats.uniqueIp || 0;
|
||||
|
||||
const topUsers = document.getElementById('topUsers');
|
||||
const users = stats.topUsers || [];
|
||||
topUsers.innerHTML = users.map((u, idx) => `
|
||||
<div class="grid-item">
|
||||
<span class="grid-rank">${idx + 1}</span>
|
||||
<span class="grid-name">${escapeHtml(u.name)}</span>
|
||||
<span class="grid-count">${u.count}</span>
|
||||
</div>
|
||||
`).join('') || '<div class="empty">Нет данных</div>';
|
||||
|
||||
const topBalancers = document.getElementById('topBalancers');
|
||||
const balancers = stats.topBalancers || [];
|
||||
topBalancers.innerHTML = balancers.map((b, idx) => `
|
||||
<div class="grid-item">
|
||||
<span class="grid-rank">${idx + 1}</span>
|
||||
<span class="grid-name">${escapeHtml(b.name)}</span>
|
||||
<span class="grid-count">${b.count}</span>
|
||||
</div>
|
||||
`).join('') || '<div class="empty">Нет данных</div>';
|
||||
} catch(e) {
|
||||
console.error('Stats error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRequests(reset = true) {
|
||||
if (reset) { skip = 0; allLoaded = false; document.getElementById('requestsList').innerHTML = ''; }
|
||||
if (loading || allLoaded) return;
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
let url = `/api?skip=${skip}&take=${take}`;
|
||||
if (currentUid) url += `&uid=${encodeURIComponent(currentUid)}`;
|
||||
|
||||
const data = await apiCall(url);
|
||||
|
||||
if (!data || (Array.isArray(data) && data.length === 0)) {
|
||||
allLoaded = true;
|
||||
if (skip === 0) document.getElementById('requestsList').innerHTML = '<div class="empty">Нет записей</div>';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (reset) document.getElementById('requestsList').innerHTML = '';
|
||||
|
||||
data.forEach(r => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'request-card';
|
||||
card.innerHTML = `
|
||||
<div class="request-header">
|
||||
<span class="request-time">${new Date(r.time).toLocaleString()}</span>
|
||||
<span class="request-uid">${escapeHtml(r.userUid || r.uid || 'unknown')}</span>
|
||||
<span class="request-ip">${escapeHtml(r.ip || 'unknown')}</span>
|
||||
</div>
|
||||
<div class="request-uri">${escapeHtml(r.uri || '')}</div>
|
||||
`;
|
||||
document.getElementById('requestsList').appendChild(card);
|
||||
});
|
||||
|
||||
skip += data.length;
|
||||
if (data.length < take) allLoaded = true;
|
||||
loading = false;
|
||||
} catch(e) {
|
||||
console.error('Load error:', e);
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/[&<>]/g, m => ({ '&': '&', '<': '<', '>': '>' }[m] || m));
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
currentUid = document.getElementById('uidInput').value.trim();
|
||||
loadStats();
|
||||
loadRequests(true);
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
document.getElementById('uidInput').value = '';
|
||||
currentUid = '';
|
||||
loadStats();
|
||||
loadRequests(true);
|
||||
}
|
||||
|
||||
document.getElementById('logoutBtn').addEventListener('click', () => {
|
||||
window.location.href = '/lite/logrequest/logout';
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
|
||||
if (!loading && !allLoaded) loadRequests(false);
|
||||
}
|
||||
});
|
||||
|
||||
loadStats();
|
||||
loadRequests(true);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user