fix: make dashboard login responsive
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user