feat(hub): dashboard SSE push, light-theme cards, simplify AI coach

Replace dashboard polling with backend SSE and snapshot refresh. Restyle for light/dark theme with soft card glow instead of neon. Remove Today's Summary from AI page; keep trading and general chat only. Update hub documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 10:53:50 +08:00
parent 582ada7e60
commit 07e8604ea6
11 changed files with 481 additions and 424 deletions
+75 -23
View File
@@ -1,12 +1,13 @@
/**
* 中控数据看板:总览 / 分户 / 平仓明细,60s 自动刷新
* 中控数据看板:后端 SSE 推送版本号,前端拉快照刷新(无轮询闪烁)
*/
(function () {
const page = document.getElementById("page-dashboard");
if (!page) return;
const POLL_MS = 60 * 1000;
let timer = null;
let dashEventSource = null;
let dashReconnectTimer = null;
let localDashVersion = 0;
let inited = false;
let loading = false;
@@ -191,10 +192,11 @@
}
}
async function fetchDashboard() {
if (loading) return;
async function fetchDashboardSnapshot(opts) {
const options = opts || {};
if (loading && !options.force) return;
loading = true;
setStatus("同步中…");
if (!options.silent) setStatus("同步中…");
try {
const r = await fetch("/api/dashboard/daily", { credentials: "same-origin" });
if (r.status === 401) {
@@ -202,9 +204,12 @@
return;
}
const data = await r.json();
if (!data.ok) throw new Error(data.detail || data.msg || "加载失败");
if (!data.ok) throw new Error(data.detail || data.msg || data.error || "加载失败");
const ver = Number(data.dashboard_version) || 0;
if (ver) localDashVersion = ver;
renderPayload(data);
setStatus(`${(data.poll_interval_sec || 60)}s 自动刷新`);
const sec = Number(data.poll_interval_sec) || 60;
setStatus(options.silent ? `SSE 已连接 · 后台每 ${sec}s 聚合` : `已更新 · 后台每 ${sec}s 聚合`);
} catch (e) {
setStatus(String(e.message || e), true);
} finally {
@@ -212,31 +217,78 @@
}
}
function startPoll() {
stopPoll();
void fetchDashboard();
timer = setInterval(fetchDashboard, POLL_MS);
}
function stopPoll() {
if (timer) {
clearInterval(timer);
timer = null;
function closeDashboardStream() {
if (dashEventSource) {
dashEventSource.close();
dashEventSource = null;
}
if (dashReconnectTimer) {
clearTimeout(dashReconnectTimer);
dashReconnectTimer = null;
}
}
function connectDashboardStream() {
closeDashboardStream();
dashEventSource = new EventSource("/api/dashboard/stream");
dashEventSource.addEventListener("dashboard", (ev) => {
try {
const st = JSON.parse(ev.data || "{}");
const ver = Number(st.dashboard_version) || 0;
if (ver && ver !== localDashVersion) {
void fetchDashboardSnapshot({ silent: true });
} else if (st.aggregating) {
setStatus("后台聚合中…");
}
} catch (_) {
/* ignore */
}
});
dashEventSource.onerror = () => {
closeDashboardStream();
setStatus("SSE 断开,8s 后重连…", true);
dashReconnectTimer = setTimeout(() => {
if (inited) {
connectDashboardStream();
void fetchDashboardSnapshot({ silent: true });
}
}, 8000);
};
}
async function requestDashboardRefresh() {
try {
await fetch("/api/dashboard/refresh", { method: "POST", credentials: "same-origin" });
} catch (_) {
/* ignore */
}
}
function startLive() {
void fetchDashboardSnapshot();
connectDashboardStream();
}
function stopLive() {
closeDashboardStream();
setStatus("");
}
if (btnRefresh) {
btnRefresh.addEventListener("click", () => void fetchDashboard());
btnRefresh.addEventListener("click", () => {
void requestDashboardRefresh();
void fetchDashboardSnapshot({ force: true });
});
}
window.hubDashboardPage = {
init() {
if (!inited) inited = true;
startPoll();
inited = true;
startLive();
},
destroy() {
stopPoll();
setStatus("");
inited = false;
stopLive();
},
};
})();