diff --git a/game/configurator/js/configurator.js b/game/configurator/js/configurator.js
index ee5f0cd..e8de347 100644
--- a/game/configurator/js/configurator.js
+++ b/game/configurator/js/configurator.js
@@ -33,11 +33,9 @@ class PvZConfiguratorApp {
this.onAuthenticated();
} else {
this.onUnauthenticated();
- this.loadPublicConfig();
}
} else {
this.onUnauthenticated();
- this.loadPublicConfig();
}
}
@@ -103,24 +101,116 @@ class PvZConfiguratorApp {
onAuthenticated() {
this.isAuthenticated = true;
- document.getElementById('admin-status-dot').className = 'status-dot';
- document.getElementById('admin-status-text').textContent = 'Администратор';
- document.getElementById('btn-login-modal-open').style.display = 'none';
+
+ const loginScreen = document.getElementById('configurator-login-screen');
+ if (loginScreen) loginScreen.style.display = 'none';
+
+ const appContainer = document.getElementById('configurator-app-container');
+ if (appContainer) appContainer.style.display = 'block';
+
+ const dot = document.getElementById('admin-status-dot');
+ if (dot) dot.className = 'status-dot';
+ const stText = document.getElementById('admin-status-text');
+ if (stText) stText.textContent = 'Администратор';
+
+ const btnLoginModal = document.getElementById('btn-login-modal-open');
+ if (btnLoginModal) btnLoginModal.style.display = 'none';
+
const btnPwd = document.getElementById('btn-admin-password-modal-open');
if (btnPwd) btnPwd.style.display = 'inline-flex';
- document.getElementById('btn-admin-logout').style.display = 'inline-flex';
+
+ const btnLogout = document.getElementById('btn-admin-logout');
+ if (btnLogout) btnLogout.style.display = 'inline-flex';
this.loadAllData();
}
onUnauthenticated() {
this.isAuthenticated = false;
- document.getElementById('admin-status-dot').className = 'status-dot offline';
- document.getElementById('admin-status-text').textContent = 'Гость (Только чтение)';
- document.getElementById('btn-login-modal-open').style.display = 'inline-flex';
+ this.token = '';
+ localStorage.removeItem('pvz_admin_token');
+ this.config = null;
+ this.stats = null;
+ this.users = [];
+ this.sessions = [];
+ this.loginLogs = [];
+ this.bugs = [];
+ this.levels = [];
+
+ const appContainer = document.getElementById('configurator-app-container');
+ if (appContainer) appContainer.style.display = 'none';
+
+ const loginScreen = document.getElementById('configurator-login-screen');
+ if (loginScreen) loginScreen.style.display = 'flex';
+
+ const loginErr = document.getElementById('login-screen-error');
+ if (loginErr) {
+ loginErr.style.display = 'none';
+ loginErr.textContent = '';
+ }
+
+ const pwdInput = document.getElementById('login-screen-password');
+ if (pwdInput) pwdInput.value = '';
+
+ const dot = document.getElementById('admin-status-dot');
+ if (dot) dot.className = 'status-dot offline';
+ const stText = document.getElementById('admin-status-text');
+ if (stText) stText.textContent = 'Гость (Только чтение)';
+
+ const btnLoginModal = document.getElementById('btn-login-modal-open');
+ if (btnLoginModal) btnLoginModal.style.display = 'inline-flex';
+
const btnPwd = document.getElementById('btn-admin-password-modal-open');
if (btnPwd) btnPwd.style.display = 'none';
- document.getElementById('btn-admin-logout').style.display = 'none';
+
+ const btnLogout = document.getElementById('btn-admin-logout');
+ if (btnLogout) btnLogout.style.display = 'none';
+ }
+
+ openModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (modal) {
+ modal.classList.add('active', 'open');
+ }
+ }
+
+ closeModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (modal) {
+ modal.classList.remove('active', 'open');
+ }
+ }
+
+ async handleScreenLogin() {
+ const pwdInput = document.getElementById('login-screen-password');
+ const errDiv = document.getElementById('login-screen-error');
+ const enteredPassword = pwdInput ? pwdInput.value.trim() : '';
+
+ if (!enteredPassword) {
+ if (errDiv) {
+ errDiv.textContent = 'Введите мастер-пароль администратора';
+ errDiv.style.display = 'flex';
+ }
+ return;
+ }
+
+ if (errDiv) {
+ errDiv.style.display = 'none';
+ errDiv.textContent = '';
+ }
+
+ const res = await this.apiRequest('/api/admin/auth', 'POST', { password: enteredPassword });
+ if (res.ok && res.data && res.data.success && res.data.token) {
+ this.token = res.data.token;
+ localStorage.setItem('pvz_admin_token', this.token);
+ this.showToast('Авторизация успешна! Добро пожаловать в панель управления.', 'success');
+ this.onAuthenticated();
+ } else {
+ if (errDiv) {
+ errDiv.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
+ errDiv.style.display = 'flex';
+ }
+ }
}
async loadPublicConfig() {
@@ -746,20 +836,24 @@ class PvZConfiguratorApp {
filtered.forEach(s => {
const tr = document.createElement('tr');
const tokenShort = (s.token || '').substring(0, 10) + '...';
- const isOnline = true;
+ const username = s.username || 'Гость';
+ const ipAddress = s.ipAddress || '127.0.0.1';
+ const deviceName = (s.deviceName && s.deviceName !== 'Unknown Device') ? s.deviceName : 'Браузер';
+ const createdAt = s.createdAt || '—';
+ const lastActive = s.lastActive || s.createdAt || 'Только что';
tr.innerHTML = `
- ${s.username || 'Гость'}
+ ${username}
${s.isAdmin ? 'ADMIN' : ''}
|
-
${s.ipAddress || '127.0.0.1'} |
-
${s.deviceName || 'Браузер'} |
-
${s.createdAt || '—'} |
-
🟢 ${s.lastActive || s.createdAt || 'Только что'} |
-
${tokenShort} |
+
${ipAddress} |
+
${deviceName} |
+
${createdAt} |
+
🟢 ${lastActive} |
+
${tokenShort} |
- |
@@ -825,7 +919,10 @@ class PvZConfiguratorApp {
filtered = filtered.filter(l => l.username && l.username.toLowerCase().includes(userFilter));
}
if (ipFilter) {
- filtered = filtered.filter(l => l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter));
+ filtered = filtered.filter(l =>
+ (l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter)) ||
+ (l.ip && l.ip.toLowerCase().includes(ipFilter))
+ );
}
if (statusFilter !== 'all') {
filtered = filtered.filter(l => l.status === statusFilter);
@@ -839,27 +936,32 @@ class PvZConfiguratorApp {
tbody.innerHTML = '';
filtered.forEach(log => {
const tr = document.createElement('tr');
+ const timestamp = log.createdAt || log.timestamp || '—';
+ const username = log.username || 'Гость';
+ const ipAddress = log.ipAddress || log.ip || '127.0.0.1';
+ const deviceName = (log.deviceName && log.deviceName !== 'Unknown Device') ? log.deviceName : 'Браузер';
+ const status = log.status || 'unknown';
- let statusBadge = `
${log.status}`;
- if (log.status === 'success') {
+ let statusBadge = `
${status}`;
+ if (status === 'success') {
statusBadge = `
✅ Успешно`;
- } else if (log.status === 'registered') {
+ } else if (status === 'registered') {
statusBadge = `
🎉 Регистрация`;
- } else if (log.status === 'wrong_password') {
+ } else if (status === 'wrong_password') {
statusBadge = `
❌ Неверный пароль`;
- } else if (log.status === 'banned_login_attempt') {
+ } else if (status === 'banned_login_attempt') {
statusBadge = `
🚫 Заблокирован`;
- } else if (log.status === 'rate_limited') {
+ } else if (status === 'rate_limited') {
statusBadge = `
⚠️ Превышен лимит`;
- } else if (log.status === 'user_not_found') {
+ } else if (status === 'user_not_found') {
statusBadge = `
❓ Не найден`;
}
tr.innerHTML = `
-
${log.timestamp || '—'} |
-
${log.username || '—'} |
-
${log.ipAddress || '—'} |
-
${log.deviceName || 'Браузер'} |
+
${timestamp} |
+
${username} |
+
${ipAddress} |
+
${deviceName} |
${statusBadge} |
`;
tbody.appendChild(tr);
@@ -902,7 +1004,7 @@ class PvZConfiguratorApp {
if (res.ok && res.data.success) {
this.showToast('Мастер-пароль администратора успешно изменен!', 'success');
- document.getElementById('modal-admin-password').classList.remove('active');
+ this.closeModal('modal-admin-password');
document.getElementById('admin-current-password').value = '';
document.getElementById('admin-new-password').value = '';
document.getElementById('admin-confirm-password').value = '';
@@ -987,8 +1089,13 @@ class PvZConfiguratorApp {
}
async openPlayerStatsModal(userId) {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const res = await this.apiRequest(`/api/admin/user/${userId}`);
- if (!res.ok || !res.data.success) {
+ if (!res.ok || !res.data || !res.data.success) {
this.showToast('Ошибка загрузки статистики игрока', 'error');
return;
}
@@ -1003,7 +1110,7 @@ class PvZConfiguratorApp {
const idInput = document.getElementById('stats-player-id');
if (idInput) idInput.value = u.id;
- document.getElementById('stats-player-username').textContent = u.username;
+ document.getElementById('stats-player-username').textContent = u.username || '—';
document.getElementById('stats-player-id-line').textContent = `#${u.id} (${prof.theme || 'classic'})`;
document.getElementById('stats-player-created').textContent = u.createdAt || '—';
document.getElementById('stats-player-login').textContent = u.lastLogin || '—';
@@ -1041,12 +1148,17 @@ class PvZConfiguratorApp {
}
document.getElementById('stats-scores-summary').innerHTML = scoreTxt;
- document.getElementById('modal-player-stats').classList.add('active');
+ this.openModal('modal-player-stats');
}
async openEditPlayerModal(userId) {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const res = await this.apiRequest(`/api/admin/user/${userId}`);
- if (!res.ok || !res.data.success) {
+ if (!res.ok || !res.data || !res.data.success) {
this.showToast('Ошибка загрузки профиля игрока', 'error');
return;
}
@@ -1057,7 +1169,7 @@ class PvZConfiguratorApp {
const zen = prof.zenGarden || u.zenGarden || {};
document.getElementById('edit-player-id').value = u.id;
- document.getElementById('edit-player-title-name').textContent = u.username;
+ document.getElementById('edit-player-title-name').textContent = u.username || '';
document.getElementById('edit-player-coins').value = prof.coins ?? u.coins ?? 150;
document.getElementById('edit-player-level').value = prof.level ?? u.level ?? 1;
document.getElementById('edit-player-maxlevel').value = prof.maxUnlockedLevel ?? u.maxUnlockedLevel ?? 1;
@@ -1081,10 +1193,15 @@ class PvZConfiguratorApp {
const banCont = document.getElementById('edit-ban-reason-container');
if (banCont) banCont.style.display = u.isBanned ? 'block' : 'none';
- document.getElementById('modal-edit-player').classList.add('active');
+ this.openModal('modal-edit-player');
}
async savePlayerEdit() {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const id = document.getElementById('edit-player-id').value;
const updates = {
coins: parseInt(document.getElementById('edit-player-coins').value, 10) || 0,
@@ -1113,17 +1230,22 @@ class PvZConfiguratorApp {
};
const res = await this.apiRequest(`/api/admin/user/${id}/edit`, 'POST', { updates });
- if (res.ok && res.data.success) {
+ if (res.ok && res.data && res.data.success) {
this.showToast('Профиль игрока и ресурсы успешно обновлены!', 'success');
- document.getElementById('modal-edit-player').classList.remove('active');
+ this.closeModal('modal-edit-player');
this.loadUsers();
this.loadStats();
} else {
- this.showToast(res.data.message || 'Ошибка сохранения', 'error');
+ this.showToast((res.data && res.data.message) || 'Ошибка сохранения', 'error');
}
}
async createPlayer() {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const username = document.getElementById('create-player-username').value.trim();
const password = document.getElementById('create-player-password').value.trim();
const coins = parseInt(document.getElementById('create-player-coins').value, 10) || 150;
@@ -1141,26 +1263,36 @@ class PvZConfiguratorApp {
initialData: { coins, level: 1, maxUnlockedLevel: 1 }
});
- if (res.ok && res.data.success) {
- this.showToast(`Игрок ${username} создан!`, 'success');
- document.getElementById('modal-create-player').classList.remove('active');
+ if (res.ok && res.data && res.data.success) {
+ this.showToast(`Игрок ${username} успешно создан!`, 'success');
+ this.closeModal('modal-create-player');
document.getElementById('create-player-username').value = '';
document.getElementById('create-player-password').value = '';
this.loadUsers();
this.loadStats();
} else {
- this.showToast(res.data.message || 'Ошибка создания игрока', 'error');
+ this.showToast((res.data && res.data.message) || 'Ошибка создания игрока', 'error');
}
}
openResetPasswordModal(id, username) {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
document.getElementById('reset-pwd-user-id').value = id;
document.getElementById('reset-pwd-username').textContent = username;
document.getElementById('reset-pwd-input').value = '';
- document.getElementById('modal-reset-password').classList.add('active');
+ this.openModal('modal-reset-password');
}
async submitResetPassword() {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const id = document.getElementById('reset-pwd-user-id').value;
const pwd = document.getElementById('reset-pwd-input').value.trim();
if (!pwd) {
@@ -1169,11 +1301,11 @@ class PvZConfiguratorApp {
}
const res = await this.apiRequest(`/api/admin/user/${id}/password`, 'POST', { password: pwd });
- if (res.ok && res.data.success) {
+ if (res.ok && res.data && res.data.success) {
this.showToast('Пароль успешно сброшен', 'success');
- document.getElementById('modal-reset-password').classList.remove('active');
+ this.closeModal('modal-reset-password');
} else {
- this.showToast(res.data.message || 'Ошибка сброса пароля', 'error');
+ this.showToast((res.data && res.data.message) || 'Ошибка сброса пароля', 'error');
}
}
@@ -1254,7 +1386,7 @@ class PvZConfiguratorApp {
document.getElementById('bug-mod-status').value = b.status || 'новый';
document.getElementById('bug-mod-reply').value = b.adminReply || '';
- document.getElementById('modal-moderate-bug').classList.add('active');
+ this.openModal('modal-moderate-bug');
}
async saveBugModeration() {
@@ -1265,7 +1397,7 @@ class PvZConfiguratorApp {
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { status, adminReply });
if (res.ok && res.data.success) {
this.showToast(`Баг #${id} обновлен`, 'success');
- document.getElementById('modal-moderate-bug').classList.remove('active');
+ this.closeModal('modal-moderate-bug');
this.loadBugs();
} else {
this.showToast(res.data.message || 'Ошибка сохранения', 'error');
@@ -1279,7 +1411,7 @@ class PvZConfiguratorApp {
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { delete: true });
if (res.ok && res.data.success) {
this.showToast(`Баг #${id} удален`, 'success');
- document.getElementById('modal-moderate-bug').classList.remove('active');
+ this.closeModal('modal-moderate-bug');
this.loadBugs();
} else {
this.showToast(res.data.message || 'Ошибка удаления', 'error');
@@ -1434,46 +1566,94 @@ class PvZConfiguratorApp {
if (pane) pane.classList.add('active');
this.activeTab = target;
- if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
+ if (target === 'tab-players') this.loadUsers();
if (target === 'tab-sessions') this.loadSessions();
if (target === 'tab-logs') this.loadLoginLogs();
+ if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
});
});
+ // Full-Screen Login Screen Bindings
+ const btnScreenSubmit = document.getElementById('btn-login-screen-submit');
+ if (btnScreenSubmit) {
+ btnScreenSubmit.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.handleScreenLogin();
+ });
+ }
+
+ const screenPwdInput = document.getElementById('login-screen-password');
+ if (screenPwdInput) {
+ screenPwdInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ this.handleScreenLogin();
+ }
+ });
+ }
+
+ const btnTogglePwd = document.getElementById('btn-toggle-login-password');
+ if (btnTogglePwd && screenPwdInput) {
+ btnTogglePwd.addEventListener('click', () => {
+ if (screenPwdInput.type === 'password') {
+ screenPwdInput.type = 'text';
+ btnTogglePwd.textContent = '🙈';
+ } else {
+ screenPwdInput.type = 'password';
+ btnTogglePwd.textContent = '👁️';
+ }
+ });
+ }
+
// Login modal
- document.getElementById('btn-login-modal-open').addEventListener('click', () => {
- document.getElementById('login-error-msg').style.display = 'none';
- document.getElementById('admin-password-input').value = '';
- document.getElementById('modal-login').classList.add('active');
- });
-
- document.getElementById('btn-submit-login').addEventListener('click', async () => {
- const pwd = document.getElementById('admin-password-input').value.trim();
- if (!pwd) return;
-
- const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
- if (res.ok && res.data.success) {
- this.token = res.data.token;
- localStorage.setItem('pvz_admin_token', this.token);
- this.onAuthenticated();
- document.getElementById('modal-login').classList.remove('active');
- this.showToast('Авторизация администратора успешна!', 'success');
- } else {
+ const btnLoginOpen = document.getElementById('btn-login-modal-open');
+ if (btnLoginOpen) {
+ btnLoginOpen.addEventListener('click', () => {
const err = document.getElementById('login-error-msg');
- err.textContent = res.data.message || 'Неверный пароль администратора';
- err.style.display = 'block';
- }
- });
+ if (err) err.style.display = 'none';
+ const input = document.getElementById('admin-password-input');
+ if (input) input.value = '';
+ this.openModal('modal-login');
+ });
+ }
+
+ const btnSubmitLogin = document.getElementById('btn-submit-login');
+ if (btnSubmitLogin) {
+ btnSubmitLogin.addEventListener('click', async () => {
+ const input = document.getElementById('admin-password-input');
+ const pwd = input ? input.value.trim() : '';
+ if (!pwd) return;
+
+ const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
+ if (res.ok && res.data && res.data.success && res.data.token) {
+ this.token = res.data.token;
+ localStorage.setItem('pvz_admin_token', this.token);
+ this.closeModal('modal-login');
+ this.showToast('Авторизация администратора успешна!', 'success');
+ this.onAuthenticated();
+ } else {
+ const err = document.getElementById('login-error-msg');
+ if (err) {
+ err.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
+ err.style.display = 'block';
+ }
+ }
+ });
+ }
// Change Admin Password Modal
const btnOpenPwd = document.getElementById('btn-admin-password-modal-open');
if (btnOpenPwd) {
btnOpenPwd.addEventListener('click', () => {
- document.getElementById('admin-password-error').style.display = 'none';
- document.getElementById('admin-current-password').value = '';
- document.getElementById('admin-new-password').value = '';
- document.getElementById('admin-confirm-password').value = '';
- document.getElementById('modal-admin-password').classList.add('active');
+ const err = document.getElementById('admin-password-error');
+ if (err) err.style.display = 'none';
+ const cur = document.getElementById('admin-current-password');
+ if (cur) cur.value = '';
+ const nw = document.getElementById('admin-new-password');
+ if (nw) nw.value = '';
+ const cf = document.getElementById('admin-confirm-password');
+ if (cf) cf.value = '';
+ this.openModal('modal-admin-password');
});
}
@@ -1483,19 +1663,29 @@ class PvZConfiguratorApp {
}
// Logout
- document.getElementById('btn-admin-logout').addEventListener('click', () => {
- this.token = '';
- localStorage.removeItem('pvz_admin_token');
- this.onUnauthenticated();
- this.showToast('Вы вышли из панели администратора', 'info');
- });
+ const btnLogout = document.getElementById('btn-admin-logout');
+ if (btnLogout) {
+ btnLogout.addEventListener('click', () => {
+ this.token = '';
+ localStorage.removeItem('pvz_admin_token');
+ this.onUnauthenticated();
+ this.showToast('Вы вышли из панели администратора', 'info');
+ });
+ }
// Close Modals
document.querySelectorAll('[data-close]').forEach(btn => {
btn.addEventListener('click', () => {
const modalId = btn.getAttribute('data-close');
- const modal = document.getElementById(modalId);
- if (modal) modal.classList.remove('active');
+ this.closeModal(modalId);
+ });
+ });
+
+ document.querySelectorAll('.modal-overlay').forEach(overlay => {
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) {
+ overlay.classList.remove('active', 'open');
+ }
});
});
@@ -1539,26 +1729,56 @@ class PvZConfiguratorApp {
});
// Player Actions
- document.getElementById('btn-create-player-open').addEventListener('click', () => {
- document.getElementById('modal-create-player').classList.add('active');
- });
- document.getElementById('btn-submit-create-player').addEventListener('click', () => this.createPlayer());
- document.getElementById('btn-save-player-edit').addEventListener('click', () => this.savePlayerEdit());
- document.getElementById('btn-submit-reset-pwd').addEventListener('click', () => this.submitResetPassword());
- document.getElementById('btn-refresh-players').addEventListener('click', () => this.loadUsers());
- document.getElementById('player-search-input').addEventListener('input', () => this.renderPlayersTable());
- document.getElementById('player-filter-status').addEventListener('change', () => this.renderPlayersTable());
- document.getElementById('player-sort-select').addEventListener('change', () => this.renderPlayersTable());
- document.getElementById('edit-player-is-banned').addEventListener('change', (e) => {
- document.getElementById('edit-ban-reason-container').style.display = e.target.checked ? 'block' : 'none';
- });
+ const btnCreatePlayerOpen = document.getElementById('btn-create-player-open');
+ if (btnCreatePlayerOpen) {
+ btnCreatePlayerOpen.addEventListener('click', () => {
+ this.openModal('modal-create-player');
+ });
+ }
+ const btnSubmitCreatePlayer = document.getElementById('btn-submit-create-player');
+ if (btnSubmitCreatePlayer) {
+ btnSubmitCreatePlayer.addEventListener('click', () => this.createPlayer());
+ }
+ const btnSavePlayerEdit = document.getElementById('btn-save-player-edit');
+ if (btnSavePlayerEdit) {
+ btnSavePlayerEdit.addEventListener('click', () => this.savePlayerEdit());
+ }
+ const btnSubmitResetPwd = document.getElementById('btn-submit-reset-pwd');
+ if (btnSubmitResetPwd) {
+ btnSubmitResetPwd.addEventListener('click', () => this.submitResetPassword());
+ }
+ const btnRefreshPlayers = document.getElementById('btn-refresh-players');
+ if (btnRefreshPlayers) {
+ btnRefreshPlayers.addEventListener('click', () => this.loadUsers());
+ }
+ const playerSearchInput = document.getElementById('player-search-input');
+ if (playerSearchInput) {
+ playerSearchInput.addEventListener('input', () => this.renderPlayersTable());
+ }
+ const playerFilterStatus = document.getElementById('player-filter-status');
+ if (playerFilterStatus) {
+ playerFilterStatus.addEventListener('change', () => this.renderPlayersTable());
+ }
+ const playerSortSelect = document.getElementById('player-sort-select');
+ if (playerSortSelect) {
+ playerSortSelect.addEventListener('change', () => this.renderPlayersTable());
+ }
+ const editPlayerIsBanned = document.getElementById('edit-player-is-banned');
+ if (editPlayerIsBanned) {
+ editPlayerIsBanned.addEventListener('change', (e) => {
+ const container = document.getElementById('edit-ban-reason-container');
+ if (container) container.style.display = e.target.checked ? 'block' : 'none';
+ });
+ }
- document.getElementById('btn-stats-jump-edit')?.addEventListener('click', () => {
- const id = document.getElementById('stats-player-id')?.value;
- const modal = document.getElementById('modal-player-stats');
- if (modal) modal.classList.remove('active');
- if (id) this.openEditPlayerModal(id);
- });
+ const btnStatsJumpEdit = document.getElementById('btn-stats-jump-edit');
+ if (btnStatsJumpEdit) {
+ btnStatsJumpEdit.addEventListener('click', () => {
+ const id = document.getElementById('stats-player-id')?.value;
+ this.closeModal('modal-player-stats');
+ if (id) this.openEditPlayerModal(id);
+ });
+ }
// Balance Save Buttons
document.getElementById('btn-save-economy').addEventListener('click', () => this.saveBalanceSection('economy'));
diff --git a/game/configurator_work/css/style.css b/game/configurator_work/css/style.css
index 8914393..f71af0f 100644
--- a/game/configurator_work/css/style.css
+++ b/game/configurator_work/css/style.css
@@ -148,20 +148,17 @@ body {
/* Nav Tabs */
.tabs-navigation {
display: flex;
+ flex-wrap: wrap;
gap: 8px;
- overflow-x: auto;
+ overflow-x: visible;
+ scrollbar-width: none;
padding-bottom: 12px;
margin-bottom: 20px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
- scrollbar-width: thin;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.tabs-navigation::-webkit-scrollbar {
- height: 4px;
-}
-.tabs-navigation::-webkit-scrollbar-thumb {
- background: var(--border-color);
- border-radius: 4px;
+ display: none;
}
.nav-tab-btn {
@@ -177,6 +174,7 @@ body {
font-weight: 600;
cursor: pointer;
white-space: nowrap;
+ flex-shrink: 0;
transition: var(--transition);
}
@@ -654,7 +652,8 @@ body {
padding: 16px;
}
-.modal-overlay.open {
+.modal-overlay.open,
+.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
@@ -821,10 +820,29 @@ input:checked + .slider-round:before {
.header-actions {
width: 100%;
justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 8px;
}
.controls-grid {
grid-template-columns: 1fr;
}
+ .tabs-navigation {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 6px;
+ overflow-x: visible;
+ }
+ .nav-tab-btn {
+ justify-content: center;
+ padding: 8px 10px;
+ font-size: 12px;
+ }
+}
+
+@media (max-width: 480px) {
+ .tabs-navigation {
+ grid-template-columns: 1fr;
+ }
}
@@ -1241,3 +1259,138 @@ input:checked + .slider-round:before {
.player-name-link:hover {
color: #a5d6a7;
}
+
+/* ==========================================================
+ Full-Screen Login Screen for Guest State (Cyberpunk Botanical)
+ ========================================================== */
+.login-page-screen {
+ min-height: 100vh;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px 16px;
+ background-color: var(--bg-main);
+ background-image:
+ radial-gradient(circle at 50% 30%, rgba(76, 175, 80, 0.12) 0%, transparent 60%),
+ radial-gradient(circle at 80% 80%, rgba(0, 229, 255, 0.08) 0%, transparent 50%),
+ linear-gradient(180deg, #090d13 0%, #0d121c 100%);
+ position: relative;
+ z-index: 10;
+}
+
+.login-card {
+ width: 100%;
+ max-width: 440px;
+ background: var(--bg-card);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-card), 0 0 40px rgba(76, 175, 80, 0.15);
+ padding: 36px 32px;
+ animation: loginCardFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+@keyframes loginCardFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(16px) scale(0.98);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.login-card-header {
+ text-align: center;
+ margin-bottom: 28px;
+}
+
+.login-brand-icon {
+ font-size: 42px;
+ margin-bottom: 8px;
+ filter: drop-shadow(0 0 16px var(--accent-green-glow));
+ animation: pulseGlow 3s ease-in-out infinite alternate;
+}
+
+@keyframes pulseGlow {
+ 0% { transform: scale(1); filter: drop-shadow(0 0 12px var(--accent-green-glow)); }
+ 100% { transform: scale(1.05); filter: drop-shadow(0 0 24px var(--accent-cyan-glow)); }
+}
+
+.login-brand-title {
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--text-main);
+ letter-spacing: -0.3px;
+ margin-bottom: 8px;
+ text-shadow: 0 0 20px rgba(76, 175, 80, 0.3);
+}
+
+.login-brand-subtitle {
+ font-size: 13px;
+ color: var(--text-muted);
+ line-height: 1.5;
+}
+
+.password-input-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.password-input-wrapper .form-input {
+ padding-right: 44px;
+}
+
+.btn-toggle-pwd {
+ position: absolute;
+ right: 10px;
+ background: transparent;
+ border: none;
+ color: var(--text-dim);
+ font-size: 16px;
+ cursor: pointer;
+ padding: 4px;
+ border-radius: var(--radius-sm);
+ transition: var(--transition);
+}
+
+.btn-toggle-pwd:hover {
+ color: var(--text-main);
+ transform: scale(1.1);
+}
+
+.login-error-alert {
+ background: rgba(255, 82, 82, 0.15);
+ border: 1px solid rgba(255, 82, 82, 0.45);
+ color: #ff8a80;
+ padding: 10px 14px;
+ border-radius: var(--radius-md);
+ font-size: 13px;
+ margin-bottom: 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ animation: shakeAlert 0.3s ease;
+}
+
+@keyframes shakeAlert {
+ 0%, 100% { transform: translateX(0); }
+ 25% { transform: translateX(-4px); }
+ 75% { transform: translateX(4px); }
+}
+
+.btn-block {
+ width: 100%;
+ padding: 12px;
+ font-size: 14px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ margin-top: 8px;
+}
diff --git a/game/configurator_work/index.html b/game/configurator_work/index.html
index 166cc6e..0bfb566 100644
--- a/game/configurator_work/index.html
+++ b/game/configurator_work/index.html
@@ -8,7 +8,31 @@
-
+
+
+
+
+
diff --git a/game/configurator_work/js/configurator.js b/game/configurator_work/js/configurator.js
index ee5f0cd..e8de347 100644
--- a/game/configurator_work/js/configurator.js
+++ b/game/configurator_work/js/configurator.js
@@ -33,11 +33,9 @@ class PvZConfiguratorApp {
this.onAuthenticated();
} else {
this.onUnauthenticated();
- this.loadPublicConfig();
}
} else {
this.onUnauthenticated();
- this.loadPublicConfig();
}
}
@@ -103,24 +101,116 @@ class PvZConfiguratorApp {
onAuthenticated() {
this.isAuthenticated = true;
- document.getElementById('admin-status-dot').className = 'status-dot';
- document.getElementById('admin-status-text').textContent = 'Администратор';
- document.getElementById('btn-login-modal-open').style.display = 'none';
+
+ const loginScreen = document.getElementById('configurator-login-screen');
+ if (loginScreen) loginScreen.style.display = 'none';
+
+ const appContainer = document.getElementById('configurator-app-container');
+ if (appContainer) appContainer.style.display = 'block';
+
+ const dot = document.getElementById('admin-status-dot');
+ if (dot) dot.className = 'status-dot';
+ const stText = document.getElementById('admin-status-text');
+ if (stText) stText.textContent = 'Администратор';
+
+ const btnLoginModal = document.getElementById('btn-login-modal-open');
+ if (btnLoginModal) btnLoginModal.style.display = 'none';
+
const btnPwd = document.getElementById('btn-admin-password-modal-open');
if (btnPwd) btnPwd.style.display = 'inline-flex';
- document.getElementById('btn-admin-logout').style.display = 'inline-flex';
+
+ const btnLogout = document.getElementById('btn-admin-logout');
+ if (btnLogout) btnLogout.style.display = 'inline-flex';
this.loadAllData();
}
onUnauthenticated() {
this.isAuthenticated = false;
- document.getElementById('admin-status-dot').className = 'status-dot offline';
- document.getElementById('admin-status-text').textContent = 'Гость (Только чтение)';
- document.getElementById('btn-login-modal-open').style.display = 'inline-flex';
+ this.token = '';
+ localStorage.removeItem('pvz_admin_token');
+ this.config = null;
+ this.stats = null;
+ this.users = [];
+ this.sessions = [];
+ this.loginLogs = [];
+ this.bugs = [];
+ this.levels = [];
+
+ const appContainer = document.getElementById('configurator-app-container');
+ if (appContainer) appContainer.style.display = 'none';
+
+ const loginScreen = document.getElementById('configurator-login-screen');
+ if (loginScreen) loginScreen.style.display = 'flex';
+
+ const loginErr = document.getElementById('login-screen-error');
+ if (loginErr) {
+ loginErr.style.display = 'none';
+ loginErr.textContent = '';
+ }
+
+ const pwdInput = document.getElementById('login-screen-password');
+ if (pwdInput) pwdInput.value = '';
+
+ const dot = document.getElementById('admin-status-dot');
+ if (dot) dot.className = 'status-dot offline';
+ const stText = document.getElementById('admin-status-text');
+ if (stText) stText.textContent = 'Гость (Только чтение)';
+
+ const btnLoginModal = document.getElementById('btn-login-modal-open');
+ if (btnLoginModal) btnLoginModal.style.display = 'inline-flex';
+
const btnPwd = document.getElementById('btn-admin-password-modal-open');
if (btnPwd) btnPwd.style.display = 'none';
- document.getElementById('btn-admin-logout').style.display = 'none';
+
+ const btnLogout = document.getElementById('btn-admin-logout');
+ if (btnLogout) btnLogout.style.display = 'none';
+ }
+
+ openModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (modal) {
+ modal.classList.add('active', 'open');
+ }
+ }
+
+ closeModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (modal) {
+ modal.classList.remove('active', 'open');
+ }
+ }
+
+ async handleScreenLogin() {
+ const pwdInput = document.getElementById('login-screen-password');
+ const errDiv = document.getElementById('login-screen-error');
+ const enteredPassword = pwdInput ? pwdInput.value.trim() : '';
+
+ if (!enteredPassword) {
+ if (errDiv) {
+ errDiv.textContent = 'Введите мастер-пароль администратора';
+ errDiv.style.display = 'flex';
+ }
+ return;
+ }
+
+ if (errDiv) {
+ errDiv.style.display = 'none';
+ errDiv.textContent = '';
+ }
+
+ const res = await this.apiRequest('/api/admin/auth', 'POST', { password: enteredPassword });
+ if (res.ok && res.data && res.data.success && res.data.token) {
+ this.token = res.data.token;
+ localStorage.setItem('pvz_admin_token', this.token);
+ this.showToast('Авторизация успешна! Добро пожаловать в панель управления.', 'success');
+ this.onAuthenticated();
+ } else {
+ if (errDiv) {
+ errDiv.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
+ errDiv.style.display = 'flex';
+ }
+ }
}
async loadPublicConfig() {
@@ -746,20 +836,24 @@ class PvZConfiguratorApp {
filtered.forEach(s => {
const tr = document.createElement('tr');
const tokenShort = (s.token || '').substring(0, 10) + '...';
- const isOnline = true;
+ const username = s.username || 'Гость';
+ const ipAddress = s.ipAddress || '127.0.0.1';
+ const deviceName = (s.deviceName && s.deviceName !== 'Unknown Device') ? s.deviceName : 'Браузер';
+ const createdAt = s.createdAt || '—';
+ const lastActive = s.lastActive || s.createdAt || 'Только что';
tr.innerHTML = `
- ${s.username || 'Гость'}
+ ${username}
${s.isAdmin ? 'ADMIN' : ''}
|
- ${s.ipAddress || '127.0.0.1'} |
- ${s.deviceName || 'Браузер'} |
- ${s.createdAt || '—'} |
- 🟢 ${s.lastActive || s.createdAt || 'Только что'} |
- ${tokenShort} |
+ ${ipAddress} |
+ ${deviceName} |
+ ${createdAt} |
+ 🟢 ${lastActive} |
+ ${tokenShort} |
-
+
🚪 Завершить
|
@@ -825,7 +919,10 @@ class PvZConfiguratorApp {
filtered = filtered.filter(l => l.username && l.username.toLowerCase().includes(userFilter));
}
if (ipFilter) {
- filtered = filtered.filter(l => l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter));
+ filtered = filtered.filter(l =>
+ (l.ipAddress && l.ipAddress.toLowerCase().includes(ipFilter)) ||
+ (l.ip && l.ip.toLowerCase().includes(ipFilter))
+ );
}
if (statusFilter !== 'all') {
filtered = filtered.filter(l => l.status === statusFilter);
@@ -839,27 +936,32 @@ class PvZConfiguratorApp {
tbody.innerHTML = '';
filtered.forEach(log => {
const tr = document.createElement('tr');
+ const timestamp = log.createdAt || log.timestamp || '—';
+ const username = log.username || 'Гость';
+ const ipAddress = log.ipAddress || log.ip || '127.0.0.1';
+ const deviceName = (log.deviceName && log.deviceName !== 'Unknown Device') ? log.deviceName : 'Браузер';
+ const status = log.status || 'unknown';
- let statusBadge = `${log.status}`;
- if (log.status === 'success') {
+ let statusBadge = `${status}`;
+ if (status === 'success') {
statusBadge = `✅ Успешно`;
- } else if (log.status === 'registered') {
+ } else if (status === 'registered') {
statusBadge = `🎉 Регистрация`;
- } else if (log.status === 'wrong_password') {
+ } else if (status === 'wrong_password') {
statusBadge = `❌ Неверный пароль`;
- } else if (log.status === 'banned_login_attempt') {
+ } else if (status === 'banned_login_attempt') {
statusBadge = `🚫 Заблокирован`;
- } else if (log.status === 'rate_limited') {
+ } else if (status === 'rate_limited') {
statusBadge = `⚠️ Превышен лимит`;
- } else if (log.status === 'user_not_found') {
+ } else if (status === 'user_not_found') {
statusBadge = `❓ Не найден`;
}
tr.innerHTML = `
- ${log.timestamp || '—'} |
- ${log.username || '—'} |
- ${log.ipAddress || '—'} |
- ${log.deviceName || 'Браузер'} |
+ ${timestamp} |
+ ${username} |
+ ${ipAddress} |
+ ${deviceName} |
${statusBadge} |
`;
tbody.appendChild(tr);
@@ -902,7 +1004,7 @@ class PvZConfiguratorApp {
if (res.ok && res.data.success) {
this.showToast('Мастер-пароль администратора успешно изменен!', 'success');
- document.getElementById('modal-admin-password').classList.remove('active');
+ this.closeModal('modal-admin-password');
document.getElementById('admin-current-password').value = '';
document.getElementById('admin-new-password').value = '';
document.getElementById('admin-confirm-password').value = '';
@@ -987,8 +1089,13 @@ class PvZConfiguratorApp {
}
async openPlayerStatsModal(userId) {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const res = await this.apiRequest(`/api/admin/user/${userId}`);
- if (!res.ok || !res.data.success) {
+ if (!res.ok || !res.data || !res.data.success) {
this.showToast('Ошибка загрузки статистики игрока', 'error');
return;
}
@@ -1003,7 +1110,7 @@ class PvZConfiguratorApp {
const idInput = document.getElementById('stats-player-id');
if (idInput) idInput.value = u.id;
- document.getElementById('stats-player-username').textContent = u.username;
+ document.getElementById('stats-player-username').textContent = u.username || '—';
document.getElementById('stats-player-id-line').textContent = `#${u.id} (${prof.theme || 'classic'})`;
document.getElementById('stats-player-created').textContent = u.createdAt || '—';
document.getElementById('stats-player-login').textContent = u.lastLogin || '—';
@@ -1041,12 +1148,17 @@ class PvZConfiguratorApp {
}
document.getElementById('stats-scores-summary').innerHTML = scoreTxt;
- document.getElementById('modal-player-stats').classList.add('active');
+ this.openModal('modal-player-stats');
}
async openEditPlayerModal(userId) {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const res = await this.apiRequest(`/api/admin/user/${userId}`);
- if (!res.ok || !res.data.success) {
+ if (!res.ok || !res.data || !res.data.success) {
this.showToast('Ошибка загрузки профиля игрока', 'error');
return;
}
@@ -1057,7 +1169,7 @@ class PvZConfiguratorApp {
const zen = prof.zenGarden || u.zenGarden || {};
document.getElementById('edit-player-id').value = u.id;
- document.getElementById('edit-player-title-name').textContent = u.username;
+ document.getElementById('edit-player-title-name').textContent = u.username || '';
document.getElementById('edit-player-coins').value = prof.coins ?? u.coins ?? 150;
document.getElementById('edit-player-level').value = prof.level ?? u.level ?? 1;
document.getElementById('edit-player-maxlevel').value = prof.maxUnlockedLevel ?? u.maxUnlockedLevel ?? 1;
@@ -1081,10 +1193,15 @@ class PvZConfiguratorApp {
const banCont = document.getElementById('edit-ban-reason-container');
if (banCont) banCont.style.display = u.isBanned ? 'block' : 'none';
- document.getElementById('modal-edit-player').classList.add('active');
+ this.openModal('modal-edit-player');
}
async savePlayerEdit() {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const id = document.getElementById('edit-player-id').value;
const updates = {
coins: parseInt(document.getElementById('edit-player-coins').value, 10) || 0,
@@ -1113,17 +1230,22 @@ class PvZConfiguratorApp {
};
const res = await this.apiRequest(`/api/admin/user/${id}/edit`, 'POST', { updates });
- if (res.ok && res.data.success) {
+ if (res.ok && res.data && res.data.success) {
this.showToast('Профиль игрока и ресурсы успешно обновлены!', 'success');
- document.getElementById('modal-edit-player').classList.remove('active');
+ this.closeModal('modal-edit-player');
this.loadUsers();
this.loadStats();
} else {
- this.showToast(res.data.message || 'Ошибка сохранения', 'error');
+ this.showToast((res.data && res.data.message) || 'Ошибка сохранения', 'error');
}
}
async createPlayer() {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const username = document.getElementById('create-player-username').value.trim();
const password = document.getElementById('create-player-password').value.trim();
const coins = parseInt(document.getElementById('create-player-coins').value, 10) || 150;
@@ -1141,26 +1263,36 @@ class PvZConfiguratorApp {
initialData: { coins, level: 1, maxUnlockedLevel: 1 }
});
- if (res.ok && res.data.success) {
- this.showToast(`Игрок ${username} создан!`, 'success');
- document.getElementById('modal-create-player').classList.remove('active');
+ if (res.ok && res.data && res.data.success) {
+ this.showToast(`Игрок ${username} успешно создан!`, 'success');
+ this.closeModal('modal-create-player');
document.getElementById('create-player-username').value = '';
document.getElementById('create-player-password').value = '';
this.loadUsers();
this.loadStats();
} else {
- this.showToast(res.data.message || 'Ошибка создания игрока', 'error');
+ this.showToast((res.data && res.data.message) || 'Ошибка создания игрока', 'error');
}
}
openResetPasswordModal(id, username) {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
document.getElementById('reset-pwd-user-id').value = id;
document.getElementById('reset-pwd-username').textContent = username;
document.getElementById('reset-pwd-input').value = '';
- document.getElementById('modal-reset-password').classList.add('active');
+ this.openModal('modal-reset-password');
}
async submitResetPassword() {
+ if (!this.isAuthenticated) {
+ this.showToast('Требуется авторизация администратора', 'warn');
+ return;
+ }
+
const id = document.getElementById('reset-pwd-user-id').value;
const pwd = document.getElementById('reset-pwd-input').value.trim();
if (!pwd) {
@@ -1169,11 +1301,11 @@ class PvZConfiguratorApp {
}
const res = await this.apiRequest(`/api/admin/user/${id}/password`, 'POST', { password: pwd });
- if (res.ok && res.data.success) {
+ if (res.ok && res.data && res.data.success) {
this.showToast('Пароль успешно сброшен', 'success');
- document.getElementById('modal-reset-password').classList.remove('active');
+ this.closeModal('modal-reset-password');
} else {
- this.showToast(res.data.message || 'Ошибка сброса пароля', 'error');
+ this.showToast((res.data && res.data.message) || 'Ошибка сброса пароля', 'error');
}
}
@@ -1254,7 +1386,7 @@ class PvZConfiguratorApp {
document.getElementById('bug-mod-status').value = b.status || 'новый';
document.getElementById('bug-mod-reply').value = b.adminReply || '';
- document.getElementById('modal-moderate-bug').classList.add('active');
+ this.openModal('modal-moderate-bug');
}
async saveBugModeration() {
@@ -1265,7 +1397,7 @@ class PvZConfiguratorApp {
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { status, adminReply });
if (res.ok && res.data.success) {
this.showToast(`Баг #${id} обновлен`, 'success');
- document.getElementById('modal-moderate-bug').classList.remove('active');
+ this.closeModal('modal-moderate-bug');
this.loadBugs();
} else {
this.showToast(res.data.message || 'Ошибка сохранения', 'error');
@@ -1279,7 +1411,7 @@ class PvZConfiguratorApp {
const res = await this.apiRequest(`/api/admin/bugs/${id}/status`, 'POST', { delete: true });
if (res.ok && res.data.success) {
this.showToast(`Баг #${id} удален`, 'success');
- document.getElementById('modal-moderate-bug').classList.remove('active');
+ this.closeModal('modal-moderate-bug');
this.loadBugs();
} else {
this.showToast(res.data.message || 'Ошибка удаления', 'error');
@@ -1434,46 +1566,94 @@ class PvZConfiguratorApp {
if (pane) pane.classList.add('active');
this.activeTab = target;
- if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
+ if (target === 'tab-players') this.loadUsers();
if (target === 'tab-sessions') this.loadSessions();
if (target === 'tab-logs') this.loadLoginLogs();
+ if (target === 'tab-levels' && this.levels.length === 0) this.loadCampaignLevels();
});
});
+ // Full-Screen Login Screen Bindings
+ const btnScreenSubmit = document.getElementById('btn-login-screen-submit');
+ if (btnScreenSubmit) {
+ btnScreenSubmit.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.handleScreenLogin();
+ });
+ }
+
+ const screenPwdInput = document.getElementById('login-screen-password');
+ if (screenPwdInput) {
+ screenPwdInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ this.handleScreenLogin();
+ }
+ });
+ }
+
+ const btnTogglePwd = document.getElementById('btn-toggle-login-password');
+ if (btnTogglePwd && screenPwdInput) {
+ btnTogglePwd.addEventListener('click', () => {
+ if (screenPwdInput.type === 'password') {
+ screenPwdInput.type = 'text';
+ btnTogglePwd.textContent = '🙈';
+ } else {
+ screenPwdInput.type = 'password';
+ btnTogglePwd.textContent = '👁️';
+ }
+ });
+ }
+
// Login modal
- document.getElementById('btn-login-modal-open').addEventListener('click', () => {
- document.getElementById('login-error-msg').style.display = 'none';
- document.getElementById('admin-password-input').value = '';
- document.getElementById('modal-login').classList.add('active');
- });
-
- document.getElementById('btn-submit-login').addEventListener('click', async () => {
- const pwd = document.getElementById('admin-password-input').value.trim();
- if (!pwd) return;
-
- const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
- if (res.ok && res.data.success) {
- this.token = res.data.token;
- localStorage.setItem('pvz_admin_token', this.token);
- this.onAuthenticated();
- document.getElementById('modal-login').classList.remove('active');
- this.showToast('Авторизация администратора успешна!', 'success');
- } else {
+ const btnLoginOpen = document.getElementById('btn-login-modal-open');
+ if (btnLoginOpen) {
+ btnLoginOpen.addEventListener('click', () => {
const err = document.getElementById('login-error-msg');
- err.textContent = res.data.message || 'Неверный пароль администратора';
- err.style.display = 'block';
- }
- });
+ if (err) err.style.display = 'none';
+ const input = document.getElementById('admin-password-input');
+ if (input) input.value = '';
+ this.openModal('modal-login');
+ });
+ }
+
+ const btnSubmitLogin = document.getElementById('btn-submit-login');
+ if (btnSubmitLogin) {
+ btnSubmitLogin.addEventListener('click', async () => {
+ const input = document.getElementById('admin-password-input');
+ const pwd = input ? input.value.trim() : '';
+ if (!pwd) return;
+
+ const res = await this.apiRequest('/api/admin/auth', 'POST', { password: pwd });
+ if (res.ok && res.data && res.data.success && res.data.token) {
+ this.token = res.data.token;
+ localStorage.setItem('pvz_admin_token', this.token);
+ this.closeModal('modal-login');
+ this.showToast('Авторизация администратора успешна!', 'success');
+ this.onAuthenticated();
+ } else {
+ const err = document.getElementById('login-error-msg');
+ if (err) {
+ err.textContent = (res.data && res.data.message) || 'Неверный пароль администратора';
+ err.style.display = 'block';
+ }
+ }
+ });
+ }
// Change Admin Password Modal
const btnOpenPwd = document.getElementById('btn-admin-password-modal-open');
if (btnOpenPwd) {
btnOpenPwd.addEventListener('click', () => {
- document.getElementById('admin-password-error').style.display = 'none';
- document.getElementById('admin-current-password').value = '';
- document.getElementById('admin-new-password').value = '';
- document.getElementById('admin-confirm-password').value = '';
- document.getElementById('modal-admin-password').classList.add('active');
+ const err = document.getElementById('admin-password-error');
+ if (err) err.style.display = 'none';
+ const cur = document.getElementById('admin-current-password');
+ if (cur) cur.value = '';
+ const nw = document.getElementById('admin-new-password');
+ if (nw) nw.value = '';
+ const cf = document.getElementById('admin-confirm-password');
+ if (cf) cf.value = '';
+ this.openModal('modal-admin-password');
});
}
@@ -1483,19 +1663,29 @@ class PvZConfiguratorApp {
}
// Logout
- document.getElementById('btn-admin-logout').addEventListener('click', () => {
- this.token = '';
- localStorage.removeItem('pvz_admin_token');
- this.onUnauthenticated();
- this.showToast('Вы вышли из панели администратора', 'info');
- });
+ const btnLogout = document.getElementById('btn-admin-logout');
+ if (btnLogout) {
+ btnLogout.addEventListener('click', () => {
+ this.token = '';
+ localStorage.removeItem('pvz_admin_token');
+ this.onUnauthenticated();
+ this.showToast('Вы вышли из панели администратора', 'info');
+ });
+ }
// Close Modals
document.querySelectorAll('[data-close]').forEach(btn => {
btn.addEventListener('click', () => {
const modalId = btn.getAttribute('data-close');
- const modal = document.getElementById(modalId);
- if (modal) modal.classList.remove('active');
+ this.closeModal(modalId);
+ });
+ });
+
+ document.querySelectorAll('.modal-overlay').forEach(overlay => {
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) {
+ overlay.classList.remove('active', 'open');
+ }
});
});
@@ -1539,26 +1729,56 @@ class PvZConfiguratorApp {
});
// Player Actions
- document.getElementById('btn-create-player-open').addEventListener('click', () => {
- document.getElementById('modal-create-player').classList.add('active');
- });
- document.getElementById('btn-submit-create-player').addEventListener('click', () => this.createPlayer());
- document.getElementById('btn-save-player-edit').addEventListener('click', () => this.savePlayerEdit());
- document.getElementById('btn-submit-reset-pwd').addEventListener('click', () => this.submitResetPassword());
- document.getElementById('btn-refresh-players').addEventListener('click', () => this.loadUsers());
- document.getElementById('player-search-input').addEventListener('input', () => this.renderPlayersTable());
- document.getElementById('player-filter-status').addEventListener('change', () => this.renderPlayersTable());
- document.getElementById('player-sort-select').addEventListener('change', () => this.renderPlayersTable());
- document.getElementById('edit-player-is-banned').addEventListener('change', (e) => {
- document.getElementById('edit-ban-reason-container').style.display = e.target.checked ? 'block' : 'none';
- });
+ const btnCreatePlayerOpen = document.getElementById('btn-create-player-open');
+ if (btnCreatePlayerOpen) {
+ btnCreatePlayerOpen.addEventListener('click', () => {
+ this.openModal('modal-create-player');
+ });
+ }
+ const btnSubmitCreatePlayer = document.getElementById('btn-submit-create-player');
+ if (btnSubmitCreatePlayer) {
+ btnSubmitCreatePlayer.addEventListener('click', () => this.createPlayer());
+ }
+ const btnSavePlayerEdit = document.getElementById('btn-save-player-edit');
+ if (btnSavePlayerEdit) {
+ btnSavePlayerEdit.addEventListener('click', () => this.savePlayerEdit());
+ }
+ const btnSubmitResetPwd = document.getElementById('btn-submit-reset-pwd');
+ if (btnSubmitResetPwd) {
+ btnSubmitResetPwd.addEventListener('click', () => this.submitResetPassword());
+ }
+ const btnRefreshPlayers = document.getElementById('btn-refresh-players');
+ if (btnRefreshPlayers) {
+ btnRefreshPlayers.addEventListener('click', () => this.loadUsers());
+ }
+ const playerSearchInput = document.getElementById('player-search-input');
+ if (playerSearchInput) {
+ playerSearchInput.addEventListener('input', () => this.renderPlayersTable());
+ }
+ const playerFilterStatus = document.getElementById('player-filter-status');
+ if (playerFilterStatus) {
+ playerFilterStatus.addEventListener('change', () => this.renderPlayersTable());
+ }
+ const playerSortSelect = document.getElementById('player-sort-select');
+ if (playerSortSelect) {
+ playerSortSelect.addEventListener('change', () => this.renderPlayersTable());
+ }
+ const editPlayerIsBanned = document.getElementById('edit-player-is-banned');
+ if (editPlayerIsBanned) {
+ editPlayerIsBanned.addEventListener('change', (e) => {
+ const container = document.getElementById('edit-ban-reason-container');
+ if (container) container.style.display = e.target.checked ? 'block' : 'none';
+ });
+ }
- document.getElementById('btn-stats-jump-edit')?.addEventListener('click', () => {
- const id = document.getElementById('stats-player-id')?.value;
- const modal = document.getElementById('modal-player-stats');
- if (modal) modal.classList.remove('active');
- if (id) this.openEditPlayerModal(id);
- });
+ const btnStatsJumpEdit = document.getElementById('btn-stats-jump-edit');
+ if (btnStatsJumpEdit) {
+ btnStatsJumpEdit.addEventListener('click', () => {
+ const id = document.getElementById('stats-player-id')?.value;
+ this.closeModal('modal-player-stats');
+ if (id) this.openEditPlayerModal(id);
+ });
+ }
// Balance Save Buttons
document.getElementById('btn-save-economy').addEventListener('click', () => this.saveBalanceSection('economy'));
diff --git a/scripts/db.py b/scripts/db.py
index 8153816..ffacc70 100644
--- a/scripts/db.py
+++ b/scripts/db.py
@@ -834,7 +834,7 @@ def get_all_active_sessions():
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.expires_at > ?
- ORDER BY s.last_active DESC
+ ORDER BY COALESCE(NULLIF(s.last_active, ''), s.created_at) DESC
""", (now_str,))
rows = cursor.fetchall()
conn.close()
@@ -844,9 +844,9 @@ def get_all_active_sessions():
"userId": r["user_id"],
"username": r["username"],
"isAdmin": bool(r["is_admin"]),
- "ipAddress": r["ip_address"] or "",
+ "ipAddress": r["ip_address"] if r["ip_address"] else "127.0.0.1",
"userAgent": r["user_agent"] or "",
- "deviceName": r["device_name"] or "Unknown Device",
+ "deviceName": r["device_name"] if r["device_name"] and r["device_name"] != "Unknown Device" else "Браузер",
"createdAt": r["created_at"],
"expiresAt": r["expires_at"],
"lastActive": r["last_active"] or r["created_at"]