From 3ce92b6428628e501b6a2ac52f9a699e7988fa18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Sun, 19 Jul 2026 22:56:19 +0300 Subject: [PATCH] fix: make dashboard login responsive --- crypto_spot_bot/web/dashboard.js | 62 ++++++++++++++++++++++---------- crypto_spot_bot/web/index.html | 6 ++-- tests/test_dashboard.py | 6 ++-- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/crypto_spot_bot/web/dashboard.js b/crypto_spot_bot/web/dashboard.js index 71d8ac3..40a562b 100644 --- a/crypto_spot_bot/web/dashboard.js +++ b/crypto_spot_bot/web/dashboard.js @@ -2,7 +2,7 @@ const state = { snapshot: null, - authorization: "", + token: "", loading: false, timer: null, marketFilter: "", @@ -78,30 +78,49 @@ function bindControls() { event.preventDefault(); const username = $("#usernameInput").value.trim(); const password = $("#passwordInput").value; + const submitButton = $("#authSubmitButton"); setText("authError", ""); if (!username || !password) return; - state.authorization = basicAuthorization(username, password); - await loadSnapshot(true, true); + if (username !== "sevenhill") { + setText("authError", "Неверный логин или пароль."); + return; + } + state.token = password.trim(); + submitButton.disabled = true; + submitButton.setAttribute("aria-busy", "true"); + setText("authSubmitButton", "Входим…"); + setText("authError", "Проверяем доступ…"); + try { + await loadSnapshot(true, true); + } finally { + submitButton.disabled = false; + submitButton.removeAttribute("aria-busy"); + setText("authSubmitButton", "Войти"); + } }); } -function basicAuthorization(username, password) { - const bytes = new TextEncoder().encode(`${username}:${password}`); - let binary = ""; - bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); - return `Basic ${btoa(binary)}`; -} - async function api(path, options = {}) { const headers = { Accept: "application/json", ...(options.headers || {}) }; if (options.body) headers["Content-Type"] = "application/json"; - if (state.authorization) headers.Authorization = state.authorization; - const response = await fetch(path, { - ...options, - headers, - credentials: "same-origin", - cache: "no-store", - }); + if (state.token) headers["X-TradeBot-Token"] = state.token; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30000); + let response; + try { + response = await fetch(path, { + ...options, + headers, + signal: controller.signal, + credentials: "omit", + cache: "no-store", + }); + } catch (error) { + if (error?.name === "AbortError") throw new Error("Сервер не ответил за 30 секунд."); + throw error; + } finally { + clearTimeout(timeout); + } if (response.status === 401) throw new AuthRequiredError("Требуется авторизация"); let payload = null; try { payload = await response.json(); } catch (_) { payload = null; } @@ -113,7 +132,7 @@ async function api(path, options = {}) { } async function loadSnapshot(manual = false, fromAuth = false) { - if (state.loading) return; + if (state.loading) return false; state.loading = true; clearTimeout(state.timer); $("#refreshButton").classList.add("is-spinning"); @@ -127,15 +146,20 @@ async function loadSnapshot(manual = false, fromAuth = false) { $("#passwordInput").value = ""; setText("authError", ""); scheduleRefresh(10000); + return true; } catch (error) { if (error instanceof AuthRequiredError) { if (fromAuth) setText("authError", "Неверный логин или пароль."); - state.authorization = ""; + state.token = ""; + showAuthDialog(); + } else if (fromAuth) { + setText("authError", `Ошибка подключения: ${error.message}`); showAuthDialog(); } else { setOffline(true, error.message); scheduleRefresh(12000); } + return false; } finally { state.loading = false; $("#refreshButton").classList.remove("is-spinning"); diff --git a/crypto_spot_bot/web/index.html b/crypto_spot_bot/web/index.html index a1b1793..508016e 100644 --- a/crypto_spot_bot/web/index.html +++ b/crypto_spot_bot/web/index.html @@ -195,7 +195,7 @@
T

Защищённый доступ

Вход в TradeBot

-

Введите логин и пароль панели управления. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.

+

Введите логин и пароль панели управления TradeBot. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.

@@ -207,7 +207,7 @@
- + @@ -221,6 +221,6 @@
- + diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index c36c502..aa19998 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -72,8 +72,10 @@ def test_web_ui_assets_are_available() -> None: assert 'id="usernameInput"' in html assert 'id="passwordInput"' in html assert "/web-api/dashboard/snapshot" in script - assert "headers.Authorization = state.authorization" in script - assert "X-TradeBot-Token" not in script + assert 'headers["X-TradeBot-Token"] = state.token' in script + assert 'credentials: "omit"' in script + assert "AbortController" in script + assert 'id="authSubmitButton"' in html def test_compact_markets_keeps_dashboard_fields_and_limits_candles() -> None: