Files
ewoooc/templates/admin/business_intel.html
OoO 90e8366a8d
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
feat(p54): chart.js 視覺微調 — KPI sparkline + verdict 圓餅 + heal 趨勢
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>
2026-05-05 01:13:31 +08:00

376 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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_outcomesADR-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 表跨 JOINai_price_recommendations / action_plans / action_outcomes /
competitor_price_history / competitor_match_attempts / competitor_prices
</small></p>
</div>
{% endblock %}