From 8a675923cdfc944aacd7d1e0b46b359a89d28555 Mon Sep 17 00:00:00 2001
From: butubb <1422726308@qq.com>
Date: Sat, 8 Aug 2026 21:33:35 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=B1=E8=B4=A5=E4=BB=BB=E5=8A=A1?=
=?UTF-8?q?=E6=B1=87=E6=80=BB=20-=20/api/summary=20+=20=E7=9B=91=E6=8E=A7?=
=?UTF-8?q?=E9=A1=B5=E5=BC=82=E5=B8=B8=E9=9D=A2=E6=9D=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 /api/summary:各状态计数 + 异常设备列表(last_error/任务/重试次数/时间)
- 监控页新增"异常汇总"面板:显示失败/异常设备及错误信息,方便长期运行观察健康度
---
static/admin/monitor.js | 37 ++++++++++++++++++++++++++++++++++++
templates/admin/monitor.html | 10 ++++++++++
web_server.py | 35 ++++++++++++++++++++++++++++++++++
3 files changed, 82 insertions(+)
diff --git a/static/admin/monitor.js b/static/admin/monitor.js
index 9a09846..699a6c9 100644
--- a/static/admin/monitor.js
+++ b/static/admin/monitor.js
@@ -364,6 +364,43 @@ async function loadMonitor(){
updateSelCount();
// 加载任务卡片
loadJobsForMonitor();
+ // 加载异常汇总
+ loadSummary();
+ }catch(e){console.error(e);}
+}
+
+// ===== 异常设备汇总 =====
+async function loadSummary(){
+ try{
+ const r=await apiGet('/api/summary');
+ if(!r||!r.ok)return;
+ const counts=r.counts||{};
+ const errors=r.errors||[];
+ const errCount=(counts.error||0)+(counts.failed||0);
+ const badge=document.getElementById('summary-badge');
+ const panel=document.getElementById('summary-panel');
+ const empty=document.getElementById('summary-empty');
+ if(badge)badge.textContent='· 异常 '+errCount+' · 运行 '+counts.running+' · 完成 '+counts.done;
+ if(!panel||!empty)return;
+ if(!errors.length){
+ panel.style.display='none';
+ empty.style.display='block';
+ return;
+ }
+ panel.style.display='block';
+ empty.style.display='none';
+ const tb=document.getElementById('tb-summary');
+ tb.innerHTML=errors.map(e=>{
+ const st=e.status==='failed'?'失败':'异常';
+ const up=e.updated?new Date(e.updated*1000).toLocaleTimeString():'';
+ return '
'+
+ '| '+esc(e.serial)+''+(e.model?' '+esc(e.model)+'':'')+' | '+
+ ''+st+' | '+
+ ''+esc(e.task||'-')+' | '+
+ ''+(e.attempt>0?e.attempt+'次':'-')+' | '+
+ ''+esc(e.last_error||'-')+' | '+
+ '
';
+ }).join('');
}catch(e){console.error(e);}
}
diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html
index 980f5df..64883d3 100644
--- a/templates/admin/monitor.html
+++ b/templates/admin/monitor.html
@@ -315,6 +315,16 @@ select.form-control{cursor:pointer}
+
+ 异常汇总
+
+ 无异常设备
+
任务运行概况
diff --git a/web_server.py b/web_server.py
index 0390012..af85996 100644
--- a/web_server.py
+++ b/web_server.py
@@ -28,6 +28,7 @@ from flask_login import (LoginManager, login_user, logout_user,
from core.stf_client import STFClient
from core.task_manager import TaskManager
+from core.device_worker import get_all_worker_status
from core.logger import get_logger, _LOG_DIR, _MODULE_FILES
from core.models import db, init_db, User, DeviceGroup, TaskJob, CustomAction
from core.apk_manager import ApkManager
@@ -147,6 +148,40 @@ def api_health():
return jsonify({"ok": False, "status": "down", "error": str(e)}), 500
+# ================== API:失败任务汇总 ==================
+@app.route("/api/summary")
+@login_required
+def api_summary():
+ """失败/异常任务汇总:各状态计数 + 异常设备列表(last_error / 重试次数)。
+
+ 供监控页"异常汇总"面板使用,便于长期运行观察设备健康度。
+ """
+ try:
+ sts = get_all_worker_status()
+ counts = {"total": 0, "running": 0, "done": 0,
+ "error": 0, "failed": 0, "idle": 0}
+ errors = []
+ for s in sts:
+ st = s.get("status", "idle")
+ counts["total"] += 1
+ if st in counts:
+ counts[st] += 1
+ if st in ("error", "failed") and s.get("last_error"):
+ errors.append({
+ "serial": s.get("serial"),
+ "model": s.get("model", ""),
+ "status": st,
+ "last_error": s.get("last_error", ""),
+ "task": s.get("task_job", ""),
+ "attempt": s.get("attempt", 0),
+ "updated": s.get("last_heartbeat", 0),
+ })
+ errors.sort(key=lambda x: x.get("updated", 0), reverse=True)
+ return jsonify({"ok": True, "counts": counts, "errors": errors[:50]})
+ except Exception as e:
+ return jsonify({"ok": False, "error": str(e)}), 500
+
+
# ================== API:状态(监控大屏用)==================
@app.route("/api/status")
@login_required