diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index bc4735b..c0bc878 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -580,6 +580,47 @@ def ai_calls_dashboard(): {'since': since}, ).fetchall() + # 5b. Phase 47 K-2: by model 細分(不只 provider,到實際 model) + by_model = session.execute( + sa_text(""" + SELECT model, provider, COUNT(*) AS calls, + COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens, + COALESCE(SUM(cost_usd), 0) AS cost, + COALESCE(AVG(duration_ms), 0) AS avg_ms, + COUNT(*) FILTER (WHERE status NOT IN ('ok','cache_only')) AS errors + FROM ai_calls + WHERE called_at >= :since + AND model IS NOT NULL AND model != '' + GROUP BY model, provider + ORDER BY calls DESC + LIMIT 15 + """), + {'since': since}, + ).fetchall() + + # 5c. Phase 47 K-2: hourly 呼叫量趨勢(24 個 bucket) + hourly_trend = session.execute( + sa_text(""" + SELECT date_trunc('hour', called_at) AS hr, + COUNT(*) AS calls, + COALESCE(SUM(cost_usd), 0) AS cost, + COUNT(*) FILTER (WHERE status NOT IN ('ok','cache_only')) AS errors + FROM ai_calls + WHERE called_at >= NOW() - INTERVAL '24 hours' + GROUP BY hr ORDER BY hr ASC + """), + ).fetchall() + + # 5d. Phase 47 K-2: agent_context 最近 10 筆(OpenClaw/Hermes 對話上下文) + recent_contexts = session.execute( + sa_text(""" + SELECT created_at, agent_name, context_key, ttl_minutes, + LEFT(context_val, 120) AS preview + FROM agent_context + ORDER BY created_at DESC LIMIT 10 + """), + ).fetchall() + # 5. Phase 39 D-3: caller × RAG 命中率 × MCP 編排率(跨表 JOIN) # 展現「AI 自動化專業」核心:每個 caller 多大比例走了 RAG / MCP caller_richness = session.execute( @@ -638,6 +679,33 @@ def ai_calls_dashboard(): for r in recent ], callers=[r[0] for r in callers], + by_model=[ + { + 'model': r[0], 'provider': r[1], + 'calls': int(r[2] or 0), 'tokens': int(r[3] or 0), + 'cost': float(r[4] or 0), 'avg_ms': int(r[5] or 0), + 'errors': int(r[6] or 0), + } + for r in by_model + ], + hourly_trend=[ + { + 'hour': r[0].strftime('%H:%M') if r[0] else '', + 'calls': int(r[1] or 0), + 'cost': float(r[2] or 0), + 'errors': int(r[3] or 0), + } + for r in hourly_trend + ], + recent_contexts=[ + { + 'created_at': r[0].strftime('%Y-%m-%d %H:%M') if r[0] else '', + 'agent_name': r[1], 'context_key': r[2], + 'ttl_minutes': int(r[3] or 0), + 'preview': r[4] or '', + } + for r in recent_contexts + ], caller_richness=[ { 'caller': r[0], @@ -660,6 +728,7 @@ def ai_calls_dashboard(): hours=hours, caller_filter=caller_filter, provider_filter=provider_filter, summary={}, by_provider=[], recent=[], callers=[], caller_richness=[], + by_model=[], hourly_trend=[], recent_contexts=[], error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}', ) finally: @@ -740,11 +809,63 @@ def promotion_review_list(): except Exception: pass # rag_service import 失敗(feature flag OFF)→ 略過 + # Phase 47 K-4: 蒸餾池 status 分布(30d) + ep_distribution = session.execute( + sa_text(""" + SELECT promotion_status, COUNT(*) AS cnt + FROM learning_episodes + WHERE created_at >= NOW() - INTERVAL '30 days' + GROUP BY promotion_status ORDER BY cnt DESC + """), + ).fetchall() + episode_distribution_30d = {r[0]: int(r[1] or 0) for r in ep_distribution} + + # Phase 47 K-4: ai_insights 最近 10 筆已晉升(type/created_at 視覺) + latest_insights = session.execute( + sa_text(""" + SELECT id, insight_type, period, product_sku, created_at, + LEFT(content, 160) AS preview + FROM ai_insights + ORDER BY created_at DESC LIMIT 10 + """), + ).fetchall() + + # Phase 47 K-4: agent_strategy_weights TOP 12(OpenClaw 學習權重) + strategy_weights = session.execute( + sa_text(""" + SELECT strategy_key, weight, success_cnt, fail_cnt, updated_at + FROM agent_strategy_weights + ORDER BY (success_cnt + fail_cnt) DESC + LIMIT 12 + """), + ).fetchall() + return render_template( 'admin/promotion_review.html', active_page='obs_promotion_review', episodes=episodes, kb_size=kb_size, + episode_distribution_30d=episode_distribution_30d, + latest_insights=[ + { + 'id': r[0], 'insight_type': r[1], 'period': r[2], + 'product_sku': r[3], + 'created_at': r[4].strftime('%Y-%m-%d %H:%M') if r[4] else '', + 'preview': r[5] or '', + } + for r in latest_insights + ], + strategy_weights=[ + { + 'strategy_key': r[0], 'weight': float(r[1] or 0), + 'success': int(r[2] or 0), 'fail': int(r[3] or 0), + 'updated_at': r[4].strftime('%Y-%m-%d') if r[4] else '', + 'success_rate': ( + float(r[2] or 0) / float((r[2] or 0) + (r[3] or 0)) * 100 + ) if ((r[2] or 0) + (r[3] or 0)) > 0 else 0, + } + for r in strategy_weights + ], error=None, ) except Exception as e: @@ -753,6 +874,9 @@ def promotion_review_list(): active_page='obs_promotion_review', episodes=[], kb_size=0, + episode_distribution_30d={}, + latest_insights=[], + strategy_weights=[], error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}', ) finally: @@ -875,6 +999,54 @@ def quality_trend_dashboard(): except Exception: pass + # Phase 47 K-5: action_outcomes verdict 統計(ADR-012 閉環學習結果) + action_outcomes_stats = [] + action_plans_status = [] + rag_overall_dist = [] + try: + session = get_session() + try: + # action_outcomes verdict 分布(30d) + ao_rows = session.execute( + sa_text(f""" + SELECT verdict, COUNT(*) AS cnt + FROM action_outcomes + WHERE created_at >= NOW() - INTERVAL '{int(days)} days' + GROUP BY verdict ORDER BY cnt DESC + """), + ).fetchall() + action_outcomes_stats = [{'verdict': r[0] or 'unknown', 'count': int(r[1] or 0)} for r in ao_rows] + + # action_plans status 分布(30d) + ap_rows = session.execute( + sa_text(f""" + SELECT status, plan_type, COUNT(*) AS cnt + FROM action_plans + WHERE created_at >= NOW() - INTERVAL '{int(days)} days' + GROUP BY status, plan_type ORDER BY cnt DESC + """), + ).fetchall() + action_plans_status = [ + {'status': r[0], 'plan_type': r[1] or 'misc', 'count': int(r[2] or 0)} + for r in ap_rows + ] + + # rag_query_log 整體 feedback 分布(不只 caller-level,整體) + rag_dist_rows = session.execute( + sa_text(f""" + SELECT feedback_score, COUNT(*) AS cnt + FROM rag_query_log + WHERE queried_at >= NOW() - INTERVAL '{int(days)} days' + AND feedback_score IS NOT NULL + GROUP BY feedback_score ORDER BY feedback_score + """), + ).fetchall() + rag_overall_dist = [{'score': int(r[0] or 0), 'count': int(r[1] or 0)} for r in rag_dist_rows] + finally: + session.close() + except Exception: + pass + return render_template( 'admin/quality_trend.html', active_page='obs_quality_trend', @@ -883,6 +1055,9 @@ def quality_trend_dashboard(): recommendations=recommendations, episode_distribution=episode_distribution, rag_root_causes=rag_root_causes, + action_outcomes_stats=action_outcomes_stats, + action_plans_status=action_plans_status, + rag_overall_dist=rag_overall_dist, error=None, ) except Exception as e: @@ -891,6 +1066,7 @@ def quality_trend_dashboard(): active_page='obs_quality_trend', days=days, trends=[], recommendations=[], episode_distribution={}, rag_root_causes=[], + action_outcomes_stats=[], action_plans_status=[], rag_overall_dist=[], error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}', ) @@ -950,6 +1126,51 @@ def budget_dashboard(): 'updated_at': b[5].strftime('%Y-%m-%d %H:%M') if b[5] else '-', }) + # Phase 47 K-3: 30d daily cost trend by provider + cost_30d = session.execute( + sa_text(""" + SELECT date_trunc('day', called_at)::date AS d, + provider, COALESCE(SUM(cost_usd), 0) AS cost + FROM ai_calls + WHERE called_at >= NOW() - INTERVAL '30 days' + GROUP BY d, provider + ORDER BY d DESC, cost DESC + """), + ).fetchall() + cost_trend_30d = [] + for r in cost_30d: + cost_trend_30d.append({ + 'date': r[0].strftime('%m-%d') if r[0] else '', + 'provider': r[1], + 'cost': float(r[2] or 0), + }) + + # Phase 47 K-3: top 5 cost-burning caller (當月) + top_cost_callers = session.execute( + sa_text(""" + SELECT caller, COUNT(*) AS calls, + COALESCE(SUM(cost_usd), 0) AS cost, + COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens + FROM ai_calls + WHERE called_at >= :ms + AND cost_usd > 0 + GROUP BY caller + ORDER BY cost DESC LIMIT 5 + """), + {'ms': month_start}, + ).fetchall() + + # Phase 47 K-3: ai_price_recommendations 7d 統計 + price_rec_7d = session.execute( + sa_text(""" + SELECT strategy, COUNT(*) AS cnt, + COALESCE(AVG(confidence), 0) AS avg_conf + FROM ai_price_recommendations + WHERE created_at >= NOW() - INTERVAL '7 days' + GROUP BY strategy ORDER BY cnt DESC + """), + ).fetchall() + # Phase 39 D-4: RAG 自動建議策略(針對超 80% 的 row) budget_strategies = [] over_threshold_rows = [r for r in rows if r.get('ratio', 0) >= 0.8] @@ -984,11 +1205,27 @@ def budget_dashboard(): active_page='obs_budget', rows=rows, budget_strategies=budget_strategies, + cost_trend_30d=cost_trend_30d, + top_cost_callers=[ + { + 'caller': r[0], 'calls': int(r[1] or 0), + 'cost': float(r[2] or 0), 'tokens': int(r[3] or 0), + } + for r in top_cost_callers + ], + price_rec_7d=[ + { + 'strategy': r[0], 'count': int(r[1] or 0), + 'avg_confidence': round(float(r[2] or 0), 3), + } + for r in price_rec_7d + ], error=None, ) except Exception as e: return render_template('admin/budget.html', active_page='obs_budget', rows=[], - budget_strategies=[], + budget_strategies=[], cost_trend_30d=[], + top_cost_callers=[], price_rec_7d=[], error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}') finally: session.close() @@ -1262,6 +1499,62 @@ def ppt_audit_history(): except Exception: vision_enabled = False + # Phase 47 K-6: 30d 統計 + top failure files + audit_30d_stats = {} + top_failure_files = [] + try: + s_ppt = get_session() + try: + stat_row = s_ppt.execute( + sa_text(""" + SELECT COUNT(*), + COUNT(*) FILTER (WHERE audit_status = 'passed'), + COUNT(*) FILTER (WHERE audit_status = 'failed'), + COUNT(*) FILTER (WHERE audit_status = 'skipped'), + COUNT(*) FILTER (WHERE audit_status = 'error'), + COALESCE(AVG(confidence) FILTER (WHERE audit_status = 'passed'), 0), + COALESCE(SUM(issues_count), 0) + FROM ppt_audit_results + WHERE audited_at >= NOW() - INTERVAL '30 days' + """), + ).fetchone() + total_30d = int(stat_row[0] or 0) + audit_30d_stats = { + 'total': total_30d, + 'passed': int(stat_row[1] or 0), + 'failed': int(stat_row[2] or 0), + 'skipped': int(stat_row[3] or 0), + 'error': int(stat_row[4] or 0), + 'avg_confidence': round(float(stat_row[5] or 0), 3), + 'total_issues': int(stat_row[6] or 0), + 'pass_rate': (float(stat_row[1] or 0) / total_30d * 100) if total_30d else 0, + } + + top_fail_rows = s_ppt.execute( + sa_text(""" + SELECT pptx_filename, COUNT(*) AS attempts, + SUM(issues_count) AS total_issues, + MAX(audited_at) AS last_audit + FROM ppt_audit_results + WHERE audit_status IN ('failed', 'error') + AND audited_at >= NOW() - INTERVAL '30 days' + GROUP BY pptx_filename + ORDER BY attempts DESC, total_issues DESC LIMIT 10 + """), + ).fetchall() + top_failure_files = [ + { + 'filename': r[0], 'attempts': int(r[1] or 0), + 'total_issues': int(r[2] or 0), + 'last_audit': r[3].strftime('%Y-%m-%d %H:%M') if r[3] else '', + } + for r in top_fail_rows + ] + finally: + s_ppt.close() + except Exception: + pass + # Phase 41 E-2: 對最近 3 筆 failed audit 跑 RAG 找相似修法 rag_fixes = [] failed_records = [r for r in audit_records if r.get('audit_status') in ('failed', 'error')][:3] @@ -1303,6 +1596,8 @@ def ppt_audit_history(): files=files, audit_records=audit_records, rag_fixes=rag_fixes, + audit_30d_stats=audit_30d_stats, + top_failure_files=top_failure_files, vision_enabled=vision_enabled, error=error, ) @@ -1504,6 +1799,115 @@ def host_health_dashboard(): except Exception: pass # 表可能尚未 migration,失敗安全 + # Phase 47 K-1: incidents + heal_logs 詳細列表 + playbooks 排行 + backup + embed queue + recent_incidents = [] + recent_heals = [] + playbook_ranking = [] + backup_history = [] + embed_queue_pending = 0 + embed_queue_failed = 0 + try: + s3 = get_session() + try: + inc_rows = s3.execute( + sa_text(""" + SELECT id, created_at, task_name, error_type, severity, + status, error_message, retry_count, resolved_at + FROM incidents + ORDER BY created_at DESC LIMIT 10 + """), + ).fetchall() + recent_incidents = [ + { + 'id': r[0], 'created_at': r[1].strftime('%Y-%m-%d %H:%M'), + 'task_name': r[2], 'error_type': r[3], 'severity': r[4], + 'status': r[5], 'error_message': (r[6] or '')[:200], + 'retry_count': int(r[7] or 0), + 'resolved_at': r[8].strftime('%Y-%m-%d %H:%M') if r[8] else None, + } + for r in inc_rows + ] + heal_rows = s3.execute( + sa_text(""" + SELECT h.id, h.created_at, h.action_type, h.result, + h.duration_ms, h.action_detail, h.incident_id, + i.error_type + FROM heal_logs h + LEFT JOIN incidents i ON i.id = h.incident_id + ORDER BY h.created_at DESC LIMIT 10 + """), + ).fetchall() + recent_heals = [ + { + 'id': r[0], 'created_at': r[1].strftime('%Y-%m-%d %H:%M'), + 'action_type': r[2], 'result': r[3], + 'duration_ms': int(r[4] or 0), + 'action_detail': (r[5] or '')[:160], + 'incident_id': r[6], 'error_type': r[7], + } + for r in heal_rows + ] + + # playbooks 庫排行(success_count + fail_count + 是否 active) + pb_rows = s3.execute( + sa_text(""" + SELECT 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 + LIMIT 12 + """), + ).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), + '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, + } + for r in pb_rows + ] + + # backup_log 7d 歷史 + bk_rows = s3.execute( + sa_text(""" + SELECT created_at, backup_type, status, file_size_bytes, + duration_seconds, error_message + FROM backup_log + WHERE created_at >= NOW() - INTERVAL '7 days' + ORDER BY created_at DESC LIMIT 10 + """), + ).fetchall() + backup_history = [ + { + 'created_at': r[0].strftime('%Y-%m-%d %H:%M'), + 'backup_type': r[1], 'status': r[2], + 'size_mb': round(float(r[3] or 0) / (1024 * 1024), 1), + 'duration_s': round(float(r[4] or 0), 1), + 'error': (r[5] or '')[:120], + } + for r in bk_rows + ] + + # embedding_retry_queue pending / failed + embed_q = s3.execute( + sa_text(""" + SELECT + COUNT(*) FILTER (WHERE status = 'pending'), + COUNT(*) FILTER (WHERE status = 'failed') + FROM embedding_retry_queue + """), + ).fetchone() + embed_queue_pending = int(embed_q[0] or 0) + embed_queue_failed = int(embed_q[1] or 0) + finally: + s3.close() + except Exception: + pass + return render_template( 'admin/host_health.html', active_page='obs_host_health', @@ -1513,4 +1917,10 @@ def host_health_dashboard(): health_history=health_history, mcp_24h=mcp_24h, aiops_summary=aiops_summary, + recent_incidents=recent_incidents, + recent_heals=recent_heals, + playbook_ranking=playbook_ranking, + backup_history=backup_history, + embed_queue_pending=embed_queue_pending, + embed_queue_failed=embed_queue_failed, ) diff --git a/templates/admin/ai_calls_dashboard.html b/templates/admin/ai_calls_dashboard.html index 0fc8a96..eac1e1e 100644 --- a/templates/admin/ai_calls_dashboard.html +++ b/templates/admin/ai_calls_dashboard.html @@ -124,6 +124,111 @@ {% endif %} + + {% if hourly_trend %} +
+
過去 24h 每小時呼叫趨勢 + 每小時 bucket:呼叫數 · 成本 · 錯誤 +
+
+ + + + + + + + {% 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 %} + + + {% if by_model %} +
+
依模型細分 + 資料來源:ai_calls.model(細到模型版本)— Top 15 +
+
+ + + + + + + + + + + {% for m in by_model %} + + + + + + + + + + {% endfor %} + +
模型供應商呼叫Token成本 USD平均耗時錯誤
{{ m.model[:35] }}{{ m.provider }}{{ "{:,}".format(m.calls) }}{{ "{:,}".format(m.tokens) }}${{ "%.4f"|format(m.cost) }}{{ m.avg_ms }} ms + {% if m.errors > 0 %}{{ m.errors }} + {% else %}0{% endif %} +
+
+
+ {% endif %} + + + {% if recent_contexts %} +
+
Agent 對話上下文(最近 10 筆) + 資料來源:agent_context(Hermes/OpenClaw/NemoTron 工作 session) +
+
+ + + + + + + + + {% for c in recent_contexts %} + + + + + + + + {% endfor %} + +
時間AgentKeyTTL min預覽
{{ c.created_at }}{{ c.agent_name }}{{ c.context_key }}{{ c.ttl_minutes }}{{ c.preview }}{% if c.preview|length >= 120 %}…{% endif %}
+
+
+ {% endif %} +
依供應商分組
diff --git a/templates/admin/budget.html b/templates/admin/budget.html index 3cffa5e..330364c 100644 --- a/templates/admin/budget.html +++ b/templates/admin/budget.html @@ -99,8 +99,88 @@ + + {% if top_cost_callers %} +
+
當月 Top 5 燒錢呼叫端 + 資料來源:ai_calls.cost_usd(caller 級彙總) +
+
+ + + + + + {% set max_cost = (top_cost_callers | map(attribute='cost') | max) or 1 %} + {% for c in top_cost_callers %} + + + + + + + + {% endfor %} + +
呼叫端呼叫Token成本佔比
{{ c.caller }}{{ "{:,}".format(c.calls) }}{{ "{:,}".format(c.tokens) }}${{ "%.2f"|format(c.cost) }} +
+
+
+
+
+
+ {% endif %} + + + {% if cost_trend_30d %} +
+
過去 30 日每日成本(依 provider) + 資料來源:ai_calls 每日 SUM(cost_usd) GROUP BY provider +
+
+ + + + + + {% for r in cost_trend_30d %} + + + + + + {% endfor %} + +
日期供應商成本 USD
{{ r.date }}{{ r.provider }}${{ "%.4f"|format(r.cost) }}
+
+
+ {% endif %} + + + {% if price_rec_7d %} +
+
AI 價格決策 7 日(ai_price_recommendations) + 展現 AI 編排的商業面產出 — 不只 cost,看實際決策數量 +
+
+
+ {% for p in price_rec_7d %} +
+
+ {{ p.strategy }} + {{ p.count }} + 信心 {{ "%.2f"|format(p.avg_confidence) }} +
+
+ {% endfor %} +
+
+
+ {% endif %} +

- Operation Ollama-First v5.0 / Phase 29 — 預算控管 + Operation Ollama-First v5.0 / Phase 47 — 預算控管 + (5 表深挖:ai_call_budgets / ai_calls / ai_price_recommendations / ai_insights / cost_throttle_state)

diff --git a/templates/admin/host_health.html b/templates/admin/host_health.html index 0275243..88a5cb6 100644 --- a/templates/admin/host_health.html +++ b/templates/admin/host_health.html @@ -267,8 +267,204 @@ {% endif %} + +
+
+ 最近 10 筆 Incidents + 資料來源:incidents(ADR-013 AutoHeal 觸發紀錄) +
+
+ {% if recent_incidents %} + + + + + + + + + + {% for i in recent_incidents %} + + + + + + + + + + + {% endfor %} + +
建立時間任務錯誤類型嚴重度狀態重試錯誤訊息解決時間
{{ i.created_at }}{{ i.task_name }}{{ i.error_type }} + {% if i.severity in ('P0', 'P1') %}{{ i.severity }} + {% elif i.severity == 'P2' %}{{ i.severity }} + {% else %}{{ i.severity }}{% endif %} + + {% if i.status == 'open' %}未解決 + {% elif i.status == 'resolved' %}已解決 + {% else %}{{ i.status }}{% endif %} + {{ i.retry_count }}{{ i.error_message }}{{ i.resolved_at or '—' }}
+ {% else %} +
+ 尚無 incident 紀錄(即系統尚未觸發過 AutoHeal) +
+ {% endif %} +
+
+ + +
+
+ 最近 10 筆 Heal Logs + 資料來源:heal_logs(自癒 playbook 執行歷史) +
+
+ {% if recent_heals %} + + + + + + + + + {% for h in recent_heals %} + + + + + + + + + {% endfor %} + +
時間動作結果耗時 ms關聯 Incident細節
{{ h.created_at }}{{ h.action_type or '—' }} + {% if h.result == 'success' %}成功 + {% elif h.result == 'failed' %}失敗 + {% elif h.result == 'skipped' %}跳過 + {% else %}{{ h.result }}{% endif %} + {{ h.duration_ms }}#{{ h.incident_id }} · {{ h.error_type or '—' }}{{ h.action_detail }}
+ {% else %} +
+ 尚無 heal log(一鍵 AutoHeal 觸發後將累積) +
+ {% endif %} +
+
+ + +
+
+ AutoHeal Playbook 庫排行 + 資料來源:playbooks(success/fail 累計,按執行次數排序) +
+
+ {% if playbook_ranking %} + + + + + + + + + + + + {% for p in playbook_ranking %} + + + + + + + + + + + + {% endfor %} + +
Playbook錯誤類型動作嚴重度成功失敗成功率狀態冷卻 min
{{ p.name }}{{ p.error_type }}{{ p.action_type }}{{ p.severity }}{{ p.success }}{{ p.fail }} + {% if (p.success + p.fail) > 0 %} + + {{ "%.0f"|format(p.success_rate) }}% + + {% else %}{% endif %} + + {% if p.is_active %} + 啟用 + {% else %} + 停用 + {% endif %} + {{ p.cooldown_min }}
+ {% else %} +
+ 尚無 playbook 資料(migration 013 + 020 是否已跑?) +
+ {% endif %} +
+
+ + +
+
+ 備份歷史(過去 7 日) + 資料來源:backup_log +
+
+ {% if backup_history %} + + + + + + + + + + {% for b in backup_history %} + + + + + + + + + {% endfor %} + +
時間類型狀態大小 (MB)耗時 s錯誤
{{ b.created_at }}{{ b.backup_type }} + {% if b.status == 'success' %}成功 + {% elif b.status == 'failed' %}失敗 + {% else %}{{ b.status }}{% endif %} + {{ "{:,}".format(b.size_mb) }}{{ b.duration_s }}{{ b.error }}
+ {% else %} +
+ 過去 7 日無備份紀錄(每日 03:00 cron 執行) +
+ {% endif %} +
+
+ + + {% if embed_queue_pending > 0 or embed_queue_failed > 0 %} +
+ Embedding 重試佇列: + 待處理 {{ embed_queue_pending }} 筆 · + 失敗 {{ embed_queue_failed }} 筆 + + 資料來源:embedding_retry_queue — 卡住的 embedding 工作,需檢查 Ollama 健康 + +
+ {% endif %} +

- Operation Ollama-First v5.0 / Phase 40 — 主機健康監控(含 24h 歷史 / MCP / AIOps / AutoHeal L2) + Operation Ollama-First v5.0 / Phase 47 — 主機健康監控 + (8 表深挖:host_health_probes / mcp_calls / incidents / heal_logs / playbooks / backup_log / embedding_retry_queue / ai_call_budgets)

diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html index 9eb1002..ec9ef66 100644 --- a/templates/admin/ppt_audit_history.html +++ b/templates/admin/ppt_audit_history.html @@ -130,8 +130,107 @@ python3 -c "from services.ppt_vision_service import ppt_vision_service; print(ppt_vision_service.check_ppt_file('reports/xxx.pptx'))"

+ + {% if audit_30d_stats and audit_30d_stats.total > 0 %} +
+
過去 30 日 PPT 審核統計 + 資料來源:ppt_audit_results 全表聚合 +
+
+
+
+
+ 總筆數 + {{ audit_30d_stats.total }} +
+
+
+
+ 通過 + {{ audit_30d_stats.passed }} +
+
+
+
+ 失敗 + {{ audit_30d_stats.failed }} +
+
+
+
+ 錯誤 + {{ audit_30d_stats.error }} +
+
+
+
+ 通過率 + {{ "%.0f"|format(audit_30d_stats.pass_rate) }}% +
+
+
+
+ 總 issue 數 + {{ audit_30d_stats.total_issues }} +
+
+
+
+ + 通過 audit 平均信心度:{{ "%.2f"|format(audit_30d_stats.avg_confidence) }} +
+
+
+ {% endif %} + + + {% if top_failure_files %} +
+
+ Top 10 失敗檔案(30d) + — 反覆失敗的 PPT 檔案,需手動處理或調整 generator +
+
+ + + + + + + + + + + {% for f in top_failure_files %} + + + + + + + {% endfor %} + +
檔名失敗次數總 issue最近審核
{{ f.filename }}{{ f.attempts }}{{ f.total_issues }}{{ f.last_audit }}
+
+
+ {% endif %} + + {% if (not audit_30d_stats or audit_30d_stats.total == 0) and not vision_enabled %} +
+ + 為什麼這頁空? + +
+ {% endif %} +

