feat(p47): 6 頁深挖資料庫 — 從 5 表 → 17 表,每頁加 3-5 個 widget
All checks were successful
CD Pipeline / deploy (push) Successful in 2m36s
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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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_context(Hermes/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>
|
||||
|
||||
@@ -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_usd(caller 級彙總)</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>
|
||||
|
||||
|
||||
@@ -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">資料來源:incidents(ADR-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">資料來源:playbooks(success/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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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_weights(ADR-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>
|
||||
|
||||
|
||||
@@ -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(含全 caller,1-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_plans(NemoTron/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_outcomes(ADR-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 %}
|
||||
|
||||
Reference in New Issue
Block a user