Download views/admin.py from globalsquiddatabase/squid-main-space: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/spaces/globalsquiddatabase/squid-main-space/resolve/8acc853bf6b9fcabad139c68351c01e8ff3a1108/views/admin.py
- Command line
-
hf download hf://spaces/globalsquiddatabase/squid-main-space@8acc853bf6b9fcabad139c68351c01e8ff3a1108/views/admin.py
-
curl -L -o admin.py https://huggingface.co/spaces/globalsquiddatabase/squid-main-space/resolve/8acc853bf6b9fcabad139c68351c01e8ff3a1108/views/admin.py
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 > 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 > 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>""" | |