Some checks failed
CD Pipeline / deploy (push) Has been cancelled
R-1: ai_calls KPI 卡片加 24h sparkline
- 呼叫次數卡片下加 24px 高 mini line chart(藍)
- 成本卡片下加 sparkline(黃)
- 錯誤次數卡片下加 sparkline(紅)
- Token / 平均耗時 / RAG 命中卡片改顯示「平均 tk/call」「cache 命中數」「RAG 命中率%」
- 整排 KPI 從乾巴巴數字 → 含 24h 趨勢視覺
- 共用 chart.js dataset,無新 query
R-2: business_intel verdict 改 doughnut + 表格雙視角
- 取代原 col-md-3 卡片網格
- 左圓餅:effective(綠)/backfired(紅)/neutral(灰) 視覺比例
- 右表格:4 欄(verdict/筆數/佔比/平均 Δ)含正負色
- 與 quality_trend RAG pie chart 視覺風格統一
R-3: host_health AIOps card 加 7d 自癒成功率 sparkline
- routes/admin_observability_routes.py 新加 heal_daily query
date_trunc('day') GROUP BY 7 天每日 success rate
- AIOps 7d card 底部加 80px 高 line chart
- Y 軸 0-100% / X 軸 7 天日期
- tooltip 顯示「ok/total 成功 (rate%)」
chart.js 視覺化從 4 個 → 7 個:
hourly trend / 30d stacked / 三主機 sparkline / RAG doughnut /
KPI sparkline × 3 / verdict doughnut / heal trend
Phase 38→54 累計 19 commits / 10 觀測頁 + topbar indicator / 7 chart.js。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
376 lines
16 KiB
HTML
376 lines
16 KiB
HTML
{% 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 統計(Phase 54 R-2: doughnut + 表格雙視角) -->
|
||
{% 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 align-items-center">
|
||
<div class="col-md-5">
|
||
<canvas id="verdictPieChart" height="180"></canvas>
|
||
</div>
|
||
<div class="col-md-7">
|
||
<table class="table table-sm mb-0" style="font-size: 0.9em;">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Verdict</th>
|
||
<th class="text-end">筆數</th>
|
||
<th class="text-end">佔比</th>
|
||
<th class="text-end">平均 Δ</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% set total_v = (verdict_stats | sum(attribute='count')) or 1 %}
|
||
{% for v in verdict_stats %}
|
||
<tr>
|
||
<td>
|
||
{% if v.verdict == 'effective' %}<i class="fas fa-check-circle text-success me-1"></i>有效
|
||
{% elif v.verdict == 'backfired' %}<i class="fas fa-times-circle text-danger me-1"></i>適得其反
|
||
{% elif v.verdict == 'neutral' %}<i class="fas fa-minus text-secondary me-1"></i>無變化
|
||
{% else %}{{ v.verdict }}{% endif %}
|
||
</td>
|
||
<td class="text-end"><strong>{{ v.count }}</strong></td>
|
||
<td class="text-end">{{ "%.0f"|format(v.count / total_v * 100) }}%</td>
|
||
<td class="text-end">
|
||
<span class="{% if v.avg_delta > 0 %}text-success{% elif v.avg_delta < 0 %}text-danger{% endif %}">
|
||
{{ "%+.1f"|format(v.avg_delta) }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||
<script>
|
||
(function() {
|
||
const data = {{ verdict_stats | tojson }};
|
||
const el = document.getElementById('verdictPieChart');
|
||
if (!el || !data.length) return;
|
||
const colorMap = { effective: '#198754', backfired: '#dc3545', neutral: '#6c757d' };
|
||
const labelMap = { effective: '有效', backfired: '適得其反', neutral: '無變化' };
|
||
new Chart(el, {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: data.map(d => labelMap[d.verdict] || d.verdict),
|
||
datasets: [{
|
||
data: data.map(d => d.count),
|
||
backgroundColor: data.map(d => colorMap[d.verdict] || '#adb5bd'),
|
||
borderWidth: 1, borderColor: '#fff',
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { position: 'right', labels: { font: { size: 12 } } },
|
||
tooltip: { callbacks: { label: c => {
|
||
const total = data.reduce((a,b)=>a+b.count, 0);
|
||
return `${c.label}: ${c.parsed} 筆 (${(c.parsed/total*100).toFixed(1)}%)`;
|
||
}}}
|
||
}
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
{% 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 %}
|