Files
dodep-simulator/templates/admin/user_detail.html
T
sasheg 47c7f17c90 RPU v2, promo/bank система, админка, оптимизации
— RPU v2: бюджет удачи, потолок (ceiling), комбек после
  проигрышей, hot/cold анализ, интеграция с promo-фазами
— Bank API: mock-банк для карты (депозит/вывод с балансом 1000₽)
— Promo-сервис: автогенерация промокодов каждый час, типы
  (card_to_site, luck_boost, ceiling_boost, free_case, reset_streak)
— Promo-фазы: LUCKY/NEUTRAL/UNLUCKY, переключение каждые 1-6ч
— Admin: история транзакций (user_history), управление промокодами,
  управление promo-фазами, API фильтров/коллекций, RPU info
— Database: WAL mode, новые поля RPU v2, TransactionLog,
  карточный баланс, статистики (streaks, ceiling, budget)
— Frontend: /deposit, /promo/activate, /withdraw, /card-balance,
  /online-count, /rpu-info, lifespan вместо startup event
— UI: user_history.html, улучшения профиля, кейсов, навигации
— Cases: обновление cases.json (1892 строки новых данных)
— Achievements: кэш коллекций, оптимизация is_custom проверок
— Sounds: дополнительные звуковые эффекты
— Upgrade: интеграция с RPU (множители, streaks)
— .gitignore: исключены .db, .bak, __pycache__
2026-07-25 13:11:08 +00:00

