Files
ewoooc/templates/admin/business_intel.html
ogt 2888bac597
All checks were successful
CD Pipeline / deploy (push) Successful in 1m9s
fix: align pchome growth comparison and promo watch
2026-06-26 11:53:14 +08:00

847 lines
28 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" %}
{% set active_page = 'obs_business_intel' %}
{% block title %}商業 AI 戰果室{% endblock %}
{% block extra_css %}
<style>
.biz-warroom {
--biz-ink: #2f211b;
--biz-muted: #8a6b5c;
--biz-card: rgba(255, 252, 247, 0.95);
--biz-line: rgba(201, 100, 66, 0.16);
--biz-alert: #b8442f;
--biz-good: #2f8f6b;
--biz-warn: #c98a2e;
position: relative;
overflow: hidden;
}
.biz-warroom::before {
content: "";
position: absolute;
inset: 0 0 auto auto;
width: min(560px, 58vw);
height: min(560px, 58vw);
background: radial-gradient(circle, rgba(201, 100, 66, 0.16), transparent 62%);
pointer-events: none;
transform: translate(26%, -34%);
}
.biz-hero {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.55fr);
gap: 1.15rem;
margin-bottom: 1.25rem;
}
.biz-command {
background:
linear-gradient(135deg, rgba(53, 35, 27, 0.96), rgba(126, 64, 42, 0.92)),
radial-gradient(circle at top right, rgba(255, 194, 115, 0.22), transparent 42%);
color: #fff8ee;
border-radius: 28px;
padding: clamp(1.35rem, 2.5vw, 2.25rem);
box-shadow: 0 28px 60px rgba(78, 45, 31, 0.24);
min-height: 320px;
}
.biz-kicker {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .35rem .65rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
color: #ffd7a2;
font-size: .82rem;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
.biz-command h1 {
font-size: clamp(2.05rem, 4vw, 3.25rem);
line-height: 1.05;
letter-spacing: 0;
margin: 1rem 0 .85rem;
font-weight: 900;
}
.biz-command p {
max-width: 760px;
color: rgba(255, 248, 238, 0.78);
font-size: 1.02rem;
line-height: 1.75;
}
.biz-meta-row {
display: flex;
flex-wrap: wrap;
gap: .7rem;
margin-top: 1.4rem;
}
.biz-meta-pill {
display: inline-flex;
align-items: center;
gap: .45rem;
padding: .65rem .85rem;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 248, 238, 0.9);
font-weight: 800;
}
.biz-filter-card {
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 28px;
padding: 1.2rem;
box-shadow: 0 18px 46px rgba(93, 57, 42, 0.12);
align-self: stretch;
}
.biz-filter-card label {
font-size: .78rem;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--biz-muted);
font-weight: 900;
}
.biz-filter-card .form-control {
border-radius: 16px;
border-color: rgba(201, 100, 66, 0.18);
min-height: 48px;
}
.biz-signal-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .9rem;
margin-bottom: 1.1rem;
}
.biz-signal {
position: relative;
overflow: hidden;
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 24px;
padding: 1.05rem;
box-shadow: 0 14px 34px rgba(93, 57, 42, 0.08);
}
.biz-signal::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 4px;
background: linear-gradient(90deg, #c96442, #f1b45a, #2f8f6b);
opacity: .82;
}
.biz-signal .label {
color: var(--biz-muted);
font-size: .8rem;
font-weight: 900;
letter-spacing: .04em;
}
.biz-signal .value {
color: var(--biz-ink);
font-size: clamp(1.7rem, 3vw, 2.45rem);
font-weight: 950;
letter-spacing: 0;
margin: .3rem 0 .05rem;
}
.biz-signal .note {
color: var(--biz-muted);
font-size: .86rem;
}
.biz-alert-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: .9rem;
padding: 1rem 1.15rem;
border-radius: 22px;
border: 1px solid rgba(184, 68, 47, 0.2);
background: linear-gradient(135deg, rgba(184, 68, 47, 0.12), rgba(255, 248, 238, 0.92));
color: var(--biz-alert);
margin-bottom: 1.1rem;
font-weight: 850;
}
.biz-promo-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: .75rem;
}
.biz-solution-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .6rem;
margin-bottom: .85rem;
}
.biz-solution-card {
min-height: 92px;
padding: .72rem;
border: 1px solid rgba(42, 37, 32, 0.09);
border-radius: 16px;
background: rgba(250, 247, 240, 0.68);
}
.biz-solution-card strong {
display: block;
color: var(--biz-ink);
font-size: .82rem;
font-weight: 950;
}
.biz-solution-card span {
display: block;
margin-top: .35rem;
color: var(--biz-muted);
font-size: .72rem;
font-weight: 780;
line-height: 1.35;
}
.biz-promo-card {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(150px, .42fr);
gap: .8rem;
align-items: start;
padding: .95rem;
border: 1px solid rgba(201, 100, 66, 0.16);
border-radius: 18px;
background: rgba(255, 250, 243, 0.9);
}
.biz-promo-source {
display: flex;
flex-wrap: wrap;
gap: .45rem;
align-items: center;
margin-bottom: .45rem;
}
.biz-promo-title {
color: var(--biz-ink);
font-size: .95rem;
font-weight: 950;
line-height: 1.35;
}
.biz-promo-solution {
margin-top: .55rem;
color: var(--biz-muted);
font-size: .82rem;
font-weight: 780;
line-height: 1.5;
}
.biz-promo-metrics {
display: grid;
gap: .45rem;
}
.biz-promo-metric {
padding: .55rem;
border-radius: 14px;
background: rgba(201, 100, 66, 0.07);
}
.biz-promo-metric strong {
display: block;
color: var(--biz-ink);
font-family: var(--momo-font-mono, monospace);
font-size: .92rem;
}
.biz-promo-metric span {
color: var(--biz-muted);
font-size: .68rem;
font-weight: 900;
}
.biz-layout {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(340px, .9fr);
gap: 1rem;
align-items: start;
}
.biz-panel {
background: var(--biz-card);
border: 1px solid var(--biz-line);
border-radius: 26px;
box-shadow: 0 14px 38px rgba(93, 57, 42, 0.08);
overflow: hidden;
margin-bottom: 1rem;
}
.biz-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.05rem 1.15rem .75rem;
border-bottom: 1px solid rgba(201, 100, 66, 0.1);
}
.biz-panel-head h3 {
margin: 0;
color: var(--biz-ink);
font-weight: 950;
letter-spacing: 0;
}
.biz-panel-head p {
margin: .25rem 0 0;
color: var(--biz-muted);
font-size: .88rem;
}
.biz-panel-body {
padding: 1rem 1.15rem 1.15rem;
}
.biz-strategy-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: .75rem;
}
.biz-strategy-card {
border: 1px solid rgba(201, 100, 66, 0.14);
border-radius: 20px;
padding: .95rem;
background: linear-gradient(135deg, #fffaf3, #fffdf9);
}
.biz-strategy-card .strategy {
color: var(--biz-ink);
font-weight: 950;
font-size: 1.02rem;
}
.biz-strategy-card .metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: .45rem;
margin-top: .75rem;
}
.biz-mini-metric {
padding: .55rem;
border-radius: 14px;
background: rgba(201, 100, 66, 0.07);
}
.biz-mini-metric b {
display: block;
color: var(--biz-ink);
font-size: 1rem;
}
.biz-mini-metric span {
color: var(--biz-muted);
font-size: .72rem;
font-weight: 800;
}
.biz-table {
width: 100%;
border-collapse: separate;
border-spacing: 0 .55rem;
}
.biz-table th {
color: var(--biz-muted);
font-size: .76rem;
letter-spacing: .04em;
text-transform: uppercase;
border: 0;
padding: 0 .7rem .15rem;
}
.biz-table td {
border: 0;
padding: .78rem .7rem;
background: rgba(255, 250, 243, 0.82);
vertical-align: middle;
}
.biz-table tr td:first-child {
border-radius: 16px 0 0 16px;
}
.biz-table tr td:last-child {
border-radius: 0 16px 16px 0;
}
.biz-decision-list {
display: grid;
gap: .75rem;
}
.biz-decision-card {
display: grid;
grid-template-columns: minmax(82px, .36fr) minmax(240px, 1.15fr) minmax(130px, .46fr);
gap: .8rem;
align-items: center;
padding: .95rem;
border: 1px solid rgba(201, 100, 66, 0.14);
border-radius: 18px;
background:
linear-gradient(135deg, rgba(255, 252, 247, 0.96), rgba(255, 248, 241, 0.86)),
radial-gradient(circle, rgba(201, 100, 66, 0.12) 1px, transparent 1.2px);
background-size: auto, 13px 13px;
}
.biz-decision-time {
color: var(--biz-muted);
font-size: .82rem;
font-weight: 850;
}
.biz-decision-sku,
.biz-decision-name {
color: var(--biz-ink);
font-weight: 900;
}
.biz-decision-name {
line-height: 1.45;
}
.biz-decision-reason {
grid-column: 2 / 4;
color: var(--biz-muted);
font-size: .82rem;
line-height: 1.5;
padding-top: .55rem;
border-top: 1px dashed rgba(201, 100, 66, 0.18);
}
.biz-price-stack {
color: var(--biz-ink);
font-weight: 850;
}
.biz-price-stack small {
display: block;
color: var(--biz-muted);
font-weight: 700;
}
.biz-badge {
display: inline-flex;
align-items: center;
padding: .28rem .55rem;
border-radius: 999px;
font-size: .76rem;
font-weight: 900;
background: rgba(201, 100, 66, 0.11);
color: #8e3d28;
}
.biz-badge.good {
background: rgba(47, 143, 107, 0.12);
color: var(--biz-good);
}
.biz-badge.warn {
background: rgba(201, 138, 46, 0.14);
color: var(--biz-warn);
}
.biz-chart-shell {
height: 250px;
padding: .4rem;
}
.biz-empty {
padding: 1rem;
border-radius: 18px;
background: rgba(201, 100, 66, 0.06);
color: var(--biz-muted);
font-weight: 800;
}
@media (max-width: 1180px) {
.biz-hero,
.biz-layout {
grid-template-columns: 1fr;
}
.biz-signal-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.biz-command h1 {
font-size: 2.45rem;
}
.biz-signal-grid,
.biz-strategy-grid,
.biz-solution-grid,
.biz-decision-card,
.biz-promo-grid,
.biz-promo-card {
grid-template-columns: 1fr;
}
.biz-decision-reason {
grid-column: auto;
}
.biz-alert-strip {
align-items: flex-start;
flex-direction: column;
}
}
</style>
{% endblock %}
{% block ewooo_content %}
{% import "admin/_observability_labels.html" as obs_label %}
{% set rec_total = rec_by_strategy|sum(attribute='count') if rec_by_strategy else 0 %}
{% set ns = namespace(conf_total=0, effective=0, backfired=0, neutral=0, verdict_total=0) %}
{% for r in rec_by_strategy %}
{% set ns.conf_total = ns.conf_total + ((r.avg_confidence or 0) * (r.count or 0)) %}
{% endfor %}
{% for v in verdict_stats %}
{% set label = v.verdict or '未分類' %}
{% set ns.verdict_total = ns.verdict_total + (v.count or 0) %}
{% if label == 'effective' or label == 'success' or label == 'positive' %}
{% set ns.effective = ns.effective + (v.count or 0) %}
{% elif label == 'backfired' or label == 'negative' or label == 'failed' %}
{% set ns.backfired = ns.backfired + (v.count or 0) %}
{% else %}
{% set ns.neutral = ns.neutral + (v.count or 0) %}
{% endif %}
{% endfor %}
{% set avg_conf = (ns.conf_total / rec_total) if rec_total else 0 %}
{% set effective_rate = ((ns.effective / ns.verdict_total) * 100) if ns.verdict_total else 0 %}
<div class="biz-warroom">
<div class="biz-hero">
<section class="biz-command">
<span class="biz-kicker"><i class="fas fa-store"></i> 商業情報</span>
<h1 class="biz-title">商業 AI 戰果室</h1>
<p>先看價格建議是否被採用,再追蹤閉環結果與競品訊號。</p>
<div class="biz-meta-row">
<span class="biz-meta-pill"><i class="fas fa-calendar-day"></i> 近 {{ days }} 天</span>
<span class="biz-meta-pill"><i class="fas fa-brain"></i> {{ rec_total }} 筆價格建議</span>
<span class="biz-meta-pill"><i class="fas fa-rotate"></i> {{ ns.verdict_total }} 筆閉環結果</span>
</div>
</section>
<aside class="biz-filter-card">
<form method="get">
<label for="days">觀測窗口</label>
<div class="d-flex gap-2 mt-2">
<input class="form-control" id="days" name="days" type="number" min="1" max="90" value="{{ days }}">
<button class="btn btn-primary" type="submit">更新</button>
</div>
</form>
<div class="mt-4">
<div class="text-muted small fw-bold text-uppercase">決策節奏</div>
<div class="h4 fw-black mb-1">先警示,再追因</div>
<p class="text-muted mb-0">高信心未跟進先處理,再看策略與競品訊號。</p>
</div>
</aside>
</div>
{% if error %}
<div class="alert alert-danger"><i class="fas fa-triangle-exclamation me-2"></i>{{ error }}</div>
{% endif %}
<section class="biz-signal-grid">
<article class="biz-signal">
<div class="label">高信心未跟進</div>
<div class="value">{{ unfollowed_count }}</div>
<div class="note">信心分 >= 0.8 且仍未轉行動計畫</div>
</article>
<article class="biz-signal">
<div class="label">平均信心分</div>
<div class="value">{{ '%.0f'|format(avg_conf * 100) }}%</div>
<div class="note">依策略建議量加權</div>
</article>
<article class="biz-signal">
<div class="label">有效率</div>
<div class="value">{{ '%.0f'|format(effective_rate) }}%</div>
<div class="note">有效 / 已回收結論</div>
</article>
<article class="biz-signal">
<div class="label">競品監測</div>
<div class="value">{{ recent_competitor_prices|length }}</div>
<div class="note">近 24h 價格變動樣本</div>
</article>
</section>
{% if unfollowed_count > 0 %}
<section class="biz-alert-strip">
<div><i class="fas fa-bell me-2"></i>{{ unfollowed_count }} 筆高信心 AI 價格建議尚未跟進,建議優先轉為行動計畫或標記原因。</div>
<span class="biz-badge warn">需人工決策</span>
</section>
{% endif %}
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>外部促銷活動監控</h3>
<p>AI Agent 以外部價格與折扣訊號監控促銷壓力,先對照 PChome 現況,再提出業績提升解法。</p>
</div>
<span class="biz-badge {% if promo_watch_rows %}warn{% else %}good{% endif %}">{{ promo_watch_rows|length }} 筆訊號</span>
</div>
<div class="biz-panel-body">
<div class="biz-solution-grid" aria-label="PChome 業績提升解法矩陣">
<div class="biz-solution-card"><strong>守價</strong><span>確認售價、折扣券與毛利底線,避免被外部促銷壓住。</span></div>
<div class="biz-solution-card"><strong>組合</strong><span>用組合包、贈品或加價購拉高感知價值,不只跟價。</span></div>
<div class="biz-solution-card"><strong>曝光</strong><span>把 PChome 有利商品推到搜尋、首頁、EDM 或活動入口。</span></div>
<div class="biz-solution-card"><strong>會員</strong><span>用會員回饋、免運或售後服務對抗短期低價。</span></div>
</div>
{% if promo_watch_rows %}
<div class="biz-promo-grid">
{% for r in promo_watch_rows %}
<article class="biz-promo-card">
<div>
<div class="biz-promo-source">
<span class="biz-badge warn">{{ r.pressure_label }}</span>
<span class="biz-badge">{{ r.source or '外部電商' }}</span>
<span class="text-muted small">{{ r.crawled_at or '-' }}</span>
</div>
<div class="biz-promo-title">{{ r.product_name or r.sku }}</div>
<div class="biz-promo-solution">PChome 解法:{{ r.recommended_action }}</div>
</div>
<div class="biz-promo-metrics">
<div class="biz-promo-metric"><strong>{{ '%.0f'|format(r.discount_pct or 0) }}%</strong><span>外部折扣</span></div>
<div class="biz-promo-metric"><strong>{{ '%.0f'|format(r.gap_abs or 0) }}</strong><span>價差壓力</span></div>
<div class="biz-promo-metric"><strong>{{ '%.2f'|format(r.match_score or 0) }}</strong><span>同款可信度</span></div>
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="biz-empty">目前尚未捕捉到可判讀的外部促銷壓力AI Agent 會持續觀察外部價格、折扣與同款可信度,捕捉到訊號後先進這裡,再進作戰清單。</div>
{% endif %}
</div>
</section>
<div class="biz-layout">
<main>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>策略族群雷達</h3>
<p>把 AI 價格建議依策略類型聚合,快速判斷目前主攻降價、防守或毛利修復。</p>
</div>
<span class="biz-badge">{{ rec_by_strategy|length }} 類策略</span>
</div>
<div class="biz-panel-body">
{% if rec_by_strategy %}
<div class="biz-strategy-grid">
{% for r in rec_by_strategy %}
<article class="biz-strategy-card">
<div class="strategy">{{ obs_label.strategy(r.strategy) }}</div>
<div class="metrics">
<div class="biz-mini-metric"><b>{{ r.count }}</b><span>建議數</span></div>
<div class="biz-mini-metric"><b>{{ '%.0f'|format((r.avg_confidence or 0) * 100) }}%</b><span>信心</span></div>
<div class="biz-mini-metric"><b>{{ '%.1f'|format(r.avg_gap_pct or 0) }}%</b><span>價差</span></div>
</div>
<div class="mt-2 text-muted small">平均銷量變化 {{ '%.1f'|format(r.avg_sales_delta or 0) }}</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="biz-empty">目前觀測窗口沒有策略建議。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>最近 AI 價格建議</h3>
<p>保留決策原因與競品價差,方便直接追到 SKU 層級。</p>
</div>
<span class="biz-badge">Latest 20</span>
</div>
<div class="biz-panel-body">
{% if latest_recommendations %}
<div class="biz-decision-list">
{% for r in latest_recommendations %}
<article class="biz-decision-card">
<div>
<div class="biz-decision-time">{{ r.created_at or '-' }}</div>
<div class="biz-decision-sku">{{ r.sku }}</div>
</div>
<div>
<div class="biz-decision-name">{{ r.name or '-' }}</div>
<div class="mt-1">
<span class="biz-badge">{{ obs_label.strategy(r.strategy, '-') }}</span>
<span class="biz-badge {% if (r.confidence or 0) >= 0.8 %}good{% endif %}">{{ '%.0f'|format((r.confidence or 0) * 100) }}%</span>
</div>
</div>
<div class="biz-price-stack">
{{ r.momo_price or '-' }} / {{ r.pchome_price or '-' }}
<small>差距 {{ '%.1f'|format(r.gap_pct or 0) }}%</small>
</div>
<div class="biz-decision-reason">{{ r.reason or '尚無原因摘要' }}</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="biz-empty">目前沒有最新價格建議。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>閉環學習紀錄</h3>
<p>追蹤行動計畫到實際結果的真實效果,這是 AI 能不能變聰明的核心證據。</p>
</div>
<span class="biz-badge good">{{ loop_records|length }} 筆紀錄</span>
</div>
<div class="biz-panel-body table-responsive">
{% if loop_records %}
<table class="biz-table">
<thead>
<tr>
<th>計畫</th>
<th>SKU</th>
<th>狀態</th>
<th>建立 / 執行</th>
<th>結論</th>
<th>指標</th>
<th>變化</th>
</tr>
</thead>
<tbody>
{% for r in loop_records %}
<tr>
<td>#{{ r.plan_id }}<br><span class="text-muted small">{{ obs_label.plan_type(r.plan_type, '-') }}</span></td>
<td><strong>{{ r.sku }}</strong></td>
<td><span class="biz-badge {% if r.status == 'done' or r.status == 'completed' %}good{% elif r.status == 'pending' %}warn{% endif %}">{{ obs_label.status(r.status, '-') }}</span></td>
<td>{{ r.created_at or '-' }} / {{ r.executed_at or '-' }}</td>
<td>{{ obs_label.verdict(r.verdict, '-') }}</td>
<td>{{ obs_label.metric(r.metric_type, '-') }}<br><span class="text-muted small">{{ r.before or '-' }} → {{ r.after or '-' }}</span></td>
<td><strong>{{ '%.1f'|format(r.change_pct or 0) }}%</strong></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚未形成行動計畫到實際結果的閉環紀錄。</div>
{% endif %}
</div>
</section>
</main>
<aside>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>結論戰果分布</h3>
<p>用結果反校正 AI 建議,不讓漂亮信心分掩蓋真實成效。</p>
</div>
</div>
<div class="biz-panel-body">
<div class="biz-chart-shell"><canvas id="verdictPieChart"></canvas></div>
{% if verdict_stats %}
<table class="biz-table mt-2">
<thead><tr><th>結論</th><th>數量</th><th>平均變化</th></tr></thead>
<tbody>
{% for v in verdict_stats %}
<tr><td>{{ obs_label.verdict(v.verdict) }}</td><td>{{ v.count }}</td><td>{{ '%.1f'|format(v.avg_delta or 0) }}%</td></tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚無結論統計。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>競品比對品質</h3>
<p>先看比對是否可靠,再拿價格差做策略判斷。</p>
</div>
</div>
<div class="biz-panel-body table-responsive">
{% if match_stats %}
<table class="biz-table">
<thead><tr><th>狀態</th><th>數量</th><th>候選數</th><th>分數</th></tr></thead>
<tbody>
{% for m in match_stats %}
<tr>
<td><span class="biz-badge {% if m.status == 'matched' or m.status == 'success' %}good{% elif m.status == 'failed' %}warn{% endif %}">{{ obs_label.status(m.status, '-') }}</span></td>
<td>{{ m.count }}</td>
<td>{{ '%.1f'|format(m.avg_candidates or 0) }}</td>
<td>{{ '%.2f'|format(m.avg_score or 0) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">尚無競品比對統計。</div>
{% endif %}
</div>
</section>
<section class="biz-panel">
<div class="biz-panel-head">
<div>
<h3>競品 24h 價格脈動</h3>
<p>保留最新市場訊號,支援人工快速複核。</p>
</div>
</div>
<div class="biz-panel-body table-responsive">
{% if recent_competitor_prices %}
<table class="biz-table">
<thead><tr><th>時間</th><th>SKU</th><th>PChome / momo</th><th>折扣</th></tr></thead>
<tbody>
{% for r in recent_competitor_prices %}
<tr>
<td>{{ r.crawled_at or '-' }}</td>
<td><strong>{{ r.sku }}</strong><br><span class="text-muted small">{{ r.product_name or '-' }}</span></td>
<td>{{ r.pchome_price or '-' }} / {{ r.momo_price or '-' }}<br><span class="text-muted small">差距 {{ r.gap or '-' }}</span></td>
<td>{{ '%.1f'|format(r.discount_pct or 0) }}%<br><span class="text-muted small">分數 {{ '%.2f'|format(r.match_score or 0) }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="biz-empty">近 24h 尚無競品價格樣本。</div>
{% endif %}
</div>
</section>
</aside>
</div>
<div class="text-muted small mt-3">資料來源AI 價格建議、行動計畫、實際結果、競品比對與競品價格歷史。</div>
</div>
{% endblock %}
{% block extra_js %}
<template id="obs-business-verdict-data">{{ verdict_stats | tojson }}</template>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/observability-charts.js') }}"></script>
{% endblock %}