From 99e6fed5ef2bd31a3bf4f417fdd068f4202545de Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 24 May 2026 16:31:29 +0800 Subject: [PATCH] refactor: replace JS polling with RefreshWidget - Delete dashboard_refresh.js (no longer needed) - Add api/dashboard_content.dspy returns dynamic UI with live data (queries llmusage + users tables server-side, returns full widget tree) - Simplify index.ui to use RefreshWidget with period_seconds=10 - ChartBar in dashboard_content.dspy auto-fetches top models via data_url - load_path.py updated separately in sage repo --- wwwroot/api/dashboard_content.dspy | 100 ++++++++++++++++ wwwroot/dashboard_refresh.js | 79 ------------- wwwroot/index.ui | 184 +---------------------------- 3 files changed, 104 insertions(+), 259 deletions(-) create mode 100644 wwwroot/api/dashboard_content.dspy delete mode 100644 wwwroot/dashboard_refresh.js diff --git a/wwwroot/api/dashboard_content.dspy b/wwwroot/api/dashboard_content.dspy new file mode 100644 index 0000000..0aa5375 --- /dev/null +++ b/wwwroot/api/dashboard_content.dspy @@ -0,0 +1,100 @@ +"""返回 dashboard 动态 UI 描述(含实时数据)""" +from datetime import date, datetime, timedelta + +db = DBPools() +today = date.today().isoformat() +now = datetime.now() +five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S') + +# 今日用量 +async with db.sqlorContext('sage') as sor: + usage = await sor.sqlExe( + "SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$", + {'today': today} + ) + cnt = int(usage[0]['cnt']) if usage else 0 + total_amount = float(usage[0]['total_amount']) if usage else 0.0 + +# 总用户数 +async with db.sqlorContext('sage') as sor: + users = await sor.sqlExe("SELECT COUNT(*) as total_users FROM users", {}) + total_users = int(users[0]['total_users']) if users else 0 + +# 并发用户 +async with db.sqlorContext('sage') as sor: + conc = await sor.sqlExe( + "SELECT COUNT(DISTINCT userid) as concurrent_users FROM llmusage WHERE use_date = ${today}$ AND use_time >= ${five_min_ago}$", + {'today': today, 'five_min_ago': five_min_ago} + ) + concurrent_users = int(conc[0]['concurrent_users']) if conc else 0 + +# ChartBar data_url — 用相对路径,bricks 会基于当前上下文解析 +chart_data_url = 'api/get_top_models.dspy' + +return { + "widgettype": "VBox", + "options": {"width": "100%"}, + "subwidgets": [ + { + "widgettype": "ResponsableBox", + "options": {"gap": "16px", "minWidth": "250px"}, + "subwidgets": [ + { + "widgettype": "VBox", + "id": "card_today_cnt", + "options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "今日调用笔数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}}, + {"widgettype": "Text", "id": "today_cnt_value", "options": {"text": str(cnt), "fontSize": "32px", "fontWeight": "bold", "color": "#1890ff"}} + ] + }, + { + "widgettype": "VBox", + "id": "card_today_amount", + "options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "今日交易金额", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}}, + {"widgettype": "Text", "id": "today_amount_value", "options": {"text": f"\u00a5{total_amount:.2f}", "fontSize": "32px", "fontWeight": "bold", "color": "#52c41a"}} + ] + }, + { + "widgettype": "VBox", + "id": "card_total_users", + "options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "用户总数", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}}, + {"widgettype": "Text", "id": "total_users_value", "options": {"text": str(total_users), "fontSize": "32px", "fontWeight": "bold", "color": "#722ed1"}} + ] + }, + { + "widgettype": "VBox", + "id": "card_concurrent_users", + "options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "flex": "1", "minHeight": "120px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "当前并发用户", "fontSize": "14px", "color": "#888", "marginBottom": "8px"}}, + {"widgettype": "Text", "id": "concurrent_users_value", "options": {"text": str(concurrent_users), "fontSize": "32px", "fontWeight": "bold", "color": "#fa8c16"}} + ] + } + ] + }, + { + "widgettype": "VBox", + "id": "chart_section", + "options": {"bgcolor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "marginTop": "20px", "minHeight": "350px", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "Top 3 模型(今日)", "fontSize": "18px", "fontWeight": "bold", "color": "#333", "marginBottom": "16px"}}, + { + "widgettype": "ChartBar", + "id": "top_models_chart", + "options": { + "height": "300px", + "width": "100%", + "data_url": chart_data_url, + "nameField": "model_name", + "valueFields": ["cnt", "total_amount"] + } + } + ] + } + ] +} diff --git a/wwwroot/dashboard_refresh.js b/wwwroot/dashboard_refresh.js deleted file mode 100644 index 2eafea0..0000000 --- a/wwwroot/dashboard_refresh.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Dashboard auto-refresh script - * Polls API every 10 seconds and updates stat cards - * ChartBar handles its own refresh via refresh_period - * Auto-loaded by Sage's header.tmpl from wwwroot/ - */ -(function() { - 'use strict'; - - function getBaseUrl() { - var path = window.location.pathname; - // path is like /dashboard_for_sage/index.ui or /dashboard_for_sage/ - var parts = path.split('/').filter(function(p) { return p.length > 0; }); - if (parts.length === 0) return ''; - return '/' + parts[0]; - } - - function buildUrl(dspyFile) { - var base = getBaseUrl(); - return base + '/' + dspyFile + '?_webbricks_=1'; - } - - function setCardText(cardId, value) { - try { - var widget = bricks.getWidgetById(cardId, bricks.app); - if (widget && typeof widget.set_text === 'function') { - widget.set_text(String(value)); - } - } catch(e) {} - } - - function formatNumber(n) { - if (typeof n === 'number') return n.toLocaleString(); - return String(n); - } - - function formatAmount(n) { - if (typeof n === 'number') return '\u00a5' + n.toFixed(2); - return '\u00a50.00'; - } - - async function refreshStats() { - try { - var url = buildUrl('api/get_today_usage.dspy'); - var resp = await fetch(url, { credentials: 'include' }); - if (!resp.ok) return; - var data = await resp.json(); - setCardText('today_cnt_value', formatNumber(data.cnt)); - setCardText('today_amount_value', formatAmount(data.total_amount)); - } catch(e) { - console.error('refreshStats error:', e); - } - } - - async function refreshUsers() { - try { - var url = buildUrl('api/get_user_stats.dspy'); - var resp = await fetch(url, { credentials: 'include' }); - if (!resp.ok) return; - var data = await resp.json(); - setCardText('total_users_value', formatNumber(data.total_users)); - setCardText('concurrent_users_value', formatNumber(data.concurrent_users)); - } catch(e) { - console.error('refreshUsers error:', e); - } - } - - async function refreshAll() { - await refreshStats(); - await refreshUsers(); - } - - // Wait for bricks framework to initialize, then start auto-refresh - setTimeout(function() { - refreshAll(); - setInterval(refreshAll, 10000); - }, 2000); - -})(); diff --git a/wwwroot/index.ui b/wwwroot/index.ui index 3001a28..23c1af9 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -18,187 +18,11 @@ } }, { - "widgettype": "ResponsableBox", + "widgettype": "RefreshWidget", + "id": "dashboard_refresh", "options": { - "gap": "16px", - "minWidth": "250px" - }, - "subwidgets": [ - { - "widgettype": "VBox", - "id": "card_today_cnt", - "options": { - "bgcolor": "#FFFFFF", - "padding": "24px", - "borderRadius": "8px", - "flex": "1", - "minHeight": "120px", - "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "text": "今日调用笔数", - "fontSize": "14px", - "color": "#888", - "marginBottom": "8px" - } - }, - { - "widgettype": "Text", - "id": "today_cnt_value", - "options": { - "text": "--", - "fontSize": "32px", - "fontWeight": "bold", - "color": "#1890ff" - } - } - ] - }, - { - "widgettype": "VBox", - "id": "card_today_amount", - "options": { - "bgcolor": "#FFFFFF", - "padding": "24px", - "borderRadius": "8px", - "flex": "1", - "minHeight": "120px", - "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "text": "今日交易金额", - "fontSize": "14px", - "color": "#888", - "marginBottom": "8px" - } - }, - { - "widgettype": "Text", - "id": "today_amount_value", - "options": { - "text": "--", - "fontSize": "32px", - "fontWeight": "bold", - "color": "#52c41a" - } - } - ] - }, - { - "widgettype": "VBox", - "id": "card_total_users", - "options": { - "bgcolor": "#FFFFFF", - "padding": "24px", - "borderRadius": "8px", - "flex": "1", - "minHeight": "120px", - "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "text": "用户总数", - "fontSize": "14px", - "color": "#888", - "marginBottom": "8px" - } - }, - { - "widgettype": "Text", - "id": "total_users_value", - "options": { - "text": "--", - "fontSize": "32px", - "fontWeight": "bold", - "color": "#722ed1" - } - } - ] - }, - { - "widgettype": "VBox", - "id": "card_concurrent_users", - "options": { - "bgcolor": "#FFFFFF", - "padding": "24px", - "borderRadius": "8px", - "flex": "1", - "minHeight": "120px", - "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "text": "当前并发用户", - "fontSize": "14px", - "color": "#888", - "marginBottom": "8px" - } - }, - { - "widgettype": "Text", - "id": "concurrent_users_value", - "options": { - "text": "--", - "fontSize": "32px", - "fontWeight": "bold", - "color": "#fa8c16" - } - } - ] - } - ] - }, - { - "widgettype": "VBox", - "id": "chart_section", - "options": { - "bgcolor": "#FFFFFF", - "padding": "24px", - "borderRadius": "8px", - "marginTop": "20px", - "minHeight": "350px", - "boxShadow": "0 2px 8px rgba(0,0,0,0.1)" - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "text": "Top 3 模型(今日)", - "fontSize": "18px", - "fontWeight": "bold", - "color": "#333", - "marginBottom": "16px" - } - }, - { - "widgettype": "ChartBar", - "id": "top_models_chart", - "options": { - "height": "300px", - "width": "100%", - "data_url": "{{entire_url('api/get_top_models.dspy')}}", - "nameField": "model_name", - "valueFields": ["cnt", "total_amount"], - "refresh_period": 10 - } - } - ] - }, - { - "widgettype": "Html", - "id": "auto_refresh", - "options": { - "html": "
数据每10秒自动刷新
" + "period_seconds": 10, + "url": "{{entire_url('api/dashboard_content.dspy')}}" } } ]