fix: expose dashboard login shell
This commit is contained in:
@@ -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; }
|
||||||
|
|||||||
@@ -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 = "") {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user