feat(hub): mobile 2x2 exchange alert dashboard tiles
Phone monitor list shows per-exchange tiles with offline, >10% float loss, and missing SL alerts; tap opens full desktop-style detail view. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -471,6 +471,129 @@ button:disabled {
|
|||||||
box-shadow: 0 0 8px var(--red);
|
box-shadow: 0 0 8px var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-dot.warn {
|
||||||
|
background: #ffb020;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 176, 32, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* —— 手机监控总览瓦片 —— */
|
||||||
|
.monitor-alert-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px 10px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
background: var(--panel);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-alert-summary.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mas-item.mas-ok {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mas-item.mas-warn {
|
||||||
|
color: #ffb020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mas-item.mas-err {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mas-sep {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-monitor.grid-monitor-tiles {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||||
|
gap: 10px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 118px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile .hub-tile-body {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px 12px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-name {
|
||||||
|
font-family: var(--display);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-pnl {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-pnl small {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-foot {
|
||||||
|
margin-top: auto;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-error {
|
||||||
|
border-color: rgba(255, 77, 109, 0.45);
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 77, 109, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-warn {
|
||||||
|
border-color: rgba(255, 176, 32, 0.45);
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 176, 32, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-ok {
|
||||||
|
border-color: var(--border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-tile-body:hover .hub-tile-name {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-family: var(--display);
|
font-family: var(--display);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -1784,12 +1907,34 @@ body.login-page {
|
|||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-monitor,
|
.grid-monitor:not(.grid-monitor-tiles),
|
||||||
.settings-grid-wrap {
|
.settings-grid-wrap {
|
||||||
grid-template-columns: minmax(0, 1fr) !important;
|
grid-template-columns: minmax(0, 1fr) !important;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-monitor.grid-monitor-tiles {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-monitor .hint-box,
|
||||||
|
#page-monitor .page-desc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-monitor .page-head {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-monitor .page-head h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-alert-summary {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-head {
|
.card-head {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
||||||
const MONITOR_BOARD_SNAPSHOT_URL = "/api/monitor/board/snapshot";
|
const MONITOR_BOARD_SNAPSHOT_URL = "/api/monitor/board/snapshot";
|
||||||
const HUB_MONITOR_SNAPSHOT_TIMEOUT_MS = 15000;
|
const HUB_MONITOR_SNAPSHOT_TIMEOUT_MS = 15000;
|
||||||
|
/** 关注:浮亏超过交易账户余额的比例(10%) */
|
||||||
|
const HUB_ALERT_FLOAT_LOSS_RATIO = 0.1;
|
||||||
let lastMonitorBoardUpdatedAt = "";
|
let lastMonitorBoardUpdatedAt = "";
|
||||||
let localBoardVersion = 0;
|
let localBoardVersion = 0;
|
||||||
let monitorBoardInFlight = false;
|
let monitorBoardInFlight = false;
|
||||||
@@ -497,6 +499,7 @@
|
|||||||
? `UPD ${ts}`
|
? `UPD ${ts}`
|
||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
updateMonitorAlertSummary(rows || []);
|
||||||
renderMonitorGrid(rows || []);
|
renderMonitorGrid(rows || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,11 +523,125 @@
|
|||||||
return window.matchMedia("(max-width: 720px)").matches;
|
return window.matchMedia("(max-width: 720px)").matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 监控卡片列数:桌面 3/2 列;手机端固定单列 */
|
function positionHasContracts(p) {
|
||||||
|
const c = Number(p && p.contracts);
|
||||||
|
return Number.isFinite(c) && Math.abs(c) >= 1e-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exchangeNeedsFlask(row) {
|
||||||
|
const caps = row.capabilities || [];
|
||||||
|
return caps.includes("key") || caps.includes("trend");
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionMissingStopLoss(pos, orders, trends) {
|
||||||
|
if (!positionHasContracts(pos)) return false;
|
||||||
|
const mo = findMonitorOrder(orders, pos.symbol, pos.side);
|
||||||
|
const tp = findTrendPlan(trends, pos.symbol, pos.side);
|
||||||
|
const tpsl = resolvePositionTpsl(pos, mo, tp);
|
||||||
|
const sl = tpsl.sl;
|
||||||
|
if (sl !== "" && sl != null && Number.isFinite(Number(sl))) return false;
|
||||||
|
const cond = condOrdersFromPosition(pos);
|
||||||
|
const picked = pickExTpslOrders(cond);
|
||||||
|
if (picked.sl && picked.sl.trigger_price != null) return false;
|
||||||
|
const et = pos.exchange_tpsl;
|
||||||
|
if (et && et.sl) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzeExchangeAlert(row) {
|
||||||
|
const ag = row.agent || {};
|
||||||
|
const hm = row.hub_monitor || {};
|
||||||
|
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
||||||
|
const flaskOk = row.flask_ok !== false && hm.ok !== false;
|
||||||
|
const upnl = Number(ag.total_unrealized_pnl);
|
||||||
|
const balance = Number(ag.balance_usdt);
|
||||||
|
const sortUpnl = Number.isFinite(upnl) ? upnl : 0;
|
||||||
|
|
||||||
|
if (!row.http_ok) {
|
||||||
|
return { level: "error", summary: "子代理离线", sortUpnl: 0 };
|
||||||
|
}
|
||||||
|
if (ag.ok === false) {
|
||||||
|
return {
|
||||||
|
level: "error",
|
||||||
|
summary: (ag.error || row.error || "子代理异常").slice(0, 24),
|
||||||
|
sortUpnl: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (exchangeNeedsFlask(row) && !flaskOk) {
|
||||||
|
const fe = row.flask_error || hm.error || hm.msg || "Flask未连通";
|
||||||
|
return { level: "error", summary: String(fe).slice(0, 24), sortUpnl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const orders = flaskOk ? hm.orders || [] : [];
|
||||||
|
const trends = flaskOk ? hm.trends || [] : [];
|
||||||
|
let missingSl = false;
|
||||||
|
for (const p of pos) {
|
||||||
|
if (positionMissingStopLoss(p, orders, trends)) {
|
||||||
|
missingSl = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(upnl) && upnl < 0 && Number.isFinite(balance) && balance > 0) {
|
||||||
|
const lossPct = (Math.abs(upnl) / balance) * 100;
|
||||||
|
if (lossPct >= HUB_ALERT_FLOAT_LOSS_RATIO * 100) {
|
||||||
|
return {
|
||||||
|
level: "warn",
|
||||||
|
summary: `浮亏超10% · ${fmt(upnl, 2)}U`,
|
||||||
|
sortUpnl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missingSl) {
|
||||||
|
return { level: "warn", summary: "缺止损", sortUpnl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCount = pos.filter(positionHasContracts).length;
|
||||||
|
return {
|
||||||
|
level: "ok",
|
||||||
|
summary: openCount ? "正常" : "空仓",
|
||||||
|
sortUpnl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortRowsForMobileDashboard(rows) {
|
||||||
|
const levelOrder = { error: 0, warn: 1, ok: 2 };
|
||||||
|
return rows
|
||||||
|
.map((r) => ({ r, a: analyzeExchangeAlert(r) }))
|
||||||
|
.sort((x, y) => {
|
||||||
|
const ld = levelOrder[x.a.level] - levelOrder[y.a.level];
|
||||||
|
if (ld !== 0) return ld;
|
||||||
|
return (x.a.sortUpnl || 0) - (y.a.sortUpnl || 0);
|
||||||
|
})
|
||||||
|
.map((x) => x.r);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMonitorAlertSummary(rows) {
|
||||||
|
const el = document.getElementById("monitor-alert-summary");
|
||||||
|
if (!el) return;
|
||||||
|
if (!isMobileLayout() || !rows.length) {
|
||||||
|
el.classList.add("hidden");
|
||||||
|
el.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let err = 0;
|
||||||
|
let warn = 0;
|
||||||
|
let ok = 0;
|
||||||
|
rows.forEach((r) => {
|
||||||
|
const lv = analyzeExchangeAlert(r).level;
|
||||||
|
if (lv === "error") err += 1;
|
||||||
|
else if (lv === "warn") warn += 1;
|
||||||
|
else ok += 1;
|
||||||
|
});
|
||||||
|
el.classList.remove("hidden");
|
||||||
|
el.innerHTML = `<span class="mas-item mas-ok">正常 ${ok}</span><span class="mas-sep">·</span><span class="mas-item mas-warn">关注 ${warn}</span><span class="mas-sep">·</span><span class="mas-item mas-err">异常 ${err}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监控卡片列数:桌面 3/2 列;手机端 2 列瓦片 */
|
||||||
function syncMonitorGridColumns(gridEl, count) {
|
function syncMonitorGridColumns(gridEl, count) {
|
||||||
if (!gridEl) return;
|
if (!gridEl) return;
|
||||||
if (isMobileLayout()) {
|
if (isMobileLayout()) {
|
||||||
gridEl.style.gridTemplateColumns = "minmax(0, 1fr)";
|
gridEl.style.gridTemplateColumns = "repeat(2, minmax(0, 1fr))";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cols = 3;
|
let cols = 3;
|
||||||
@@ -538,12 +655,22 @@
|
|||||||
|
|
||||||
function initMobileLayout() {
|
function initMobileLayout() {
|
||||||
let resizeTimer = null;
|
let resizeTimer = null;
|
||||||
|
let wasMobile = isMobileLayout();
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
clearTimeout(resizeTimer);
|
clearTimeout(resizeTimer);
|
||||||
resizeTimer = setTimeout(() => {
|
resizeTimer = setTimeout(() => {
|
||||||
|
const nowMobile = isMobileLayout();
|
||||||
|
if (lastMonitorRows.length && nowMobile !== wasMobile) {
|
||||||
|
wasMobile = nowMobile;
|
||||||
|
renderMonitorGrid(lastMonitorRows);
|
||||||
|
updateMonitorAlertSummary(lastMonitorRows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wasMobile = nowMobile;
|
||||||
const box = document.getElementById("monitor-grid");
|
const box = document.getElementById("monitor-grid");
|
||||||
if (box && lastMonitorRows.length) {
|
if (box && lastMonitorRows.length) {
|
||||||
syncMonitorGridColumns(box, lastMonitorRows.length);
|
syncMonitorGridColumns(box, lastMonitorRows.length);
|
||||||
|
updateMonitorAlertSummary(lastMonitorRows);
|
||||||
}
|
}
|
||||||
}, 120);
|
}, 120);
|
||||||
});
|
});
|
||||||
@@ -766,9 +893,14 @@
|
|||||||
if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
|
if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
|
||||||
closeExchangeFullscreen();
|
closeExchangeFullscreen();
|
||||||
}
|
}
|
||||||
|
const mobileTiles = isMobileLayout() && !expandedExchangeId;
|
||||||
|
const displayRows = mobileTiles ? sortRowsForMobileDashboard(rows) : rows;
|
||||||
|
box.classList.toggle("grid-monitor-tiles", mobileTiles);
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
rows.map((r) => renderMonitorCard(r)).join("") || '<div class="err">无已启用账户</div>';
|
displayRows
|
||||||
syncMonitorGridColumns(box, rows.length);
|
.map((r) => (mobileTiles ? renderMonitorTile(r) : renderMonitorCard(r)))
|
||||||
|
.join("") || '<div class="err">无已启用账户</div>';
|
||||||
|
syncMonitorGridColumns(box, displayRows.length);
|
||||||
bindMonitorInteractions(box);
|
bindMonitorInteractions(box);
|
||||||
|
|
||||||
if (expandedExchangeId && fs && fsInner) {
|
if (expandedExchangeId && fs && fsInner) {
|
||||||
@@ -1775,6 +1907,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMonitorTile(row) {
|
||||||
|
const ag = row.agent || {};
|
||||||
|
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
||||||
|
const alert = analyzeExchangeAlert(row);
|
||||||
|
const upnl = ag.total_unrealized_pnl;
|
||||||
|
const openCount = pos.filter(positionHasContracts).length;
|
||||||
|
const dotCls =
|
||||||
|
alert.level === "error" ? "bad" : alert.level === "warn" ? "warn" : "ok";
|
||||||
|
const tileCls =
|
||||||
|
alert.level === "error"
|
||||||
|
? "hub-tile-error"
|
||||||
|
: alert.level === "warn"
|
||||||
|
? "hub-tile-warn"
|
||||||
|
: "hub-tile-ok";
|
||||||
|
const ts = (lastMonitorBoardUpdatedAt || "").replace("T", " ");
|
||||||
|
const tsShort = ts ? ts.slice(-8) : "—";
|
||||||
|
const posLine =
|
||||||
|
openCount > 0 ? `${openCount}仓 · ${alert.summary}` : alert.summary;
|
||||||
|
return `<div class="card hub-tile ${tileCls}" data-ex-id="${esc(row.id)}">
|
||||||
|
<div class="hub-tile-body card-expand-zone" title="点击进入全屏详情">
|
||||||
|
<div class="hub-tile-top">
|
||||||
|
<span class="status-dot ${dotCls}" aria-hidden="true"></span>
|
||||||
|
<span class="hub-tile-name">${esc(row.name)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hub-tile-pnl ${pnlCls(upnl)}">${fmt(upnl, 2)} <small>U</small></div>
|
||||||
|
<div class="hub-tile-meta">${esc(posLine)}</div>
|
||||||
|
<div class="hub-tile-foot">UPD ${esc(tsShort)}</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMonitorCard(row) {
|
function renderMonitorCard(row) {
|
||||||
const ag = row.agent || {};
|
const ag = row.agent || {};
|
||||||
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260528-hub-ind-fs" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260604-hub-mobile-tiles" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<div id="page-monitor" class="page">
|
<div id="page-monitor" class="page">
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1><span class="head-tag">MON</span> 监控区</h1>
|
<h1><span class="head-tag">MON</span> 监控区</h1>
|
||||||
<p class="page-desc">实时聚合持仓、关键位与趋势计划</p>
|
<p class="page-desc">实时聚合持仓、关键位与趋势计划;手机端为四所状态总览,点卡片进入全屏详情。</p>
|
||||||
</div>
|
</div>
|
||||||
<details class="hint-box">
|
<details class="hint-box">
|
||||||
<summary>数据来源与复盘链接</summary>
|
<summary>数据来源与复盘链接</summary>
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
点「实例 / 策略交易 / 复盘」在<strong>本页内嵌</strong>打开(SSO 免密);按住 Ctrl 点击可在新标签打开。
|
点「实例 / 策略交易 / 复盘」在<strong>本页内嵌</strong>打开(SSO 免密);按住 Ctrl 点击可在新标签打开。
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<div id="monitor-alert-summary" class="monitor-alert-summary hidden" aria-live="polite"></div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button type="button" id="btn-monitor-refresh" class="primary">立即刷新</button>
|
<button type="button" id="btn-monitor-refresh" class="primary">立即刷新</button>
|
||||||
<label class="chk-label">
|
<label class="chk-label">
|
||||||
@@ -245,6 +246,6 @@
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260603-hub-binance-tick"></script>
|
<script src="/assets/chart.js?v=20260603-hub-binance-tick"></script>
|
||||||
<script src="/assets/app.js?v=20260604-hub-board-refresh"></script>
|
<script src="/assets/app.js?v=20260604-hub-mobile-tiles"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user