363 lines
20 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.
{% extends "admin/base.html" %}
{% block title %}{{ target_user.username }} — Админ-панель{% endblock %}
{% block nav_users %}active{% endblock %}
{% block page_title %}
👤 {{ target_user.username }}
{% if target_user.is_admin %}<span class="a-badge a-badge-admin" style="margin-left:0.5rem">Админ</span>{% endif %}
{% if target_user.is_banned %}<span class="a-badge a-badge-banned" style="margin-left:0.5rem">Забанен</span>{% endif %}
{% endblock %}
{% block top_actions %}
<a href="/admin/users" class="at-back">&larr; Назад</a>
<a href="/admin/user/{{ target_user.id }}/history" class="a-btn" style="margin-left:0.5rem;font-size:0.82rem;">📜 История</a>
{% endblock %}
{% block content %}
<div class="a-stats">
<div class="a-stat">
<div class="as-label">Баланс</div>
<div class="as-value">{{ target_user.balance|money }} ₽</div>
</div>
<div class="a-stat">
<div class="as-label">Предметов</div>
<div class="as-value">{{ inventory|length }}</div>
</div>
<div class="a-stat">
<div class="as-label">Открытий кейсов</div>
<div class="as-value">{{ total_openings }}</div>
</div>
<div class="a-stat">
<div class="as-label">Контрактов</div>
<div class="as-value">{{ total_contracts }}</div>
</div>
</div>
<!-- RPU -->
<div class="a-card">
<div class="a-card-header">🎲 РПУ (Режим Персонального Угнетения)</div>
<div class="a-row a-gap-sm a-mb" id="rpuStats" style="justify-content:space-around;text-align:center">
<div><div class="as-value" style="font-size:1.4rem" id="rpuLevel">--</div><div class="as-label" style="font-size:0.7rem">Уровень</div></div>
<div><div class="as-value" style="font-size:1.4rem" id="rpuLuck">--</div><div class="as-label" style="font-size:0.7rem">Множитель</div></div>
<div><div class="as-value" style="font-size:1.4rem" id="rpuSpent">--</div><div class="as-label" style="font-size:0.7rem">Потрачено</div></div>
<div><div class="as-value" style="font-size:1.4rem" id="rpuOpened">--</div><div class="as-label" style="font-size:0.7rem">Открыто</div></div>
<div><div class="as-value" style="font-size:1.4rem" id="rpuAutoStatus">--</div><div class="as-label" style="font-size:0.7rem">Авто</div></div>
</div>
<div class="a-flex" style="flex-wrap:wrap">
<button class="a-btn a-btn-primary" onclick="openRPUModal()">⚙️ Настроить</button>
<button class="a-btn" style="border-color:#10b981;color:#10b981" onclick="setRPUPreset('lucky')">🍀 Везучий</button>
<button class="a-btn" style="border-color:#f59e0b;color:#f59e0b" onclick="setRPUPreset('normal')">📊 Обычный</button>
<button class="a-btn" style="border-color:#ef4444;color:#ef4444" onclick="setRPUPreset('unlucky')">🌧️ Невезучий</button>
<button class="a-btn a-btn-danger" onclick="setRPUPreset('very_unlucky')">💀 Проклятый</button>
</div>
</div>
<!-- RPU v2 -->
<div class="a-card">
<div class="a-card-header">🎲 RPU v2 — подробно</div>
<div id="rpuV2Stats" class="a-mb" style="font-size:0.82rem">
<div class="a-row a-gap-sm" style="justify-content:space-around;text-align:center;margin-bottom:0.75rem">
<div><div class="as-value" id="v2Ceiling" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Потолок / Депнуто</div></div>
<div><div class="as-value" id="v2Budget" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Luck Budget</div></div>
<div><div class="as-value" id="v2SessionSpent" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Сессия потрачено</div></div>
<div><div class="as-value" id="v2HotScore" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Hot Score</div></div>
<div><div class="as-value" id="v2PromoPhase" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Промо-фаза</div></div>
</div>
<div class="a-row a-gap-sm" style="justify-content:space-around;text-align:center">
<div><div class="as-value" id="v2Comeback" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Комбек</div></div>
<div><div class="as-value" id="v2BreakCount" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Пробитий</div></div>
<div><div class="as-value" id="v2LossStreak" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Проигрышей подряд</div></div>
<div><div class="as-value" id="v2SessionReset" style="font-size:1.1rem">--</div><div class="as-label" style="font-size:0.65rem">Сброс сессии</div></div>
</div>
</div>
</div>
<!-- Действия -->
<div class="a-card">
<div class="a-card-header">🔧 Действия</div>
<div class="a-flex" style="flex-wrap:wrap">
<button class="a-btn a-btn-primary" onclick="openBalanceModal()">💰 Баланс</button>
<button class="a-btn" onclick="openGiveItemModal()">🎁 Выдать предмет</button>
{% if target_user.id != user.id %}
<button class="a-btn" onclick="toggleAdmin()">{{ "👑 Снять админа" if target_user.is_admin else "👑 Сделать админом" }}</button>
{% endif %}
<button class="a-btn {{ 'a-btn-danger' if not target_user.is_banned else 'a-btn-success' }}" onclick="toggleBan()">
{{ "🔨 Забанить" if not target_user.is_banned else "✅ Разбанить" }}
</button>
</div>
</div>
<!-- Инвентарь -->
<div class="a-card">
<div class="a-card-header">🎒 Инвентарь (последние 50)</div>
<div class="a-table-wrap">
<table class="a-table">
<thead><tr><th>ID</th><th>Название</th><th>Редкость</th><th>Float</th><th></th></tr></thead>
<tbody>
{% for item in inventory %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.market_hash_name[:35] }}{{'…' if item.market_hash_name|length > 35}}</td>
<td>{{ item.rarity }}</td>
<td>{{ "%.4f"|format(item.float_value) }}</td>
<td class="td-actions">
<button class="a-btn a-btn-danger a-btn-sm" onclick="deleteItem({{ item.id }})">🗑️</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="a-table-card-view">
{% for item in inventory %}
<div class="a-table-card">
<div class="tc-row">
<span class="tc-label">ID</span>
<span class="tc-value">{{ item.id }}</span>
</div>
<div class="tc-row">
<span class="tc-label">Название</span>
<span class="tc-value">{{ item.market_hash_name[:40] }}{{'…' if item.market_hash_name|length > 40}}</span>
</div>
<div class="tc-row">
<span class="tc-label">Редкость / Float</span>
<span class="tc-value">{{ item.rarity }} / {{ "%.4f"|format(item.float_value) }}</span>
</div>
<div class="tc-actions">
<button class="a-btn a-btn-danger a-btn-sm" onclick="deleteItem({{ item.id }})">🗑️ Удалить</button>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block modals %}
<!-- Баланс -->
<div class="a-overlay" id="balanceModal">
<div class="a-modal">
<div class="a-modal-title">💰 Изменить баланс</div>
<p style="font-size:0.85rem;color:rgba(255,255,255,0.5);margin-bottom:1rem">
Текущий: <strong style="color:#f59e0b">{{ target_user.balance|money }} ₽</strong>
</p>
<div class="a-mb">
<label class="a-label">Сумма</label>
<input type="number" id="balanceAmount" class="a-input" min="0" step="100" value="1000">
</div>
<div class="a-mb">
<label class="a-label">Операция</label>
<select id="balanceOperation" class="a-input">
<option value="add"> Добавить</option>
<option value="remove"> Снять</option>
<option value="set">🎯 Установить</option>
</select>
</div>
<div class="a-modal-actions">
<button class="a-btn" onclick="closeModal('balanceModal')">Отмена</button>
<button class="a-btn a-btn-primary" onclick="updateBalance()">Применить</button>
</div>
</div>
</div>
<!-- Выдать предмет -->
<div class="a-overlay" id="giveItemModal">
<div class="a-modal" style="max-width:560px">
<div class="a-modal-title">🎁 Выдать предмет</div>
<div class="a-mb">
<label class="a-label">Поиск</label>
<input type="text" id="itemSearchInput" class="a-input" placeholder="Введите название..." oninput="searchItems()">
</div>
<div id="itemSearchResults" style="max-height:300px;overflow-y:auto"></div>
<div class="a-modal-actions">
<button class="a-btn" onclick="closeModal('giveItemModal')">Закрыть</button>
</div>
</div>
</div>
<!-- RPU -->
<div class="a-overlay" id="rpuModal">
<div class="a-modal" style="max-width:500px">
<div class="a-modal-title">⚙️ Настройка РПУ</div>
<div id="rpuSliders" class="a-mb"></div>
<div class="a-mb">
<label class="a-label" style="display:flex;align-items:center;gap:0.5rem;cursor:pointer">
<input type="checkbox" id="rpuAutoAdjust"> 🔄 Автоматическая подкрутка
</label>
</div>
<div class="a-modal-actions">
<button class="a-btn" onclick="closeModal('rpuModal')">Отмена</button>
<button class="a-btn a-btn-primary" onclick="saveRPU()">Сохранить</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_styles %}
<style>
.slider-group { margin-bottom: 0.75rem; }
.slider-group label { display:flex; justify-content:space-between; font-size:0.78rem; color:rgba(255,255,255,0.6); margin-bottom:0.2rem; }
.slider-group input[type=range] { width:100%; accent-color:#f59e0b; }
#itemSearchResults > div {
display:flex; align-items:center; gap:0.75rem;
padding:0.5rem; border-bottom:1px solid rgba(255,255,255,0.04);
}
#itemSearchResults > div img { width:40px; height:32px; object-fit:contain; }
#itemSearchResults > div .ir-info { flex:1; }
#itemSearchResults > div .ir-info .ir-name { font-size:0.82rem; }
#itemSearchResults > div .ir-info .ir-meta { font-size:0.7rem; color:rgba(255,255,255,0.35); }
</style>
{% endblock %}
{% block scripts %}
<script>
const userId = {{ target_user.id }};
let currentRPU = null;
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
function openModal(id) { document.getElementById(id).classList.add('open'); }
// ====== BALANCE ======
function openBalanceModal() { openModal('balanceModal'); }
async function updateBalance() {
const fd = new FormData();
fd.append('amount', document.getElementById('balanceAmount').value);
fd.append('operation', document.getElementById('balanceOperation').value);
const res = await fetch(`/admin/api/user/${userId}/balance`, { method:'POST', body:fd });
const d = await res.json();
if (d.success) { Notify.success(d.message); window.location.reload(); }
else Notify.error(d.error);
}
// ====== GIVE ITEM ======
function openGiveItemModal() { openModal('giveItemModal'); searchItems(); }
async function searchItems() {
const q = document.getElementById('itemSearchInput').value;
const res = await fetch(`/admin/api/items/search?q=${encodeURIComponent(q)}`);
const items = await res.json();
document.getElementById('itemSearchResults').innerHTML = items.map(item => `
<div>
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
<div class="ir-info">
<div class="ir-name">${item.name}</div>
<div class="ir-meta">${item.rarity}${item.price_rub || 0} ₽</div>
</div>
<button class="a-btn a-btn-primary a-btn-sm" onclick="giveItem(${item.id})">Выдать</button>
</div>
`).join('');
}
async function giveItem(itemId) {
const fd = new FormData(); fd.append('item_id', itemId);
const res = await fetch(`/admin/api/user/${userId}/give-item`, { method:'POST', body:fd });
const d = await res.json();
if (d.success) { Notify.success(d.message); closeModal('giveItemModal'); window.location.reload(); }
else Notify.error(d.error);
}
// ====== DELETE ITEM ======
async function deleteItem(invId) {
Notify.confirm('Удалить предмет?', 'Подтверждение', async function(ok) {
if (!ok) return;
const res = await fetch(`/admin/api/user/${userId}/delete-item/${invId}`, { method:'POST' });
const d = await res.json();
if (d.success) window.location.reload();
else Notify.error(d.error);
});
}
// ====== TOGGLE ADMIN / BAN ======
async function toggleAdmin() {
Notify.confirm('Изменить статус админа?', 'Подтверждение', async function(ok) {
if (!ok) return;
const res = await fetch(`/admin/api/user/${userId}/toggle-admin`, { method:'POST' });
const d = await res.json();
if (d.success) window.location.reload(); else Notify.error(d.error);
});
}
async function toggleBan() {
Notify.confirm('{{ "Забанить" if not target_user.is_banned else "Разбанить" }}?', 'Подтверждение', async function(ok) {
if (!ok) return;
const res = await fetch(`/admin/api/user/${userId}/toggle-ban`, { method:'POST' });
const d = await res.json();
if (d.success) window.location.reload(); else Notify.error(d.error);
});
}
// ====== RPU ======
async function loadRPU() {
try {
const res = await fetch(`/admin/api/user/${userId}/rpu`);
const d = await res.json();
currentRPU = d;
const lv = Math.round(d.rpu_level);
document.getElementById('rpuLevel').textContent = lv + '%';
document.getElementById('rpuLevel').style.color = lv < 40 ? '#22c55e' : lv > 60 ? '#ef4444' : '#f59e0b';
document.getElementById('rpuLuck').textContent = d.luck_multiplier.toFixed(2) + 'x';
document.getElementById('rpuSpent').textContent = Math.round(d.stats.total_spent).toLocaleString() + ' ₽';
document.getElementById('rpuOpened').textContent = d.stats.total_opened.toLocaleString();
document.getElementById('rpuAutoStatus').textContent = d.auto_adjust ? '✅' : '❌';
// ── RPU v2 ──
if (d.v2) {
const v = d.v2;
document.getElementById('v2Ceiling').textContent = Math.round(v.ceiling.current_ceiling).toLocaleString('ru') + '₽ / ' + Math.round(v.ceiling.total_deposited).toLocaleString('ru') + '₽';
document.getElementById('v2Budget').textContent = v.session.luck_budget + '%';
document.getElementById('v2Budget').style.color = v.session.luck_budget < 30 ? '#ef4444' : v.session.luck_budget > 70 ? '#22c55e' : '#f59e0b';
document.getElementById('v2SessionSpent').textContent = Math.round(v.session.session_spent).toLocaleString('ru') + '₽';
document.getElementById('v2HotScore').textContent = v.hot_cold.hot_score;
document.getElementById('v2HotScore').style.color = v.hot_cold.hot_score > 0.5 ? '#22c55e' : v.hot_cold.hot_score < -0.5 ? '#ef4444' : '#f59e0b';
const ph = v.promo_phase;
document.getElementById('v2PromoPhase').textContent = ph.phase;
document.getElementById('v2PromoPhase').style.color = ph.phase === 'LUCKY' ? '#22c55e' : ph.phase === 'UNLUCKY' ? '#ef4444' : '#f59e0b';
document.getElementById('v2Comeback').textContent = v.comeback.active ? '✅ x' + v.comeback.multiplier.toFixed(1) + ' (' + v.comeback.openings_left + ')' : '❌';
document.getElementById('v2BreakCount').textContent = v.ceiling.break_count + ' (' + v.ceiling.break_session + ' сесс)';
document.getElementById('v2LossStreak').textContent = v.comeback.consecutive_loss_count + ' (' + Math.round(v.comeback.consecutive_loss_value).toLocaleString('ru') + '₽)';
document.getElementById('v2SessionReset').textContent = v.session.reset_date ? new Date(v.session.reset_date).toLocaleString('ru') : '—';
}
} catch(_) {}
}
function openRPUModal() {
const sliders = document.getElementById('rpuSliders');
const rarities = [
['consumer','Consumer Grade'],['industrial','Industrial Grade'],
['mil_spec','Mil-Spec'],['restricted','Restricted'],
['classified','Classified'],['covert','Covert'],['rare_special','Rare Special']
];
let html = '';
rarities.forEach(([k,l]) => {
const v = currentRPU?.multipliers[k] || 1.0;
html += `<div class="slider-group"><label><span>${l}</span><span id="rp_${k}_v">${v.toFixed(2)}x</span></label>
<input type="range" id="rp_${k}" min="0.1" max="3.0" step="0.05" value="${v}"
oninput="document.getElementById('rp_${k}_v').textContent=this.value+'x'"></div>`;
});
const lv = currentRPU?.luck_multiplier || 1.0;
html += `<div class="slider-group"><label><span>🍀 Общая удача</span><span id="rp_luck_v">${lv.toFixed(2)}x</span></label>
<input type="range" id="rp_luck" min="0.1" max="3.0" step="0.05" value="${lv}"
oninput="document.getElementById('rp_luck_v').textContent=this.value+'x'"></div>`;
sliders.innerHTML = html;
document.getElementById('rpuAutoAdjust').checked = currentRPU?.auto_adjust || false;
openModal('rpuModal');
}
async function saveRPU() {
const fd = new FormData();
['consumer','industrial','mil_spec','restricted','classified','covert','rare_special'].forEach(k => fd.append(k, document.getElementById('rp_'+k).value));
fd.append('luck', document.getElementById('rp_luck').value);
fd.append('auto_adjust', document.getElementById('rpuAutoAdjust').checked);
const res = await fetch(`/admin/api/user/${userId}/rpu/update`, { method:'POST', body:fd });
const d = await res.json();
if (d.success) { Notify.success(d.message); closeModal('rpuModal'); loadRPU(); }
else Notify.error(d.error);
}
async function setRPUPreset(preset) {
const names = { lucky:'🍀 Везучий', normal:'📊 Обычный', unlucky:'🌧️ Невезучий', very_unlucky:'💀 Проклятый' };
Notify.confirm('Применить "'+names[preset]+'"?', 'Подтверждение', async function(ok) {
if (!ok) return;
const fd = new FormData(); fd.append('preset', preset);
const res = await fetch(`/admin/api/user/${userId}/rpu/preset`, { method:'POST', body:fd });
const d = await res.json();
if (d.success) { Notify.success(d.message); loadRPU(); } else Notify.error(d.error);
});
}
document.addEventListener('DOMContentLoaded', loadRPU);
</script>
{% endblock %}