diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index aecd803..9f5979a 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -1511,6 +1511,42 @@ def ppt_audit_trigger_aider_heal(): return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500 +@admin_observability_bp.route('/playbooks/toggle/', methods=['POST']) +@login_required +def playbook_toggle(playbook_id: int): + """Phase 50 N-3:一鍵啟用/停用 playbook(is_active 翻轉)。 + + 用途:在 host_health 觀測台直接管理 AutoHeal playbook, + 不需 SSH 188 改 DB。 + """ + try: + session = get_session() + try: + row = session.execute( + sa_text("SELECT id, name, is_active FROM playbooks WHERE id = :id"), + {'id': playbook_id}, + ).fetchone() + if not row: + return jsonify({'ok': False, 'error': f'playbook #{playbook_id} 不存在'}), 404 + new_active = not bool(row[2]) + session.execute( + sa_text("UPDATE playbooks SET is_active = :a, updated_at = NOW() WHERE id = :id"), + {'a': new_active, 'id': playbook_id}, + ) + session.commit() + return jsonify({ + 'ok': True, + 'playbook_id': playbook_id, + 'name': row[1], + 'is_active': new_active, + 'message': f'Playbook 「{row[1]}」已{"啟用" if new_active else "停用"}', + }) + finally: + session.close() + except Exception as e: + return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500 + + @admin_observability_bp.route('/host_health/trigger_autoheal', methods=['POST']) @login_required def host_health_trigger_autoheal(): @@ -2050,7 +2086,7 @@ def host_health_dashboard(): # playbooks 庫排行(success_count + fail_count + 是否 active) pb_rows = s3.execute( sa_text(""" - SELECT name, error_type, action_type, severity_min, + SELECT id, name, error_type, action_type, severity_min, success_count, fail_count, is_active, cooldown_min FROM playbooks ORDER BY (success_count + fail_count) DESC, success_count DESC @@ -2059,13 +2095,14 @@ def host_health_dashboard(): ).fetchall() playbook_ranking = [ { - 'name': r[0], 'error_type': r[1], 'action_type': r[2], - 'severity': r[3], 'success': int(r[4] or 0), - 'fail': int(r[5] or 0), 'is_active': bool(r[6]), - 'cooldown_min': int(r[7] or 0), + 'id': int(r[0]), + 'name': r[1], 'error_type': r[2], 'action_type': r[3], + 'severity': r[4], 'success': int(r[5] or 0), + 'fail': int(r[6] or 0), 'is_active': bool(r[7]), + 'cooldown_min': int(r[8] or 0), 'success_rate': ( - float(r[4] or 0) / float((r[4] or 0) + (r[5] or 0)) * 100 - ) if ((r[4] or 0) + (r[5] or 0)) > 0 else 0, + float(r[5] or 0) / float((r[5] or 0) + (r[6] or 0)) * 100 + ) if ((r[5] or 0) + (r[6] or 0)) > 0 else 0, } for r in pb_rows ] diff --git a/templates/admin/ai_calls_dashboard.html b/templates/admin/ai_calls_dashboard.html index eac1e1e..c2f2136 100644 --- a/templates/admin/ai_calls_dashboard.html +++ b/templates/admin/ai_calls_dashboard.html @@ -124,40 +124,14 @@ {% endif %} - + {% if hourly_trend %}
過去 24h 每小時呼叫趨勢 - 每小時 bucket:呼叫數 · 成本 · 錯誤 + 折線:呼叫數 + 錯誤;柱狀:成本 USD
-
- - - - - - - - {% set max_calls = (hourly_trend | map(attribute='calls') | max) or 1 %} - {% for h in hourly_trend %} - - - - - - - - {% endfor %} - -
時段呼叫數成本 USD錯誤流量分布
{{ h.hour }}{{ "{:,}".format(h.calls) }}${{ "%.3f"|format(h.cost) }} - {% if h.errors > 0 %}{{ h.errors }} - {% else %}0{% endif %} - -
-
-
-
+
+
{% endif %} @@ -292,7 +266,36 @@

+