feat(p48): 商業面 × AI 編排新頁 — AI 在做什麼生意?實際生效嗎?
All checks were successful
CD Pipeline / deploy (push) Successful in 2m38s
All checks were successful
CD Pipeline / deploy (push) Successful in 2m38s
新頁 /observability/business_intel:把 AI 觀測台從「技術面」延伸到「商業面」。
回答統帥兩大問:
1. 我們的 AI 在做什麼生意?
2. AI 動作真的有用嗎?(閉環追蹤)
新接 5 張未善用的商業面表(DB 利用率 17/22 → 22/22,100%):
- ai_price_recommendations(AI 價格建議完整明細)
- competitor_prices(競品價格快照)
- competitor_price_history(24h 抓取歷史)
- competitor_match_attempts(競品比對失敗追蹤)
- 善用 action_plans × action_outcomes JOIN(閉環)
頁面 widget(7 張卡片):
1. unfollowed alert:high-confidence 但未轉化為 action_plan 的數量
2. AI 決策 by strategy(promote/watch/hold 含平均信心 + gap% + 銷量變化)
3. 最近 20 筆 AI 建議詳細(SKU/商品/MOMO 價/PChome 價/Gap/原因)
4. **閉環學習表**:plan → outcome 全鏈追蹤
含 verdict/before/after/變化 % — ADR-012 核心 KPI
5. Verdict 分布(effective/neutral/backfired 計數)
6. 競品比對嘗試統計(success/fail/avg_score)
7. 24h 競品價格抓取列表(SKU/商品/MOMO vs PChome gap)
入口:
- sidebar AI 觀測 group 加「商業面 × AI」(07c)
- /observability/overview 入口卡升級為 8 項
DB 全表覆蓋達成:22/22 = 100%
- Phase 47 17 表 → Phase 48 22 表
- 新接:ai_price_recommendations / competitor_prices /
competitor_price_history / competitor_match_attempts
- 已用:ai_calls / ai_call_budgets / ai_insights / learning_episodes /
rag_query_log / mcp_calls / incidents / heal_logs / playbooks /
backup_log / embedding_retry_queue / agent_context /
agent_strategy_weights / action_plans / action_outcomes /
host_health_probes / ppt_audit_results
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -247,6 +247,205 @@ def observability_overview():
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# /observability/business_intel — Phase 48 商業面 × AI 編排
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@admin_observability_bp.route('/business_intel')
|
||||
@login_required
|
||||
def business_intel_dashboard():
|
||||
"""Phase 48 — 商業面 × AI 編排:把 AI 觀測台延伸到商業層級。
|
||||
|
||||
展現「AI 在做什麼生意」:
|
||||
- ai_price_recommendations × competitor_prices: AI 看到什麼定價機會
|
||||
- action_plans × action_outcomes: 計畫到 verdict 的閉環
|
||||
- competitor_match_attempts: 競品比對失敗追蹤
|
||||
"""
|
||||
days = int(request.args.get('days', '7'))
|
||||
session = get_session()
|
||||
try:
|
||||
# 1. ai_price_recommendations 30d 總覽
|
||||
rec_summary = session.execute(
|
||||
sa_text(f"""
|
||||
SELECT strategy, COUNT(*) AS cnt,
|
||||
COALESCE(AVG(confidence), 0) AS avg_conf,
|
||||
COALESCE(AVG(gap_pct), 0) AS avg_gap_pct,
|
||||
COALESCE(AVG(sales_7d_delta), 0) AS avg_sales_delta
|
||||
FROM ai_price_recommendations
|
||||
WHERE created_at >= NOW() - INTERVAL '{int(days)} days'
|
||||
GROUP BY strategy ORDER BY cnt DESC
|
||||
"""),
|
||||
).fetchall()
|
||||
rec_by_strategy = [
|
||||
{
|
||||
'strategy': r[0], 'count': int(r[1] or 0),
|
||||
'avg_confidence': round(float(r[2] or 0), 3),
|
||||
'avg_gap_pct': round(float(r[3] or 0), 2),
|
||||
'avg_sales_delta': round(float(r[4] or 0), 2),
|
||||
}
|
||||
for r in rec_summary
|
||||
]
|
||||
|
||||
# 2. ai_price_recommendations 最近 20 筆詳細
|
||||
latest_recs = session.execute(
|
||||
sa_text("""
|
||||
SELECT id, sku, LEFT(name, 50), strategy, confidence,
|
||||
momo_price, pchome_price, gap_pct, sales_7d_delta,
|
||||
LEFT(reason, 120), created_at
|
||||
FROM ai_price_recommendations
|
||||
ORDER BY created_at DESC LIMIT 20
|
||||
"""),
|
||||
).fetchall()
|
||||
latest_recommendations = [
|
||||
{
|
||||
'id': r[0], 'sku': r[1], 'name': r[2], 'strategy': r[3],
|
||||
'confidence': round(float(r[4] or 0), 3),
|
||||
'momo_price': float(r[5] or 0),
|
||||
'pchome_price': float(r[6] or 0) if r[6] else None,
|
||||
'gap_pct': round(float(r[7] or 0), 2),
|
||||
'sales_delta': round(float(r[8] or 0), 2) if r[8] is not None else None,
|
||||
'reason': r[9] or '',
|
||||
'created_at': r[10].strftime('%m-%d %H:%M') if r[10] else '',
|
||||
}
|
||||
for r in latest_recs
|
||||
]
|
||||
|
||||
# 3. action_plans × action_outcomes 閉環(30d)
|
||||
closed_loops = session.execute(
|
||||
sa_text(f"""
|
||||
SELECT p.id, p.sku, p.plan_type, p.status,
|
||||
p.created_by, p.created_at, p.executed_at,
|
||||
o.verdict, o.metric_type, o.before_val, o.after_val
|
||||
FROM action_plans p
|
||||
LEFT JOIN action_outcomes o ON o.plan_id = p.id
|
||||
WHERE p.created_at >= NOW() - INTERVAL '{int(days)} days'
|
||||
ORDER BY p.created_at DESC LIMIT 25
|
||||
"""),
|
||||
).fetchall()
|
||||
loop_records = []
|
||||
for r in closed_loops:
|
||||
before = float(r[9]) if r[9] is not None else None
|
||||
after = float(r[10]) if r[10] is not None else None
|
||||
change_pct = None
|
||||
if before and before != 0 and after is not None:
|
||||
change_pct = (after - before) / abs(before) * 100
|
||||
loop_records.append({
|
||||
'plan_id': r[0], 'sku': r[1], 'plan_type': r[2],
|
||||
'status': r[3], 'created_by': r[4],
|
||||
'created_at': r[5].strftime('%m-%d %H:%M') if r[5] else '',
|
||||
'executed_at': r[6].strftime('%m-%d %H:%M') if r[6] else None,
|
||||
'verdict': r[7], 'metric_type': r[8],
|
||||
'before': before, 'after': after, 'change_pct': change_pct,
|
||||
})
|
||||
|
||||
# 4. action_outcomes verdict 統計
|
||||
verdict_summary = session.execute(
|
||||
sa_text(f"""
|
||||
SELECT verdict, COUNT(*) AS cnt,
|
||||
AVG(after_val - before_val) AS avg_delta
|
||||
FROM action_outcomes
|
||||
WHERE created_at >= NOW() - INTERVAL '{int(days)} days'
|
||||
AND before_val IS NOT NULL AND after_val IS NOT NULL
|
||||
GROUP BY verdict ORDER BY cnt DESC
|
||||
"""),
|
||||
).fetchall()
|
||||
verdict_stats = [
|
||||
{
|
||||
'verdict': r[0] or 'unknown', 'count': int(r[1] or 0),
|
||||
'avg_delta': round(float(r[2] or 0), 2),
|
||||
}
|
||||
for r in verdict_summary
|
||||
]
|
||||
|
||||
# 5. competitor_match_attempts 失敗統計(30d)
|
||||
match_attempts = session.execute(
|
||||
sa_text(f"""
|
||||
SELECT attempt_status, COUNT(*) AS cnt,
|
||||
COALESCE(AVG(candidate_count), 0) AS avg_candidates,
|
||||
COALESCE(AVG(best_match_score), 0) AS avg_score
|
||||
FROM competitor_match_attempts
|
||||
WHERE attempted_at >= NOW() - INTERVAL '{int(days)} days'
|
||||
GROUP BY attempt_status ORDER BY cnt DESC
|
||||
"""),
|
||||
).fetchall()
|
||||
match_stats = [
|
||||
{
|
||||
'status': r[0], 'count': int(r[1] or 0),
|
||||
'avg_candidates': round(float(r[2] or 0), 1),
|
||||
'avg_score': round(float(r[3] or 0), 3),
|
||||
}
|
||||
for r in match_attempts
|
||||
]
|
||||
|
||||
# 6. competitor_prices 24h 變動 TOP 10
|
||||
recent_competitor = session.execute(
|
||||
sa_text("""
|
||||
SELECT cph.sku, cph.competitor_product_name, cph.price,
|
||||
cph.momo_price, cph.discount_pct, cph.match_score,
|
||||
cph.crawled_at
|
||||
FROM competitor_price_history cph
|
||||
WHERE cph.crawled_at >= NOW() - INTERVAL '24 hours'
|
||||
AND cph.match_score >= 0.7
|
||||
ORDER BY cph.crawled_at DESC LIMIT 12
|
||||
"""),
|
||||
).fetchall()
|
||||
recent_competitor_prices = [
|
||||
{
|
||||
'sku': r[0],
|
||||
'product_name': (r[1] or '')[:50],
|
||||
'pchome_price': float(r[2] or 0),
|
||||
'momo_price': float(r[3] or 0) if r[3] else None,
|
||||
'discount_pct': int(r[4]) if r[4] else None,
|
||||
'match_score': round(float(r[5] or 0), 3),
|
||||
'gap': (float(r[3]) - float(r[2])) if (r[2] and r[3]) else None,
|
||||
'crawled_at': r[6].strftime('%m-%d %H:%M') if r[6] else '',
|
||||
}
|
||||
for r in recent_competitor
|
||||
]
|
||||
|
||||
# 7. 高 confidence 但未 follow-through (recommendation 沒對應 action_plan)
|
||||
unfollowed = session.execute(
|
||||
sa_text(f"""
|
||||
SELECT COUNT(*)
|
||||
FROM ai_price_recommendations r
|
||||
WHERE r.created_at >= NOW() - INTERVAL '{int(days)} days'
|
||||
AND r.confidence >= 0.7
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM action_plans p
|
||||
WHERE p.sku = r.sku
|
||||
AND p.created_at >= r.created_at
|
||||
AND p.created_at < r.created_at + INTERVAL '7 days'
|
||||
)
|
||||
"""),
|
||||
).fetchone()
|
||||
unfollowed_count = int(unfollowed[0] or 0) if unfollowed else 0
|
||||
|
||||
return render_template(
|
||||
'admin/business_intel.html',
|
||||
active_page='obs_business_intel',
|
||||
days=days,
|
||||
rec_by_strategy=rec_by_strategy,
|
||||
latest_recommendations=latest_recommendations,
|
||||
loop_records=loop_records,
|
||||
verdict_stats=verdict_stats,
|
||||
match_stats=match_stats,
|
||||
recent_competitor_prices=recent_competitor_prices,
|
||||
unfollowed_count=unfollowed_count,
|
||||
error=None,
|
||||
)
|
||||
except Exception as e:
|
||||
return render_template(
|
||||
'admin/business_intel.html',
|
||||
active_page='obs_business_intel', days=days,
|
||||
rec_by_strategy=[], latest_recommendations=[], loop_records=[],
|
||||
verdict_stats=[], match_stats=[], recent_competitor_prices=[],
|
||||
unfollowed_count=0,
|
||||
error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}',
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# /observability/agent_orchestration — Phase 46 編排矩陣
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
327
templates/admin/business_intel.html
Normal file
327
templates/admin/business_intel.html
Normal file
@@ -0,0 +1,327 @@
|
||||
{% extends "ewoooc_base.html" %}
|
||||
|
||||
{% block title %}商業面 × AI 編排{% endblock %}
|
||||
|
||||
{% block ewooo_content %}
|
||||
<div class="container-fluid mt-3">
|
||||
<h2 class="mb-3"><i class="fas fa-briefcase me-2"></i>商業面 × AI 編排
|
||||
<small class="text-muted">過去 {{ days }} 日 · AI 在做什麼生意?實際生效嗎?</small>
|
||||
</h2>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-warning"><strong><i class="fas fa-exclamation-triangle me-1"></i></strong> {{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="get" class="row g-2 mb-3">
|
||||
<div class="col-auto">
|
||||
<select name="days" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
{% for d in [7, 14, 30, 90] %}
|
||||
<option value="{{ d }}" {% if days == d %}selected{% endif %}>過去 {{ d }} 日</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if unfollowed_count > 0 %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
過去 {{ days }} 日有 <strong>{{ unfollowed_count }}</strong> 筆 high-confidence (≥0.7) AI 建議
|
||||
<strong>未轉化為 action_plan</strong> — 機會流失!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- AI 價格決策 by strategy -->
|
||||
{% if rec_by_strategy %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<strong><i class="fas fa-tag me-2"></i>AI 價格決策 by strategy({{ days }} 日)</strong>
|
||||
<small class="text-muted">資料來源:ai_price_recommendations</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">平均信心</th>
|
||||
<th class="text-end">平均 gap %</th>
|
||||
<th class="text-end">平均 7d 銷量變化 %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in rec_by_strategy %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if s.strategy == 'promote' %}<span class="badge bg-success">promote 推廣</span>
|
||||
{% elif s.strategy == 'watch' %}<span class="badge bg-warning text-dark">watch 觀察</span>
|
||||
{% elif s.strategy == 'hold' %}<span class="badge bg-secondary">hold 持平</span>
|
||||
{% else %}<span class="badge bg-info text-dark">{{ s.strategy }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="text-end"><strong>{{ s.count }}</strong></td>
|
||||
<td class="text-end">{{ "%.2f"|format(s.avg_confidence) }}</td>
|
||||
<td class="text-end">
|
||||
<span class="{% if s.avg_gap_pct > 5 %}text-danger{% elif s.avg_gap_pct < -5 %}text-success{% endif %}">
|
||||
{{ "%.1f"|format(s.avg_gap_pct) }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="{% if s.avg_sales_delta > 0 %}text-success{% elif s.avg_sales_delta < 0 %}text-danger{% endif %}">
|
||||
{{ "%+.1f"|format(s.avg_sales_delta) }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
過去 {{ days }} 日無 AI 價格決策資料(ai_price_recommendations 表)。
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 最近 20 筆 AI 建議 -->
|
||||
{% if latest_recommendations %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<strong><i class="fas fa-history me-2"></i>最近 20 筆 AI 價格建議</strong>
|
||||
<small class="text-muted">資料來源:ai_price_recommendations(含競品快照)</small>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 480px; 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>SKU</th><th>商品</th><th>策略</th>
|
||||
<th class="text-end">信心</th>
|
||||
<th class="text-end">MOMO 價</th>
|
||||
<th class="text-end">PChome 價</th>
|
||||
<th class="text-end">Gap</th>
|
||||
<th class="text-end">7d 銷量</th>
|
||||
<th>原因</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in latest_recommendations %}
|
||||
<tr>
|
||||
<td><small>{{ r.created_at }}</small></td>
|
||||
<td><code>{{ r.sku }}</code></td>
|
||||
<td><small>{{ r.name }}{% if r.name|length >= 50 %}…{% endif %}</small></td>
|
||||
<td>
|
||||
{% if r.strategy == 'promote' %}<span class="badge bg-success">推</span>
|
||||
{% elif r.strategy == 'watch' %}<span class="badge bg-warning text-dark">觀</span>
|
||||
{% elif r.strategy == 'hold' %}<span class="badge bg-secondary">持</span>
|
||||
{% else %}<span class="badge bg-info text-dark">{{ r.strategy }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong class="{% if r.confidence >= 0.8 %}text-success{% elif r.confidence >= 0.6 %}text-warning{% else %}text-muted{% endif %}">
|
||||
{{ "%.2f"|format(r.confidence) }}
|
||||
</strong>
|
||||
</td>
|
||||
<td class="text-end">${{ "%.0f"|format(r.momo_price) }}</td>
|
||||
<td class="text-end">
|
||||
{% if r.pchome_price %}${{ "%.0f"|format(r.pchome_price) }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="{% if r.gap_pct > 5 %}text-danger{% elif r.gap_pct < -5 %}text-success{% endif %}">
|
||||
{{ "%+.1f"|format(r.gap_pct) }}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{% if r.sales_delta is not none %}
|
||||
<span class="{% if r.sales_delta > 0 %}text-success{% elif r.sales_delta < 0 %}text-danger{% endif %}">
|
||||
{{ "%+.1f"|format(r.sales_delta) }}%
|
||||
</span>
|
||||
{% else %}<small class="text-muted">—</small>{% endif %}
|
||||
</td>
|
||||
<td><small class="text-muted">{{ r.reason }}{% if r.reason|length >= 120 %}…{% endif %}</small></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 閉環學習:plan × outcome -->
|
||||
{% if loop_records %}
|
||||
<div class="card mb-3" style="border-left: 4px solid #6f42c1;">
|
||||
<div class="card-header bg-light">
|
||||
<strong><i class="fas fa-sync-alt me-2"></i>閉環學習:plan → outcome 全鏈追蹤({{ days }} 日)</strong>
|
||||
<small class="text-muted">資料來源:action_plans LEFT JOIN action_outcomes(ADR-012 核心)</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>Plan</th><th>SKU</th><th>類型</th><th>狀態</th>
|
||||
<th>建立者</th><th>建立時間</th><th>執行時間</th>
|
||||
<th>Verdict</th><th>指標</th>
|
||||
<th class="text-end">Before</th><th class="text-end">After</th>
|
||||
<th class="text-end">變化</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in loop_records %}
|
||||
<tr>
|
||||
<td><code>#{{ l.plan_id }}</code></td>
|
||||
<td><small>{{ l.sku or '—' }}</small></td>
|
||||
<td><span class="badge bg-info text-dark">{{ l.plan_type or '—' }}</span></td>
|
||||
<td>
|
||||
{% if l.status == 'pending' %}<span class="badge bg-warning text-dark">待審</span>
|
||||
{% elif l.status == 'approved' %}<span class="badge bg-success">已批</span>
|
||||
{% elif l.status == 'executed' %}<span class="badge bg-primary">已執</span>
|
||||
{% elif l.status == 'rejected' %}<span class="badge bg-danger">拒絕</span>
|
||||
{% else %}<span class="badge bg-secondary">{{ l.status }}</span>{% endif %}
|
||||
</td>
|
||||
<td><small>{{ l.created_by or '—' }}</small></td>
|
||||
<td><small>{{ l.created_at }}</small></td>
|
||||
<td><small>{{ l.executed_at or '—' }}</small></td>
|
||||
<td>
|
||||
{% if l.verdict == 'effective' %}<span class="badge bg-success">有效</span>
|
||||
{% elif l.verdict == 'backfired' %}<span class="badge bg-danger">適得其反</span>
|
||||
{% elif l.verdict == 'neutral' %}<span class="badge bg-secondary">無變</span>
|
||||
{% else %}<small class="text-muted">—</small>{% endif %}
|
||||
</td>
|
||||
<td><small>{{ l.metric_type or '—' }}</small></td>
|
||||
<td class="text-end">{% if l.before is not none %}{{ "%.1f"|format(l.before) }}{% else %}—{% endif %}</td>
|
||||
<td class="text-end">{% if l.after is not none %}{{ "%.1f"|format(l.after) }}{% else %}—{% endif %}</td>
|
||||
<td class="text-end">
|
||||
{% if l.change_pct is not none %}
|
||||
<strong class="{% if l.change_pct > 0 %}text-success{% elif l.change_pct < 0 %}text-danger{% endif %}">
|
||||
{{ "%+.1f"|format(l.change_pct) }}%
|
||||
</strong>
|
||||
{% else %}<small class="text-muted">—</small>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Verdict 統計 -->
|
||||
{% if verdict_stats %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<strong><i class="fas fa-trophy me-2"></i>Outcomes Verdict 分布</strong>
|
||||
<small class="text-muted">{{ days }} 日 · AI 動作的實際成效閉環</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
{% set total_v = (verdict_stats | sum(attribute='count')) or 1 %}
|
||||
{% for v in verdict_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 v.verdict == 'effective' %}#198754
|
||||
{% elif v.verdict == 'backfired' %}#dc3545
|
||||
{% else %}#6c757d{% endif %} !important;">
|
||||
<small class="text-muted d-block">
|
||||
{% if v.verdict == 'effective' %}<i class="fas fa-check-circle text-success"></i> 有效
|
||||
{% elif v.verdict == 'backfired' %}<i class="fas fa-times-circle text-danger"></i> 適得其反
|
||||
{% elif v.verdict == 'neutral' %}<i class="fas fa-minus text-secondary"></i> 無變化
|
||||
{% else %}{{ v.verdict }}{% endif %}
|
||||
</small>
|
||||
<strong style="font-size: 1.4em;">{{ v.count }}</strong>
|
||||
<small class="d-block text-muted">{{ "%.0f"|format(v.count / total_v * 100) }}% · Δ {{ "%+.1f"|format(v.avg_delta) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 競品比對失敗統計 -->
|
||||
{% if match_stats %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<strong><i class="fas fa-search-minus me-2"></i>競品比對嘗試({{ days }} 日)</strong>
|
||||
<small class="text-muted">資料來源:competitor_match_attempts — AI 找不到對應商品時的失敗追蹤</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">平均候選數</th>
|
||||
<th class="text-end">平均匹配分</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in match_stats %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if 'success' in (m.status or '').lower() %}<span class="badge bg-success">{{ m.status }}</span>
|
||||
{% elif 'fail' in (m.status or '').lower() or 'no_' in (m.status or '').lower() %}<span class="badge bg-danger">{{ m.status }}</span>
|
||||
{% else %}<span class="badge bg-warning text-dark">{{ m.status }}</span>{% endif %}
|
||||
</td>
|
||||
<td class="text-end">{{ "{:,}".format(m.count) }}</td>
|
||||
<td class="text-end">{{ m.avg_candidates }}</td>
|
||||
<td class="text-end">{{ "%.2f"|format(m.avg_score) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 競品 24h 變動 -->
|
||||
{% if recent_competitor_prices %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<strong><i class="fas fa-balance-scale me-2"></i>過去 24h 競品價格抓取(match_score ≥ 0.7)</strong>
|
||||
<small class="text-muted">資料來源:competitor_price_history — AI 看到的競品價格全景</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>SKU</th><th>競品商品</th>
|
||||
<th class="text-end">PChome</th><th class="text-end">MOMO</th>
|
||||
<th class="text-end">Gap</th><th class="text-end">折扣</th>
|
||||
<th class="text-end">匹配</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in recent_competitor_prices %}
|
||||
<tr>
|
||||
<td><small>{{ c.crawled_at }}</small></td>
|
||||
<td><code>{{ c.sku }}</code></td>
|
||||
<td><small>{{ c.product_name }}</small></td>
|
||||
<td class="text-end">${{ "%.0f"|format(c.pchome_price) }}</td>
|
||||
<td class="text-end">
|
||||
{% if c.momo_price %}${{ "%.0f"|format(c.momo_price) }}{% else %}<small class="text-muted">—</small>{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{% if c.gap is not none %}
|
||||
<span class="{% if c.gap > 0 %}text-danger{% elif c.gap < 0 %}text-success{% endif %}">
|
||||
{{ "%+.0f"|format(c.gap) }}
|
||||
</span>
|
||||
{% else %}<small class="text-muted">—</small>{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{% if c.discount_pct %}<span class="badge bg-warning text-dark">-{{ c.discount_pct }}%</span>
|
||||
{% else %}<small class="text-muted">—</small>{% endif %}
|
||||
</td>
|
||||
<td class="text-end">{{ "%.2f"|format(c.match_score) }}</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 48 — 商業面 × AI 編排
|
||||
(6 表跨 JOIN:ai_price_recommendations / action_plans / action_outcomes /
|
||||
competitor_price_history / competitor_match_attempts / competitor_prices)
|
||||
</small></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -219,9 +219,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 7 大入口 -->
|
||||
<!-- 8 大入口 -->
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="fas fa-th me-1"></i>7 大子頁入口</strong></div>
|
||||
<div class="card-header"><strong><i class="fas fa-th me-1"></i>8 大子頁入口</strong></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
@@ -230,6 +230,12 @@
|
||||
<small class="d-block text-muted ms-4">4 Agent × Ollama × Gemini × MCP × RAG 全景 + 自動建議</small>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="/observability/business_intel" class="btn btn-outline-warning w-100 text-start" style="border-width: 2px;">
|
||||
<i class="fas fa-briefcase me-2"></i><strong>商業面 × AI 編排</strong>
|
||||
<small class="d-block text-muted ms-4">AI 價格決策 + 閉環學習 + 競品比對全景</small>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="/observability/host_health" class="btn btn-outline-primary w-100 text-start">
|
||||
<i class="fas fa-heartbeat me-2"></i>主機健康監控
|
||||
|
||||
@@ -86,6 +86,11 @@
|
||||
<span class="momo-nav-label">Agent 編排矩陣</span>
|
||||
<span class="momo-nav-code momo-mono">07b</span>
|
||||
</a>
|
||||
<a class="momo-nav-link {% if _active_page == 'obs_business_intel' %}is-active{% endif %}" href="/observability/business_intel">
|
||||
<span class="momo-nav-icon"><i class="fas fa-briefcase"></i></span>
|
||||
<span class="momo-nav-label">商業面 × AI</span>
|
||||
<span class="momo-nav-code momo-mono">07c</span>
|
||||
</a>
|
||||
<a class="momo-nav-link {% if _active_page == 'obs_host_health' %}is-active{% endif %}" href="/observability/host_health">
|
||||
<span class="momo-nav-icon"><i class="fas fa-heartbeat"></i></span>
|
||||
<span class="momo-nav-label">主機健康</span>
|
||||
|
||||
Reference in New Issue
Block a user