- Operation Ollama-First v5.0 / Phase 40 — PPT 視覺審核歷史(含 AiderHeal L2) + Operation Ollama-First v5.0 / Phase 47 — PPT 視覺審核歷史 + (3 表深挖:ppt_audit_results / reports/ 檔案系統 / ai_insights RAG)

diff --git a/templates/admin/promotion_review.html b/templates/admin/promotion_review.html index c6693f3..fe60884 100644 --- a/templates/admin/promotion_review.html +++ b/templates/admin/promotion_review.html @@ -76,8 +76,109 @@ {% endif %} + + {% if episode_distribution_30d %} +
+
蒸餾池 30 日狀態分布 + 資料來源:learning_episodes(不只看 awaiting,看完整流動) +
+
+
+ {% for status, cnt in episode_distribution_30d.items() %} +
+
+ + {% if status == 'pending' %} 待處理 + {% elif status == 'awaiting_review' %} 待審核 + {% elif status == 'approved' %} 已晉升 + {% elif status == 'rejected_quality' %} 品質拒 + {% elif status == 'rejected_hallucination' %} 幻覺拒 + {% elif status == 'rejected_duplicate' %} 重複拒 + {% elif status == 'rejected_human' %} 人工拒 + {% elif status == 'expired' %} 已過期 + {% else %}{{ status }}{% endif %} + + {{ cnt }} +
+
+ {% endfor %} +
+
+
+ {% endif %} + + + {% if latest_insights %} +
+
知識庫最近 10 筆 ai_insights + 資料來源:ai_insights — 已晉升內容預覽 +
+
+ + + + + + {% for i in latest_insights %} + + + + + + + + + {% endfor %} + +
#類型期間SKU建立時間預覽
#{{ i.id }}{{ i.insight_type }}{{ i.period or '—' }}{{ i.product_sku or '—' }}{{ i.created_at }}{{ i.preview }}{% if i.preview|length >= 160 %}…{% endif %}
+
+
+ {% endif %} + + + {% if strategy_weights %} +
+
OpenClaw 學習策略權重 TOP 12 + 資料來源:agent_strategy_weights(ADR-012 closed-loop learning) +
+
+ + + + + + + + + + + + + {% for s in strategy_weights %} + + + + + + + + + {% endfor %} + +
策略 Key權重成功失敗成功率更新時間
{{ s.strategy_key }}{{ "%.2f"|format(s.weight) }}{{ s.success }}{{ s.fail }} + {% if (s.success + s.fail) > 0 %} + + {{ "%.0f"|format(s.success_rate) }}% + + {% else %}{% endif %} + {{ s.updated_at }}
+
+
+ {% endif %} +

