diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index b37bcef..0be0aa2 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -1742,6 +1742,126 @@ def ppt_audit_trigger_aider_heal(): return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500 +@admin_observability_bp.route('/api/health_indicator') +@login_required +def health_indicator_api(): + """Phase 52 P-1:給 topbar 觀測台 indicator 用的輕量 JSON API。 + + 回傳當前是否有「需要關注」的事件: + - 三主機掛掉 + - 待審 episode > 0 + - 過去 1h 錯誤率 ≥ 30% + - 預算 ≥ 90% + """ + try: + session = get_session() + try: + # 三主機最新狀態 + host_unhealthy = 0 + try: + rows = session.execute( + sa_text(""" + WITH latest AS ( + SELECT host_label, + FIRST_VALUE(healthy) OVER ( + PARTITION BY host_label ORDER BY probed_at DESC + ) AS healthy + FROM host_health_probes + WHERE probed_at >= NOW() - INTERVAL '1 hour' + ) + SELECT host_label, BOOL_AND(NOT healthy) AS down + FROM latest + GROUP BY host_label + """), + ).fetchall() + host_unhealthy = sum(1 for r in rows if r[1]) + except Exception: + pass + + # 待審 episode + ep_pending = 0 + try: + ep_pending = int(session.execute( + sa_text("SELECT COUNT(*) FROM learning_episodes WHERE promotion_status = 'awaiting_review' AND reviewed_at IS NULL"), + ).fetchone()[0] or 0) + except Exception: + pass + + # 1h 錯誤率 + error_rate = 0 + try: + row = session.execute( + sa_text(""" + SELECT COUNT(*), + COUNT(*) FILTER (WHERE status NOT IN ('ok','cache_only')) + FROM ai_calls WHERE called_at >= NOW() - INTERVAL '1 hour' + """), + ).fetchone() + total = int(row[0] or 0) + errs = int(row[1] or 0) + error_rate = (errs / total * 100) if total > 20 else 0 + except Exception: + pass + + # 預算告警(任一 ≥ 90%) + budget_alert = False + try: + from datetime import datetime as _dt + today = _dt.now() + ms = _dt(today.year, today.month, 1) + bgs = session.execute( + sa_text(""" + SELECT b.budget_usd, + COALESCE((SELECT SUM(cost_usd) FROM ai_calls + WHERE called_at >= :ms + AND (b.provider IS NULL OR provider = b.provider)), 0) AS spent + FROM ai_call_budgets b + """), + {'ms': ms}, + ).fetchall() + for budget, spent in bgs: + if budget and float(budget) > 0 and float(spent) / float(budget) >= 0.9: + budget_alert = True + break + except Exception: + pass + + alert_count = ( + host_unhealthy + + (1 if ep_pending > 0 else 0) + + (1 if error_rate >= 30 else 0) + + (1 if budget_alert else 0) + ) + return jsonify({ + 'ok': True, + 'alert_count': alert_count, + 'host_unhealthy': host_unhealthy, + 'ep_pending': ep_pending, + 'error_rate_high': error_rate >= 30, + 'budget_alert': budget_alert, + 'tooltip': _build_indicator_tooltip(host_unhealthy, ep_pending, error_rate, budget_alert), + }) + finally: + session.close() + except Exception as e: + return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500 + + +def _build_indicator_tooltip(host_unhealthy, ep_pending, error_rate, budget_alert) -> str: + parts = [] + if host_unhealthy: + parts.append(f"{host_unhealthy} 主機異常") + if ep_pending > 0: + parts.append(f"{ep_pending} 待審") + if error_rate >= 30: + parts.append(f"錯誤率 {error_rate:.0f}%") + if budget_alert: + parts.append("預算 ≥ 90%") + if not parts: + return "AI 觀測台(一切正常)" + return "AI 觀測台 — " + " / ".join(parts) + + @admin_observability_bp.route('/playbooks/toggle/', methods=['POST']) @login_required def playbook_toggle(playbook_id: int): diff --git a/templates/admin/quality_trend.html b/templates/admin/quality_trend.html index 2f44caa..00c15a6 100644 --- a/templates/admin/quality_trend.html +++ b/templates/admin/quality_trend.html @@ -150,27 +150,38 @@ - + {% if rag_overall_dist %}
RAG 整體反饋分布(過去 {{ days }} 日) 資料來源:rag_query_log.feedback_score(含全 caller,1-5 分)
-
- {% set total_fb = (rag_overall_dist | sum(attribute='count')) or 1 %} - {% for r in rag_overall_dist %} -
-
- - {% for _ in range(r.score) %}{% endfor %} - {% for _ in range(5 - r.score) %}{% endfor %} - - {{ r.count }} - {{ "%.1f"|format(r.count / total_fb * 100) }}% -
+
+
+ +
+
+ + + + + + {% set total_fb = (rag_overall_dist | sum(attribute='count')) or 1 %} + {% for r in rag_overall_dist %} + + + + + + {% endfor %} + +
星等筆數佔比
+ {% for _ in range(r.score) %}{% endfor %} + {% for _ in range(5 - r.score) %}{% endfor %} + {{ r.score }} 分 + {{ r.count }}{{ "%.1f"|format(r.count / total_fb * 100) }}%
- {% endfor %}
@@ -240,8 +251,39 @@ {% endif %}

- Operation Ollama-First v5.0 / Phase 47 — Caller 反饋趨勢 + Operation Ollama-First v5.0 / Phase 52 — Caller 反饋趨勢(含 RAG 圓餅圖) (6 表深挖:rag_query_log / learning_episodes / ai_insights / action_plans / action_outcomes / agent_strategy_weights)

+ +{% if rag_overall_dist %} + + +{% endif %} {% endblock %} diff --git a/templates/ewoooc_base.html b/templates/ewoooc_base.html index 646b9eb..04c1a74 100644 --- a/templates/ewoooc_base.html +++ b/templates/ewoooc_base.html @@ -46,6 +46,15 @@
{% endif %} + + + + @@ -323,6 +332,34 @@ } })(); + + + {% block extra_js %}{% endblock %}