feat(p47): 6 頁深挖資料庫 — 從 5 表 → 17 表,每頁加 3-5 個 widget
All checks were successful
CD Pipeline / deploy (push) Successful in 2m36s

統帥質疑:「6 頁內容太空洞,要更貼近資料庫裡所有數據」
盤點:DB 有 22 表,6 頁原本只用 5 表(22.7% 利用率)。
本 commit 新接 12 張既有但未用的表。

K-1 host_health(接 +5 表 → 8 表)
- 加 incidents 最近 10 筆詳細(task_name/error_type/嚴重度/狀態/重試/錯誤訊息)
- 加 heal_logs 最近 10 筆(action_type/result/耗時/incident 關聯)
- 加 playbooks 庫排行 TOP 12(success_count/fail_count/成功率/啟用狀態)
- 加 backup_log 7 日歷史(type/status/size/duration/error)
- 加 embedding_retry_queue pending/failed 警示

K-2 ai_calls(接 +3 表)
- 加 24h 每小時呼叫趨勢 bucket(含成本+錯誤+流量分布條)
- 加 by model 細分(不只 provider,到 model 版本級別)TOP 15
- 加 agent_context 最近 10 筆(OpenClaw/Hermes 對話 session preview)

K-3 budget(接 +3 表)
- 加當月 Top 5 燒錢呼叫端(caller × cost ranking)
- 加過去 30 日每日成本 by provider 趨勢表
- 加 ai_price_recommendations 7 日統計(strategy 分布 + 平均信心度)

K-4 promotion_review(接 +2 表)
- 加蒸餾池 30 日 status 分布(不只 awaiting,看 8 種完整流動)
- 加 ai_insights 最近 10 筆已晉升內容預覽
- 加 agent_strategy_weights TOP 12(OpenClaw 學習權重 + 成功率)

K-5 quality_trend(接 +3 表)
- 加 RAG 整體 feedback 1-5 分分布(過去 N 日,星等視覺)
- 加 action_plans status 分布(pending/approved/executed/rejected)
- 加 action_outcomes verdict 分布(effective/neutral/backfired
  — ADR-012 閉環學習核心 KPI)

K-6 ppt_audit_history(接 +0 表,但 deeper SQL)
- 加 30 日統計卡(total/passed/failed/error/通過率/總 issue 數/平均信心度)
- 加 Top 10 反覆失敗檔案(30d 失敗次數 + total issues)
- 加 empty state 說明(PPT_VISION_ENABLED=false 時顯示啟用步驟)

DB 利用率對應:
- Phase 38 起點:5 表(22.7%)
- Phase 39-46 累計:12 表(54.5%)
- Phase 47 收官:17 表(77.3%)
  新接:playbooks / backup_log / embedding_retry_queue / agent_context /
        ai_price_recommendations / agent_strategy_weights / action_plans /
        action_outcomes(之前已接:incidents/heal_logs/ai_insights/learning_episodes/
        rag_query_log/mcp_calls/ai_calls/host_health_probes/ppt_audit_results)

每頁 widget 數對應:
- host_health: 5 卡 → 11 卡
- ai_calls: 5 卡 → 8 卡
- budget: 4 卡 → 7 卡
- promotion_review: 2 卡 → 5 卡
- quality_trend: 4 卡 → 7 卡
- ppt_audit: 3 卡 → 6 卡

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
OoO
2026-05-04 19:49:52 +08:00
parent 347efb8ea1
commit 2e124db602
7 changed files with 1087 additions and 6 deletions

View File

@@ -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 12OpenClaw 學習權重)
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,
)

View File