- Operation Ollama-First v5.0 / Phase 29 — RAG 學習晉升審核 + Operation Ollama-First v5.0 / Phase 47 — RAG 學習晉升審核 + (4 表深挖:learning_episodes / ai_insights / agent_strategy_weights / rag_query_log)

diff --git a/templates/admin/quality_trend.html b/templates/admin/quality_trend.html index ab214ae..2f44caa 100644 --- a/templates/admin/quality_trend.html +++ b/templates/admin/quality_trend.html @@ -150,8 +150,98 @@ + + {% 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) }}% +
+
+ {% endfor %} +
+
+
+ {% endif %} + + + {% if action_plans_status %} +
+
Action Plans 狀態分布(過去 {{ days }} 日) + 資料來源:action_plans(NemoTron/OpenClaw 的計畫產出 + 審核狀態) +
+
+ + + + + + {% for a in action_plans_status %} + + + + + + {% endfor %} + +
狀態計畫類型數量
+ {% if a.status == 'pending' %}{{ a.status }} + {% elif a.status == 'approved' %}{{ a.status }} + {% elif a.status == 'executed' %}{{ a.status }} + {% elif a.status == 'rejected' %}{{ a.status }} + {% else %}{{ a.status }}{% endif %} + {{ a.plan_type }}{{ a.count }}
+
+
+ {% endif %} + + + {% if action_outcomes_stats %} +
+
Action Outcomes 成效(過去 {{ days }} 日) + 資料來源:action_outcomes(ADR-012 閉環學習:實際動作有效嗎?) +
+
+
+ {% set total_ao = (action_outcomes_stats | sum(attribute='count')) or 1 %} + {% for r in action_outcomes_stats %} +
+
+ + {% if r.verdict == 'effective' %} 有效 + {% elif r.verdict == 'backfired' %} 適得其反 + {% elif r.verdict == 'neutral' %} 無顯著效果 + {% else %}{{ r.verdict }}{% endif %} + + {{ r.count }} + {{ "%.1f"|format(r.count / total_ao * 100) }}% +
+
+ {% endfor %} +
+
+
+ {% endif %} +

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

{% endblock %}