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
This commit is contained in:
parent
c7180bda77
commit
99e6fed5ef
100
wwwroot/api/dashboard_content.dspy
Normal file
100
wwwroot/api/dashboard_content.dspy
Normal file
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
|
|
||||||
})();
|
|
||||||
184
wwwroot/index.ui
184
wwwroot/index.ui
@ -18,187 +18,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"widgettype": "ResponsableBox",
|
"widgettype": "RefreshWidget",
|
||||||
|
"id": "dashboard_refresh",
|
||||||
"options": {
|
"options": {
|
||||||
"gap": "16px",
|
"period_seconds": 10,
|
||||||
"minWidth": "250px"
|
"url": "{{entire_url('api/dashboard_content.dspy')}}"
|
||||||
},
|
|
||||||
"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": "<div id='refresh_indicator' style='position:fixed;bottom:10px;right:10px;font-size:12px;color:#999;'>数据每10秒自动刷新</div>"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user