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