Файл: new_top/modules/admin/index.php
Строк: 1411
<?php
/**
* Multifunctional Admin Panel
*/
declare(strict_types=1);
$isAdmin = !empty($_SESSION['user_id']) && !empty($_SESSION['user_level']) && (int)$_SESSION['user_level'] >= 9;
$action = $_GET['action'] ?? 'dashboard';
if (!$isAdmin) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
if (!login_throttle_check('admin_login')) {
$error = 'Слишком много попыток. Подождите 15 минут.';
} else {
$login = trim($_POST['login'] ?? '');
$pass = $_POST['password'] ?? '';
$user = Database::fetch('SELECT * FROM user WHERE login = ? AND ban = 0', [$login]);
if ($user && password_verify($pass, $user['password']) && (int)$user['level'] >= 9) {
login_throttle_reset('admin_login');
session_regenerate_id(true);
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['user_level'] = (int)$user['level'];
$_SESSION['user_login'] = $user['login'];
redirect('/?module=admin');
}
login_throttle_hit('admin_login');
$error = 'Неверный логин или пароль';
}
}
?>
<!DOCTYPE html>
<html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<?= pwa_meta() ?>
<title>Вход — Админ</title>
<link rel="stylesheet" href="/assets/css/style.css">
</head><body>
<div class="auth-wrap">
<div class="auth-box">
<div class="logo-lg">A</div>
<h1>Админ-панель</h1>
<?php if (!empty($error)): ?><div class="alert alert-error"><?= e($error) ?></div><?php endif; ?>
<form method="post">
<?= csrf_field() ?>
<label>Логин</label>
<input type="text" name="login" required autofocus>
<label>Пароль</label>
<input type="password" name="password" required>
<button type="submit" class="btn btn-primary btn-block">Войти</button>
</form>
<p class="text-center text-muted" style="margin-top:18px;font-size:.85rem"><a href="/">← На сайт</a></p>
</div>
</div>
<?= pwa_register() ?>
</body></html>
<?php
exit;
}
if ($action === 'logout') {
session_destroy();
redirect('/?module=admin');
}
$flash = $_SESSION['flash'] ?? '';
unset($_SESSION['flash']);
$msg = '';
$err = '';
// ---- POST handlers ----
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$postAction = $_POST['do'] ?? '';
try {
if ($postAction === 'ban_pf') {
$id = (int)($_POST['id'] ?? 0);
Database::query('UPDATE platforms SET ban = 1 WHERE id = ?', [$id]);
$_SESSION['flash'] = 'Площадка заблокирована';
redirect('/?module=admin&action=platforms');
}
if ($postAction === 'unban_pf') {
$id = (int)($_POST['id'] ?? 0);
Database::query('UPDATE platforms SET ban = 0 WHERE id = ?', [$id]);
$_SESSION['flash'] = 'Площадка разблокирована';
redirect('/?module=admin&action=platforms');
}
if ($postAction === 'approve_pf') {
$id = (int)($_POST['id'] ?? 0);
Database::query('UPDATE platforms SET mode = 0 WHERE id = ?', [$id]);
$_SESSION['flash'] = 'Площадка одобрена';
redirect('/?module=admin&action=platforms');
}
if ($postAction === 'mod_pf') {
$id = (int)($_POST['id'] ?? 0);
Database::query('UPDATE platforms SET mode = 1 WHERE id = ?', [$id]);
$_SESSION['flash'] = 'Отправлена на модерацию';
redirect('/?module=admin&action=platforms');
}
if ($postAction === 'del_pf') {
$id = (int)($_POST['id'] ?? 0);
Database::query('DELETE FROM platforms WHERE id = ?', [$id]);
Database::query('DELETE FROM hs WHERE pf = ?', [$id]);
Database::query('DELETE FROM ht WHERE pf = ?', [$id]);
Database::query('DELETE FROM on_log WHERE pf = ?', [$id]);
Database::query('DELETE FROM in_log WHERE pf = ?', [$id]);
$_SESSION['flash'] = 'Площадка удалена';
redirect('/?module=admin&action=platforms');
}
if ($postAction === 'gold_pf') {
$id = (int)($_POST['id'] ?? 0);
$days = max(1, (int)($_POST['days'] ?? 7));
$until = time() + $days * 86400;
Database::query('UPDATE platforms SET gold = ? WHERE id = ?', [$until, $id]);
$_SESSION['flash'] = "GOLD на {$days} дн.";
redirect('/?module=admin&action=platforms');
}
if ($postAction === 'ban_user') {
$id = (int)($_POST['id'] ?? 0);
if ($id !== (int)$_SESSION['user_id']) {
Database::query('UPDATE user SET ban = 1 WHERE id = ?', [$id]);
}
$_SESSION['flash'] = 'Пользователь заблокирован';
redirect('/?module=admin&action=users');
}
if ($postAction === 'unban_user') {
$id = (int)($_POST['id'] ?? 0);
Database::query('UPDATE user SET ban = 0 WHERE id = ?', [$id]);
$_SESSION['flash'] = 'Пользователь разблокирован';
redirect('/?module=admin&action=users');
}
if ($postAction === 'set_level') {
$id = (int)($_POST['id'] ?? 0);
$lvl = (int)($_POST['level'] ?? 1);
if ($id !== (int)$_SESSION['user_id']) {
Database::query('UPDATE user SET level = ? WHERE id = ?', [$lvl, $id]);
}
$_SESSION['flash'] = 'Уровень обновлён';
redirect('/?module=admin&action=users');
}
if ($postAction === 'add_cat') {
$name = trim($_POST['name'] ?? '');
$sort = (int)($_POST['sort'] ?? 0);
if ($name !== '') {
Database::insert('cat', ['name' => mb_substr($name, 0, 80), 'sort' => $sort]);
$_SESSION['flash'] = 'Категория добавлена';
}
redirect('/?module=admin&action=cats');
}
if ($postAction === 'del_cat') {
$id = (int)($_POST['id'] ?? 0);
Database::query('DELETE FROM cat WHERE id = ?', [$id]);
$_SESSION['flash'] = 'Категория удалена';
redirect('/?module=admin&action=cats');
}
if ($postAction === 'save_config') {
Database::query(
'UPDATE config SET title = ?, keywords = ?, description = ?, pf_max = ?, pf_mod = ?, reg = ?, reg_mod = ? WHERE id = 1',
[
trim($_POST['title'] ?? ''),
trim($_POST['keywords'] ?? ''),
trim($_POST['description'] ?? ''),
max(1, (int)($_POST['pf_max'] ?? 100)),
!empty($_POST['pf_mod']) ? 1 : 0,
!empty($_POST['reg']) ? 1 : 0,
!empty($_POST['reg_mod']) ? 1 : 0,
]
);
$_SESSION['flash'] = 'Настройки сохранены';
redirect('/?module=admin&action=config');
}
if ($postAction === 'ban_ip') {
$ip = trim($_POST['ip'] ?? '');
$reason = trim($_POST['reason'] ?? '');
$hours = (int)($_POST['hours'] ?? 0);
if ($ip !== '' && filter_var($ip, FILTER_VALIDATE_IP)) {
AntiCheat::banIp($ip, $reason, $hours, (int)$_SESSION['user_id']);
$_SESSION['flash'] = 'IP заблокирован';
} else {
$_SESSION['flash'] = 'Некорректный IP';
}
redirect('/?module=admin&action=anticheat');
}
if ($postAction === 'unban_ip') {
$ip = trim($_POST['ip'] ?? '');
AntiCheat::unbanIp($ip);
$_SESSION['flash'] = 'IP разблокирован';
redirect('/?module=admin&action=anticheat');
}
if ($postAction === 'save_points_cfg') {
Points::setCfg('enabled', !empty($_POST['enabled']) ? '1' : '0');
Points::setCfg('in_reward', (string)max(0, (int)($_POST['in_reward'] ?? 1)));
Points::setCfg('gold_day_cost', (string)max(1, (int)($_POST['gold_day_cost'] ?? 50)));
Points::setCfg('ad_point_per_currency', (string)max(1, (int)($_POST['ad_point_per_currency'] ?? 1)));
$_SESSION['flash'] = 'Настройки баллов сохранены';
redirect('/?module=admin&action=points');
}
if ($postAction === 'adjust_points') {
$uid = (int)($_POST['user_id'] ?? 0);
$delta = (int)($_POST['delta'] ?? 0);
$note = mb_substr(trim($_POST['note'] ?? 'admin'), 0, 40);
if ($uid > 0 && $delta !== 0) {
if (Points::add($uid, $delta, 'admin', 0)) {
$_SESSION['flash'] = 'Баллы обновлены';
} else {
$_SESSION['flash'] = 'Не удалось (недостаточно при списании?)';
}
}
redirect('/?module=admin&action=points');
}
if ($postAction === 'add_ad') {
$pos = trim($_POST['position'] ?? 'home_top');
if (!isset(Ads::positions()[$pos])) $pos = 'home_top';
$url = sanitize_platform_url((string)($_POST['url'] ?? '')) ?? '';
$name = mb_substr(trim($_POST['name'] ?? ''), 0, 64);
$text = mb_substr(trim($_POST['text'] ?? ''), 0, 200);
$days = max(1, (int)($_POST['days'] ?? 7));
$now = time();
if ($url === '' || $name === '') {
$_SESSION['flash'] = 'URL и название обязательны';
} else {
Database::insert('ads', [
'position' => $pos,
'user_id' => 0,
'status' => 1,
'url' => $url,
'name' => $name,
'text' => $text,
'color' => mb_substr(trim($_POST['color'] ?? ''), 0, 32),
'bold' => !empty($_POST['bold']) ? 1 : 0,
'italic' => !empty($_POST['italic']) ? 1 : 0,
'days' => $days,
'price' => 0,
'time_pay' => $now,
'time_add' => $now,
'time_end' => $now + $days * 86400,
]);
$_SESSION['flash'] = 'Реклама добавлена';
}
redirect('/?module=admin&action=ads');
}
if ($postAction === 'ad_status') {
$id = (int)($_POST['id'] ?? 0);
$st = (int)($_POST['status'] ?? 0);
if ($st === 1) {
$ad = Database::fetch('SELECT * FROM ads WHERE id = ?', [$id]);
$days = max(1, (int)($ad['days'] ?? 7));
$end = time() + $days * 86400;
if ($ad && (int)$ad['time_end'] > time()) {
$end = (int)$ad['time_end'];
}
Database::query('UPDATE ads SET status = 1, time_pay = ?, time_end = ? WHERE id = ?', [time(), $end, $id]);
} else {
Database::query('UPDATE ads SET status = ? WHERE id = ?', [$st, $id]);
}
$_SESSION['flash'] = 'Статус обновлён';
redirect('/?module=admin&action=ads');
}
if ($postAction === 'del_ad') {
Database::query('DELETE FROM ads WHERE id = ?', [(int)($_POST['id'] ?? 0)]);
$_SESSION['flash'] = 'Реклама удалена';
redirect('/?module=admin&action=ads');
}
if ($postAction === 'save_ad_prices') {
$positions = Ads::positions();
foreach ($positions as $pos => $title) {
$price = (float)($_POST['price'][$pos] ?? 0);
$en = !empty($_POST['enabled'][$pos]) ? 1 : 0;
Database::query(
'INSERT INTO ad_prices (position, title, price_day, enabled, sort) VALUES (?, ?, ?, ?, 0)
ON DUPLICATE KEY UPDATE title = VALUES(title), price_day = VALUES(price_day), enabled = VALUES(enabled)',
[$pos, $title, $price, $en]
);
}
$_SESSION['flash'] = 'Цены сохранены';
redirect('/?module=admin&action=ads');
}
if ($postAction === 'add_news') {
$title = trim($_POST['title'] ?? '');
$text = trim($_POST['text'] ?? '');
if ($title !== '' && $text !== '') {
try {
Database::insert('news', [
'title' => mb_substr($title, 0, 150),
'text' => $text,
'time_add' => time(),
'active' => 1,
]);
$_SESSION['flash'] = 'Новость добавлена';
} catch (Throwable $e) {
$_SESSION['flash'] = 'Ошибка: создайте таблицу news (см. sql/migrate_news.sql)';
}
}
redirect('/?module=admin&action=news');
}
if ($postAction === 'del_news') {
$id = (int)($_POST['id'] ?? 0);
try { Database::query('DELETE FROM news WHERE id = ?', [$id]); } catch (Throwable $e) {}
$_SESSION['flash'] = 'Новость удалена';
redirect('/?module=admin&action=news');
}
if ($postAction === 'edit_pf') {
$id = (int)($_POST['id'] ?? 0);
$safeUrl = sanitize_platform_url((string)($_POST['url'] ?? ''));
if ($safeUrl === null) {
throw new RuntimeException('Некорректный URL');
}
Database::query(
'UPDATE platforms SET url = ?, message = ?, cat_id = ? WHERE id = ?',
[
$safeUrl,
mb_substr(trim($_POST['message'] ?? ''), 0, 120),
(int)($_POST['cat_id'] ?? 13),
$id,
]
);
$_SESSION['flash'] = 'Площадка обновлена';
redirect('/?module=admin&action=platforms');
}
} catch (Throwable $e) {
$err = $e->getMessage();
}
}
function adminHeader(string $title, string $active): void
{
$tabs = [
'dashboard' => 'Обзор',
'stats' => 'Графики',
'platforms' => 'Площадки',
'users' => 'Юзеры',
'cats' => 'Категории',
'config' => 'Настройки',
'news' => 'Новости',
'anticheat' => 'Антинакрутка',
'ads' => 'Реклама',
'points' => 'Баллы',
];
echo '<!DOCTYPE html><html lang="ru"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<?= pwa_meta() ?>
<title>' . e($title) . ' — Админ</title>
<link rel="stylesheet" href="/assets/css/style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head><body><div class="app">
<header class="topbar">
<div class="topbar-brand"><div class="logo">A</div><span>Админ</span></div>
<nav class="topbar-nav">
<a href="/">Топ</a>
<a href="/?module=admin&action=logout">Выход</a>
</nav>
</header>
<nav class="tabs">';
foreach ($tabs as $k => $label) {
$cls = $k === $active ? 'active' : '';
echo '<a class="' . $cls . '" href="/?module=admin&action=' . $k . '">' . e($label) . '</a>';
}
echo '</nav>';
}
function adminFooter(): void
{
echo pwa_register() . '</div></body></html>';
}
// ================= DASHBOARD =================
if ($action === 'dashboard') {
$s = Stats::summary();
$top = Stats::topPlatforms(5);
adminHeader('Обзор', 'dashboard');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if ($err) echo '<div class="alert alert-error">' . e($err) . '</div>';
?>
<h1 class="page-title">Панель управления</h1>
<p class="page-sub">Сводка по системе</p>
<div class="metric-row metric-row-4">
<div class="metric"><div class="m-label">Площадки</div><div class="m-val blue"><?= $s['platforms'] ?></div></div>
<div class="metric"><div class="m-label">Активные</div><div class="m-val"><?= $s['active'] ?></div></div>
<div class="metric"><div class="m-label">На модерации</div><div class="m-val gold"><?= $s['mod'] ?></div></div>
<div class="metric"><div class="m-label">Бан</div><div class="m-val danger"><?= $s['banned'] ?></div></div>
<div class="metric"><div class="m-label">Пользователи</div><div class="m-val blue"><?= $s['users'] ?></div></div>
<div class="metric"><div class="m-label">Хосты</div><div class="m-val"><?= number_format($s['hs'], 0, '', ' ') ?></div></div>
<div class="metric"><div class="m-label">Хиты</div><div class="m-val"><?= number_format($s['ht'], 0, '', ' ') ?></div></div>
<div class="metric"><div class="m-label">Онлайн</div><div class="m-val gold"><?= $s['online'] ?></div></div>
</div>
<div class="card">
<h2>Топ-5 площадок</h2>
<?php if (!$top): ?>
<p class="text-muted">Нет данных</p>
<?php else: ?>
<div class="table-wrap"><table class="data">
<thead><tr><th>#</th><th>URL</th><th>Хосты</th><th>Хиты</th></tr></thead>
<tbody>
<?php foreach ($top as $i => $p): ?>
<tr>
<td><?= $i + 1 ?></td>
<td><strong><?= e($p['url']) ?></strong></td>
<td><?= (int)$p['hs'] ?></td>
<td><?= (int)$p['ht'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
<?php endif; ?>
</div>
<?php
$catsSum = Stats::categorySummary();
$movers = Stats::movers(5);
$stale = Stats::stalePlatforms(7, 10);
?>
<div class="card">
<h2>Категории (активные)</h2>
<div class="table-wrap"><table class="data">
<thead><tr><th>Категория</th><th>Пл.</th><th>Хосты</th><th>Хиты</th><th>In</th></tr></thead>
<tbody>
<?php foreach ($catsSum as $c): if ((int)$c['pf_cnt'] < 1) continue; ?>
<tr>
<td><?= e($c['name']) ?></td>
<td><?= (int)$c['pf_cnt'] ?></td>
<td><?= number_format((int)$c['hs'], 0, '', ' ') ?></td>
<td><?= number_format((int)$c['ht'], 0, '', ' ') ?></td>
<td><?= (int)$c['inn'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
</div>
<div class="chart-grid chart-grid-2">
<div class="card">
<h2>Рост в топе</h2>
<?php if (empty($movers['up'])): ?>
<p class="text-muted">Нет снимков ранга за вчера (нужен cron)</p>
<?php else: foreach ($movers['up'] as $m): ?>
<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:.88rem">
<strong><?= e($m['url']) ?></strong>
<span style="color:var(--accent)">↑<?= (int)$m['delta'] ?></span>
<span class="text-muted">#<?= (int)$m['prev'] ?> → #<?= (int)$m['rank'] ?></span>
</div>
<?php endforeach; endif; ?>
</div>
<div class="card">
<h2>Падение в топе</h2>
<?php if (empty($movers['down'])): ?>
<p class="text-muted">Нет данных</p>
<?php else: foreach ($movers['down'] as $m): ?>
<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:.88rem">
<strong><?= e($m['url']) ?></strong>
<span style="color:var(--danger)">↓<?= abs((int)$m['delta']) ?></span>
<span class="text-muted">#<?= (int)$m['prev'] ?> → #<?= (int)$m['rank'] ?></span>
</div>
<?php endforeach; endif; ?>
</div>
</div>
<div class="card">
<h2>Без хитов 7+ дней</h2>
<?php if (!$stale): ?>
<p class="text-muted">Таких площадок нет</p>
<?php else: foreach ($stale as $s): ?>
<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:.88rem">
#<?= (int)$s['id'] ?> · <strong><?= e($s['url']) ?></strong>
<span class="text-muted">· добавлена <?= $s['time_add'] ? date('d.m.Y', (int)$s['time_add']) : '—' ?></span>
</div>
<?php endforeach; endif; ?>
</div>
<?php
adminFooter();
exit;
}
// ================= STATS / CHARTS =================
if ($action === 'stats') {
$days = (int)($_GET['days'] ?? 14);
if (!in_array($days, [7, 14, 30], true)) {
$days = 14;
}
$hosts = Stats::dailyHosts(0, $days);
$hits = Stats::dailyHits(0, $days);
$ins = Stats::dailyIn(0, $days);
$labels = Stats::shortLabels(array_keys($hosts));
$dev = Stats::deviceSplit(0);
$top = Stats::topPlatforms(10);
$topIn = Stats::topByIn(8);
$hourly = Stats::hourlyHits(0);
$hourLabels = array_map(static function ($h) { return sprintf('%02d', $h); }, range(0, 23));
$weekday = Stats::weekdayHits(0, 28);
$weekLabels = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
$weekData = [];
for ($i = 1; $i <= 7; $i++) {
$weekData[] = (int)($weekday[$i] ?? 0);
}
$sum = Stats::summary();
$growth = Stats::hostGrowth(0);
$peak = Stats::peakHour(0);
$hits24 = Stats::hitsLastHours(24, 0);
$hosts24 = Stats::hostsLastHours(24, 0);
$newPf = Stats::newPlatformsCount(7);
$cats = Stats::categorySummary();
$movers = Stats::movers(8);
$stale = Stats::stalePlatforms(7, 15);
adminHeader('Статистика', 'stats');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
?>
<h1 class="page-title">Статистика системы</h1>
<p class="page-sub">Онлайн <?= (int)$sum['online'] ?> · пик сегодня <?= sprintf('%02d', $peak['hour']) ?>:00 (<?= (int)$peak['hits'] ?> хитов)</p>
<div class="tabs" style="margin-bottom:14px">
<a class="<?= $days===7?'active':'' ?>" href="/?module=admin&action=stats&days=7">7 дней</a>
<a class="<?= $days===14?'active':'' ?>" href="/?module=admin&action=stats&days=14">14 дней</a>
<a class="<?= $days===30?'active':'' ?>" href="/?module=admin&action=stats&days=30">30 дней</a>
</div>
<div class="metrics">
<div class="metric"><div class="m-label">Активные</div><div class="m-val"><?= (int)$sum['active'] ?></div></div>
<div class="metric"><div class="m-label">Хосты</div><div class="m-val accent"><?= (int)$sum['hs'] ?></div></div>
<div class="metric"><div class="m-label">Хиты</div><div class="m-val"><?= (int)$sum['ht'] ?></div></div>
<div class="metric"><div class="m-label">In</div><div class="m-val gold"><?= (int)$sum['in'] ?></div></div>
<div class="metric"><div class="m-label">24ч хиты</div><div class="m-val"><?= (int)$hits24 ?></div></div>
<div class="metric"><div class="m-label">24ч хосты</div><div class="m-val"><?= (int)$hosts24 ?></div></div>
<div class="metric"><div class="m-label">Рост хостов</div><div class="m-val <?= $growth['pct']>=0?'accent':'' ?>"><?= ($growth['pct']>=0?'+':'') . e((string)$growth['pct']) ?>%</div></div>
<div class="metric"><div class="m-label">Новые 7д</div><div class="m-val"><?= (int)$newPf ?></div></div>
</div>
<div class="card">
<h2>Хосты · хиты · переходы (In)</h2>
<div class="chart-box" style="height:260px"><canvas id="chartMain"></canvas></div>
</div>
<div class="card">
<h2>Сегодня по часам</h2>
<div class="chart-box" style="height:200px"><canvas id="chartHour"></canvas></div>
</div>
<div class="card">
<h2>По дням недели (28 дн.)</h2>
<div class="chart-box" style="height:180px"><canvas id="chartWeek"></canvas></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div class="card">
<h2>Устройства</h2>
<div class="chart-box" style="height:200px"><canvas id="chartDev"></canvas></div>
</div>
<div class="card">
<h2>Топ по хостам</h2>
<div class="chart-box" style="height:200px"><canvas id="chartTop"></canvas></div>
</div>
</div>
<div class="card">
<h2>Топ по переходам (In)</h2>
<div class="chart-box" style="height:220px"><canvas id="chartIn"></canvas></div>
</div>
<div class="card">
<h2>Категории</h2>
<div class="chart-box" style="height:200px"><canvas id="chartCat"></canvas></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div class="card">
<h2>Рост в топе</h2>
<?php if (empty($movers['up'])): ?>
<p class="text-muted">Нет снимков (нужен cron)</p>
<?php else: foreach ($movers['up'] as $m): ?>
<div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:.85rem">
<strong><?= e($m['url']) ?></strong>
<span style="color:var(--accent)">↑<?= (int)$m['delta'] ?></span>
<span class="text-muted">#<?= (int)$m['prev'] ?>→#<?= (int)$m['rank'] ?></span>
</div>
<?php endforeach; endif; ?>
</div>
<div class="card">
<h2>Падение в топе</h2>
<?php if (empty($movers['down'])): ?>
<p class="text-muted">Нет данных</p>
<?php else: foreach ($movers['down'] as $m): ?>
<div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:.85rem">
<strong><?= e($m['url']) ?></strong>
<span style="color:var(--danger)">↓<?= abs((int)$m['delta']) ?></span>
<span class="text-muted">#<?= (int)$m['prev'] ?>→#<?= (int)$m['rank'] ?></span>
</div>
<?php endforeach; endif; ?>
</div>
</div>
<div class="card">
<h2>Без активности 7+ дней</h2>
<?php if (!$stale): ?>
<p class="text-muted">Нет</p>
<?php else: foreach ($stale as $s): ?>
<div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:.85rem">
#<?= (int)$s['id'] ?> · <strong><?= e($s['url']) ?></strong>
</div>
<?php endforeach; endif; ?>
</div>
<script>
const grid='rgba(255,255,255,0.06)';
Chart.defaults.color='#8b9cb8';
Chart.defaults.borderColor=grid;
const common={responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'top',labels:{boxWidth:12}}},scales:{x:{grid:{color:grid}},y:{beginAtZero:true,grid:{color:grid}}}};
new Chart(document.getElementById('chartMain'),{type:'line',data:{labels:<?= json_encode($labels, JSON_UNESCAPED_UNICODE) ?>,datasets:[
{label:'Хосты',data:<?= json_encode(array_values($hosts)) ?>,borderColor:'#2ee6b8',backgroundColor:'rgba(46,230,184,0.12)',fill:true,tension:0.35,pointRadius:2},
{label:'Хиты',data:<?= json_encode(array_values($hits)) ?>,borderColor:'#6b9fff',backgroundColor:'rgba(107,159,255,0.1)',fill:true,tension:0.35,pointRadius:2},
{label:'In',data:<?= json_encode(array_values($ins)) ?>,borderColor:'#ffc94a',backgroundColor:'rgba(255,201,74,0.1)',fill:true,tension:0.35,pointRadius:2}
]},options:common});
new Chart(document.getElementById('chartHour'),{type:'bar',data:{labels:<?= json_encode($hourLabels) ?>,datasets:[{label:'Хиты',data:<?= json_encode(array_values($hourly)) ?>,backgroundColor:'rgba(107,159,255,0.7)',borderRadius:4}]},options:{...common,plugins:{legend:{display:false}}}});
new Chart(document.getElementById('chartWeek'),{type:'bar',data:{labels:<?= json_encode($weekLabels, JSON_UNESCAPED_UNICODE) ?>,datasets:[{label:'Хиты',data:<?= json_encode($weekData) ?>,backgroundColor:'rgba(46,230,184,0.65)',borderRadius:6}]},options:{...common,plugins:{legend:{display:false}}}});
new Chart(document.getElementById('chartDev'),{type:'doughnut',data:{labels:['Mobile','PC'],datasets:[{data:[<?= (int)$dev['mb'] ?>,<?= (int)$dev['pc'] ?>],backgroundColor:['#2ee6b8','#6b9fff'],borderWidth:0}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom'}}}});
new Chart(document.getElementById('chartTop'),{type:'bar',data:{labels:<?= json_encode(array_map(function($p){return mb_substr($p['url'],0,16);},$top), JSON_UNESCAPED_UNICODE) ?>,datasets:[{label:'Хосты',data:<?= json_encode(array_map(function($p){return (int)$p['hs'];},$top)) ?>,backgroundColor:'#6b9fff',borderRadius:5}]},options:{...common,indexAxis:'y',plugins:{legend:{display:false}}}});
new Chart(document.getElementById('chartIn'),{type:'bar',data:{labels:<?= json_encode(array_map(function($p){return mb_substr($p['url'],0,16);},$topIn), JSON_UNESCAPED_UNICODE) ?>,datasets:[{label:'In',data:<?= json_encode(array_map(function($p){return (int)$p['in_count'];},$topIn)) ?>,backgroundColor:'#ffc94a',borderRadius:5}]},options:{...common,indexAxis:'y',plugins:{legend:{display:false}}}});
new Chart(document.getElementById('chartCat'),{type:'bar',data:{labels:<?= json_encode(array_map(function($c){return mb_substr($c['name']??'',0,14);},$cats), JSON_UNESCAPED_UNICODE) ?>,datasets:[{label:'Площадки',data:<?= json_encode(array_map(function($c){return (int)($c['cnt']??$c['c']??0);},$cats)) ?>,backgroundColor:'rgba(107,159,255,0.75)',borderRadius:5}]},options:{...common,plugins:{legend:{display:false}}}});
</script>
<?php
adminFooter();
exit;
}
if ($action === 'platforms') {
$filter = $_GET['f'] ?? 'all';
$sql = 'SELECT p.*, c.name AS cat_name, u.login AS owner FROM platforms p
LEFT JOIN cat c ON c.id = p.cat_id LEFT JOIN user u ON u.id = p.user_id WHERE 1=1';
if ($filter === 'mod') $sql .= ' AND p.mode = 1';
elseif ($filter === 'ban') $sql .= ' AND p.ban = 1';
elseif ($filter === 'ok') $sql .= ' AND p.mode = 0 AND p.ban = 0';
$sql .= ' ORDER BY p.id DESC LIMIT 200';
$list = Database::fetchAll($sql);
$cats = Database::fetchAll('SELECT * FROM cat ORDER BY sort, id');
$editId = (int)($_GET['edit'] ?? 0);
$edit = $editId ? Database::fetch('SELECT * FROM platforms WHERE id = ?', [$editId]) : null;
adminHeader('Площадки', 'platforms');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
?>
<h1 class="page-title">Площадки</h1>
<nav class="tabs">
<a class="<?= $filter==='all'?'active':'' ?>" href="/?module=admin&action=platforms">Все</a>
<a class="<?= $filter==='ok'?'active':'' ?>" href="/?module=admin&action=platforms&f=ok">Активные</a>
<a class="<?= $filter==='mod'?'active':'' ?>" href="/?module=admin&action=platforms&f=mod">Модерация</a>
<a class="<?= $filter==='ban'?'active':'' ?>" href="/?module=admin&action=platforms&f=ban">Бан</a>
</nav>
<?php if ($edit): ?>
<div class="card">
<h2>Редактировать #<?= (int)$edit['id'] ?></h2>
<form method="post"><?= csrf_field() ?>
<input type="hidden" name="do" value="edit_pf">
<input type="hidden" name="id" value="<?= (int)$edit['id'] ?>">
<label>URL</label>
<input type="text" name="url" value="<?= e($edit['url']) ?>" required>
<label>Описание</label>
<input type="text" name="message" value="<?= e($edit['message']) ?>">
<label>Категория</label>
<select name="cat_id">
<?php foreach ($cats as $c): ?>
<option value="<?= (int)$c['id'] ?>" <?= (int)$c['id']===(int)$edit['cat_id']?'selected':'' ?>><?= e($c['name']) ?></option>
<?php endforeach; ?>
</select>
<button class="btn btn-primary" type="submit">Сохранить</button>
<a class="btn btn-ghost" href="/?module=admin&action=platforms">Отмена</a>
</form>
</div>
<?php endif; ?>
<?php if (!$list): ?>
<div class="empty"><div>Список пуст</div></div>
<?php else: foreach ($list as $p): ?>
<article class="rank-card" style="grid-template-columns:1fr; gap:8px">
<div style="display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:center">
<div>
<strong>#<?= (int)$p['id'] ?> · <?= e($p['url']) ?></strong>
<?php if ((int)$p['ban']): ?><span class="badge badge-ban">BAN</span><?php endif; ?>
<?php if ((int)$p['mode']): ?><span class="badge badge-mod">MOD</span><?php endif; ?>
<?php if ((int)$p['gold'] > time()): ?><span class="badge-gold">★ Gold</span><?php endif; ?>
<div class="text-muted" style="font-size:.8rem;margin-top:4px">
<?= e($p['cat_name'] ?? '—') ?> · <?= e($p['owner'] ?? '?') ?> · hs <?= (int)$p['hs'] ?> / ht <?= (int)$p['ht'] ?>
</div>
</div>
</div>
<div class="act-btns">
<a href="/?module=admin&action=platforms&edit=<?= (int)$p['id'] ?>">Изменить</a>
<a href="/?module=stats&id=<?= (int)$p['id'] ?>">Статистика</a>
<?php if ((int)$p['ban']): ?>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="do" value="unban_pf"><input type="hidden" name="id" value="<?= (int)$p['id'] ?>"><button class="ok" type="submit">Разбан</button></form>
<?php else: ?>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="do" value="ban_pf"><input type="hidden" name="id" value="<?= (int)$p['id'] ?>"><button class="danger" type="submit">Бан</button></form>
<?php endif; ?>
<?php if ((int)$p['mode']): ?>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="do" value="approve_pf"><input type="hidden" name="id" value="<?= (int)$p['id'] ?>"><button class="ok" type="submit">Одобрить</button></form>
<?php else: ?>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="do" value="mod_pf"><input type="hidden" name="id" value="<?= (int)$p['id'] ?>"><button type="submit">На мод.</button></form>
<?php endif; ?>
<form method="post" style="display:inline" onsubmit="return confirm('Удалить?')"><?= csrf_field() ?>
<input type="hidden" name="do" value="del_pf"><input type="hidden" name="id" value="<?= (int)$p['id'] ?>">
<button class="danger" type="submit">Удалить</button>
</form>
<form method="post" style="display:inline;display:flex;gap:4px;align-items:center"><?= csrf_field() ?>
<input type="hidden" name="do" value="gold_pf"><input type="hidden" name="id" value="<?= (int)$p['id'] ?>">
<input type="number" name="days" value="7" min="1" max="365" style="width:56px;margin:0;padding:4px 6px;font-size:.75rem">
<button type="submit">GOLD</button>
</form>
</div>
</article>
<?php endforeach; endif;
adminFooter();
exit;
}
// ================= USERS =================
if ($action === 'users') {
$users = Database::fetchAll('SELECT * FROM user ORDER BY id DESC LIMIT 200');
adminHeader('Пользователи', 'users');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
?>
<h1 class="page-title">Пользователи</h1>
<p class="page-sub"><?= count($users) ?> записей</p>
<?php foreach ($users as $u): ?>
<article class="rank-card" style="grid-template-columns:1fr;gap:8px">
<div>
<strong>#<?= (int)$u['id'] ?> · <?= e($u['login']) ?></strong>
<?php if ((int)$u['ban']): ?><span class="badge badge-ban">BAN</span><?php endif; ?>
<?php if ((int)$u['level'] >= 9): ?><span class="badge badge-ok">ADMIN</span><?php endif; ?>
<div class="text-muted" style="font-size:.8rem;margin-top:4px">
<?= e($u['email'] ?: 'без email') ?> · level <?= (int)$u['level'] ?>
· рег. <?= $u['reg_time'] ? date('d.m.Y', (int)$u['reg_time']) : '—' ?>
</div>
</div>
<div class="act-btns">
<?php if ((int)$u['id'] !== (int)$_SESSION['user_id']): ?>
<?php if ((int)$u['ban']): ?>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="do" value="unban_user"><input type="hidden" name="id" value="<?= (int)$u['id'] ?>"><button class="ok" type="submit">Разбан</button></form>
<?php else: ?>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="do" value="ban_user"><input type="hidden" name="id" value="<?= (int)$u['id'] ?>"><button class="danger" type="submit">Бан</button></form>
<?php endif; ?>
<form method="post" style="display:inline;display:flex;gap:4px;align-items:center"><?= csrf_field() ?>
<input type="hidden" name="do" value="set_level"><input type="hidden" name="id" value="<?= (int)$u['id'] ?>">
<select name="level" style="width:auto;margin:0;padding:4px 8px;font-size:.75rem">
<option value="1" <?= (int)$u['level']===1?'selected':'' ?>>User</option>
<option value="9" <?= (int)$u['level']>=9?'selected':'' ?>>Admin</option>
</select>
<button type="submit">OK</button>
</form>
<?php else: ?>
<span class="text-muted" style="font-size:.8rem">это вы</span>
<?php endif; ?>
</div>
</article>
<?php endforeach;
adminFooter();
exit;
}
// ================= CATEGORIES =================
if ($action === 'cats') {
$cats = Database::fetchAll('SELECT c.*, (SELECT COUNT(*) FROM platforms p WHERE p.cat_id = c.id) AS cnt FROM cat c ORDER BY sort, id');
adminHeader('Категории', 'cats');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
?>
<h1 class="page-title">Категории</h1>
<div class="card">
<h2>Добавить</h2>
<form method="post" class="form-row form-row-2">
<input type="hidden" name="do" value="add_cat">
<div>
<label>Название</label>
<input type="text" name="name" required maxlength="80">
</div>
<div>
<label>Сортировка</label>
<input type="number" name="sort" value="0">
</div>
<button class="btn btn-primary" type="submit">Добавить</button>
</form>
</div>
<?php foreach ($cats as $c): ?>
<article class="rank-card" style="grid-template-columns:1fr auto;gap:10px">
<div>
<strong><?= e($c['name']) ?></strong>
<div class="text-muted" style="font-size:.8rem">id <?= (int)$c['id'] ?> · sort <?= (int)$c['sort'] ?> · площадок <?= (int)$c['cnt'] ?></div>
</div>
<form method="post" onsubmit="return confirm('Удалить категорию?')"><?= csrf_field() ?>
<input type="hidden" name="do" value="del_cat">
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<button class="btn btn-sm btn-ghost" type="submit" style="color:var(--danger)">Удалить</button>
</form>
</article>
<?php endforeach;
adminFooter();
exit;
}
// ================= CONFIG =================
if ($action === 'config') {
$cfg = Database::fetch('SELECT * FROM config WHERE id = 1') ?: [];
adminHeader('Настройки', 'config');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
?>
<h1 class="page-title">Настройки сайта</h1>
<div class="card">
<form method="post"><?= csrf_field() ?>
<input type="hidden" name="do" value="save_config">
<label>Название</label>
<input type="text" name="title" value="<?= e($cfg['title'] ?? '') ?>" required>
<label>Keywords</label>
<input type="text" name="keywords" value="<?= e($cfg['keywords'] ?? '') ?>">
<label>Description</label>
<input type="text" name="description" value="<?= e($cfg['description'] ?? '') ?>">
<label>Макс. площадок в топе</label>
<input type="number" name="pf_max" value="<?= (int)($cfg['pf_max'] ?? 100) ?>" min="1">
<label style="display:flex;align-items:center;gap:8px;color:var(--text)">
<input type="checkbox" name="pf_mod" value="1" <?= !empty($cfg['pf_mod'])?'checked':'' ?> style="width:auto;margin:0">
Модерация новых площадок
</label>
<label style="display:flex;align-items:center;gap:8px;color:var(--text);margin-top:10px">
<input type="checkbox" name="reg" value="1" <?= !empty($cfg['reg'])?'checked':'' ?> style="width:auto;margin:0">
Регистрация открыта
</label>
<label style="display:flex;align-items:center;gap:8px;color:var(--text);margin-top:10px;margin-bottom:16px">
<input type="checkbox" name="reg_mod" value="1" <?= !empty($cfg['reg_mod'])?'checked':'' ?> style="width:auto;margin:0">
Модерация регистрации
</label>
<button class="btn btn-primary btn-block" type="submit">Сохранить</button>
</form>
</div>
<div class="card">
<h2>Cron (суточный сброс)</h2>
<p class="text-muted" style="font-size:.88rem;line-height:1.6">
Вызывайте раз в сутки (00:05):
<code class="mono">/?module=cron&key=КЛЮЧ</code><br>
Ключ: <code>cron_key</code> в <code>core/config/database.php</code>
</p>
</div>
<?php
adminFooter();
exit;
}
if ($action === 'anticheat') {
$suspect = AntiCheat::suspiciousPlatforms(15);
$topIps = AntiCheat::topIpsToday(25);
$blocks = AntiCheat::recentBlocks(40);
$bans = [];
try {
$bans = Database::fetchAll('SELECT * FROM ip_ban ORDER BY time_add DESC LIMIT 100');
} catch (Throwable $e) {
$err = 'Выполните sql/migrate_anticheat.sql';
}
adminHeader('Антинакрутка', 'anticheat');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if (!empty($err)) echo '<div class="alert alert-error">' . e($err) . '</div>';
?>
<h1 class="page-title">Антинакрутка</h1>
<p class="page-sub">Лимиты: <?= (int)AntiCheat::HIT_INTERVAL ?>с между хитами · max <?= (int)AntiCheat::MAX_HITS_IP_PF_DAY ?>/день на IP+площадку · <?= (int)AntiCheat::MAX_HITS_IP_HOUR ?>/час на IP</p>
<div class="card">
<h2>Забанить IP</h2>
<form method="post" class="form-row form-row-2">
<input type="hidden" name="do" value="ban_ip">
<div>
<label>IP</label>
<input type="text" name="ip" required placeholder="1.2.3.4">
</div>
<div>
<label>Часов (0 = навсегда)</label>
<input type="number" name="hours" value="24" min="0">
</div>
<div style="grid-column:1/-1">
<label>Причина</label>
<input type="text" name="reason" maxlength="200">
</div>
<button class="btn btn-primary" type="submit">Забанить</button>
</form>
</div>
<div class="card">
<h2>Подозрительные площадки (ht/hs ≥ <?= (int)AntiCheat::SUSPECT_HT_HS_RATIO ?>)</h2>
<?php if (!$suspect): ?>
<p class="text-muted">Нет аномалий</p>
<?php else: foreach ($suspect as $s): ?>
<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:.88rem;display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap">
<div>
#<?= (int)$s['id'] ?> · <strong><?= e($s['url']) ?></strong>
<span class="text-muted">hs <?= (int)$s['hs'] ?> / ht <?= (int)$s['ht'] ?></span>
</div>
<span class="badge badge-ban">ratio <?= e((string)$s['ratio']) ?></span>
</div>
<?php endforeach; endif; ?>
</div>
<div class="card">
<h2>Топ IP сегодня</h2>
<div class="table-wrap"><table class="data">
<thead><tr><th>IP</th><th>Хиты</th><th>Площадок</th><th></th></tr></thead>
<tbody>
<?php foreach ($topIps as $row): ?>
<tr>
<td class="mono"><?= e($row['ip']) ?></td>
<td><?= (int)$row['hits'] ?></td>
<td><?= (int)$row['platforms'] ?></td>
<td>
<form method="post" style="display:inline"><?= csrf_field() ?>
<input type="hidden" name="do" value="ban_ip">
<input type="hidden" name="ip" value="<?= e($row['ip']) ?>">
<input type="hidden" name="hours" value="24">
<input type="hidden" name="reason" value="auto from top IPs">
<button type="submit" class="btn btn-sm btn-ghost" style="color:var(--danger)">Бан 24ч</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
</div>
<div class="card">
<h2>Активные баны IP</h2>
<?php if (!$bans): ?>
<p class="text-muted">Список пуст</p>
<?php else: foreach ($bans as $b): ?>
<div style="padding:8px 0;border-bottom:1px solid var(--border);font-size:.88rem;display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:center">
<div>
<strong class="mono"><?= e($b['ip']) ?></strong>
· <?= e($b['reason'] ?: '—') ?>
· до <?= (int)$b['until_time'] === 0 ? '∞' : date('d.m.Y H:i', (int)$b['until_time']) ?>
</div>
<form method="post"><?= csrf_field() ?>
<input type="hidden" name="do" value="unban_ip">
<input type="hidden" name="ip" value="<?= e($b['ip']) ?>">
<button type="submit" class="btn btn-sm btn-ghost">Разбан</button>
</form>
</div>
<?php endforeach; endif; ?>
</div>
<div class="card">
<h2>Последние блокировки</h2>
<?php if (!$blocks): ?>
<p class="text-muted">Пока нет записей (или нет таблицы cheat_log)</p>
<?php else: foreach ($blocks as $b): ?>
<div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:.8rem;color:var(--text-muted)">
<?= date('d.m H:i:s', (int)$b['time']) ?>
· <span class="mono"><?= e($b['ip']) ?></span>
· pf #<?= (int)$b['pf'] ?>
· <?= e($b['kind']) ?>
· <strong style="color:var(--danger)"><?= e(AntiCheat::reasonLabel($b['reason'])) ?></strong>
</div>
<?php endforeach; endif; ?>
</div>
<?php
adminFooter();
exit;
}
if ($action === 'points') {
$err = '';
try {
$cfg = [
'enabled' => Points::cfg('enabled', '1'),
'in_reward' => Points::cfg('in_reward', '1'),
'gold_day_cost' => Points::cfg('gold_day_cost', '50'),
'ad_point_per_currency' => Points::cfg('ad_point_per_currency', '1'),
];
$topUsers = Database::fetchAll('SELECT id, login, points, level FROM user ORDER BY points DESC LIMIT 30');
} catch (Throwable $e) {
$err = 'Выполните sql/migrate_points.sql';
$cfg = ['enabled'=>'1','in_reward'=>'1','gold_day_cost'=>'50','ad_point_per_currency'=>'1'];
$topUsers = [];
}
adminHeader('Баллы', 'points');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if ($err) echo '<div class="alert alert-error">' . e($err) . '</div>';
?>
<h1 class="page-title">Баллы</h1>
<p class="page-sub">Начисление за переходы In, трата на GOLD и рекламу</p>
<div class="card">
<h2>Настройки</h2>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="save_points_cfg">
<label style="display:flex;gap:8px;align-items:center;color:var(--text)">
<input type="checkbox" name="enabled" value="1" <?= $cfg['enabled']==='1'?'checked':'' ?> style="width:auto;margin:0"> Система баллов включена
</label>
<label>Баллов за уникальный In</label>
<input type="number" name="in_reward" value="<?= e($cfg['in_reward']) ?>" min="0" max="100">
<label>Стоимость GOLD (баллов / день)</label>
<input type="number" name="gold_day_cost" value="<?= e($cfg['gold_day_cost']) ?>" min="1">
<label>Баллов за 1 ₽ цены рекламы</label>
<input type="number" name="ad_point_per_currency" value="<?= e($cfg['ad_point_per_currency']) ?>" min="1">
<button class="btn btn-primary" type="submit">Сохранить</button>
</form>
</div>
<div class="card">
<h2>Начислить / списать</h2>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="adjust_points">
<label>ID пользователя</label>
<input type="number" name="user_id" required min="1">
<label>Изменение (+/−)</label>
<input type="number" name="delta" required>
<label>Комментарий</label>
<input type="text" name="note" maxlength="40" value="admin">
<button class="btn btn-primary" type="submit">Применить</button>
</form>
</div>
<h2 class="page-title" style="font-size:1.05rem">Топ по баллам</h2>
<div class="table-wrap"><table class="data">
<thead><tr><th>ID</th><th>Логин</th><th>Баллы</th><th>Ур.</th></tr></thead>
<tbody>
<?php foreach ($topUsers as $u): ?>
<tr>
<td><?= (int)$u['id'] ?></td>
<td><?= e($u['login']) ?></td>
<td><strong><?= (int)$u['points'] ?></strong></td>
<td><?= (int)$u['level'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
<?php
adminFooter();
exit;
}
if ($action === 'ads') {
$list = [];
$prices = [];
try {
$list = Database::fetchAll('SELECT a.*, u.login FROM ads a LEFT JOIN user u ON u.id = a.user_id ORDER BY a.id DESC LIMIT 100');
$prices = Database::fetchAll('SELECT * FROM ad_prices ORDER BY sort, position');
} catch (Throwable $e) {
$err = 'Выполните sql/migrate_ads.sql';
}
$positions = Ads::positions();
adminHeader('Реклама', 'ads');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if (!empty($err)) echo '<div class="alert alert-error">' . e($err) . '</div>';
?>
<h1 class="page-title">Реклама</h1>
<p class="page-sub">Места размещения и заявки пользователей</p>
<div class="card">
<h2>Добавить рекламу (админ)</h2>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="add_ad">
<label>Место</label>
<select name="position">
<?php foreach ($positions as $k => $lab): ?>
<option value="<?= e($k) ?>"><?= e($lab) ?></option>
<?php endforeach; ?>
</select>
<label>URL (без http://)</label>
<input type="text" name="url" required maxlength="60" placeholder="example.com">
<label>Название</label>
<input type="text" name="name" required maxlength="64">
<label>Текст</label>
<input type="text" name="text" maxlength="200">
<label>Дней показа</label>
<input type="number" name="days" value="7" min="1" max="365">
<label>Цвет (CSS, опционально)</label>
<input type="text" name="color" placeholder="#00d4aa">
<label style="display:flex;gap:8px;align-items:center;color:var(--text)">
<input type="checkbox" name="bold" value="1" style="width:auto;margin:0"> Жирный
</label>
<button class="btn btn-primary" type="submit" style="margin-top:12px">Опубликовать</button>
</form>
</div>
<div class="card">
<h2>Цены (₽ / день)</h2>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="do" value="save_ad_prices">
<?php
$priceMap = [];
foreach ($prices as $pr) { $priceMap[$pr['position']] = $pr; }
foreach ($positions as $k => $lab):
$pr = $priceMap[$k] ?? ['price_day' => 0, 'enabled' => 1];
?>
<div style="display:flex;gap:10px;align-items:center;margin-bottom:10px;flex-wrap:wrap">
<label style="flex:1;min-width:140px;margin:0;color:var(--text)"><?= e($lab) ?></label>
<input type="number" step="0.01" name="price[<?= e($k) ?>]" value="<?= e((string)$pr['price_day']) ?>" style="width:110px;margin:0">
<label style="display:flex;gap:6px;align-items:center;margin:0;color:var(--text);font-size:.85rem">
<input type="checkbox" name="enabled[<?= e($k) ?>]" value="1" <?= !empty($pr['enabled'])?'checked':'' ?> style="width:auto;margin:0"> вкл.
</label>
</div>
<?php endforeach; ?>
<button class="btn btn-primary" type="submit">Сохранить цены</button>
</form>
</div>
<h2 class="page-title" style="font-size:1.1rem;margin-top:8px">Объявления</h2>
<?php if (!$list): ?>
<div class="empty"><div>Пока нет</div></div>
<?php else: foreach ($list as $ad): ?>
<article class="rank-card" style="grid-template-columns:1fr;gap:8px">
<div>
<strong><?= e($ad['name']) ?></strong>
<span class="badge <?= (int)$ad['status']===1?'badge-ok':((int)$ad['status']===0?'badge-mod':'badge-ban') ?>">
<?= e(Ads::statusLabel((int)$ad['status'])) ?>
</span>
<div class="text-muted" style="font-size:.8rem;margin-top:4px">
<?= e($positions[$ad['position']] ?? $ad['position']) ?>
· <?= e($ad['url']) ?>
· <?= e($ad['login'] ?: 'админ') ?>
· <?= (int)$ad['days'] ?> дн.
· до <?= (int)$ad['time_end'] ? date('d.m.Y', (int)$ad['time_end']) : '—' ?>
· <?= number_format((float)$ad['price'], 2, '.', ' ') ?> ₽
</div>
<?php if ($ad['text']): ?><div style="font-size:.88rem;margin-top:4px"><?= e($ad['text']) ?></div><?php endif; ?>
</div>
<div class="act-btns">
<?php if ((int)$ad['status'] !== 1): ?>
<form method="post" style="display:inline"><?= csrf_field() ?>
<input type="hidden" name="do" value="ad_status"><input type="hidden" name="id" value="<?= (int)$ad['id'] ?>">
<input type="hidden" name="status" value="1"><button class="ok" type="submit">Активировать</button>
</form>
<?php endif; ?>
<?php if ((int)$ad['status'] === 0): ?>
<form method="post" style="display:inline"><?= csrf_field() ?>
<input type="hidden" name="do" value="ad_status"><input type="hidden" name="id" value="<?= (int)$ad['id'] ?>">
<input type="hidden" name="status" value="2"><button class="danger" type="submit">Отклонить</button>
</form>
<?php endif; ?>
<form method="post" style="display:inline" onsubmit="return confirm('Удалить?')"><?= csrf_field() ?>
<input type="hidden" name="do" value="del_ad"><input type="hidden" name="id" value="<?= (int)$ad['id'] ?>">
<button class="danger" type="submit">Удалить</button>
</form>
</div>
</article>
<?php endforeach; endif;
adminFooter();
exit;
}
if ($action === 'news') {
$list = [];
try {
$list = Database::fetchAll('SELECT * FROM news ORDER BY time_add DESC LIMIT 50');
} catch (Throwable $e) {
$err = 'Таблица news не найдена. Выполните sql/migrate_news.sql';
}
adminHeader('Новости', 'news');
if ($flash) echo '<div class="alert alert-ok">' . e($flash) . '</div>';
if (!empty($err)) echo '<div class="alert alert-error">' . e($err) . '</div>';
echo '<h1 class="page-title">Новости</h1>';
echo '<div class="card"><h2>Добавить</h2><form method="post">' . csrf_field();
echo '<input type="hidden" name="do" value="add_news">';
echo '<label>Заголовок</label><input type="text" name="title" required maxlength="150">';
echo '<label>Текст</label><textarea name="text" required rows="4"></textarea>';
echo '<button class="btn btn-primary" type="submit">Опубликовать</button></form></div>';
foreach ($list as $n) {
echo '<article class="rank-card" style="grid-template-columns:1fr auto;gap:10px">';
echo '<div><strong>' . e($n['title']) . '</strong>';
echo '<div class="text-muted" style="font-size:.8rem">' . date('d.m.Y H:i', (int)$n['time_add']) . '</div>';
echo '<div style="font-size:.88rem;margin-top:6px;color:var(--text-muted)">' . e(mb_substr($n['text'], 0, 200)) . '</div></div>';
echo '<form method="post" onsubmit="return confirm('Удалить?')">' . csrf_field() . '<input type="hidden" name="do" value="del_news">';
echo '<input type="hidden" name="id" value="' . (int)$n['id'] . '">';
echo '<button class="btn btn-sm btn-ghost" type="submit" style="color:var(--danger)">Удалить</button></form></article>';
}
adminFooter();
exit;
}
redirect('/?module=admin&action=dashboard');