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