refactor: use individual RefreshWidget per stat card with Jinja2 .ui templates
Architecture: - load_dashboard.py: async data functions registered via load_dashboard() - init.py: calls load_dashboard() to expose on ServerEnv - Individual .ui files (Jinja2 templates): - today_usage.ui: calls get_today_usage(request) - today_amount.ui: calls get_today_amount(request) - total_users.ui: calls get_total_users(request) - concurrent_users.ui: calls get_concurrent_users(request) - top_models_chart.ui: calls get_top_models(request) for ChartBar - index.ui: each stat card wrapped in own RefreshWidget (10s period) - Removed deprecated .dspy files (dashboard_cards, get_today_usage, get_user_stats, get_top_models)
This commit is contained in:
parent
d2210a2996
commit
a2a6e9a2d5
@ -1,12 +1 @@
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
MODULE_NAME = "dashboard_for_sage"
|
||||
MODULE_VERSION = "1.0.0"
|
||||
|
||||
|
||||
def load_dashboard_for_sage():
|
||||
"""Register dashboard module with ServerEnv."""
|
||||
env = ServerEnv()
|
||||
# Dashboard is pure display - all logic is in .dspy files
|
||||
# which use globals() functions directly.
|
||||
return True
|
||||
from .load_dashboard import load_dashboard
|
||||
|
||||
92
dashboard_for_sage/load_dashboard.py
Normal file
92
dashboard_for_sage/load_dashboard.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""Dashboard data functions - exposed via load_dashboard() on ServerEnv"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from sqlor.dbpools import get_sor_context
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
|
||||
async def get_today_usage(request):
|
||||
"""获取当天llmusage笔数"""
|
||||
today = date.today().isoformat()
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, 'sage') as sor:
|
||||
sql = "SELECT COUNT(*) as cnt FROM llmusage WHERE use_date = ${today}$"
|
||||
recs = await sor.sqlExe(sql, {'today': today})
|
||||
cnt = int(recs[0].get('cnt', 0)) if recs else 0
|
||||
return cnt
|
||||
|
||||
|
||||
async def get_today_amount(request):
|
||||
"""获取当天交易金额"""
|
||||
today = date.today().isoformat()
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, 'sage') as sor:
|
||||
sql = "SELECT COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$"
|
||||
recs = await sor.sqlExe(sql, {'today': today})
|
||||
amount = float(recs[0].get('total_amount', 0)) if recs else 0.0
|
||||
return amount
|
||||
|
||||
|
||||
async def get_total_users(request):
|
||||
"""获取用户总数"""
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, 'sage') as sor:
|
||||
sql = "SELECT COUNT(*) as total_users FROM users"
|
||||
recs = await sor.sqlExe(sql, {})
|
||||
total = int(recs[0].get('total_users', 0)) if recs else 0
|
||||
return total
|
||||
|
||||
|
||||
async def get_concurrent_users(request):
|
||||
"""获取当前并发用户数(近5分钟有活跃记录的用户)"""
|
||||
now = datetime.now()
|
||||
five_min_ago = (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
today = now.strftime('%Y-%m-%d')
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, 'sage') as sor:
|
||||
sql = """
|
||||
SELECT COUNT(DISTINCT userid) as concurrent_users
|
||||
FROM llmusage
|
||||
WHERE use_date = ${today}$
|
||||
AND use_time >= ${five_min_ago}$
|
||||
"""
|
||||
recs = await sor.sqlExe(sql, {'today': today, 'five_min_ago': five_min_ago})
|
||||
cnt = int(recs[0].get('concurrent_users', 0)) if recs else 0
|
||||
return cnt
|
||||
|
||||
|
||||
async def get_top_models(request):
|
||||
"""获取当天排名前三的模型(返回记录列表,供ChartBar使用)"""
|
||||
today = date.today().isoformat()
|
||||
env = request._run_ns
|
||||
async with get_sor_context(env, 'sage') as sor:
|
||||
sql = """
|
||||
SELECT
|
||||
b.name as model_name,
|
||||
COUNT(*) as cnt,
|
||||
COALESCE(SUM(a.amount), 0) as total_amount
|
||||
FROM llmusage a
|
||||
LEFT JOIN llm b ON a.llmid = b.id
|
||||
WHERE a.use_date = ${today}$
|
||||
GROUP BY a.llmid, b.name
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 3
|
||||
"""
|
||||
recs = await sor.sqlExe(sql, {'today': today})
|
||||
result = []
|
||||
for r in recs:
|
||||
result.append({
|
||||
'model_name': r.get('model_name', 'Unknown'),
|
||||
'cnt': int(r.get('cnt', 0)),
|
||||
'total_amount': float(r.get('total_amount', 0))
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def load_dashboard():
|
||||
"""Register dashboard functions on ServerEnv"""
|
||||
g = ServerEnv()
|
||||
g.get_today_usage = get_today_usage
|
||||
g.get_today_amount = get_today_amount
|
||||
g.get_total_users = get_total_users
|
||||
g.get_concurrent_users = get_concurrent_users
|
||||
g.get_top_models = get_top_models
|
||||
@ -1,73 +0,0 @@
|
||||
"""获取当天llmusage笔数和交易金额 + 用户统计 — 返回完整卡片widget树供RefreshWidget加载"""
|
||||
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
now = datetime.datetime.now()
|
||||
five_min_ago = (now - datetime.timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
db = DBPools()
|
||||
|
||||
# 今日用量
|
||||
async with db.sqlorContext('sage') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total_amount FROM llmusage WHERE use_date = ${today}$",
|
||||
{'today': today}
|
||||
)
|
||||
cnt = int(recs[0].get('cnt', 0)) if recs else 0
|
||||
total_amount = float(recs[0].get('total_amount', 0)) if recs else 0.0
|
||||
|
||||
# 总用户数
|
||||
async with db.sqlorContext('sage') as sor:
|
||||
user_recs = await sor.sqlExe("SELECT COUNT(*) as total_users FROM users", {})
|
||||
total_users = int(user_recs[0].get('total_users', 0)) if user_recs 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].get('concurrent_users', 0)) if conc else 0
|
||||
|
||||
return {
|
||||
"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": "\u00a5%.2f" % total_amount, "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"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
"""获取当天llmusage笔数和交易金额"""
|
||||
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
COUNT(*) as cnt,
|
||||
COALESCE(SUM(amount), 0) as total_amount
|
||||
FROM llmusage
|
||||
WHERE use_date = ${today}$
|
||||
"""
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('sage') as sor:
|
||||
recs = await sor.sqlExe(sql, {'today': today})
|
||||
if recs:
|
||||
return {
|
||||
'cnt': int(recs[0].get('cnt', 0)),
|
||||
'total_amount': float(recs[0].get('total_amount', 0))
|
||||
}
|
||||
return {'cnt': 0, 'total_amount': 0}
|
||||
@ -1,31 +0,0 @@
|
||||
"""获取当天排名前三的模型数量和金额"""
|
||||
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
b.name as model_name,
|
||||
COUNT(*) as cnt,
|
||||
COALESCE(SUM(a.amount), 0) as total_amount
|
||||
FROM llmusage a
|
||||
LEFT JOIN llm b ON a.llmid = b.id
|
||||
WHERE a.use_date = ${today}$
|
||||
GROUP BY a.llmid, b.name
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 3
|
||||
"""
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('sage') as sor:
|
||||
recs = await sor.sqlExe(sql, {'today': today})
|
||||
result = []
|
||||
for r in recs:
|
||||
result.append({
|
||||
'model_name': r.get('model_name', 'Unknown'),
|
||||
'cnt': int(r.get('cnt', 0)),
|
||||
'total_amount': float(r.get('total_amount', 0))
|
||||
})
|
||||
return result
|
||||
|
||||
return []
|
||||
@ -1,31 +0,0 @@
|
||||
"""获取当前用户总数和并发用户数(近5分钟有活跃记录的用户)"""
|
||||
# datetime, json, DBPools 由 ahserver 预加载,无需 import
|
||||
|
||||
now = datetime.datetime.now()
|
||||
five_min_ago = (now - datetime.timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
today = now.strftime('%Y-%m-%d')
|
||||
|
||||
sql_users = "SELECT COUNT(*) as total_users FROM users"
|
||||
|
||||
sql_concurrent = """
|
||||
SELECT COUNT(DISTINCT userid) as concurrent_users
|
||||
FROM llmusage
|
||||
WHERE use_date = ${today}$
|
||||
AND use_time >= ${five_min_ago}$
|
||||
"""
|
||||
|
||||
db = DBPools()
|
||||
async with db.sqlorContext('sage') as sor:
|
||||
user_recs = await sor.sqlExe(sql_users, {})
|
||||
total_users = int(user_recs[0].get('total_users', 0)) if user_recs else 0
|
||||
|
||||
conc_recs = await sor.sqlExe(sql_concurrent, {
|
||||
'today': today,
|
||||
'five_min_ago': five_min_ago
|
||||
})
|
||||
concurrent_users = int(conc_recs[0].get('concurrent_users', 0)) if conc_recs else 0
|
||||
|
||||
return {
|
||||
'total_users': total_users,
|
||||
'concurrent_users': concurrent_users
|
||||
}
|
||||
32
wwwroot/concurrent_users.ui
Normal file
32
wwwroot/concurrent_users.ui
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"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": "{{get_concurrent_users(request)}}",
|
||||
"fontSize": "32px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#fa8c16"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -18,16 +18,48 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "RefreshWidget",
|
||||
"id": "cards_refresh",
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"period_seconds": 10,
|
||||
"url": "{{entire_url('api/dashboard_cards.dspy')}}"
|
||||
}
|
||||
"gap": "16px",
|
||||
"minWidth": "250px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "RefreshWidget",
|
||||
"id": "refresh_today_usage",
|
||||
"options": {
|
||||
"period_seconds": 10,
|
||||
"url": "{{entire_url('today_usage.ui')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "RefreshWidget",
|
||||
"id": "refresh_today_amount",
|
||||
"options": {
|
||||
"period_seconds": 10,
|
||||
"url": "{{entire_url('today_amount.ui')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "RefreshWidget",
|
||||
"id": "refresh_total_users",
|
||||
"options": {
|
||||
"period_seconds": 10,
|
||||
"url": "{{entire_url('total_users.ui')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "RefreshWidget",
|
||||
"id": "refresh_concurrent_users",
|
||||
"options": {
|
||||
"period_seconds": 10,
|
||||
"url": "{{entire_url('concurrent_users.ui')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "chart_section",
|
||||
"options": {
|
||||
"bgcolor": "#FFFFFF",
|
||||
"padding": "24px",
|
||||
@ -48,15 +80,11 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "ChartBar",
|
||||
"id": "top_models_chart",
|
||||
"widgettype": "RefreshWidget",
|
||||
"id": "refresh_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
|
||||
"period_seconds": 10,
|
||||
"url": "{{entire_url('top_models_chart.ui')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
32
wwwroot/today_amount.ui
Normal file
32
wwwroot/today_amount.ui
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"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": "¥{{get_today_amount(request)|round(2)}}",
|
||||
"fontSize": "32px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#52c41a"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
32
wwwroot/today_usage.ui
Normal file
32
wwwroot/today_usage.ui
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"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": "{{get_today_usage(request)}}",
|
||||
"fontSize": "32px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#1890ff"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
10
wwwroot/top_models_chart.ui
Normal file
10
wwwroot/top_models_chart.ui
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"widgettype": "ChartBar",
|
||||
"options": {
|
||||
"height": "300px",
|
||||
"width": "100%",
|
||||
"nameField": "model_name",
|
||||
"valueFields": ["cnt", "total_amount"],
|
||||
"data": {{get_top_models(request)|tojson}}
|
||||
}
|
||||
}
|
||||
32
wwwroot/total_users.ui
Normal file
32
wwwroot/total_users.ui
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"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": "{{get_total_users(request)}}",
|
||||
"fontSize": "32px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#722ed1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user