squid-main-space / views /admin.py
Zin299's picture
Deploy v3.7.0 P0 architecture refactor
6724416
Raw History Blame
19.6 kB
from __future__ import annotations
import html
def _render_admin_dashboard(data,tasks=None):
esc=lambda x:html.escape(str(x if x is not None else ""))
summary=data.get("summary") or {}
users=data.get("top_users") or []
daily=data.get("daily") or []
event_types=data.get("event_types") or []
tasks=tasks or []
event_counts={
str(x.get("event_type") or ""):
int(x.get("count") or 0)
for x in event_types
}
data_operations=sum(
count
for name,count in event_counts.items()
if (
name.startswith("marine_")
or name.startswith("fisheries_")
or name.startswith("processing_")
or name.startswith("upload")
)
)
cards=[
("总用户",summary.get("total_users",0)),
("近24h活跃",summary.get("active_24h",0)),
("7天活跃",summary.get("active_7d",0)),
("30天活跃",summary.get("active_30d",0)),
("聊天次数",summary.get("total_chats",0)),
("会话数",summary.get("total_threads",0)),
("长期记忆",summary.get("total_memories",0)),
("数据请求",data_operations),
("数据查询",
event_counts.get("marine_query",0)
+ event_counts.get("fisheries_query",0)),
("数据处理",
event_counts.get("marine_subset",0)
+ event_counts.get("marine_export",0)
+ event_counts.get("processing_completed",0)),
("数据导出",event_counts.get("marine_export",0)),
("用户上传",event_counts.get("upload_completed",0)),
("数据资产",summary.get("total_assets",0)),
("点击下载",event_counts.get("download_clicked",0)),
]
latest_processing_html="""
<section>
<h2>⚙️ 最近一次数据处理</h2>
<div class="empty">暂无数据处理记录</div>
</section>
"""
latest_task=None
for task in tasks:
if (
task.get("event_type")=="processing_completed"
or task.get("status")=="completed"
):
latest_task=task
break
if latest_task:
payload=latest_task.get("payload") or {}
result=payload.get("result") or {}
results=result.get("results") or []
uid=latest_task.get("user_id") or "-"
created=latest_task.get("created_at") or "-"
operation=latest_task.get("operation") or "-"
runtime=payload.get("runtime") or "-"
status=latest_task.get("status") or "completed"
filename="-"
rows="-"
cols="-"
missing=0
missing_fields="-"
duplicate_rows=0
month_text="-"
bad_lon=0
bad_lat=0
grid_groups=0
grid_rows=0
fishing_bad=0
if results:
r=results[0]
filename=r.get("filename") or "-"
rows=r.get("row_count","-")
cols=r.get("column_count","-")
mv=r.get("missing_values") or {}
missing=mv.get("total_missing_cells",0)
bycol=mv.get("by_column") or {}
if bycol:
missing_fields=";".join(
f"{k}={v}"
for k,v in bycol.items()
)
else:
missing_fields="无"
dup=r.get("exact_duplicates") or {}
duplicate_rows=dup.get("count",0)
month=r.get("month_check") or {}
if month.get("column"):
mm=month.get("missing_months") or []
month_text="无" if not mm else "、".join(map(str,mm))
else:
month_text="未识别到月份字段"
coord=r.get("coordinate_check") or {}
bad_lon=coord.get("invalid_longitude_count",0)
bad_lat=coord.get("invalid_latitude_count",0)
grid=r.get("duplicate_grid_check") or {}
grid_groups=grid.get("duplicate_group_count",0)
grid_rows=grid.get("duplicate_row_count",0)
fh=r.get("fishing_hours_check") or {}
fishing_bad=fh.get("invalid_count",0)
latest_processing_html=f"""
<section>
<div class="section-head">
<div>
<h2>⚙️ 最近一次数据处理</h2>
<div class="muted-small">
学校服务器 memory.db 中最新完成任务
</div>
</div>
<a class="detail-btn" href="/admin/user/{esc(uid)}">
查看该用户完整档案 →
</a>
</div>
<div class="latest-meta">
<div><span>用户</span><b>{esc(uid)}</b></div>
<div><span>时间</span><b>{esc(created)}</b></div>
<div><span>任务</span><b>{esc(operation)}</b></div>
<div><span>状态</span><b>{esc(status)}</b></div>
<div><span>Runtime</span><b>{esc(runtime)}</b></div>
</div>
<div class="latest-file">
<b>{esc(filename)}</b>
<span>{esc(rows)} 行 × {esc(cols)} 列</span>
</div>
<div class="result-grid">
<div><span>缺失值</span><b>{esc(missing)} 个</b></div>
<div><span>完全重复行</span><b>{esc(duplicate_rows)} 条</b></div>
<div><span>异常经度</span><b>{esc(bad_lon)} 条</b></div>
<div><span>异常纬度</span><b>{esc(bad_lat)} 条</b></div>
<div><span>重复格点</span><b>{esc(grid_groups)} 组 / {esc(grid_rows)} 行</b></div>
<div><span>fishing &gt; total</span><b>{esc(fishing_bad)} 条</b></div>
</div>
<div class="result-line">
<span>缺失字段</span>
<b>{esc(missing_fields)}</b>
</div>
<div class="result-line">
<span>月份检查</span>
<b>{esc(month_text)}</b>
</div>
</section>
"""
cards_html="".join(
f'<div class="card"><b>{esc(v)}</b><span>{esc(k)}</span></div>'
for k,v in cards
)
user_rows="".join(
"<tr>"
f'<td><a class="userlink" href="/admin/user/{esc(x.get("user_id"))}">{esc(x.get("user_id"))}</a></td>'
f"<td>{esc(x.get('chats'))}</td>"
f"<td>{esc(x.get('threads'))}</td>"
f"<td>{esc(x.get('memories'))}</td>"
f"<td>{esc(x.get('assets'))}</td>"
f"<td>{esc(x.get('last_active') or '-')}</td>"
"</tr>"
for x in users
)
daily_rows="".join(
"<tr>"
f"<td>{esc(x.get('day'))}</td>"
f"<td>{esc(x.get('active_users'))}</td>"
f"<td>{esc(x.get('chats'))}</td>"
f"<td>{esc(x.get('threads'))}</td>"
f"<td>{esc(x.get('events'))}</td>"
"</tr>"
for x in daily
)
return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="refresh" content="30">
<title>Squid 用户统计</title>
<style>
body{{margin:0;background:#061525;color:#edf7ff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}}
main{{max-width:1180px;margin:auto;padding:32px}}
h1{{margin-bottom:6px}}
.muted{{color:#8faec8;margin-bottom:24px}}
.cards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(135px,1fr));gap:12px;margin:22px 0}}
.card{{background:#0b2743;border:1px solid #173d61;border-radius:14px;padding:18px}}
.card b{{display:block;font-size:28px}}
.card span{{color:#91aec7;font-size:13px}}
section{{background:#092038;border:1px solid #173d61;border-radius:16px;padding:20px;margin-top:18px;overflow:auto}}
table{{width:100%;border-collapse:collapse;font-size:14px}}
th,td{{padding:11px;border-bottom:1px solid #173d61;text-align:left;white-space:nowrap}}
th{{color:#8ec8ff}}
.section-head{{
display:flex;
align-items:center;
justify-content:space-between;
gap:16px;
}}
.section-head h2{{margin-bottom:4px}}
.muted-small{{
color:#819fba;
font-size:13px;
}}
.detail-btn{{
display:inline-block;
padding:9px 13px;
border:1px solid #23527a;
border-radius:9px;
color:#85ceff;
text-decoration:none;
white-space:nowrap;
}}
.detail-btn:hover{{
background:#103451;
}}
.latest-meta{{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
gap:10px;
margin-top:18px;
}}
.latest-meta div,
.result-grid div,
.result-line{{
background:#071a2d;
border:1px solid #173d61;
border-radius:10px;
padding:12px;
}}
.latest-meta span,
.result-grid span,
.result-line span{{
display:block;
color:#87a8c2;
font-size:12px;
margin-bottom:5px;
}}
.latest-meta b,
.result-grid b,
.result-line b{{
word-break:break-all;
}}
.latest-file{{
margin-top:12px;
padding:15px;
background:#0b2743;
border-radius:11px;
border:1px solid #1b456a;
}}
.latest-file b{{
display:block;
font-size:17px;
}}
.latest-file span{{
display:block;
color:#91afc9;
margin-top:5px;
}}
.result-grid{{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
gap:10px;
margin-top:12px;
}}
.result-line{{
margin-top:10px;
}}
a.userlink{{color:#75c8ff;text-decoration:none;font-weight:600}}
a.userlink:hover{{text-decoration:underline}}
</style>
</head>
<body>
<main>
<h1>🦑 Squid 用户使用统计</h1>
<div class="muted">自动刷新:30 秒 · 数据来自学校服务器 memory.db</div>
<div class="cards">{cards_html}</div>
{latest_processing_html}
<section>
<h2>用户使用情况</h2>
<table>
<thead><tr>
<th>User ID</th><th>聊天</th><th>会话</th>
<th>长期记忆</th><th>数据资产</th><th>最近活跃</th>
</tr></thead>
<tbody>{user_rows}</tbody>
</table>
</section>
<section>
<h2>最近每日活动</h2>
<table>
<thead><tr>
<th>日期</th><th>活跃用户</th><th>聊天</th>
<th>新会话</th><th>全部事件</th>
</tr></thead>
<tbody>{daily_rows}</tbody>
</table>
</section>
</main>
</body>
</html>"""
def _render_admin_user_detail(data,tasks):
esc=lambda x:html.escape(str(x if x is not None else ""))
user=data.get("user") or {}
control=data.get("control") or {}
memories=data.get("memories") or []
assets=data.get("assets") or []
events=data.get("events") or []
uid=user.get("user_id") or control.get("user_id") or ""
user_tasks=[
x for x in (tasks or [])
if str(x.get("user_id") or "")==str(uid)
]
status_text="已禁用" if control.get("disabled") else "正常"
upload_text="允许" if control.get("allow_upload") else "禁止"
download_text="允许" if control.get("allow_download") else "禁止"
quota=control.get("daily_chat_quota",0)
quota_text="不限" if not quota else str(quota)
memory_rows=""
for m in memories[:30]:
text=(
m.get("content")
or m.get("text")
or m.get("value")
or m.get("memory")
or ""
)
kind=m.get("kind") or m.get("type") or ""
if text:
memory_rows += (
"<div class='memory'>"
f"<b>{esc(text)}</b>"
f"<span>{esc(kind)}</span>"
"</div>"
)
if not memory_rows:
memory_rows="<div class='empty'>暂无长期记忆</div>"
asset_rows=""
for a in assets[:50]:
name=a.get("name") or a.get("filename") or "未命名"
op=a.get("operation") or "-"
status=a.get("status") or "-"
size=a.get("size_bytes") or a.get("size") or 0
created=a.get("created_at") or "-"
try:
size=int(size)
if size<1024:
size_text=f"{size} B"
elif size<1024**2:
size_text=f"{size/1024:.1f} KB"
elif size<1024**3:
size_text=f"{size/1024**2:.1f} MB"
else:
size_text=f"{size/1024**3:.2f} GB"
except Exception:
size_text="-"
asset_rows += (
"<tr>"
f"<td>{esc(name)}</td>"
f"<td>{esc(op)}</td>"
f"<td>{esc(size_text)}</td>"
f"<td>{esc(status)}</td>"
f"<td>{esc(created)}</td>"
"</tr>"
)
if not asset_rows:
asset_rows="<tr><td colspan='5' class='empty'>暂无数据资产</td></tr>"
task_html=""
for t in user_tasks[:30]:
payload=t.get("payload") or {}
result=payload.get("result") or {}
results=result.get("results") or []
runtime=payload.get("runtime") or "-"
status=t.get("status") or t.get("event_type") or "-"
operation=t.get("operation") or "-"
created=t.get("created_at") or "-"
abnormal=t.get("abnormal")
cancel=t.get("cancel_requested")
detail=""
if results:
r=results[0]
filename=r.get("filename") or "-"
rows=r.get("row_count","-")
cols=r.get("column_count","-")
mv=r.get("missing_values") or {}
miss=mv.get("total_missing_cells",0)
dup=r.get("exact_duplicates") or {}
dup_count=dup.get("count",0)
month=r.get("month_check") or {}
if month.get("column"):
mm=month.get("missing_months") or []
month_text="无" if not mm else ", ".join(map(str,mm))
else:
month_text="未识别月份字段"
coord=r.get("coordinate_check") or {}
bad_lon=coord.get("invalid_longitude_count",0)
bad_lat=coord.get("invalid_latitude_count",0)
grid=r.get("duplicate_grid_check") or {}
grid_groups=grid.get("duplicate_group_count",0)
fh=r.get("fishing_hours_check") or {}
fh_bad=fh.get("invalid_count",0)
detail=f"""
<div class="taskresult">
<div><b>输入文件</b><span>{esc(filename)}</span></div>
<div><b>数据规模</b><span>{esc(rows)} 行 × {esc(cols)} 列</span></div>
<div><b>缺失值</b><span>{esc(miss)} 个</span></div>
<div><b>完全重复行</b><span>{esc(dup_count)} 条</span></div>
<div><b>月份检查</b><span>{esc(month_text)}</span></div>
<div><b>异常经度</b><span>{esc(bad_lon)} 条</span></div>
<div><b>异常纬度</b><span>{esc(bad_lat)} 条</span></div>
<div><b>重复格点</b><span>{esc(grid_groups)} 组</span></div>
<div><b>fishing &gt; total</b><span>{esc(fh_bad)} 条</span></div>
</div>
"""
flags=[]
if abnormal:
flags.append("⚠️ 异常")
if cancel:
flags.append("已请求取消")
flag_text=" · ".join(flags)
task_html += f"""
<div class="task">
<div class="taskhead">
<div>
<b>{esc(operation)}</b>
<span>{esc(created)}</span>
</div>
<div class="badges">
<span>{esc(status)}</span>
<span>{esc(runtime)}</span>
{f'<span class="warn">{esc(flag_text)}</span>' if flag_text else ''}
</div>
</div>
{detail}
</div>
"""
if not task_html:
task_html="<div class='empty'>暂无数据处理任务</div>"
names={
"thread_created":"新建对话",
"chat":"发送消息",
"upload_completed":"上传文件",
"attachment_used":"使用上传文件",
"processing_started":"开始数据处理",
"processing_completed":"完成数据处理",
"processing_failed":"数据处理失败",
"marine_query":"查询海洋数据",
"marine_subset":"裁剪海洋数据",
"marine_export":"导出海洋数据",
"fisheries_query":"查询渔业数据",
"download_clicked":"下载文件",
"admin_asset_deleted":"管理员删除资产记录",
}
timeline=""
for e in events[:40]:
et=e.get("event_type") or e.get("type") or "-"
tm=e.get("created_at") or "-"
timeline += (
"<div class='timeline-row'>"
f"<span>{esc(tm)}</span>"
f"<b>{esc(names.get(et,et))}</b>"
"</div>"
)
if not timeline:
timeline="<div class='empty'>暂无操作记录</div>"
return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Squid 用户详情</title>
<style>
body{{
margin:0;background:#061525;color:#edf7ff;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif
}}
main{{max-width:1200px;margin:auto;padding:30px}}
a{{color:#79caff;text-decoration:none}}
a:hover{{text-decoration:underline}}
.top{{display:flex;justify-content:space-between;align-items:center;gap:12px}}
.uid{{color:#8eb2d0;word-break:break-all}}
.cards{{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
gap:12px;margin:22px 0
}}
.card{{
background:#0b2743;border:1px solid #173d61;
border-radius:14px;padding:18px
}}
.card b{{display:block;font-size:24px}}
.card span{{color:#91aec7;font-size:13px}}
section{{
background:#092038;border:1px solid #173d61;
border-radius:16px;padding:20px;margin-top:18px;overflow:auto
}}
table{{width:100%;border-collapse:collapse;font-size:14px}}
th,td{{
padding:11px;border-bottom:1px solid #173d61;
text-align:left;white-space:nowrap
}}
th{{color:#8ec8ff}}
.memory{{
padding:12px 0;border-bottom:1px solid #173d61
}}
.memory b{{display:block}}
.memory span{{font-size:12px;color:#89a8c1}}
.task{{
margin:14px 0;padding:16px;background:#071a2d;
border:1px solid #173d61;border-radius:14px
}}
.taskhead{{
display:flex;justify-content:space-between;gap:12px;align-items:flex-start
}}
.taskhead b{{font-size:18px}}
.taskhead span{{display:block;color:#88a9c4;font-size:12px;margin-top:4px}}
.badges{{display:flex;gap:7px;flex-wrap:wrap;justify-content:flex-end}}
.badges span{{
background:#123b5e;padding:5px 8px;border-radius:8px;
color:#b7ddff
}}
.badges .warn{{background:#5a3416}}
.taskresult{{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
gap:8px;margin-top:15px
}}
.taskresult div{{
background:#0b2743;padding:10px;border-radius:9px
}}
.taskresult b{{display:block;color:#8fc9ff;font-size:12px}}
.taskresult span{{display:block;margin-top:4px}}
.timeline-row{{
display:flex;gap:18px;padding:9px 0;
border-bottom:1px solid #173d61
}}
.timeline-row span{{min-width:270px;color:#82a5c0}}
.empty{{color:#7898b0;padding:10px 0}}
</style>
</head>
<body>
<main>
<div class="top">
<div>
<a href="/admin">← 返回统一管控后台</a>
<h1>👤 用户详情</h1>
<div class="uid">{esc(uid)}</div>
</div>
<a href="/admin/user/{esc(uid)}">↻ 刷新</a>
</div>
<div class="cards">
<div class="card"><b>{esc(status_text)}</b><span>用户状态</span></div>
<div class="card"><b>{esc(control.get("today_chats",0))}</b><span>今日聊天</span></div>
<div class="card"><b>{esc(quota_text)}</b><span>每日聊天配额</span></div>
<div class="card"><b>{esc(upload_text)}</b><span>上传权限</span></div>
<div class="card"><b>{esc(download_text)}</b><span>下载权限</span></div>
<div class="card"><b>{len(assets)}</b><span>数据资产</span></div>
<div class="card"><b>{len(memories)}</b><span>长期记忆</span></div>
<div class="card"><b>{len(user_tasks)}</b><span>数据处理任务</span></div>
</div>
<section>
<h2>🧠 长期记忆</h2>
{memory_rows}
</section>
<section>
<h2>📦 数据资产</h2>
<table>
<thead>
<tr>
<th>文件</th><th>操作</th><th>大小</th><th>状态</th><th>时间</th>
</tr>
</thead>
<tbody>
{asset_rows}
</tbody>
</table>
</section>
<section>
<h2>⚙️ 数据处理任务</h2>
{task_html}
</section>
<section>
<h2>🕒 最近操作时间线</h2>
{timeline}
</section>
</main>
</body>
</html>"""