diff --git a/static/app.js b/static/app.js
new file mode 100644
index 0000000..1af0f4d
--- /dev/null
+++ b/static/app.js
@@ -0,0 +1,411 @@
+let currentUser = null;
+let currentPartner = null;
+let selectedUserElement = null;
+let lastMessageId = 0;
+let messagesInterval = null;
+
+const BASE_PATH = document.querySelector('base')?.getAttribute('href') || '/';
+const API_PREFIX = BASE_PATH.replace(/\/$/, '');
+
+// Переключение вкладок логин/регистрация
+function switchTab(tab) {
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ document.querySelectorAll('.auth-form').forEach(f => f.style.display = 'none');
+
+ if (tab === 'login') {
+ document.querySelectorAll('.tab')[0].classList.add('active');
+ document.getElementById('login-form').style.display = 'flex';
+ } else {
+ document.querySelectorAll('.tab')[1].classList.add('active');
+ document.getElementById('register-form').style.display = 'flex';
+ }
+}
+
+// Регистрация
+async function register() {
+ const username = document.getElementById('reg-username').value.trim();
+ const password = document.getElementById('reg-password').value.trim();
+
+ if (!username || !password) {
+ showAuthError('Заполните все поля');
+ return;
+ }
+
+ try {
+ const res = await fetch(`${API_PREFIX}/api/register`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({username, password})
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ showAuthError(data.message, 'green');
+ } else {
+ showAuthError(data.error);
+ }
+ } catch (err) {
+ showAuthError('Ошибка соединения');
+ }
+}
+
+function showAuthError(message, color = 'red') {
+ const errorEl = document.getElementById('auth-error');
+ errorEl.textContent = message;
+ errorEl.style.color = color;
+}
+
+// Вход
+async function login() {
+ const username = document.getElementById('login-username').value.trim();
+ const password = document.getElementById('login-password').value.trim();
+
+ if (!username || !password) {
+ showAuthError('Заполните все поля');
+ return;
+ }
+
+ try {
+ const res = await fetch(`${API_PREFIX}/api/login`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({username, password})
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ currentUser = data;
+ document.getElementById('auth-screen').style.display = 'none';
+ document.getElementById('chat-screen').style.display = 'flex';
+ document.getElementById('current-username').textContent = `👤 ${data.username}`;
+
+ if (data.is_admin) {
+ document.getElementById('admin-btn').style.display = 'block';
+ }
+
+ // На десктопе показываем сайдбар
+ if (window.innerWidth > 768) {
+ document.getElementById('sidebar').classList.remove('hidden');
+ }
+
+ loadUsers();
+
+ // Запускаем проверку новых сообщений
+ startPolling();
+ } else {
+ showAuthError(data.error);
+ }
+ } catch (err) {
+ showAuthError('Ошибка соединения');
+ }
+}
+
+// Загрузка списка пользователей
+async function loadUsers() {
+ if (!currentUser) return;
+
+ try {
+ const res = await fetch(`${API_PREFIX}/api/users?user_id=${currentUser.user_id}`);
+
+ if (!res.ok) {
+ console.error('Error loading users');
+ return;
+ }
+
+ const users = await res.json();
+
+ const usersList = document.getElementById('users-list');
+ usersList.innerHTML = '';
+
+ if (!users || users.length === 0) {
+ usersList.innerHTML = '
+ ${msg.from_id !== currentUser.user_id ? `
${escapeHtml(msg.username)}
` : ''}
+ ${escapeHtml(msg.text)}
+
${time}
+
+ `;
+
+ messagesDiv.appendChild(msgEl);
+ });
+
+ // Скроллим вниз только если были внизу или это принудительно
+ if (isAtBottom || forceScroll) {
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
+ }
+ } catch (err) {
+ console.error('Error loading messages:', err);
+ }
+}
+
+// Защита от XSS
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+// Отправка сообщения
+async function sendMessage() {
+ const input = document.getElementById('message-input');
+ const text = input.value.trim();
+
+ if (!text || !currentPartner) return;
+
+ // Блокируем кнопку на время отправки
+ const sendBtn = document.getElementById('send-btn');
+ sendBtn.disabled = true;
+ input.disabled = true;
+
+ try {
+ const res = await fetch(`${API_PREFIX}/api/messages`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ from_id: currentUser.user_id,
+ to_id: currentPartner.id,
+ text: text
+ })
+ });
+
+ if (res.ok) {
+ input.value = '';
+ // Принудительно обновляем сообщения
+ await loadMessages(true);
+ }
+ } catch (err) {
+ console.error('Error sending message:', err);
+ } finally {
+ sendBtn.disabled = false;
+ input.disabled = false;
+ input.focus();
+ }
+}
+
+// Умный поллинг
+function startPolling() {
+ if (messagesInterval) {
+ clearInterval(messagesInterval);
+ }
+
+ // Проверяем новые сообщения каждые 2 секунды
+ messagesInterval = setInterval(() => {
+ if (currentPartner) {
+ loadMessages(false);
+ }
+ }, 2000);
+
+ // Также проверяем список пользователей раз в 30 секунд
+ setInterval(() => {
+ if (currentUser) {
+ loadUsers();
+ }
+ }, 30000);
+}
+
+// Показать боковую панель на мобильных
+function showSidebar() {
+ if (window.innerWidth <= 768) {
+ document.getElementById('sidebar').classList.remove('hidden');
+ }
+}
+
+// Enter для отправки
+document.addEventListener('DOMContentLoaded', () => {
+ const messageInput = document.getElementById('message-input');
+
+ messageInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage();
+ }
+ });
+
+ // Фикс для мобильных устройств - поднятие поля ввода при фокусе
+ messageInput.addEventListener('focus', () => {
+ setTimeout(() => {
+ const messagesDiv = document.getElementById('messages');
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
+ messageInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }, 300);
+ });
+
+ messageInput.addEventListener('blur', () => {
+ window.scrollTo(0, 0);
+ });
+});
+
+// Обработка изменения размера окна
+let resizeTimeout;
+window.addEventListener('resize', () => {
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(() => {
+ if (window.innerWidth > 768) {
+ document.getElementById('sidebar').classList.remove('hidden');
+ } else if (!currentPartner) {
+ document.getElementById('sidebar').classList.remove('hidden');
+ }
+ }, 250);
+});
+
+// Админ панель
+async function showAdminPanel() {
+ const panel = document.getElementById('admin-panel');
+ panel.style.display = 'flex';
+
+ try {
+ const res = await fetch(`${API_PREFIX}/api/admin/users?admin_id=${currentUser.user_id}`);
+ const users = await res.json();
+
+ const adminList = document.getElementById('admin-users-list');
+ adminList.innerHTML = '
+ ${user.id !== currentUser.user_id ? `
+
+
+ ` : ''}
+
+ `;
+ adminList.appendChild(userEl);
+ });
+ } catch (err) {
+ console.error('Error loading admin panel:', err);
+ }
+}
+
+function closeAdminPanel() {
+ document.getElementById('admin-panel').style.display = 'none';
+}
+
+async function toggleUser(userId, active) {
+ try {
+ await fetch(`${API_PREFIX}/api/admin/toggle`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ admin_id: currentUser.user_id,
+ user_id: userId,
+ active: active
+ })
+ });
+
+ // Обновляем админ-панель и список пользователей
+ await showAdminPanel();
+ await loadUsers();
+ } catch (err) {
+ console.error('Error toggling user:', err);
+ }
+}
\ No newline at end of file
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..a691766
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,86 @@
+
+
+
+