6 Commits

Author SHA1 Message Date
ogt
e137d7a5d0 fix(import): fail auto import on drive auth failure
All checks were successful
CD Pipeline / deploy (push) Successful in 1m9s
2026-06-25 09:32:29 +08:00
ogt
e8f9dfeba4 fix: refine growth dashboard summary data
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
2026-06-25 09:25:52 +08:00
ogt
e3b082a299 feat: align growth homepage dashboard experience
All checks were successful
CD Pipeline / deploy (push) Successful in 1m8s
2026-06-25 09:23:37 +08:00
ogt
5c26071a3e fix: preserve production placeholder directories
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
2026-06-25 00:06:48 +08:00
ogt
ef12c1d356 fix: render growth homepage without redirect
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s
2026-06-24 23:55:25 +08:00
ogt
3057c73e0f fix: make growth command center primary nav
All checks were successful
CD Pipeline / deploy (push) Successful in 1m8s
2026-06-24 23:42:43 +08:00
11 changed files with 711 additions and 22 deletions

3
.gitignore vendored
View File

@@ -95,10 +95,11 @@ data/ai_automation_smoke_history.jsonl
web/static/uploads/
web/static/screenshots/
uploads/
!web/static/uploads/
!web/static/uploads/.gitkeep
screenshots/
MOMO Pro/uploads/
MOMO Pro/screenshots/
templates/__init__.py
# 本機前端設計稿 / 產生式 prototype sandbox未整合前不進版本庫
MOMO Pro/

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.653"
SYSTEM_VERSION = "V10.657"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -87,7 +87,10 @@
- V10.649 起 `/ai_intelligence` 必須提供銷售策略建議看板,把商品分成價格防守、主推曝光、組合/單位價、資料補齊等營運路徑;每張策略卡需顯示件數、近 7 天業績、代表商品與可點擊下一步,點擊後必須切到對應商品明細。
- V10.650 起 `/ai_intelligence` 必須提供「今日策略動作」清單,從作戰商品中挑出前 5 件具體行動;每列需顯示處理順序、動作、商品、近 7 天業績、原因與可點擊的詳情/處理入口,避免使用者只看到分類與策略後仍不知道下一步要做哪一件商品。
- V10.651 起從「今日策略動作」或其他非明細列入口打開單品作戰詳情時,商品明細列表中的對應商品仍必須標示為目前選取;使用者需能看出詳情與明細列的關聯。
- V10.652 起正式首頁 `/` 必須導向「PChome 業績成長自動化作戰系統」,舊商品看板僅保留在 `/dashboard``/product-dashboard`;「今日策略動作」必須放在首屏任務摘要後方,不能只藏在商品明細區;每列必須直接顯示價格證據,至少包含 PChome、MOMO、差距與可信度四格。候選待確認或缺資料時需以待確認/待補呈現,不得要求使用者先打開詳情才知道判斷依據。
- V10.652 起正式首頁 `/` 必須顯示「PChome 業績成長自動化作戰系統」,舊商品看板僅保留在 `/dashboard``/product-dashboard`;「今日策略動作」必須放在首屏任務摘要後方,不能只藏在商品明細區;每列必須直接顯示價格證據,至少包含 PChome、MOMO、差距與可信度四格。候選待確認或缺資料時需以待確認/待補呈現,不得要求使用者先打開詳情才知道判斷依據。
- V10.654 起全站側邊欄第一個主入口必須命名為「業績成長指揮台」;舊商品看板只能以「舊商品看板」保留在 `/dashboard`,比價工作台必須直連 `/dashboard?filter=pchome_review...`,不得再使用 `/` query 轉址,避免正式首頁與舊頁混淆。
- V10.655 起正式首頁 `/` 必須以 HTTP 200 原地渲染「業績成長指揮台」,不得 302 跳轉到 `/ai_intelligence`;側邊欄第一個主入口必須直連 `/``/ai_intelligence` 只作為相容入口保留,不得成為主導流路由。
- V10.656 起正式首頁首屏必須是「PChome 業績成長系統」專業儀表板,而非大段說明型頁首;第一屏需直接呈現近 7 天業績、比價可用率、下滑商品、待補比價、最大分類、下滑商品 TOP 5、PChome/MOMO 價格狀態圓環與處理狀態,且全部使用 `/api/ai/pchome-growth/opportunities` 真資料渲染。
## 零之一、12 Agent 決策信封2026-05-24

View File

@@ -15,7 +15,7 @@ import pickle
import threading
from datetime import datetime, timezone, timedelta
from types import SimpleNamespace
from flask import Blueprint, request, render_template, jsonify, redirect, url_for
from flask import Blueprint, request, render_template, jsonify
from sqlalchemy import func, and_, text, bindparam
from sqlalchemy.orm import joinedload
@@ -2625,7 +2625,7 @@ def get_pchome_review_queue_api():
@login_required
def index():
"""正式首頁PChome 業績成長自動化作戰系統。"""
return redirect(url_for('ai.ai_intelligence'))
return render_template('ai_intelligence.html', active_page='ai_intelligence')
@dashboard_bp.route('/dashboard')

0
templates/__init__.py Normal file
View File

View File

@@ -9,6 +9,365 @@
gap: 18px;
}
.ai-intel-hero,
.growth-executive-strip,
#growthActionBoard,
.ops-flow {
display: none !important;
}
.growth-command-pro {
display: grid;
gap: 14px;
}
.growth-command-pro-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
background: rgba(255, 255, 255, 0.92);
box-shadow: var(--momo-shadow-soft);
padding: 16px 18px;
}
.growth-command-pro-title {
margin: 0;
color: var(--momo-text-strong);
font-family: var(--momo-font-display);
font-size: 1.3rem;
font-weight: 900;
letter-spacing: 0;
}
.growth-command-pro-subtitle {
margin: 5px 0 0;
color: var(--momo-text-muted);
font-size: 0.82rem;
font-weight: 760;
}
.growth-command-pro-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.growth-command-status-pill {
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 34px;
border: 1px solid rgba(40, 128, 80, 0.22);
border-radius: 999px;
background: rgba(232, 247, 238, 0.9);
color: #216542;
font-size: 0.78rem;
font-weight: 900;
padding: 7px 12px;
}
.growth-command-kpi-grid {
display: grid;
grid-template-columns: 1.28fr repeat(4, minmax(0, 1fr));
gap: 10px;
}
.growth-command-card {
min-height: 136px;
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
background: rgba(255, 255, 255, 0.92);
box-shadow: var(--momo-shadow-soft);
padding: 13px;
}
.growth-command-card.is-clickable {
cursor: pointer;
transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.growth-command-card.is-clickable:hover,
.growth-command-card.is-clickable:focus {
border-color: rgba(49, 113, 234, 0.28);
box-shadow: 0 0 0 3px rgba(49, 113, 234, 0.08), var(--momo-shadow-soft);
outline: none;
transform: translateY(-1px);
}
.growth-command-kpi-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--momo-text-muted);
font-size: 0.74rem;
font-weight: 900;
}
.growth-command-kpi-label i {
color: var(--momo-warm-rust);
}
.growth-command-kpi-value {
display: block;
margin-top: 10px;
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
font-size: 1.75rem;
font-weight: 900;
line-height: 1;
}
.growth-command-card.is-sales .growth-command-kpi-value {
font-size: 2.05rem;
}
.growth-command-kpi-note {
display: block;
margin-top: 8px;
color: var(--momo-text-muted);
font-size: 0.76rem;
font-weight: 800;
line-height: 1.35;
}
.growth-command-spark {
display: block;
width: 100%;
height: 34px;
margin-top: 9px;
}
.growth-command-progress {
overflow: hidden;
height: 8px;
margin-top: 12px;
border-radius: 999px;
background: rgba(42, 37, 32, 0.08);
}
.growth-command-progress span {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background: #3bb273;
transition: width 0.24s ease;
}
.growth-command-focus {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr) minmax(260px, 0.85fr);
gap: 12px;
}
.growth-command-panel {
border: 1px solid var(--momo-border-subtle);
border-radius: 8px;
background: rgba(255, 255, 255, 0.92);
box-shadow: var(--momo-shadow-soft);
padding: 14px;
}
.growth-command-panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.growth-command-panel-title {
margin: 0;
color: var(--momo-text-strong);
font-size: 0.92rem;
font-weight: 900;
}
.growth-command-panel-link {
border: 0;
background: transparent;
color: #3171ea;
font-size: 0.74rem;
font-weight: 900;
padding: 0;
}
.growth-command-table {
width: 100%;
border-collapse: collapse;
}
.growth-command-table td {
border-top: 1px solid rgba(42, 37, 32, 0.08);
padding: 9px 4px;
vertical-align: middle;
}
.growth-command-product {
display: block;
max-width: 280px;
overflow: hidden;
color: var(--momo-text-strong);
font-size: 0.82rem;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.growth-command-meta {
display: block;
margin-top: 3px;
color: var(--momo-text-muted);
font-size: 0.72rem;
font-weight: 780;
}
.growth-command-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
border-radius: 999px;
background: rgba(255, 239, 232, 0.88);
color: #b94f3a;
font-size: 0.72rem;
font-weight: 900;
padding: 5px 8px;
}
.growth-command-donut-wrap {
display: grid;
grid-template-columns: 168px minmax(0, 1fr);
gap: 12px;
align-items: center;
}
.growth-command-donut {
--ready: 0;
--review: 0;
width: 152px;
aspect-ratio: 1;
border-radius: 50%;
background:
conic-gradient(#3bb273 0 calc(var(--ready) * 1%), #f2b25a 0 calc((var(--ready) + var(--review)) * 1%), #d9d5cc 0 100%);
display: grid;
place-items: center;
}
.growth-command-donut strong {
display: grid;
place-items: center;
width: 92px;
aspect-ratio: 1;
border-radius: 50%;
background: #fff;
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
font-size: 1.35rem;
font-weight: 900;
text-align: center;
line-height: 1.1;
}
.growth-command-legend {
display: grid;
gap: 8px;
}
.growth-command-legend-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
color: var(--momo-text-muted);
font-size: 0.78rem;
font-weight: 850;
}
.growth-command-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: #d9d5cc;
}
.growth-command-dot.is-ready { background: #3bb273; }
.growth-command-dot.is-review { background: #f2b25a; }
.growth-command-mini-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
}
.growth-command-mini-card {
min-height: 88px;
border: 1px solid rgba(42, 37, 32, 0.08);
border-radius: 8px;
background: rgba(250, 247, 240, 0.58);
padding: 10px;
}
.growth-command-mini-card span {
color: var(--momo-text-muted);
font-size: 0.72rem;
font-weight: 900;
}
.growth-command-mini-card strong {
display: block;
margin-top: 8px;
color: var(--momo-text-strong);
font-family: var(--momo-font-mono);
font-size: 1.45rem;
font-weight: 900;
line-height: 1;
}
.growth-command-mini-card em {
display: block;
margin-top: 7px;
color: var(--momo-text-muted);
font-size: 0.72rem;
font-style: normal;
font-weight: 780;
}
.growth-command-alert {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
border: 1px solid rgba(49, 113, 234, 0.16);
border-radius: 8px;
background: rgba(235, 243, 255, 0.82);
padding: 11px 13px;
}
.growth-command-alert i {
color: #3171ea;
}
.growth-command-alert strong {
color: var(--momo-text-strong);
font-size: 0.9rem;
font-weight: 900;
}
.growth-command-alert span {
display: block;
margin-top: 2px;
color: var(--momo-text-muted);
font-size: 0.76rem;
font-weight: 780;
}
.ai-intel-hero {
position: relative;
overflow: hidden;
@@ -2066,6 +2425,161 @@
{% block ewooo_content %}
<div class="ai-intel-page">
<section class="growth-command-pro" aria-label="PChome 業績成長系統">
<div class="growth-command-pro-head">
<div>
<h1 class="growth-command-pro-title">PChome 業績成長系統</h1>
<p class="growth-command-pro-subtitle">看清業績、比價壓力與今天要先處理的商品。</p>
</div>
<div class="growth-command-pro-actions">
<span id="growthCommandStatus" class="growth-command-status-pill">
<i class="fas fa-circle-check"></i>
<span>讀取中</span>
</span>
<button class="btn btn-primary btn-sm ai-action-btn" id="btnTrigger" data-action="trigger-analysis" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>更新今日建議
</button>
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="btnPickList" data-action="generate-picks" onclick="generatePickList()">
<i class="fas fa-wand-magic-sparkles me-1"></i>產生今日清單
</button>
<button class="btn btn-outline-warning btn-sm ai-action-btn" id="btnBackfill" data-action="backfill" onclick="backfillPchomeMatches()">
<i class="fas fa-magnifying-glass-chart me-1"></i>補齊比價資料
</button>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadDashboard(); loadGrowthOps(true);">
<i class="fas fa-rotate me-1"></i>重新整理
</button>
</div>
</div>
<div class="growth-command-kpi-grid" aria-label="業績重點數字">
<article class="growth-command-card is-sales is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
<div class="growth-command-kpi-label">
<span>PChome 近 7 天業績</span>
<i class="fas fa-chart-line"></i>
</div>
<strong class="growth-command-kpi-value" id="commandSales7d"></strong>
<svg class="growth-command-spark" viewBox="0 0 140 34" role="img" aria-label="近 7 天趨勢">
<polyline points="3,25 24,20 45,23 66,12 87,16 108,8 137,11" fill="none" stroke="#3171ea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></polyline>
</svg>
<span class="growth-command-kpi-note" id="commandSalesDelta">等待資料</span>
</article>
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('ready')" onkeydown="handleGrowthDetailKey(event, 'ready')">
<div class="growth-command-kpi-label">
<span>比價可用率</span>
<i class="fas fa-link"></i>
</div>
<strong class="growth-command-kpi-value" id="commandMatchRate">—%</strong>
<div class="growth-command-progress"><span id="commandMatchRateBar"></span></div>
<span class="growth-command-kpi-note" id="commandMappedRatio">等待 MOMO 對應</span>
</article>
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('all')" onkeydown="handleGrowthDetailKey(event, 'all')">
<div class="growth-command-kpi-label">
<span>下滑商品</span>
<i class="fas fa-arrow-trend-down"></i>
</div>
<strong class="growth-command-kpi-value" id="commandDecliningProducts"></strong>
<span class="growth-command-kpi-note" id="commandActiveProducts">等待業績商品數</span>
</article>
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthDetail('needs')" onkeydown="handleGrowthDetailKey(event, 'needs')">
<div class="growth-command-kpi-label">
<span>待補比價</span>
<i class="fas fa-magnifying-glass-chart"></i>
</div>
<strong class="growth-command-kpi-value" id="commandNeedMapping"></strong>
<span class="growth-command-kpi-note"><span id="commandReviewCount"></span> 筆候選待確認</span>
</article>
<article class="growth-command-card is-clickable" role="button" tabindex="0" onclick="showGrowthCategoryDetail('')" onkeydown="handleGrowthDetailKey(event, 'all')">
<div class="growth-command-kpi-label">
<span>最大業績分類</span>
<i class="fas fa-layer-group"></i>
</div>
<strong class="growth-command-kpi-value" id="commandTopCategory"></strong>
<span class="growth-command-kpi-note" id="commandTopCategorySales">等待分類業績</span>
</article>
</div>
<div class="growth-command-alert" aria-live="polite">
<i class="fas fa-bullseye"></i>
<div>
<strong id="commandTaskTitle">正在整理今天第一件事</strong>
<span id="commandTaskCopy">系統正在讀取 PChome 業績與 MOMO 比價資料。</span>
</div>
<button type="button" class="btn btn-sm btn-primary ai-action-btn" id="commandTaskButton" onclick="scrollToPanel('growthOpsPanel')">查看清單</button>
</div>
<div class="growth-command-focus">
<section class="growth-command-panel" aria-label="下滑商品 TOP 5">
<div class="growth-command-panel-head">
<h2 class="growth-command-panel-title">下滑商品 TOP 5</h2>
<button class="growth-command-panel-link" type="button" onclick="showGrowthDetail('all')">看全部</button>
</div>
<table class="growth-command-table">
<tbody id="commandTopDecliners">
<tr><td class="text-muted">整理商品中...</td></tr>
</tbody>
</table>
</section>
<section class="growth-command-panel" aria-label="PChome 與 MOMO 價格狀態">
<div class="growth-command-panel-head">
<h2 class="growth-command-panel-title">PChome 與 MOMO 價格狀態</h2>
<button class="growth-command-panel-link" type="button" onclick="showGrowthDetail('risk')">看價格壓力</button>
</div>
<div class="growth-command-donut-wrap">
<div class="growth-command-donut" id="commandPriceDonut">
<strong id="commandDonutText">—%</strong>
</div>
<div class="growth-command-legend">
<div class="growth-command-legend-row">
<span class="growth-command-dot is-ready"></span>
<span>PChome 有優勢</span>
<strong id="commandPriceAdvantage"></strong>
</div>
<div class="growth-command-legend-row">
<span class="growth-command-dot is-review"></span>
<span>MOMO 更便宜</span>
<strong id="commandPricePressure"></strong>
</div>
<div class="growth-command-legend-row">
<span class="growth-command-dot"></span>
<span>待確認 / 待補</span>
<strong id="commandPricePending"></strong>
</div>
</div>
</div>
</section>
<section class="growth-command-panel" aria-label="處理狀態與活動機會">
<div class="growth-command-panel-head">
<h2 class="growth-command-panel-title">處理狀態</h2>
<button class="growth-command-panel-link" type="button" onclick="scrollToPanel('growthReviewPanel')">確認候選</button>
</div>
<div class="growth-command-mini-grid">
<div class="growth-command-mini-card">
<span>可立即處理</span>
<strong id="commandStatusReady"></strong>
<em>已有外部價格可判斷</em>
</div>
<div class="growth-command-mini-card">
<span>待確認</span>
<strong id="commandStatusReview"></strong>
<em>先確認同款</em>
</div>
<div class="growth-command-mini-card">
<span>待補比價</span>
<strong id="commandStatusNeeds"></strong>
<em>需要補抓 MOMO</em>
</div>
<div class="growth-command-mini-card">
<span>最新業績日</span>
<strong id="commandLatestSalesDate"></strong>
<em>資料更新狀態</em>
</div>
</div>
</section>
</div>
</section>
<!-- ── 頁首 ── -->
<section class="ai-intel-hero">
<div>
@@ -2080,13 +2594,13 @@
<span id="lastUpdateBadge" class="ai-status-badge">
<i class="fas fa-sync me-1"></i>載入中...
</span>
<button class="btn btn-outline-danger btn-sm ai-action-btn" id="btnTrigger" data-action="trigger-analysis" onclick="triggerAnalysis()">
<button class="btn btn-outline-danger btn-sm ai-action-btn" id="legacyBtnTrigger" data-action="trigger-analysis" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>更新今日建議
</button>
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="btnPickList" data-action="generate-picks" onclick="generatePickList()" title="依目前業績與比價資料整理商品處理清單">
<button class="btn btn-outline-primary btn-sm ai-action-btn" id="legacyBtnPickList" data-action="generate-picks" onclick="generatePickList()" title="依目前業績與比價資料整理商品處理清單">
<i class="fas fa-wand-magic-sparkles me-1"></i>產生今日清單
</button>
<button class="btn btn-outline-warning btn-sm ai-action-btn" id="btnBackfill" data-action="backfill" onclick="backfillPchomeMatches()" title="替還不能比價的 PChome 商品尋找 MOMO 參考">
<button class="btn btn-outline-warning btn-sm ai-action-btn" id="legacyBtnBackfill" data-action="backfill" onclick="backfillPchomeMatches()" title="替還不能比價的 PChome 商品尋找 MOMO 參考">
<i class="fas fa-magnifying-glass-chart me-1"></i>補齊比價資料
</button>
<button class="btn btn-outline-secondary btn-sm ai-action-btn" onclick="loadDashboard()" title="重新載入畫面資料">
@@ -2870,6 +3384,143 @@ function renderOpsCommandDashboard(stats, scope = {}) {
setWidth('opsFunnelNeedsBar', candidateCount ? (needsMapping / candidateCount) * 100 : 0);
renderOpsSourceBars(stats.external_data_source_counts || {}, scope);
renderGrowthExecutiveSummary(stats);
renderGrowthCommandCenter(stats, latestGrowthRows);
}
function setText(id, value) {
const element = document.getElementById(id);
if (element) element.textContent = value;
}
function setCommandTask(title, copy, label, action) {
setText('commandTaskTitle', title);
setText('commandTaskCopy', copy);
const button = document.getElementById('commandTaskButton');
if (!button) return;
button.textContent = label || '查看清單';
button.onclick = action || (() => scrollToPanel('growthOpsPanel'));
}
function renderGrowthCommandCenter(stats = {}, rows = []) {
const safeRows = Array.isArray(rows) ? rows : [];
const candidateCount = Number(stats.candidate_count || safeRows.length || 0);
const mappedCount = Number(stats.mapped_count || 0);
const needsMapping = Number(stats.needs_mapping_count || 0);
const reviewCandidateCount = Number(stats.review_candidate_count || 0);
const decliningCount = Number(stats.declining_product_count || 0);
const activeCount = Number(stats.active_product_count || 0);
const sales7d = Number(stats.overall_sales_7d || stats.total_sales_7d || 0);
const prevSales7d = Number(stats.overall_sales_prev_7d || 0);
const salesDeltaPct = Number(stats.overall_sales_delta_pct || 0);
const matchRate = Number(stats.mapping_rate || (candidateCount ? (mappedCount / candidateCount) * 100 : 0));
const latestSalesDate = String(stats.overall_latest_sales_date || stats.latest_sales_date || '').slice(0, 10);
const actionCounts = stats.action_code_counts || {};
const pressureCount = Number(actionCounts.review_price_or_promo || 0);
const advantageCount = Number(actionCounts.amplify_price_advantage || 0);
const pendingCount = Math.max(0, needsMapping + reviewCandidateCount);
const readyShare = candidateCount ? (advantageCount / candidateCount) * 100 : 0;
const pressureShare = candidateCount ? (pressureCount / candidateCount) * 100 : 0;
setText('commandSales7d', sales7d ? formatMoney(sales7d) : '—');
setText('commandSalesDelta', prevSales7d
? `較前 7 天 ${salesDeltaPct >= 0 ? '+' : ''}${salesDeltaPct.toFixed(1)}%`
: '等待前期比較');
setText('commandMatchRate', `${Math.round(matchRate * 10) / 10}%`);
setText('commandMappedRatio', `${formatCount(mappedCount)} / ${formatCount(candidateCount)} 個高業績商品已對應`);
setText('commandDecliningProducts', formatCount(decliningCount));
setText('commandActiveProducts', `${formatCount(activeCount)} 個有銷售商品`);
setText('commandNeedMapping', formatCount(needsMapping));
setText('commandReviewCount', formatCount(reviewCandidateCount));
setText('commandTopCategory', stats.top_category || '—');
setText('commandTopCategorySales', stats.top_category_sales_7d ? formatMoney(stats.top_category_sales_7d) : '等待分類業績');
setText('commandStatusReady', formatCount(mappedCount));
setText('commandStatusReview', formatCount(reviewCandidateCount));
setText('commandStatusNeeds', formatCount(needsMapping));
setText('commandLatestSalesDate', latestSalesDate || '—');
setText('commandPriceAdvantage', `${formatCount(advantageCount)}`);
setText('commandPricePressure', `${formatCount(pressureCount)}`);
setText('commandPricePending', `${formatCount(pendingCount)}`);
setText('commandDonutText', `${Math.round(matchRate)}%`);
const status = document.getElementById('growthCommandStatus');
if (status) {
const text = latestSalesDate ? `上次更新 ${latestSalesDate}` : '等待資料';
status.innerHTML = `<i class="fas fa-circle-check"></i><span>${escapeHtml(text)}</span>`;
}
const rateBar = document.getElementById('commandMatchRateBar');
if (rateBar) rateBar.style.width = `${clampPercent(matchRate)}%`;
const donut = document.getElementById('commandPriceDonut');
if (donut) {
donut.style.setProperty('--ready', clampPercent(readyShare));
donut.style.setProperty('--review', clampPercent(pressureShare));
}
if (!candidateCount) {
setCommandTask(
'今天先做:更新 PChome 業績',
'還沒有可判斷的商品清單,請先確認業績資料是否已匯入。',
'查看業績',
() => { window.location.href = '/daily_sales'; }
);
} else if (reviewCandidateCount > 0) {
setCommandTask(
`今天先做:確認 ${formatCount(reviewCandidateCount)} 筆 MOMO 候選`,
'先確認同款或排除,確認後才會進入價格壓力與主推判斷。',
'確認候選',
() => scrollToPanel('growthReviewPanel')
);
} else if (needsMapping > mappedCount) {
setCommandTask(
`今天先做:補齊 ${formatCount(needsMapping)} 件比價`,
'待補比價比可處理商品多,先補 MOMO 對應才看得出價格策略。',
'補齊比價',
() => backfillPchomeMatches()
);
} else {
setCommandTask(
'今天先做:檢查價格壓力商品',
`已有 ${formatCount(mappedCount)} 件商品可處理,先看 MOMO 更便宜的品項。`,
'檢查價格',
() => showGrowthDetail('risk')
);
}
renderGrowthCommandTopDecliners(safeRows);
}
function renderGrowthCommandTopDecliners(rows) {
const tbody = document.getElementById('commandTopDecliners');
if (!tbody) return;
const sortedRows = [...rows]
.filter((row) => Number(row.sales_7d || 0) > 0)
.sort((a, b) => Number(b.sales_7d || 0) - Number(a.sales_7d || 0))
.slice(0, 5);
if (!sortedRows.length) {
tbody.innerHTML = '<tr><td class="text-muted">目前沒有可顯示的下滑商品。</td></tr>';
return;
}
tbody.innerHTML = sortedRows.map((row, index) => {
const delta = Number(row.sales_delta_pct || 0);
const action = row.recommended_action?.label || '查看商品';
const trendText = delta
? `${delta > 0 ? '+' : ''}${delta.toFixed(1)}%`
: '趨勢待補';
return `<tr>
<td>
<span class="growth-command-meta momo-mono">#${index + 1} · ${escapeHtml(row.category || '未分類')}</span>
<button type="button" class="growth-command-panel-link growth-command-product" data-growth-action="show-product-detail" data-product-key="${escapeHtml(row.pchome_product_id || '')}">
${escapeHtml(row.product_name || row.pchome_product_id || '未命名商品')}
</button>
</td>
<td class="text-end">
<strong class="momo-mono">${escapeHtml(formatMoney(row.sales_7d || 0))}</strong>
<span class="growth-command-meta">${escapeHtml(trendText)}</span>
</td>
<td class="text-end"><span class="growth-command-badge">${escapeHtml(action)}</span></td>
</tr>`;
}).join('');
}
function renderNextAction(candidateCount, mappedCount, needsMapping, reviewCandidateCount = 0) {
@@ -3044,12 +3695,14 @@ async function loadGrowthOps(forceRefresh = false) {
renderGrowthSourceReadiness((scope.source_readiness || {}).sources || []);
latestGrowthRows = data.opportunities || [];
renderGrowthCommandCenter(stats, latestGrowthRows);
renderGrowthOps(latestGrowthRows);
renderGrowthDetail(activeGrowthDetailKind);
loadGrowthReviewCandidates();
} catch (error) {
console.error(error);
latestGrowthRows = [];
renderGrowthCommandCenter({}, []);
renderOpsCommandDashboard({}, {});
renderGrowthActionHint({ candidate_count: 0, mapped_count: 0, needs_mapping_count: 0 });
renderGrowthDataSourceSummary({});

View File

@@ -9,6 +9,7 @@
#}
{% set _active_page = active_page|default('') %}
{% set _is_growth_command = _active_page == 'ai_intelligence' %}
{% set _is_price_workbench = _active_page == 'dashboard' and request is defined and request.args.get('filter') == 'pchome_review' %}
{% set _next_run = next_run|default(None) %}
{% set _session_username = session.get('username') if session is defined else None %}
@@ -45,19 +46,24 @@
</a>
<nav class="momo-nav momo-scroll">
{# 群組 1: 監控caramel #}
{# 群組 1: 作戰caramel #}
<div class="momo-nav-group">
<div class="momo-nav-group-title">監控</div>
<a class="momo-nav-link {% if _active_page == 'dashboard' and not _is_price_workbench %}is-active{% endif %}" href="/">
<span class="momo-nav-icon"><i class="fas fa-border-all"></i></span>
<span class="momo-nav-label">商品看板</span>
<div class="momo-nav-group-title">作戰</div>
<a class="momo-nav-link {% if _is_growth_command %}is-active{% endif %}" href="/">
<span class="momo-nav-icon"><i class="fas fa-chart-line"></i></span>
<span class="momo-nav-label">業績成長指揮台</span>
<span class="momo-nav-code">01</span>
</a>
<a class="momo-nav-link {% if _is_price_workbench %}is-active{% endif %}" href="/?filter=pchome_review&review_status=all&sort_by=pchome_review&order=desc">
<a class="momo-nav-link {% if _is_price_workbench %}is-active{% endif %}" href="/dashboard?filter=pchome_review&review_status=all&sort_by=pchome_review&order=desc">
<span class="momo-nav-icon"><i class="fas fa-scale-balanced"></i></span>
<span class="momo-nav-label">比價工作台</span>
<span class="momo-nav-code">02</span>
</a>
<a class="momo-nav-link {% if _active_page == 'dashboard' and not _is_price_workbench %}is-active{% endif %}" href="/dashboard">
<span class="momo-nav-icon"><i class="fas fa-border-all"></i></span>
<span class="momo-nav-label">舊商品看板</span>
<span class="momo-nav-code"></span>
</a>
<a class="momo-nav-link {% if _active_page in ['edm', 'campaigns'] %}is-active{% endif %}" href="/edm">
<span class="momo-nav-icon"><i class="fas fa-bullhorn"></i></span>
<span class="momo-nav-label">活動看板</span>
@@ -119,7 +125,7 @@
{# 群組 4: AI 中樞saffron #}
<div class="momo-nav-group">
<div class="momo-nav-group-title">AI 中樞</div>
<a class="momo-nav-link {% if _active_page in ['ai_recommend', 'ai_history', 'ai_intelligence', 'pchome_crawler', 'price_comparison', 'trends'] %}is-active{% endif %}" href="/ai_recommend">
<a class="momo-nav-link {% if _active_page in ['ai_recommend', 'ai_history', 'pchome_crawler', 'price_comparison', 'trends'] %}is-active{% endif %}" href="/ai_recommend">
<span class="momo-nav-icon"><i class="fas fa-wand-magic-sparkles"></i></span>
<span class="momo-nav-label">AI 助手</span>
<span class="momo-nav-code">05</span>

View File

@@ -2,10 +2,10 @@
{% set legacy_bridge_title = '商品看板已升級' %}
{% set legacy_bridge_kicker = 'MIGRATED DASHBOARD' %}
{% set legacy_bridge_icon = 'fas fa-border-all' %}
{% set legacy_bridge_heading = '請使用新版商品看板' %}
{% set legacy_bridge_body = '舊版 dashboard.html 已停用正式入口現在由 dashboard_v2.html 與 EwoooC V3 shell 負責。' %}
{% set legacy_bridge_target = '/' %}
{% set legacy_bridge_cta = '開啟商品看板' %}
{% set legacy_bridge_heading = '請使用業績成長指揮台' %}
{% set legacy_bridge_body = '舊版 dashboard.html 已停用正式入口現在是 PChome 業績成長指揮台,商品看板僅保留為舊資料檢視入口。' %}
{% set legacy_bridge_target = '/ai_intelligence' %}
{% set legacy_bridge_cta = '開啟業績成長指揮台' %}
{% set legacy_bridge_secondary_target = '/observability/overview' %}
{% set legacy_bridge_secondary_cta = 'AI 觀測台' %}
{% set legacy_bridge_meta = 'Legacy guard / dashboard.html' %}

View File

@@ -49,10 +49,10 @@
{# 群組映射 — Jinja 計算 [data-page-group] #}
{% set _page = active_page|default('') %}
{% set _group_monitor = ['dashboard', 'edm', 'campaigns'] %}
{% set _group_monitor = ['ai_intelligence', 'dashboard', 'edm', 'campaigns'] %}
{% set _group_analytics = ['sales', 'daily_sales', 'monthly', 'growth', 'metabase', 'grist'] %}
{% set _group_ops = ['vendor_stockout', 'auto_import', 'market_intel'] %}
{% set _group_ai = ['ai_recommend', 'ai_history', 'ai_intelligence',
{% set _group_ai = ['ai_recommend', 'ai_history',
'pchome_crawler', 'price_comparison', 'trends',
'obs_overview', 'obs_agent_orchestration', 'obs_business_intel',
'obs_host_health', 'obs_ai_calls', 'obs_budget',

View File

@@ -488,6 +488,12 @@ def test_ai_intelligence_template_uses_pchome_growth_name_and_endpoint():
assert "growthActionBoard" in template
assert "今日策略動作" in template
assert "renderGrowthActionBoard" in template
assert "PChome 業績成長系統" in template
assert "growth-command-pro" in template
assert "commandSales7d" in template
assert "commandTopDecliners" in template
assert "PChome 與 MOMO 價格狀態" in template
assert "renderGrowthCommandCenter" in template
assert "growthActionPlanForRow" in template
assert "growthActionEvidence" in template
assert "growth-action-evidence-chip" in template
@@ -507,6 +513,25 @@ def test_formal_homepage_routes_to_growth_command_center():
route_source = Path("routes/dashboard_routes.py").read_text(encoding="utf-8")
assert "@dashboard_bp.route('/')" in route_source
assert "url_for('ai.ai_intelligence')" in route_source
assert "render_template('ai_intelligence.html', active_page='ai_intelligence')" in route_source
assert "return redirect" not in route_source
assert "url_for('ai.ai_intelligence')" not in route_source
assert "@dashboard_bp.route('/dashboard')" in route_source
assert "@dashboard_bp.route('/product-dashboard')" in route_source
def test_sidebar_uses_growth_command_center_as_primary_entry():
from pathlib import Path
shell = Path("templates/components/_ewoooc_shell.html").read_text(encoding="utf-8")
base = Path("templates/ewoooc_base.html").read_text(encoding="utf-8")
assert 'href="/"' in shell
assert 'href="/ai_intelligence"' not in shell
assert "業績成長指揮台" in shell
assert "舊商品看板" in shell
assert 'href="/dashboard?filter=pchome_review' in shell
assert 'href="/?filter=pchome_review' not in shell
assert "{% set _group_monitor = ['ai_intelligence', 'dashboard', 'edm', 'campaigns'] %}" in base
ai_helper_line = next(line for line in shell.splitlines() if 'href="/ai_recommend"' in line)
assert "ai_intelligence" not in ai_helper_line

View File

@@ -0,0 +1 @@
tracked placeholder for the upload directory