fix: make dashboard login responsive

This commit is contained in:
Курнат Андрей
2026-07-19 22:56:19 +03:00
parent 393454c9e0
commit 3ce92b6428
3 changed files with 50 additions and 24 deletions
+43 -19
View File
@@ -2,7 +2,7 @@
const state = { const state = {
snapshot: null, snapshot: null,
authorization: "", token: "",
loading: false, loading: false,
timer: null, timer: null,
marketFilter: "", marketFilter: "",
@@ -78,30 +78,49 @@ function bindControls() {
event.preventDefault(); event.preventDefault();
const username = $("#usernameInput").value.trim(); const username = $("#usernameInput").value.trim();
const password = $("#passwordInput").value; const password = $("#passwordInput").value;
const submitButton = $("#authSubmitButton");
setText("authError", ""); setText("authError", "");
if (!username || !password) return; if (!username || !password) return;
state.authorization = basicAuthorization(username, password); if (username !== "sevenhill") {
await loadSnapshot(true, true); 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 = {}) { async function api(path, options = {}) {
const headers = { Accept: "application/json", ...(options.headers || {}) }; const headers = { Accept: "application/json", ...(options.headers || {}) };
if (options.body) headers["Content-Type"] = "application/json"; if (options.body) headers["Content-Type"] = "application/json";
if (state.authorization) headers.Authorization = state.authorization; if (state.token) headers["X-TradeBot-Token"] = state.token;
const response = await fetch(path, { const controller = new AbortController();
...options, const timeout = setTimeout(() => controller.abort(), 30000);
headers, let response;
credentials: "same-origin", try {
cache: "no-store", 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("Требуется авторизация"); if (response.status === 401) throw new AuthRequiredError("Требуется авторизация");
let payload = null; let payload = null;
try { payload = await response.json(); } catch (_) { 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) { async function loadSnapshot(manual = false, fromAuth = false) {
if (state.loading) return; if (state.loading) return false;
state.loading = true; state.loading = true;
clearTimeout(state.timer); clearTimeout(state.timer);
$("#refreshButton").classList.add("is-spinning"); $("#refreshButton").classList.add("is-spinning");
@@ -127,15 +146,20 @@ async function loadSnapshot(manual = false, fromAuth = false) {
$("#passwordInput").value = ""; $("#passwordInput").value = "";
setText("authError", ""); setText("authError", "");
scheduleRefresh(10000); scheduleRefresh(10000);
return true;
} catch (error) { } catch (error) {
if (error instanceof AuthRequiredError) { if (error instanceof AuthRequiredError) {
if (fromAuth) setText("authError", "Неверный логин или пароль."); if (fromAuth) setText("authError", "Неверный логин или пароль.");
state.authorization = ""; state.token = "";
showAuthDialog();
} else if (fromAuth) {
setText("authError", `Ошибка подключения: ${error.message}`);
showAuthDialog(); showAuthDialog();
} else { } else {
setOffline(true, error.message); setOffline(true, error.message);
scheduleRefresh(12000); scheduleRefresh(12000);
} }
return false;
} finally { } finally {
state.loading = false; state.loading = false;
$("#refreshButton").classList.remove("is-spinning"); $("#refreshButton").classList.remove("is-spinning");
+3 -3
View File
@@ -195,7 +195,7 @@
<div class="dialog-icon">T</div> <div class="dialog-icon">T</div>
<p class="eyebrow">Защищённый доступ</p> <p class="eyebrow">Защищённый доступ</p>
<h2>Вход в TradeBot</h2> <h2>Вход в TradeBot</h2>
<p>Введите логин и пароль панели управления. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.</p> <p>Введите логин и пароль панели управления TradeBot. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.</p>
<div class="auth-fields"> <div class="auth-fields">
<div> <div>
<label for="usernameInput">Логин</label> <label for="usernameInput">Логин</label>
@@ -207,7 +207,7 @@
</div> </div>
</div> </div>
<p class="form-error" id="authError" role="alert"></p> <p class="form-error" id="authError" role="alert"></p>
<button class="button button-primary button-wide" type="submit">Войти</button> <button class="button button-primary button-wide" id="authSubmitButton" type="submit">Войти</button>
</form> </form>
</dialog> </dialog>
@@ -221,6 +221,6 @@
</dialog> </dialog>
<div class="toast" id="toast" role="status" aria-live="polite"></div> <div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="/assets/dashboard.js?v=4" defer></script> <script src="/assets/dashboard.js?v=5" defer></script>
</body> </body>
</html> </html>
+4 -2
View File
@@ -72,8 +72,10 @@ def test_web_ui_assets_are_available() -> None:
assert 'id="usernameInput"' in html assert 'id="usernameInput"' in html
assert 'id="passwordInput"' in html assert 'id="passwordInput"' in html
assert "/web-api/dashboard/snapshot" in script assert "/web-api/dashboard/snapshot" in script
assert "headers.Authorization = state.authorization" in script assert 'headers["X-TradeBot-Token"] = state.token' in script
assert "X-TradeBot-Token" not 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: def test_compact_markets_keeps_dashboard_fields_and_limits_candles() -> None: