fix: harden import auth and utility page copy
Some checks failed
CD Pipeline / deploy (push) Failing after 6m19s
Some checks failed
CD Pipeline / deploy (push) Failing after 6m19s
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,背景匯入不得因主機重啟或工作目錄變動而改找瀏覽器授權。 |
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>更新今日建議
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>下載範本
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 上下文", "知識與工具編排矩陣"],
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user