fix: expose dashboard login shell

This commit is contained in:
Курнат Андрей
2026-07-19 22:20:14 +03:00
parent 0ec64645b6
commit 393454c9e0
4 changed files with 39 additions and 16 deletions
+1
View File
@@ -338,6 +338,7 @@ tbody tr:hover td { background: rgba(255,255,255,.012); }
.dialog p:not(.eyebrow):not(.form-error) { margin: 11px 0 22px; color: var(--muted); font-size: 11px; line-height: 1.6; } .dialog p:not(.eyebrow):not(.form-error) { margin: 11px 0 22px; color: var(--muted); font-size: 11px; line-height: 1.6; }
.dialog label { display: block; margin-bottom: 8px; color: var(--muted); font-size: 10px; } .dialog label { display: block; margin-bottom: 8px; color: var(--muted); font-size: 10px; }
.dialog input { width: 100%; height: 40px; padding: 0 11px; border: 1px solid var(--line); border-radius: 7px; outline: 0; background: #090c10; color: var(--text); } .dialog input { width: 100%; height: 40px; padding: 0 11px; border: 1px solid var(--line); border-radius: 7px; outline: 0; background: #090c10; color: var(--text); }
.auth-fields { display: grid; gap: 14px; }
.dialog .button-wide { margin-top: 14px; } .dialog .button-wide { margin-top: 14px; }
.form-error { min-height: 16px; margin: 8px 0 0; color: var(--red); font-size: 9px; } .form-error { min-height: 16px; margin: 8px 0 0; color: var(--red); font-size: 9px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 9px; } .dialog-actions { display: flex; justify-content: flex-end; gap: 9px; }
+18 -7
View File
@@ -2,7 +2,7 @@
const state = { const state = {
snapshot: null, snapshot: null,
token: "", authorization: "",
loading: false, loading: false,
timer: null, timer: null,
marketFilter: "", marketFilter: "",
@@ -32,7 +32,7 @@ document.addEventListener("DOMContentLoaded", () => {
bindNavigation(); bindNavigation();
bindControls(); bindControls();
selectPage(location.hash.slice(1) || "overview", false); selectPage(location.hash.slice(1) || "overview", false);
loadSnapshot(); showAuthDialog();
}); });
function bindNavigation() { function bindNavigation() {
@@ -76,17 +76,26 @@ function bindControls() {
}); });
$("#authForm").addEventListener("submit", async (event) => { $("#authForm").addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();
state.token = $("#tokenInput").value.trim(); const username = $("#usernameInput").value.trim();
const password = $("#passwordInput").value;
setText("authError", ""); setText("authError", "");
if (!state.token) return; if (!username || !password) return;
state.authorization = basicAuthorization(username, password);
await loadSnapshot(true, true); await loadSnapshot(true, true);
}); });
} }
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.token) headers["X-TradeBot-Token"] = state.token; if (state.authorization) headers.Authorization = state.authorization;
const response = await fetch(path, { const response = await fetch(path, {
...options, ...options,
headers, headers,
@@ -115,11 +124,13 @@ async function loadSnapshot(manual = false, fromAuth = false) {
render(snapshot); render(snapshot);
setOffline(false); setOffline(false);
if ($("#authDialog").open) $("#authDialog").close(); if ($("#authDialog").open) $("#authDialog").close();
$("#passwordInput").value = "";
setText("authError", ""); setText("authError", "");
scheduleRefresh(10000); scheduleRefresh(10000);
} catch (error) { } catch (error) {
if (error instanceof AuthRequiredError) { if (error instanceof AuthRequiredError) {
if (fromAuth) setText("authError", "Токен не принят сервером."); if (fromAuth) setText("authError", "Неверный логин или пароль.");
state.authorization = "";
showAuthDialog(); showAuthDialog();
} else { } else {
setOffline(true, error.message); setOffline(true, error.message);
@@ -140,7 +151,7 @@ function showAuthDialog() {
const dialog = $("#authDialog"); const dialog = $("#authDialog");
if (!dialog.open) dialog.showModal(); if (!dialog.open) dialog.showModal();
setText("syncLabel", "Нужна авторизация"); setText("syncLabel", "Нужна авторизация");
setTimeout(() => $("#tokenInput").focus(), 50); setTimeout(() => $("#usernameInput").focus(), 50);
} }
function setOffline(offline, message = "") { function setOffline(offline, message = "") {
+14 -6
View File
@@ -194,12 +194,20 @@
<form id="authForm"> <form id="authForm">
<div class="dialog-icon">T</div> <div class="dialog-icon">T</div>
<p class="eyebrow">Защищённый доступ</p> <p class="eyebrow">Защищённый доступ</p>
<h2>Требуется API-токен</h2> <h2>Вход в TradeBot</h2>
<p>Прокси-авторизация не обнаружена. Токен останется только в памяти этой вкладки и не будет сохранён.</p> <p>Введите логин и пароль панели управления. Данные используются только для запросов из этой вкладки и не сохраняются в браузере.</p>
<label for="tokenInput">Токен TradeBot</label> <div class="auth-fields">
<input id="tokenInput" type="password" autocomplete="current-password" required> <div>
<label for="usernameInput">Логин</label>
<input id="usernameInput" type="text" autocomplete="username" required>
</div>
<div>
<label for="passwordInput">Пароль</label>
<input id="passwordInput" type="password" autocomplete="current-password" required>
</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" type="submit">Войти</button>
</form> </form>
</dialog> </dialog>
@@ -213,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=3" defer></script> <script src="/assets/dashboard.js?v=4" defer></script>
</body> </body>
</html> </html>
+6 -3
View File
@@ -64,13 +64,16 @@ def test_safe_config_summarizes_torch_forecast_artifact(make_settings, tmp_path)
def test_web_ui_assets_are_available() -> None: def test_web_ui_assets_are_available() -> None:
html = WEB_INDEX.read_text(encoding="utf-8") html = WEB_INDEX.read_text(encoding="utf-8")
script = WEB_INDEX.with_name("dashboard.js").read_text(encoding="utf-8")
assert "TradeBot — панель управления" in html assert "TradeBot — панель управления" in html
assert "/assets/dashboard.css" in html assert "/assets/dashboard.css" in html
assert "/assets/dashboard.js" in html assert "/assets/dashboard.js" in html
assert "/web-api/dashboard/snapshot" in WEB_INDEX.with_name("dashboard.js").read_text( assert 'id="usernameInput"' in html
encoding="utf-8" 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
def test_compact_markets_keeps_dashboard_fields_and_limits_candles() -> None: def test_compact_markets_keeps_dashboard_fields_and_limits_candles() -> None: