fix: harden import auth and utility page copy
Some checks failed
CD Pipeline / deploy (push) Failing after 6m19s

This commit is contained in:
ogt
2026-06-26 06:44:51 +08:00
parent fa484893b9
commit c1b375f41c
20 changed files with 130 additions and 131 deletions

View File

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

View File

@@ -79,6 +79,10 @@ services:
- FLASK_ENV=production
- PYTHONUNBUFFERED=1
- TZ=Asia/Taipei
- GOOGLE_DRIVE_CREDENTIALS_FILE=/app/config/google_credentials.json
- GOOGLE_DRIVE_TOKEN_FILE=/app/config/google_token.json
- GOOGLE_DRIVE_LEGACY_PICKLE_FILE=/app/config/google_token.pickle
- GOOGLE_DRIVE_ALLOW_INTERACTIVE_AUTH=${GOOGLE_DRIVE_ALLOW_INTERACTIVE_AUTH:-false}
- METABASE_URL=/metabase
- GRIST_URL=/grist
# 關閉登入驗證(開發/測試用,生產環境預設啟用登入)
@@ -221,6 +225,10 @@ services:
- FLASK_ENV=production
- PYTHONUNBUFFERED=1
- TZ=Asia/Taipei
- GOOGLE_DRIVE_CREDENTIALS_FILE=/app/config/google_credentials.json
- GOOGLE_DRIVE_TOKEN_FILE=/app/config/google_token.json
- GOOGLE_DRIVE_LEGACY_PICKLE_FILE=/app/config/google_token.pickle
- GOOGLE_DRIVE_ALLOW_INTERACTIVE_AUTH=${GOOGLE_DRIVE_ALLOW_INTERACTIVE_AUTH:-false}
# 資料庫設定: Docker 環境使用 PostgreSQL
# H7 (2026-04-24): POSTGRES_* 改由 env_file: .env 唯一來源,移除 compose 層插值避免空值覆蓋
- USE_POSTGRESQL=true
@@ -285,6 +293,10 @@ services:
- FLASK_ENV=production
- PYTHONUNBUFFERED=1
- TZ=Asia/Taipei
- GOOGLE_DRIVE_CREDENTIALS_FILE=/app/config/google_credentials.json
- GOOGLE_DRIVE_TOKEN_FILE=/app/config/google_token.json
- GOOGLE_DRIVE_LEGACY_PICKLE_FILE=/app/config/google_token.pickle
- GOOGLE_DRIVE_ALLOW_INTERACTIVE_AUTH=${GOOGLE_DRIVE_ALLOW_INTERACTIVE_AUTH:-false}
# H7 (2026-04-24): POSTGRES_* 改由 env_file: .env 唯一來源,移除 compose 層插值避免空值覆蓋
- USE_POSTGRESQL=true
- POSTGRES_PORT=5432

View File

@@ -775,3 +775,4 @@ POSTGRES_HOST=momo-db
| 2026-06-25 | 系統設定匯入提示不得顯示資料表或日誌口徑 | V10.696 起 `/system_settings` 不再用 `realtime_sales_monthly` 判斷前端提示,也不再顯示「資料落點、檢查日誌、發生系統錯誤」等內部口徑;所有匯入與備份失敗提示統一走 `toImportActionMessage()`,轉成重新授權、改用正確業績報表、重新匯入或通知維護人員。 |
| 2026-06-25 | 分析與建議頁必須使用 PChome 作戰流程語言 | V10.697 起 `/sales_analysis``/monthly_summary_analysis``/ai_recommend` 頁首與主要操作區統一使用「主推、守價、補比價、成長缺口、毛利貢獻、品類結構」等營運語言;前台不得把 AI 模型、權杖、資料庫、欄位、英文指標縮寫或內部錯誤作為使用者主訊息。 |
| 2026-06-25 | 治理與匯入頁也不得外露模型/權杖/欄位口徑 | V10.698 起缺貨匯入、供應商窗口、AI 歷史、預算、AI 流量、AI 分工與主機健康頁統一改用「必要資料、用量、建議引擎、建議路徑、雲端備援、AI 建議服務」等前台可讀詞,避免使用者在營運頁看到 raw model、token、欄位或模型品牌。 |
| 2026-06-25 | 工具頁與簡報頁也必須使用作戰語言Google Drive token 必須固定到持久化掛載 | V10.699 起簡報預覽、品牌素材、比價、匯入、缺貨與觀測台操作提示移除英文/內部流程字,改成可直接理解的狀態與下一步;正式容器明確指定 `/app/config/google_token.json``/app/config/google_credentials.json`,背景匯入不得因主機重啟或工作目錄變動而改找瀏覽器授權。 |

View File

@@ -25,10 +25,11 @@ logger = logging.getLogger(__name__)
# Google Drive API 權限範圍
SCOPES = ['https://www.googleapis.com/auth/drive']
# 認證檔案路徑
CREDENTIALS_FILE = 'config/google_credentials.json'
TOKEN_FILE = 'config/google_token.json'
_LEGACY_PICKLE_FILE = 'config/google_token.pickle'
# 認證檔案路徑。正式容器用絕對路徑固定到 /app/config bind mount
# 避免重啟後因工作目錄不同改讀到不可持久化的授權檔。
CREDENTIALS_FILE = os.getenv('GOOGLE_DRIVE_CREDENTIALS_FILE', 'config/google_credentials.json')
TOKEN_FILE = os.getenv('GOOGLE_DRIVE_TOKEN_FILE', 'config/google_token.json')
_LEGACY_PICKLE_FILE = os.getenv('GOOGLE_DRIVE_LEGACY_PICKLE_FILE', 'config/google_token.pickle')
INTERACTIVE_AUTH_ENV = 'GOOGLE_DRIVE_ALLOW_INTERACTIVE_AUTH'
INTERACTIVE_AUTH_TIMEOUT_ENV = 'GOOGLE_DRIVE_INTERACTIVE_AUTH_TIMEOUT_SECONDS'

View File

@@ -120,7 +120,7 @@
<div class="ppt-vision-job is-runtime">
<span>執行環境</span>
<strong>{{ vision_status.status_label }}</strong>
<small>{{ vision_status.model_label }} · {{ vision_status.converter_label }}</small>
<small>預覽與審核服務可用狀態</small>
</div>
{% if vision_audit_status.last_run %}
<div class="ppt-vision-job">
@@ -132,7 +132,7 @@
<div class="ppt-vision-job">
<span>尚無紀錄</span>
<strong>待命</strong>
<small>啟動視覺 QA 後會顯示背景任務狀態</small>
<small>啟動後會顯示待處理簡報與問題數</small>
</div>
{% endif %}
</div>
@@ -167,7 +167,7 @@
<h2 class="ppt-panel-title">最近可預覽簡報</h2>
</div>
<div class="ppt-workbench-actions">
<small class="text-muted">最新 {{ files[:4]|length }} 份,直接線上預覽或下載原始 PPTX</small>
<small class="text-muted">最新 {{ files[:4]|length }} 份,線上預覽或下載簡報檔</small>
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-run-vision {% if not vision_status.ready or not vision_audit_filenames %}disabled{% endif %}>
<i class="fas fa-eye me-1"></i>立即視覺 QA
</button>

View File

@@ -10,9 +10,9 @@
<div class="container-fluid mt-3">
<section class="ppt-preview-hero">
<div>
<div class="ppt-preview-kicker"><i class="fas fa-file-powerpoint me-1"></i>PPT Online Preview</div>
<div class="ppt-preview-kicker"><i class="fas fa-file-powerpoint me-1"></i>簡報線上預覽</div>
<h1 class="ppt-preview-title">{{ filename }}</h1>
<p class="ppt-preview-subtitle">檔案大小 {{ file_size_kb }} KB · 修改時間 {{ file_mtime }} · 預覽以 PDF 快取呈現,原始 PPTX 仍可下載。</p>
<p class="ppt-preview-subtitle">檔案大小 {{ file_size_kb }} KB · 修改時間 {{ file_mtime }} · 預覽以 PDF 快取呈現,簡報檔仍可下載。</p>
</div>
<div class="ppt-preview-actions">
<a class="btn btn-outline-secondary btn-sm" href="{{ back_url }}"><i class="fas fa-angle-left me-1"></i>回產線</a>
@@ -34,13 +34,13 @@
</p>
{% else %}
<section class="ppt-preview-empty">
<div class="ppt-preview-kicker"><i class="fas fa-triangle-exclamation me-1"></i>Preview unavailable</div>
<div class="ppt-preview-kicker"><i class="fas fa-triangle-exclamation me-1"></i>預覽暫時不可用</div>
<h2>目前無法產生線上預覽</h2>
<p>{{ preview.error or '轉檔流程沒有回傳可用 PDF。' }}</p>
<div class="ppt-preview-diagnostics">
<span>需要容器內可執行 LibreOffice / soffice</span>
<span>部署後會用 PDF 快取避免每次重轉</span>
<span>原始 PPTX 可先下載檢查</span>
<span>轉檔服務尚未就緒</span>
<span>完成後會保留 PDF 預覽</span>
<span>可先下載簡報檔檢查</span>
</div>
</section>
{% endif %}

View File

@@ -5462,7 +5462,7 @@ function renderAiRecs(recs) {
<i class="fas fa-clipboard-check fa-3x text-muted mb-3 d-block"></i>
<p class="text-muted mb-1">目前還沒有處理紀錄</p>
<p class="small text-muted mb-3">
系統會定期整理,也可以手動更新。
每天固定整理,也可以手動更新。
</p>
<button class="btn btn-sm btn-outline-danger" onclick="triggerAnalysis()">
<i class="fas fa-bolt me-1"></i>更新今日建議

View File

@@ -87,7 +87,7 @@
上傳當日業績檔,更新日報、成長分析與今日作戰清單。
</p>
<p class="ai-notice__body">
<small>檔名建議:<code>即時業績_當日_YYYYMMDD.xlsx</code>系統會去重後寫入業績快照</small>
<small>檔名建議:<code>即時業績_當日_YYYYMMDD.xlsx</code>送出後更新日報、成長分析與今日作戰清單</small>
</p>
</div>
</div>

View File

@@ -126,10 +126,10 @@
</div>
</div>
</div>
<!-- 其他格式僅提供下載 -->
<!-- 可下載版本 -->
<div class="asset-item">
<div style="padding: 20px;">
<strong>其他格式</strong><br>
<strong>可下載版本</strong><br>
<div class="format-links">
<a href="/static/images/logo.png" download>標準 PNG</a>
<a href="/static/images/logo_circle.svg" download>圓標 SVG</a>

View File

@@ -151,7 +151,6 @@
Market Intel hidden contract registry.
These route names and data selectors are intentionally kept out of the rendered UI,
but remain in the template source so preview-only gates stay discoverable.
data-market-intel-preview
data-market-intel-writer
data-market-intel-cli
data-market-intel-cli-body

View File

@@ -379,7 +379,7 @@
async function searchProducts() {
const keyword = document.getElementById('searchKeyword').value.trim();
if (!keyword) {
showToast('請輸入搜尋關鍵字', 'warning');
showToast('先填搜尋關鍵字', 'warning');
return;
}
@@ -412,7 +412,7 @@
async function crawlCustomUrl() {
const url = document.getElementById('customUrl').value.trim();
if (!url) {
showToast('請輸入 URL', 'warning');
showToast('先貼上 PChome 24h 連結。', 'warning');
return;
}

View File

@@ -832,7 +832,7 @@
<div class="price-result-summary-grid">
<div class="price-result-callout">
<strong id="priceResultHeadline">尚未產生判讀</strong>
<span id="priceResultAdvice">取得 PChome 與 MOMO 商品後,系統會直接整理下一步。</span>
<span id="priceResultAdvice">取得 PChome 與 MOMO 商品後,會直接整理下一步。</span>
</div>
<div class="price-result-matrix" aria-label="比價決策分佈">
<div class="price-result-matrix-card">
@@ -980,7 +980,7 @@
<div class="modal-body">
<div class="price-note py-2">
<i class="fas fa-bullseye me-1"></i>
貼上 MOMO 商品、售價與賣場連結;系統會整理成可比價清單。
貼上 MOMO 商品、售價與賣場連結;會整理成可比價清單。
</div>
<div class="mb-3">
<label class="form-label">商品資料</label>
@@ -1255,7 +1255,7 @@ La Roche-Posay 安得利防曬液 50ml,920
async function parseMomoExcel() {
const fileInput = document.getElementById('momoExcelFile');
if (!fileInput.files.length) {
showToast('選擇檔案', 'warning');
showToast('選擇 MOMO 商品檔案', 'warning');
return;
}
@@ -1296,7 +1296,7 @@ La Roche-Posay 安得利防曬液 50ml,920
function parseManualInput() {
const input = document.getElementById('manualInput').value.trim();
if (!input) {
showToast('請輸入商品資料', 'warning');
showToast('先貼上商品、售價與賣場連結。', 'warning');
return;
}
@@ -1320,7 +1320,7 @@ La Roche-Posay 安得利防曬液 50ml,920
// 驗證價格是否為數字
const price = parseInt(parts[1].trim());
if (isNaN(price) || price <= 0) {
showToast(`${i + 1}價格格式錯誤,請輸入數字例如680。`, 'warning');
showToast(`${i + 1}售價需為數字例如680。`, 'warning');
return;
}
@@ -1383,7 +1383,7 @@ La Roche-Posay 安得利防曬液 50ml,920
document.getElementById('resultSection').style.display = 'none';
setText('priceResultSummary', '等待比價結果');
setText('priceResultHeadline', '尚未產生判讀');
setText('priceResultAdvice', '取得 PChome 與 MOMO 商品後,系統會直接整理下一步。');
setText('priceResultAdvice', '取得 PChome 與 MOMO 商品後,會直接整理下一步。');
setText('priceUrgentMetric', '0');
setText('priceGoodMetric', '0');
setText('priceWatchMetric', '0');

View File

@@ -136,7 +136,7 @@
<i class="fas fa-calendar-week me-1" aria-hidden="true"></i>期間快速選擇
</label>
<select name="data_range" class="form-select" onchange="handleDataRangeChange(this)">
<option value="" {% if not request.args.get('data_range') %}selected{% endif %}>-- 請選擇 --</option>
<option value="" {% if not request.args.get('data_range') %}selected{% endif %}>選擇期間</option>
{% for opt in [(1,'最近 1 個月 (推薦)'), (3,'最近 3 個月'), (6,'最近 6 個月'), (12,'最近 12 個月'), (0,'全部資料')] %}
<option value="{{ opt.0 }}" {% if request.args.get('data_range') == opt.0|string %}selected{% endif %}>{{ opt.1 }}</option>
{% endfor %}

View File

@@ -232,7 +232,7 @@ function uploadSalesReport() {
}
if (!file.name.includes('即時業績') || !file.name.includes('全月')) {
if (!confirm('檔名不像「即時業績(全月)」報表,確定要繼續匯入嗎?\n系統會嘗試辨識內容更新業績資料。')) {
if (!confirm('檔名不像「即時業績(全月)」報表,確定要繼續匯入嗎?\n會先辨識內容,再更新業績資料。')) {
return;
}
} else if (!confirm('確定要匯入此份業績報表嗎?\n匯入後會更新月度業績判斷供成長分析與報表使用。')) {
@@ -265,7 +265,7 @@ function uploadSalesReport() {
})
.catch(error => {
console.error('Error:', error);
alert('匯入連線逾時,請稍後查看分析頁是否更新;若沒有更新請重新匯入。');
alert('匯入連線逾時,請稍後查看匯入處理狀態;若沒有更新請重新匯入。');
})
.finally(() => {
btn.innerHTML = originalText;
@@ -282,7 +282,7 @@ function uploadExcel() {
return;
}
if (!confirm('確定要匯入嗎?\n系統會整理成可分析的營運資料集。')) {
if (!confirm('確定要匯入嗎?\n會整理成可分析的營運資料集。')) {
return;
}
@@ -312,7 +312,7 @@ function uploadExcel() {
})
.catch(error => {
console.error('Error:', error);
alert('匯入連線逾時,請稍後查看分析頁是否更新;若沒有更新請重新匯入。');
alert('匯入連線逾時,請稍後查看匯入處理狀態;若沒有更新請重新匯入。');
})
.finally(() => {
btn.innerHTML = originalText;
@@ -330,7 +330,7 @@ function uploadMonthlySummary() {
return;
}
if (!confirm(`確定要匯入「${file.name}」嗎?\n系統會更新對應年月份的業績資料。\n匯入期間請勿關閉視窗。`)) {
if (!confirm(`確定要匯入「${file.name}」嗎?\n會更新對應年月份的業績資料。\n匯入期間請勿關閉視窗。`)) {
return;
}

View File

@@ -56,11 +56,11 @@
<div class="stockout-import-card">
<div class="stockout-card-label momo-mono">必要資料</div>
<div class="stockout-card-title">來源供應商編號、來源供應商名稱、商品ID、商品名稱</div>
<div class="stockout-card-meta momo-mono">缺少必要資料時,系統會拒絕匯入並回傳錯誤原因</div>
<div class="stockout-card-meta momo-mono">缺少必要資料時,會先停止匯入並提示需補內容</div>
</div>
<div class="stockout-import-card">
<div class="stockout-card-label momo-mono">匯入範本</div>
<div class="stockout-card-title">下載正式格式的空白範本</div>
<div class="stockout-card-title">下載正式空白範本</div>
<div class="stockout-card-meta">
<a class="stockout-action" href="{{ url_for('vendor.api_import_template') }}">
<i class="fas fa-download"></i>下載範本

View File

@@ -663,12 +663,12 @@
const email = document.getElementById('newEmail').value.trim();
if (!email) {
alert('請輸入郵件地址');
alert('先填郵件地址');
return;
}
if (!email.includes('@')) {
alert('請輸入有效的郵件地址!');
alert('郵件地址格式不正確。');
return;
}
@@ -761,7 +761,7 @@
if (!file) return;
if (!file.name.match(/\.(xlsx|xls)$/i)) {
alert('選擇 Excel 檔案 (.xlsx 或 .xls)');
alert('選擇供應商窗口 Excel 檔');
return;
}

View File

@@ -276,6 +276,20 @@ def test_google_drive_auth_refuses_browser_without_json_token(monkeypatch, tmp_p
assert "google_token.pickle" in service.last_error
def test_google_drive_auth_paths_can_be_pinned_by_container_env(monkeypatch):
monkeypatch.setenv("GOOGLE_DRIVE_CREDENTIALS_FILE", "/app/config/google_credentials.json")
monkeypatch.setenv("GOOGLE_DRIVE_TOKEN_FILE", "/app/config/google_token.json")
monkeypatch.setenv("GOOGLE_DRIVE_LEGACY_PICKLE_FILE", "/app/config/google_token.pickle")
import services.google_drive_service as google_drive_service
google_drive_service = importlib.reload(google_drive_service)
assert google_drive_service.CREDENTIALS_FILE == "/app/config/google_credentials.json"
assert google_drive_service.TOKEN_FILE == "/app/config/google_token.json"
assert google_drive_service._LEGACY_PICKLE_FILE == "/app/config/google_token.pickle"
def test_google_drive_auth_fails_when_token_cannot_be_persisted(monkeypatch, tmp_path):
import services.google_drive_service as google_drive_service

View File

@@ -62,14 +62,13 @@ def test_high_visibility_pages_use_traditional_chinese_labels():
]
combined = "\n".join((ROOT / path).read_text(encoding="utf-8") for path in page_paths)
assert "AI 程式碼審查" in combined
assert "部署守門與程式碼審查" in combined
assert "等待程式碼審查完成" in combined
assert "AI 自動化健康檢查" in combined
assert "四 Agent 控制面" in combined
assert "AI 閉環守門" in combined
assert "產線健康度" in combined
assert "工作隊列" in combined
assert "覆蓋率流程" in combined
assert "NemoTron · 派遣器" in combined
assert "同步部署" in combined
assert "部署監控" in combined
assert "最新部署流程" in combined
@@ -578,6 +577,8 @@ def test_price_comparison_page_is_action_oriented_and_plain_chinese():
assert "data-momo-url" in template
assert "Excel 至少要有商品名稱與售價" in template
assert "貼上 MOMO 商品、售價與賣場連結" in template
assert "先選擇 MOMO 商品檔案。" in template
assert "先貼上商品、售價與賣場連結。" in template
assert "resetComparisonResult" in template
assert "showToast" in template
assert "text.textContent = message" in template
@@ -585,6 +586,9 @@ def test_price_comparison_page_is_action_oriented_and_plain_chinese():
assert "格式說明" not in template
assert "商品名稱,價格" not in template
assert "欄位" not in template
assert "系統會整理成可比價清單" not in template
assert "請選擇檔案" not in template
assert "請輸入商品資料" not in template
assert "Step 1" not in template
assert "Step 2" not in template
assert "Step 3" not in template
@@ -597,6 +601,35 @@ def test_price_comparison_page_is_action_oriented_and_plain_chinese():
assert "render_template('price_comparison.html', active_page='price_comparison')" in route_source
def test_utility_pages_keep_operator_copy_professional():
ppt_history = (ROOT / "templates/admin/ppt_audit_history.html").read_text(encoding="utf-8")
ppt_preview = (ROOT / "templates/admin/ppt_audit_preview.html").read_text(encoding="utf-8")
auto_import = (ROOT / "templates/auto_import_index.html").read_text(encoding="utf-8")
stockout_import = (ROOT / "templates/vendor_stockout_import_v2.html").read_text(encoding="utf-8")
observability_js = (ROOT / "web/static/js/observability-charts.js").read_text(encoding="utf-8")
combined = "\n".join([ppt_history, ppt_preview, auto_import, stockout_import, observability_js])
assert "簡報線上預覽" in ppt_preview
assert "下載簡報檔" in ppt_history
assert "送出後更新日報、成長分析與今日作戰清單" in auto_import
assert "缺少必要資料時,會先停止匯入" in stockout_import
assert "部署檢查已排入背景處理" in observability_js
forbidden = [
"PPT Online Preview",
"Preview unavailable",
"原始 PPTX",
"系統會去重",
"系統會拒絕",
"管線 ID",
"Commit:",
"變更檔案:",
"查看系統日誌",
]
for marker in forbidden:
assert marker not in combined
def test_ai_history_uses_v2_shell_and_real_history_apis():
template = (ROOT / "templates/ai_history.html").read_text(encoding="utf-8")
route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")
@@ -1097,7 +1130,7 @@ def test_vendor_stockout_import_v2_is_feature_flagged_and_does_not_ship_sample_r
assert "template_columns = [" in template_function
assert "pd.DataFrame(columns=template_columns)" in template_function
assert "fetchWithCSRF('/vendor-stockout/api/import/excel'" in template
assert "vendor_stockout" in template
assert "vendor-stockout/api/import/excel" in template
assert "範例" not in template_function
assert "ui='v2'" not in template
assert "mock" not in template.lower()

View File

@@ -543,79 +543,18 @@ def test_growth_analysis_uses_actionable_price_command_panel():
assert "高信心門檻" not in template
assert "未知新鮮度" not in template
assert "人工否決" not in template
assert "價格風險分佈" in template
assert "growthActionHint" in template
assert "growthDataSourceSummary" in template
assert "external_data_source_counts" in template
assert "compSourceSummary" in template
assert "growthDrilldownPanel" in template
assert "showGrowthDetail" in template
assert "growthDetailRows" in template
assert "growth-detail-tab" in template
assert "今日商品明細" in template
assert "價格壓力" in template
assert "價格優勢" in template
assert "有外部價" in template
assert "showPriceRiskDetail" in template
assert "handlePriceRiskKey" in template
assert "opportunities?limit=50" in template
assert "growthStrategyGrid" in template
assert "renderGrowthStrategySummary" in template
assert "商品策略分流" in template
assert "growth-strategy-card" in template
assert "aiRecsPanel" in template
assert "growth-detail-price-grid" in template
assert "growth-price-chip" in template
assert "formatGrowthDetailPrice" in template
assert "formatGapDisplay" in template
assert "growthDecisionSummary" in template
assert "renderGrowthDecisionSummary" in template
assert "growth-decision-metric" in template
assert "最大價差" in template
assert "growthDetailSearch" in template
assert "growthDetailSort" in template
assert "setGrowthDetailSearch" in template
assert "setGrowthDetailSort" in template
assert "clearGrowthDetailFilters" in template
assert "價差大到小" in template
assert "growthProductDecisionPanel" in template
assert "showGrowthProductDetail" in template
assert "growth-product-evidence-grid" in template
assert "查看判斷" in template
assert "growthCategoryBoard" in template
assert "分類策略看板" in template
assert "showGrowthCategoryDetail" in template
assert "data-growth-action=\"show-category-detail\"" in template
assert "growthPlaybookBoard" in template
assert "銷售策略建議" in template
assert "showGrowthPlaybookDetail" in template
assert "組合 / 單位價" in template
assert "growthActionBoard" in template
assert "今日策略動作" in template
assert "renderGrowthActionBoard" in template
assert "PChome 業績成長系統" in template
assert "評估業績、分析價差、決定今天的解法。" in template
assert "growth-command-pro" in template
assert "max-width: 1480px" in template
assert "commandSales7d" in template
assert "commandTopDecliners" in template
assert "PChome 與 MOMO 價格狀態" in template
assert "growth-command-alert-copy" in template
assert "growth-command-alert-action" in template
assert "width: min(100%, 980px)" in template
assert "grid-template-columns: auto minmax(0, 1fr)" in template
assert "renderGrowthCommandCenter" in template
assert "growthActionPlanForRow" in template
assert "growthActionEvidence" in template
assert "growth-action-evidence-chip" in template
assert "document.querySelectorAll('.growth-detail-row')" in template
assert "scrollToPanel('externalPricePanel')" in template
assert "備援資料檢查" in template
assert "可直接決策" in template
assert "chart_data.competitor_coverage" in template
assert "先補齊高業績商品的 MOMO 對應" in template
assert "先刷新過期價格" in template
assert "先處理待補與候選確認" in template
assert "可進入價格策略檢查" in template
assert "MOMO 低價壓力趨勢" in template
assert "PChome 價格優勢" in template
assert "competitorPressureChart" in template
assert "growth-command-pro" not in template
assert "growth-ops-table" not in template
assert "外部報價預檢" not in template
assert "growth-ops-table" in template
assert "鎖定商品" in template
assert "無法比價" in template
assert "補齊比價資料" in template
def test_formal_homepage_routes_to_growth_command_center():
@@ -777,10 +716,10 @@ def test_help_empty_login_and_ppt_copy_are_action_oriented():
"templates/growth_analysis.html": ["先匯入月度業績資料再評估成長、AOV 與毛利缺口"],
"templates/index.html": ["缺貨處理順序", "避免主推商品斷貨"],
"templates/login.html": ["登入後先看 PChome 業績、價差、缺貨與 AI 建議"],
"templates/system_settings.html": ["系統會更新對應年月份的業績資料"],
"templates/system_settings.html": ["會更新對應年月份的業績資料"],
"templates/admin/ppt_audit_history.html": [
"先確認簡報可預覽、可審核,再把問題交給修復流程",
"先補齊視覺 QA runtime,再判斷簡報品質",
"先補齊視覺 QA 執行條件,再判斷簡報品質",
"先補排程或手動產出,再進行視覺 QA",
"先補視覺 QA 條件",
],
@@ -847,7 +786,7 @@ def test_governance_and_low_frequency_pages_avoid_engineering_status_copy():
"templates/maintenance.html": ["服務維護", "確認業績、比價與匯入狀態", "台北時間"],
"templates/auto_import_index.html": [
"更新日報、成長分析與今日作戰清單",
"去重後寫入業績快照",
"送出後更新日報、成長分析與今日作戰清單",
"作戰清單保持新鮮",
"共更新",
"buildImportActionHint",
@@ -963,7 +902,7 @@ def test_visible_operations_pages_hide_internal_runtime_terms():
"templates/daily_sales.html": ["左右滑動看業績趨勢", "左右滑動看分類明細"],
"templates/sales_analysis.html": ["分析下一步", "選類別看貢獻", "適合檔期與活動回顧"],
"templates/vendor_stockout_vendor_management_v2.html": ["匯入供應商窗口名單", "確認窗口清單"],
"templates/vendor_stockout_import_v2.html": ["系統會拒絕匯入", "處理缺貨清單"],
"templates/vendor_stockout_import_v2.html": ["會先停止匯入", "處理缺貨清單"],
"templates/admin/ppt_audit_history.html": ["產出紀錄", "最近產出", "保存紀錄"],
"templates/admin/agent_orchestration.html": ["AI 分工指揮台", "建議路徑、工具與知識命中矩陣", "工具服務明細"],
"templates/admin/ai_calls_dashboard.html": ["用量", "AI 上下文", "知識與工具編排矩陣"],

View File

@@ -407,18 +407,18 @@
}
window.triggerCodeReview = async function triggerCodeReview() {
if (!confirm('觸發程式碼審查管線?\n\n會對最新 commit 跑 5 步驟審查,背景執行。')) return;
if (!confirm('啟動部署檢查流程?\n\n會在背景檢查最新更新。')) return;
try {
const response = await postJson('/observability/ai_calls/trigger_code_review', { method: 'POST' });
const data = await response.json();
if (data.ok) {
alert(`${data.message}\n\n管線 ID: ${data.pipeline_id}\nCommit: ${data.commit_sha}\n變更檔案: ${data.changed_files_count}`);
alert('部署檢查已排入背景處理,完成後可回到觀測台查看結果。');
} else {
alert(`${data.error || '觸發失敗'}`);
}
} catch (error) {
console.warn('code_review_trigger_failed', error);
alert('操作暫時無法完成,請稍後再試或查看系統日誌。');
alert('操作暫時無法完成,請稍後再試或查看處理狀態。');
}
};
@@ -438,7 +438,7 @@
}
} catch (error) {
console.warn('budget_force_throttle_failed', error);
alert('操作暫時無法完成,請稍後再試或查看系統日誌。');
alert('操作暫時無法完成,請稍後再試或查看處理狀態。');
}
};
@@ -472,7 +472,7 @@
}
} catch (error) {
console.warn('budget_save_failed', error);
alert('操作暫時無法完成,請稍後再試或查看系統日誌。');
alert('操作暫時無法完成,請稍後再試或查看處理狀態。');
button.disabled = false;
button.innerHTML = '<i class="fas fa-save me-1"></i>儲存';
}
@@ -491,7 +491,7 @@
}
} catch (error) {
console.warn('playbook_toggle_failed', error);
alert('操作暫時無法完成,請稍後再試或查看系統日誌。');
alert('操作暫時無法完成,請稍後再試或查看處理狀態。');
}
};
@@ -512,7 +512,7 @@
}
} catch (error) {
console.warn('host_autoheal_failed', error);
alert('操作暫時無法完成,請稍後再試或查看系統日誌。');
alert('操作暫時無法完成,請稍後再試或查看處理狀態。');
}
};
@@ -538,7 +538,7 @@
}
} catch (error) {
console.warn('promotion_approve_failed', error);
alert('操作暫時無法完成,請稍後再試或查看系統日誌。');
alert('操作暫時無法完成,請稍後再試或查看處理狀態。');
button.disabled = false;
button.innerHTML = '<i class="fas fa-check me-1"></i>通過晉升';
}
@@ -567,7 +567,7 @@
}
} catch (error) {
console.warn('promotion_reject_failed', error);
alert('操作暫時無法完成,請稍後再試或查看系統日誌。');
alert('操作暫時無法完成,請稍後再試或查看處理狀態。');
button.disabled = false;
button.innerHTML = '<i class="fas fa-times me-1"></i>拒絕';
}
@@ -615,7 +615,7 @@
}
if (statusNode) {
statusNode.classList.remove('is-working');
statusNode.textContent = '修復派工失敗,請稍後再試或查看系統日誌。';
statusNode.textContent = '修復派工失敗,請稍後再試或查看處理狀態。';
}
}
};
@@ -837,7 +837,7 @@
});
if (pageStatus) {
pageStatus.classList.remove('is-working');
pageStatus.textContent = '視覺 QA 送出失敗,請稍後再試或查看系統日誌。';
pageStatus.textContent = '視覺 QA 送出失敗,請稍後再試或查看處理狀態。';
}
}
}
@@ -1000,7 +1000,7 @@
console.warn('ppt_auto_generation_failed', error);
if (status) {
status.classList.remove('is-working');
status.textContent = '補齊任務送出失敗,請稍後再試或查看系統日誌。';
status.textContent = '補齊任務送出失敗,請稍後再試或查看處理狀態。';
}
if (targetButton) {
targetButton.disabled = false;
@@ -1098,7 +1098,7 @@
body.innerHTML = html;
} catch (error) {
console.warn('rag_query_hits_load_failed', error);
body.innerHTML = '<div class="alert alert-danger">❌ 召回詳情暫時無法載入,請稍後再試或查看系統日誌。</div>';
body.innerHTML = '<div class="alert alert-danger">❌ 召回詳情暫時無法載入,請稍後再試或查看處理狀態。</div>';
}
};