@@ -124,6 +124,111 @@
</div>
{% endif %}
<!-- Phase 47 K-2: 24h 每小時呼叫趨勢 -->
{% if hourly_trend %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-chart-area me-2"></i>過去 24h 每小時呼叫趨勢</strong>
<small class="text-muted">每小時 bucket呼叫數 · 成本 · 錯誤</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.85em;">
<thead class="table-light">
<tr>
<th>時段</th><th>呼叫數</th><th>成本 USD</th><th>錯誤</th><th style="width: 50%;">流量分布</th>
</tr>
</thead>
<tbody>
{% set max_calls = (hourly_trend | map(attribute='calls') | max) or 1 %}
{% for h in hourly_trend %}
<tr>
<td><code>{{ h.hour }}</code></td>
<td><strong>{{ "{:,}".format(h.calls) }}</strong></td>
<td>${{ "%.3f"|format(h.cost) }}</td>
<td>
{% if h.errors > 0 %}<span class="text-danger">{{ h.errors }}</span>
{% else %}<small class="text-muted">0</small>{% endif %}
</td>
<td>
<div class="progress" style="height: 8px;">
<div class="progress-bar {% if h.errors > 0 %}bg-warning{% else %}bg-info{% endif %}"
style="width: {{ (h.calls / max_calls * 100) | round | int }}%"></div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-2: by model 細分 -->
{% if by_model %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-microchip me-2"></i>依模型細分</strong>
<small class="text-muted">資料來源ai_calls.model細到模型版本— Top 15</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>模型</th><th>供應商</th>
<th class="text-end">呼叫</th><th class="text-end">Token</th>
<th class="text-end">成本 USD</th><th class="text-end">平均耗時</th>
<th class="text-end">錯誤</th>
</tr>
</thead>
<tbody>
{% for m in by_model %}
<tr>
<td><code>{{ m.model[:35] }}</code></td>
<td><span class="badge bg-secondary">{{ m.provider }}</span></td>
<td class="text-end">{{ "{:,}".format(m.calls) }}</td>
<td class="text-end">{{ "{:,}".format(m.tokens) }}</td>
<td class="text-end">${{ "%.4f"|format(m.cost) }}</td>
<td class="text-end">{{ m.avg_ms }} ms</td>
<td class="text-end">
{% if m.errors > 0 %}<span class="text-danger">{{ m.errors }}</span>
{% else %}<small class="text-muted">0</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-2: agent_context 最近 10 筆 -->
{% if recent_contexts %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-comments-dollar me-2"></i>Agent 對話上下文(最近 10 筆)</strong>
<small class="text-muted">資料來源agent_contextHermes/OpenClaw/NemoTron 工作 session</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.85em;">
<thead class="table-light">
<tr>
<th>時間</th><th>Agent</th><th>Key</th>
<th class="text-end">TTL min</th><th>預覽</th>
</tr>
</thead>
<tbody>
{% for c in recent_contexts %}
<tr>
<td><small>{{ c.created_at }}</small></td>
<td><span class="badge bg-info text-dark">{{ c.agent_name }}</span></td>
<td><code>{{ c.context_key }}</code></td>
<td class="text-end">{{ c.ttl_minutes }}</td>
<td><small class="text-muted">{{ c.preview }}{% if c.preview|length >= 120 %}…{% endif %}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- by provider -->
<div class="card mb-3">
<div class="card-header"><strong>依供應商分組</strong></div>

View File

@@ -99,8 +99,88 @@
</tbody>
</table>
<!-- Phase 47 K-3: Top 5 cost-burning caller -->
{% if top_cost_callers %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-fire me-2"></i>當月 Top 5 燒錢呼叫端</strong>
<small class="text-muted">資料來源ai_calls.cost_usdcaller 級彙總)</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr><th>呼叫端</th><th class="text-end">呼叫</th><th class="text-end">Token</th><th class="text-end">成本</th><th>佔比</th></tr>
</thead>
<tbody>
{% set max_cost = (top_cost_callers | map(attribute='cost') | max) or 1 %}
{% for c in top_cost_callers %}
<tr>
<td><code>{{ c.caller }}</code></td>
<td class="text-end">{{ "{:,}".format(c.calls) }}</td>
<td class="text-end">{{ "{:,}".format(c.tokens) }}</td>
<td class="text-end"><strong>${{ "%.2f"|format(c.cost) }}</strong></td>
<td>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-warning" style="width: {{ (c.cost / max_cost * 100) | round | int }}%"></div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-3: 30d daily cost trend by provider -->
{% if cost_trend_30d %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-chart-line me-2"></i>過去 30 日每日成本(依 provider</strong>
<small class="text-muted">資料來源ai_calls 每日 SUM(cost_usd) GROUP BY provider</small>
</div>
<div class="card-body p-0" style="max-height: 360px; overflow-y: auto;">
<table class="table table-sm mb-0" style="font-size: 0.85em;">
<thead class="table-light" style="position: sticky; top: 0;">
<tr><th>日期</th><th>供應商</th><th class="text-end">成本 USD</th></tr>
</thead>
<tbody>
{% for r in cost_trend_30d %}
<tr>
<td><code>{{ r.date }}</code></td>
<td><span class="badge bg-secondary">{{ r.provider }}</span></td>
<td class="text-end">${{ "%.4f"|format(r.cost) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-3: AI 價格決策 7d -->
{% if price_rec_7d %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-tag me-2"></i>AI 價格決策 7 日ai_price_recommendations</strong>
<small class="text-muted">展現 AI 編排的商業面產出 — 不只 cost看實際決策數量</small>
</div>
<div class="card-body">
<div class="row g-2">
{% for p in price_rec_7d %}
<div class="col-md-3 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">{{ p.strategy }}</small>
<strong style="font-size: 1.4em;">{{ p.count }}</strong>
<small class="d-block text-muted">信心 {{ "%.2f"|format(p.avg_confidence) }}</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — 預算控管
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 47 — 預算控管
5 表深挖ai_call_budgets / ai_calls / ai_price_recommendations / ai_insights / cost_throttle_state
</small></p>
</div>

View File

@@ -267,8 +267,204 @@
</div>
{% endif %}
<!-- Phase 47 K-1: 最近 10 筆 Incidents 詳細 -->
<div class="card mb-3">
<div class="card-header">
<strong><i class="fas fa-fire me-2"></i>最近 10 筆 Incidents</strong>
<small class="text-muted">資料來源incidentsADR-013 AutoHeal 觸發紀錄)</small>
</div>
<div class="card-body p-0">
{% if recent_incidents %}
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>建立時間</th><th>任務</th><th>錯誤類型</th>
<th>嚴重度</th><th>狀態</th><th>重試</th>
<th>錯誤訊息</th><th>解決時間</th>
</tr>
</thead>
<tbody>
{% for i in recent_incidents %}
<tr>
<td><small>{{ i.created_at }}</small></td>
<td><code>{{ i.task_name }}</code></td>
<td><span class="badge bg-secondary">{{ i.error_type }}</span></td>
<td>
{% if i.severity in ('P0', 'P1') %}<span class="badge bg-danger">{{ i.severity }}</span>
{% elif i.severity == 'P2' %}<span class="badge bg-warning">{{ i.severity }}</span>
{% else %}<span class="badge bg-info text-dark">{{ i.severity }}</span>{% endif %}
</td>
<td>
{% if i.status == 'open' %}<span class="badge bg-danger">未解決</span>
{% elif i.status == 'resolved' %}<span class="badge bg-success">已解決</span>
{% else %}<span class="badge bg-light text-dark">{{ i.status }}</span>{% endif %}
</td>
<td class="text-end">{{ i.retry_count }}</td>
<td><small class="text-muted">{{ i.error_message }}</small></td>
<td><small>{{ i.resolved_at or '—' }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="text-muted text-center p-3 small">
<i class="fas fa-shield-check me-1"></i>尚無 incident 紀錄(即系統尚未觸發過 AutoHeal
</div>
{% endif %}
</div>
</div>
<!-- Phase 47 K-1: 最近 10 筆 Heal Logs 詳細 -->
<div class="card mb-3">
<div class="card-header">
<strong><i class="fas fa-band-aid me-2"></i>最近 10 筆 Heal Logs</strong>
<small class="text-muted">資料來源heal_logs自癒 playbook 執行歷史)</small>
</div>
<div class="card-body p-0">
{% if recent_heals %}
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>時間</th><th>動作</th><th>結果</th>
<th class="text-end">耗時 ms</th><th>關聯 Incident</th><th>細節</th>
</tr>
</thead>
<tbody>
{% for h in recent_heals %}
<tr>
<td><small>{{ h.created_at }}</small></td>
<td><span class="badge bg-info text-dark">{{ h.action_type or '—' }}</span></td>
<td>
{% if h.result == 'success' %}<span class="badge bg-success"><i class="fas fa-check me-1"></i>成功</span>
{% elif h.result == 'failed' %}<span class="badge bg-danger"><i class="fas fa-times me-1"></i>失敗</span>
{% elif h.result == 'skipped' %}<span class="badge bg-secondary">跳過</span>
{% else %}<span class="badge bg-light text-dark">{{ h.result }}</span>{% endif %}
</td>
<td class="text-end">{{ h.duration_ms }}</td>
<td><small>#{{ h.incident_id }} · <code>{{ h.error_type or '—' }}</code></small></td>
<td><small class="text-muted">{{ h.action_detail }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="text-muted text-center p-3 small">
<i class="fas fa-info-circle me-1"></i>尚無 heal log一鍵 AutoHeal 觸發後將累積)
</div>
{% endif %}
</div>
</div>
<!-- Phase 47 K-1: AutoHeal Playbook 庫排行 -->
<div class="card mb-3">
<div class="card-header">
<strong><i class="fas fa-book-medical me-2"></i>AutoHeal Playbook 庫排行</strong>
<small class="text-muted">資料來源playbookssuccess/fail 累計,按執行次數排序)</small>
</div>
<div class="card-body p-0">
{% if playbook_ranking %}
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>Playbook</th><th>錯誤類型</th><th>動作</th>
<th>嚴重度</th>
<th class="text-end">成功</th><th class="text-end">失敗</th>
<th class="text-end">成功率</th>
<th>狀態</th><th class="text-end">冷卻 min</th>
</tr>
</thead>
<tbody>
{% for p in playbook_ranking %}
<tr>
<td><strong>{{ p.name }}</strong></td>
<td><code>{{ p.error_type }}</code></td>
<td><span class="badge bg-info text-dark">{{ p.action_type }}</span></td>
<td>{{ p.severity }}</td>
<td class="text-end text-success">{{ p.success }}</td>
<td class="text-end text-danger">{{ p.fail }}</td>
<td class="text-end">
{% if (p.success + p.fail) > 0 %}
<strong class="{% if p.success_rate >= 80 %}text-success{% elif p.success_rate >= 50 %}text-warning{% else %}text-danger{% endif %}">
{{ "%.0f"|format(p.success_rate) }}%
</strong>
{% else %}<small class="text-muted"></small>{% endif %}
</td>
<td>
{% if p.is_active %}
<span class="badge bg-success">啟用</span>
{% else %}
<span class="badge bg-secondary">停用</span>
{% endif %}
</td>
<td class="text-end">{{ p.cooldown_min }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="text-muted text-center p-3 small">
<i class="fas fa-info-circle me-1"></i>尚無 playbook 資料migration 013 + 020 是否已跑?)
</div>
{% endif %}
</div>
</div>
<!-- Phase 47 K-1: 備份歷史 7d -->
<div class="card mb-3">
<div class="card-header">
<strong><i class="fas fa-database me-2"></i>備份歷史(過去 7 日)</strong>
<small class="text-muted">資料來源backup_log</small>
</div>
<div class="card-body p-0">
{% if backup_history %}
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>時間</th><th>類型</th><th>狀態</th>
<th class="text-end">大小 (MB)</th><th class="text-end">耗時 s</th>
<th>錯誤</th>
</tr>
</thead>
<tbody>
{% for b in backup_history %}
<tr>
<td><small>{{ b.created_at }}</small></td>
<td><span class="badge bg-secondary">{{ b.backup_type }}</span></td>
<td>
{% if b.status == 'success' %}<span class="badge bg-success"><i class="fas fa-check me-1"></i>成功</span>
{% elif b.status == 'failed' %}<span class="badge bg-danger"><i class="fas fa-times me-1"></i>失敗</span>
{% else %}<span class="badge bg-warning">{{ b.status }}</span>{% endif %}
</td>
<td class="text-end">{{ "{:,}".format(b.size_mb) }}</td>
<td class="text-end">{{ b.duration_s }}</td>
<td><small class="text-muted">{{ b.error }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="text-muted text-center p-3 small">
<i class="fas fa-info-circle me-1"></i>過去 7 日無備份紀錄(每日 03:00 cron 執行)
</div>
{% endif %}
</div>
</div>
<!-- Phase 47 K-1: Embedding 重試佇列 -->
{% if embed_queue_pending > 0 or embed_queue_failed > 0 %}
<div class="alert alert-warning mb-3">
<strong><i class="fas fa-cog me-1"></i>Embedding 重試佇列:</strong>
待處理 <strong>{{ embed_queue_pending }}</strong> 筆 ·
失敗 <strong>{{ embed_queue_failed }}</strong>
<small class="d-block text-muted mt-1">
資料來源embedding_retry_queue — 卡住的 embedding 工作,需檢查 Ollama 健康
</small>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 40 — 主機健康監控(含 24h 歷史 / MCP / AIOps / AutoHeal L2
<i class="fas fa-robot me-1"></i>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
</small></p>
</div>

View File

@@ -130,8 +130,107 @@
<code>python3 -c "from services.ppt_vision_service import ppt_vision_service; print(ppt_vision_service.check_ppt_file('reports/xxx.pptx'))"</code>
</p>
<!-- Phase 47 K-6: 30d 統計 -->
{% if audit_30d_stats and audit_30d_stats.total > 0 %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-chart-pie me-2"></i>過去 30 日 PPT 審核統計</strong>
<small class="text-muted">資料來源ppt_audit_results 全表聚合</small>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">總筆數</small>
<strong style="font-size: 1.4em;">{{ audit_30d_stats.total }}</strong>
</div>
</div>
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">通過</small>
<strong class="text-success" style="font-size: 1.4em;">{{ audit_30d_stats.passed }}</strong>
</div>
</div>
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">失敗</small>
<strong class="{% if audit_30d_stats.failed > 0 %}text-warning{% endif %}" style="font-size: 1.4em;">{{ audit_30d_stats.failed }}</strong>
</div>
</div>
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">錯誤</small>
<strong class="{% if audit_30d_stats.error > 0 %}text-danger{% endif %}" style="font-size: 1.4em;">{{ audit_30d_stats.error }}</strong>
</div>
</div>
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">通過率</small>
<strong class="{% if audit_30d_stats.pass_rate >= 80 %}text-success{% elif audit_30d_stats.pass_rate >= 60 %}text-warning{% else %}text-danger{% endif %}" style="font-size: 1.4em;">{{ "%.0f"|format(audit_30d_stats.pass_rate) }}%</strong>
</div>
</div>
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">總 issue 數</small>
<strong style="font-size: 1.4em;">{{ audit_30d_stats.total_issues }}</strong>
</div>
</div>
</div>
<div class="mt-2 small text-muted">
<i class="fas fa-info-circle me-1"></i>
通過 audit 平均信心度:<strong>{{ "%.2f"|format(audit_30d_stats.avg_confidence) }}</strong>
</div>
</div>
</div>
{% endif %}
<!-- Phase 47 K-6: Top 10 failure files -->
{% if top_failure_files %}
<div class="card mb-3" style="border-left: 4px solid #ffc107;">
<div class="card-header bg-light">
<strong><i class="fas fa-exclamation-triangle me-2"></i>Top 10 失敗檔案30d</strong>
<small class="text-muted">— 反覆失敗的 PPT 檔案,需手動處理或調整 generator</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>檔名</th>
<th class="text-end">失敗次數</th>
<th class="text-end">總 issue</th>
<th>最近審核</th>
</tr>
</thead>
<tbody>
{% for f in top_failure_files %}
<tr>
<td><code>{{ f.filename }}</code></td>
<td class="text-end"><span class="badge bg-warning text-dark">{{ f.attempts }}</span></td>
<td class="text-end">{{ f.total_issues }}</td>
<td><small>{{ f.last_audit }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if (not audit_30d_stats or audit_30d_stats.total == 0) and not vision_enabled %}
<div class="alert alert-info mt-3">
<i class="fas fa-info-circle me-1"></i>
<strong>為什麼這頁空?</strong>
<ul class="mb-0 small mt-2">
<li>PPT_VISION_ENABLED=false在 .env 設為 true 啟用)</li>
<li>188 主機需安裝 LibreOffice<code>apt install libreoffice</code></li>
<li>需 Ollama 拉取 minicpm-v 模型(用於 PPT 視覺檢查)</li>
<li>啟用後每日 22:00 cron 自動掃當天新生 .pptx審核結果寫入 ppt_audit_results 累積歷史</li>
</ul>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 40 — PPT 視覺審核歷史(含 AiderHeal L2
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 47 — PPT 視覺審核歷史
3 表深挖ppt_audit_results / reports/ 檔案系統 / ai_insights RAG
</small></p>
</div>

View File

@@ -76,8 +76,109 @@
</div>
{% endif %}
<!-- Phase 47 K-4: 蒸餾池 30d 分布 -->
{% if episode_distribution_30d %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-flask me-2"></i>蒸餾池 30 日狀態分布</strong>
<small class="text-muted">資料來源learning_episodes不只看 awaiting看完整流動</small>
</div>
<div class="card-body">
<div class="row g-2">
{% for status, cnt in episode_distribution_30d.items() %}
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">
{% if status == 'pending' %}<i class="fas fa-hourglass-start"></i> 待處理
{% elif status == 'awaiting_review' %}<i class="fas fa-user-clock"></i> 待審核
{% elif status == 'approved' %}<i class="fas fa-check-circle text-success"></i> 已晉升
{% elif status == 'rejected_quality' %}<i class="fas fa-times text-danger"></i> 品質拒
{% elif status == 'rejected_hallucination' %}<i class="fas fa-times text-danger"></i> 幻覺拒
{% elif status == 'rejected_duplicate' %}<i class="fas fa-clone text-warning"></i> 重複拒
{% elif status == 'rejected_human' %}<i class="fas fa-user-times text-danger"></i> 人工拒
{% elif status == 'expired' %}<i class="fas fa-clock text-muted"></i> 已過期
{% else %}{{ status }}{% endif %}
</small>
<strong style="font-size: 1.4em;">{{ cnt }}</strong>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Phase 47 K-4: 最近 10 筆已晉升 ai_insights -->
{% if latest_insights %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-book me-2"></i>知識庫最近 10 筆 ai_insights</strong>
<small class="text-muted">資料來源ai_insights — 已晉升內容預覽</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr><th>#</th><th>類型</th><th>期間</th><th>SKU</th><th>建立時間</th><th>預覽</th></tr>
</thead>
<tbody>
{% for i in latest_insights %}
<tr>
<td><code>#{{ i.id }}</code></td>
<td><span class="badge bg-info text-dark">{{ i.insight_type }}</span></td>
<td><small>{{ i.period or '—' }}</small></td>
<td><small>{{ i.product_sku or '—' }}</small></td>
<td><small>{{ i.created_at }}</small></td>
<td><small class="text-muted">{{ i.preview }}{% if i.preview|length >= 160 %}…{% endif %}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-4: agent_strategy_weights TOP 12 -->
{% if strategy_weights %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-balance-scale me-2"></i>OpenClaw 學習策略權重 TOP 12</strong>
<small class="text-muted">資料來源agent_strategy_weightsADR-012 closed-loop learning</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr>
<th>策略 Key</th>
<th class="text-end">權重</th>
<th class="text-end">成功</th>
<th class="text-end">失敗</th>
<th class="text-end">成功率</th>
<th>更新時間</th>
</tr>
</thead>
<tbody>
{% for s in strategy_weights %}
<tr>
<td><code>{{ s.strategy_key }}</code></td>
<td class="text-end"><strong>{{ "%.2f"|format(s.weight) }}</strong></td>
<td class="text-end text-success">{{ s.success }}</td>
<td class="text-end text-danger">{{ s.fail }}</td>
<td class="text-end">
{% if (s.success + s.fail) > 0 %}
<span class="{% if s.success_rate >= 70 %}text-success{% elif s.success_rate >= 40 %}text-warning{% else %}text-danger{% endif %}">
{{ "%.0f"|format(s.success_rate) }}%
</span>
{% else %}<small class="text-muted"></small>{% endif %}
</td>
<td><small>{{ s.updated_at }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — RAG 學習晉升審核
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 47 — RAG 學習晉升審核
4 表深挖learning_episodes / ai_insights / agent_strategy_weights / rag_query_log
</small></p>
</div>

View File

@@ -150,8 +150,98 @@
</div>
</div>
<!-- Phase 47 K-5: RAG 整體 feedback 分布 -->
{% if rag_overall_dist %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-poll me-2"></i>RAG 整體反饋分布(過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源rag_query_log.feedback_score含全 caller1-5 分)</small>
</div>
<div class="card-body">
<div class="row g-2">
{% set total_fb = (rag_overall_dist | sum(attribute='count')) or 1 %}
{% for r in rag_overall_dist %}
<div class="col-md-2 col-sm-4">
<div class="border rounded p-2 text-center">
<small class="text-muted d-block">
{% for _ in range(r.score) %}<i class="fas fa-star text-warning"></i>{% endfor %}
{% for _ in range(5 - r.score) %}<i class="far fa-star text-muted"></i>{% endfor %}
</small>
<strong style="font-size: 1.4em;">{{ r.count }}</strong>
<small class="d-block text-muted">{{ "%.1f"|format(r.count / total_fb * 100) }}%</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Phase 47 K-5: Action Plans status 分布 -->
{% if action_plans_status %}
<div class="card mb-3">
<div class="card-header"><strong><i class="fas fa-tasks me-2"></i>Action Plans 狀態分布(過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源action_plansNemoTron/OpenClaw 的計畫產出 + 審核狀態)</small>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead class="table-light">
<tr><th>狀態</th><th>計畫類型</th><th class="text-end">數量</th></tr>
</thead>
<tbody>
{% for a in action_plans_status %}
<tr>
<td>
{% if a.status == 'pending' %}<span class="badge bg-warning text-dark">{{ a.status }}</span>
{% elif a.status == 'approved' %}<span class="badge bg-success">{{ a.status }}</span>
{% elif a.status == 'executed' %}<span class="badge bg-primary">{{ a.status }}</span>
{% elif a.status == 'rejected' %}<span class="badge bg-danger">{{ a.status }}</span>
{% else %}<span class="badge bg-secondary">{{ a.status }}</span>{% endif %}
</td>
<td><code>{{ a.plan_type }}</code></td>
<td class="text-end">{{ a.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Phase 47 K-5: Action Outcomes verdict 分布 -->
{% if action_outcomes_stats %}
<div class="card mb-3" style="border-left: 4px solid #198754;">
<div class="card-header bg-light"><strong><i class="fas fa-trophy me-2"></i>Action Outcomes 成效(過去 {{ days }} 日)</strong>
<small class="text-muted">資料來源action_outcomesADR-012 閉環學習:實際動作有效嗎?)</small>
</div>
<div class="card-body">
<div class="row g-2">
{% set total_ao = (action_outcomes_stats | sum(attribute='count')) or 1 %}
{% for r in action_outcomes_stats %}
<div class="col-md-3 col-sm-6">
<div class="border rounded p-2 text-center"
style="border-left-width: 4px !important;
border-left-color: {% if r.verdict == 'effective' %}#198754
{% elif r.verdict == 'backfired' %}#dc3545
{% else %}#6c757d{% endif %} !important;">
<small class="text-muted d-block">
{% if r.verdict == 'effective' %}<i class="fas fa-check-circle text-success"></i> 有效
{% elif r.verdict == 'backfired' %}<i class="fas fa-times-circle text-danger"></i> 適得其反
{% elif r.verdict == 'neutral' %}<i class="fas fa-minus text-secondary"></i> 無顯著效果
{% else %}{{ r.verdict }}{% endif %}
</small>
<strong style="font-size: 1.4em;">{{ r.count }}</strong>
<small class="d-block text-muted">{{ "%.1f"|format(r.count / total_ao * 100) }}%</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<p class="text-muted mt-3"><small>
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 29 — Caller 反饋趨勢
<i class="fas fa-robot me-1"></i>Operation Ollama-First v5.0 / Phase 47 — Caller 反饋趨勢
6 表深挖rag_query_log / learning_episodes / ai_insights / action_plans / action_outcomes / agent_strategy_weights
</small></p>
</div>
{% endblock %}