254 lines
9.0 KiB
JavaScript
254 lines
9.0 KiB
JavaScript
// app.js
|
||
|
||
// ── utils ──────────────────────────────────────────────────────────────────────
|
||
const fmtNum = (x, d = 2) =>
|
||
x === null || x === undefined || Number.isNaN(x) ? "–" : Number(x).toFixed(d);
|
||
|
||
// Potencjalne adresy backendu (API). Pierwszy to aktualny origin UI.
|
||
const apiCandidates = [
|
||
window.location.origin,
|
||
"http://127.0.0.1:8000",
|
||
"http://localhost:8000",
|
||
"http://172.27.20.120:8000", // z logów serwera
|
||
];
|
||
|
||
let API_BASE = null;
|
||
let warnedMixed = false;
|
||
|
||
function withTimeout(ms = 6000) {
|
||
const ctrl = new AbortController();
|
||
const id = setTimeout(() => ctrl.abort("timeout"), ms);
|
||
return { signal: ctrl.signal, done: () => clearTimeout(id) };
|
||
}
|
||
|
||
function makeUrl(path) {
|
||
return `${API_BASE}${path}${path.includes("?") ? "&" : "?"}_ts=${Date.now()}`;
|
||
}
|
||
|
||
function setBadge(id, text, ok = true) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.textContent = text;
|
||
el.className = ok ? "badge ok" : "badge err";
|
||
}
|
||
|
||
function setBadgeTitle(id, title) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.title = title || "";
|
||
}
|
||
|
||
function warnMixedContent(base) {
|
||
if (!warnedMixed && location.protocol === "https:" && base?.startsWith("http://")) {
|
||
warnedMixed = true;
|
||
console.warn(
|
||
"[api] UI działa przez HTTPS, a API przez HTTP — przeglądarka może blokować żądania (mixed content)."
|
||
);
|
||
alert(
|
||
"UI działa przez HTTPS, a API przez HTTP. Uruchom UI przez HTTP albo włącz HTTPS dla API — inaczej przeglądarka zablokuje żądania."
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── autodetekcja backendu ─────────────────────────────────────────────────────
|
||
async function pickBackend() {
|
||
for (const base of apiCandidates) {
|
||
try {
|
||
const t = withTimeout(2500);
|
||
const r = await fetch(`${base}/api/status?_ts=${Date.now()}`, {
|
||
cache: "no-store",
|
||
signal: t.signal,
|
||
});
|
||
t.done();
|
||
if (r.ok) {
|
||
API_BASE = base;
|
||
console.debug("[api] using", API_BASE);
|
||
warnMixedContent(API_BASE);
|
||
setBadgeTitle("loopState", `API: ${API_BASE}`);
|
||
return;
|
||
}
|
||
console.debug("[api] probe", base, "->", r.status);
|
||
} catch (e) {
|
||
// ignorujemy i próbujemy kolejny kandydat
|
||
console.debug("[api] probe fail", base, e?.message || e);
|
||
}
|
||
}
|
||
throw new Error("Nie znaleziono działającego backendu (API_BASE). Upewnij się, że server.py działa na porcie 8000.");
|
||
}
|
||
|
||
// ── API helpers ───────────────────────────────────────────────────────────────
|
||
async function apiGet(path) {
|
||
const url = makeUrl(path);
|
||
const t0 = performance.now();
|
||
const t = withTimeout(6000);
|
||
try {
|
||
const r = await fetch(url, { cache: "no-store", signal: t.signal });
|
||
const t1 = performance.now();
|
||
console.debug("[api] GET", url, r.status, (t1 - t0).toFixed(1) + "ms");
|
||
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
||
return r.json();
|
||
} finally {
|
||
t.done();
|
||
}
|
||
}
|
||
|
||
async function apiPost(path, body) {
|
||
const url = makeUrl(path);
|
||
const t0 = performance.now();
|
||
const t = withTimeout(6000);
|
||
try {
|
||
const r = await fetch(url, {
|
||
method: "POST",
|
||
headers: body ? { "Content-Type": "application/json" } : undefined,
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
signal: t.signal,
|
||
});
|
||
const t1 = performance.now();
|
||
console.debug("[api] POST", url, r.status, (t1 - t0).toFixed(1) + "ms");
|
||
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
||
return r.json();
|
||
} finally {
|
||
t.done();
|
||
}
|
||
}
|
||
|
||
// ── refreshers ────────────────────────────────────────────────────────────────
|
||
async function refreshStatus() {
|
||
try {
|
||
const s = await apiGet("/api/status");
|
||
setBadge("loopState", s.running ? "RUNNING" : "STOPPED", s.running);
|
||
setBadgeTitle("loopState", `API: ${API_BASE} | last_action=${s.last_action || "–"}`);
|
||
|
||
const roundEl = document.getElementById("roundNo");
|
||
if (roundEl) roundEl.textContent = s.round ?? "–";
|
||
|
||
const cashEl = document.getElementById("cash");
|
||
if (cashEl) cashEl.textContent = fmtNum(s.cash, 2);
|
||
|
||
// now-playing
|
||
const stageEl = document.getElementById("stage");
|
||
if (stageEl) stageEl.textContent = (s.stage || "idle").toUpperCase();
|
||
|
||
const tickerEl = document.getElementById("ticker");
|
||
if (tickerEl) tickerEl.textContent = s.current_ticker || "–";
|
||
|
||
const idx = s.current_index ?? 0;
|
||
const total = s.tickers_total ?? 0;
|
||
|
||
const progressTextEl = document.getElementById("progressText");
|
||
if (progressTextEl) progressTextEl.textContent = `${Math.min(idx + 1, total)} / ${total}`;
|
||
|
||
const prog = document.getElementById("progress");
|
||
if (prog) {
|
||
prog.max = total || 1;
|
||
prog.value = Math.min(idx + 1, total) || 0;
|
||
}
|
||
|
||
const lastActionEl = document.getElementById("lastAction");
|
||
if (lastActionEl) lastActionEl.textContent = s.last_action || "–";
|
||
} catch (e) {
|
||
console.error("status error:", e);
|
||
setBadge("loopState", "ERR", false);
|
||
setBadgeTitle("loopState", `API: ${API_BASE || "—"} | ${e?.message || e}`);
|
||
}
|
||
}
|
||
|
||
async function refreshPositions() {
|
||
try {
|
||
const { positions } = await apiGet("/api/positions");
|
||
const tbody = document.querySelector("#positions tbody");
|
||
if (!tbody) return;
|
||
tbody.innerHTML = "";
|
||
for (const p of positions) {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td>${p.ticker}</td>
|
||
<td>${p.side}</td>
|
||
<td class="num">${fmtNum(p.size, 0)}</td>
|
||
<td class="num">${fmtNum(p.entry_price)}</td>
|
||
<td class="num">${fmtNum(p.last_price)}</td>
|
||
<td class="num">${fmtNum(p.pnl)}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
}
|
||
} catch (e) {
|
||
console.error("positions error:", e);
|
||
}
|
||
}
|
||
|
||
async function refreshTrades() {
|
||
try {
|
||
const { trades } = await apiGet("/api/trades");
|
||
const tbody = document.querySelector("#trades tbody");
|
||
if (!tbody) return;
|
||
tbody.innerHTML = "";
|
||
trades.slice(-50).reverse().forEach((t) => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td>${t.time ?? ""}</td>
|
||
<td>${t.ticker ?? ""}</td>
|
||
<td>${t.action ?? ""}</td>
|
||
<td class="num">${fmtNum(t.price)}</td>
|
||
<td class="num">${fmtNum(t.size, 0)}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
} catch (e) {
|
||
console.error("trades error:", e);
|
||
}
|
||
}
|
||
|
||
async function refreshAll() {
|
||
await Promise.all([refreshStatus(), refreshPositions(), refreshTrades()]);
|
||
}
|
||
|
||
// ── auto refresh ──────────────────────────────────────────────────────────────
|
||
let timer = null;
|
||
let currentInterval = 2000; // domyślnie 2s (zgodne z Twoim selectem)
|
||
|
||
function startAutoRefresh() {
|
||
if (timer) return;
|
||
timer = setInterval(refreshAll, currentInterval);
|
||
console.debug("[ui] auto refresh started", currentInterval, "ms");
|
||
}
|
||
function stopAutoRefresh() {
|
||
if (!timer) return;
|
||
clearInterval(timer);
|
||
timer = null;
|
||
console.debug("[ui] auto refresh stopped");
|
||
}
|
||
|
||
// ── bootstrap ────────────────────────────────────────────────────────────────
|
||
document.addEventListener("DOMContentLoaded", async () => {
|
||
// Przyciski
|
||
document.getElementById("btnStart")?.addEventListener("click", async () => {
|
||
try { await apiPost("/api/start"); await refreshStatus(); } catch (e) { console.error(e); }
|
||
});
|
||
document.getElementById("btnStop")?.addEventListener("click", async () => {
|
||
try { await apiPost("/api/stop"); await refreshStatus(); } catch (e) { console.error(e); }
|
||
});
|
||
document.getElementById("btnTick")?.addEventListener("click", async () => {
|
||
try { await apiPost("/api/run-once"); await refreshAll(); } catch (e) { console.error(e); }
|
||
});
|
||
|
||
const sel = document.getElementById("refreshMs");
|
||
sel?.addEventListener("change", () => {
|
||
currentInterval = parseInt(sel.value, 10);
|
||
if (timer) { stopAutoRefresh(); startAutoRefresh(); }
|
||
});
|
||
|
||
document.getElementById("autoOn")?.addEventListener("click", startAutoRefresh);
|
||
document.getElementById("autoOff")?.addEventListener("click", stopAutoRefresh);
|
||
|
||
// Autodetekcja backendu przed pierwszym odświeżeniem
|
||
try {
|
||
await pickBackend();
|
||
await refreshAll();
|
||
startAutoRefresh();
|
||
} catch (e) {
|
||
console.error(e);
|
||
setBadge("loopState", "NO API", false);
|
||
setBadgeTitle("loopState", e?.message || String(e));
|
||
alert("UI nie może połączyć się z backendem (port 8000). Uruchom server.py lub zaktualizuj API_BASE w app.js.");
|
||
}
|
||
});
|