chore: initial commit — CS2 Simulator

This commit is contained in:
2026-07-05 10:40:00 +00:00
commit c268557997
40 changed files with 824236 additions and 0 deletions
+294
View File
@@ -0,0 +1,294 @@
{% 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>
{% endblock %}
{% block content %}
<div class="a-stats" style="grid-template-columns:repeat(4,1fr)">
<div class="a-stat">
<div class="as-label">Баланс</div>
<div class="as-value">{{ "%.0f"|format(target_user.balance) }} ₽</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>
<!-- Действия -->
<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>
</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">{{ "%.2f"|format(target_user.balance) }} ₽</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) { alert(d.message); window.location.reload(); }
else alert(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) { alert(d.message); closeModal('giveItemModal'); window.location.reload(); }
else alert(d.error);
}
// ====== DELETE ITEM ======
async function deleteItem(invId) {
if (!confirm('Удалить предмет?')) 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 alert(d.error);
}
// ====== TOGGLE ADMIN / BAN ======
async function toggleAdmin() {
if (!confirm('Изменить статус админа?')) 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 alert(d.error);
}
async function toggleBan() {
if (!confirm('{{ "Забанить" if not target_user.is_banned else "Разбанить" }}?')) 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 alert(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 ? '✅' : '❌';
} 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) { alert(d.message); closeModal('rpuModal'); loadRPU(); }
else alert(d.error);
}
async function setRPUPreset(preset) {
const names = { lucky:'🍀 Везучий', normal:'📊 Обычный', unlucky:'🌧️ Невезучий', very_unlucky:'💀 Проклятый' };
if (!confirm('Применить "'+names[preset]+'"?')) 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) { alert(d.message); loadRPU(); } else alert(d.error);
}
document.addEventListener('DOMContentLoaded', loadRPU);
</script>
{% endblock %}