diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt
index f27cfdb..2e05b9b 100644
--- a/TODO_NEXT_STEPS.txt
+++ b/TODO_NEXT_STEPS.txt
@@ -1,4 +1,37 @@
+================================================================================
+ 前端 V3 守門落地 + FastAPI 重新評估 (2026-05-12) [IN PROGRESS]
+================================================================================
+
+【已完成】
+ - 新版設計系統資產已落到正式 Flask static path:`web/static/css/ewoooc-tokens.css`、`web/static/css/ewoooc-shell.css`、`web/static/css/ewoooc-dotmatrix.css`、`web/static/css/page-*.css` 與對應 `web/static/js/page-*.js`。
+ - V3 page-level 資產已補做設計規範清理:移除 UI 表面殘留 radial-gradient / pure-white rgba / `bg-white` header,AI 推薦頁 keyword 狀態改用語意 class。
+ - `ewoooc_base.html` 補 `extra_head` / `content` / `extra_scripts` 相容 block,支援新版包混用 block 命名,避免頁面空白或互動 JS 不輸出。
+ - Base CSS 載入順序已補齊 `ewoooc-tokens-v2-alias.css`,符合 V3 spec 的 tokens → alias → shell → dotmatrix 順序。
+ - `_ewoooc_shell.html` 更新為 V3 側欄結構,保留正式路由連結與 AI 觀測台完整導覽 label。
+ - 高風險替換已退回:`dashboard_v2.html` 與 `templates/admin/*` 觀測台頁保留現行真實功能版,拒絕使用會移除 PChome / AI 挑品 / 歷史價格 / 觀測台真實資料的 prototype 空殼。
+ - AI 觀測台 CSS canonical 與 Flask mirror 已通過同步檢查:`./scripts/quick_review.sh --check-observability-css`。
+ - AI 觀測台 UI guard 已更新為 V3 token 化規範,不再要求舊版 sidebar 漸層片段,並通過 `./scripts/quick_review.sh --observability-ui`。
+ - 新增 ADR-036:FastAPI 可作為 strangler migration 中期方向,但不得作為前端 V3 前置條件;後端重構先抽 service 與補 contract,不做 Flask→FastAPI 大爆炸。
+ - `/sales_analysis` 修正 V3 首次進頁 JSON context 合約:`category_data` 與模板 fallback 補齊,避免 Jinja `Undefined` 進入 `tojson` 造成 error handler。
+ - 本機瀏覽器 QA 完成:`/sales_analysis`、`/daily_sales`、`/monthly_summary_analysis`、`/ai_recommend`、`/auto_import`、`/vendor-stockout/*` 均載入 V3 shell、無錯誤 banner、console error 0。
+ - 正式環境已推版:白名單同步 55 個 V3 runtime 檔案與 `config.py` 版本號,僅 recreate `momo-app`,未碰 `momo-db`、未使用 `--remove-orphans`;`https://mo.wooo.work/health` 回報 V10.91,V3 頁面 smoke 與觀測台 production QA 均通過。
+ - 版本同步至 V10.91。
+ - 依使用者截圖回饋修正 `/daily_sales`:KPI 改為首屏摘要格、日曆密度提升、桌機 1440 與手機 390 均無整頁水平 overflow。
+ - 依使用者截圖回饋修正 `/edm`:寬螢幕內容最大寬度調整至 1600px、活動表格限制在局部 scroll container、手機版無整頁水平 overflow。
+ - 新增 `scripts/check_v3_responsive_ui.js`,覆蓋 23 個主要 route 的 desktop/mobile responsive smoke;本機通過 46/46。
+ - 移除 V3 主要頁面 CSS 內殘留負字距,符合新版排版規範。
+ - 版本同步至 V10.92。
+ - 正式環境 V10.92 已白名單部署:備份 `/tmp/codex_v3_1092_predeploy_20260512_164549.tgz`,只 recreate `momo-app`,未碰 `momo-db`、未使用 `--remove-orphans`。
+ - 正式驗證通過:`https://mo.wooo.work/health` 回報 V10.92,觀測台 production QA PASS,23 個主要 route 的 desktop/mobile responsive smoke 46/46 PASS。
+ - 人工視覺 QA 追修 `/growth_analysis` 手機 KPI 可讀性:KPI 卡片改為暖墨文字、左側 accent、明確數字層級,避免淡底反白造成首屏不可讀。
+ - 版本同步至 V10.93。
+
+【下次待辦】
+ - V10.93 白名單部署後補正式 health 與 responsive smoke 結果。
+ - 為 FastAPI strangler 補 endpoint inventory:先列 route owner、資料來源、auth/CSRF/session 需求與 smoke URL,再決定第一批可遷移 API。
+ - `production_v3 3/templates/dashboard.html` 與 `production_v3 3/templates/observability/*` 不得直接覆蓋正式頁;需先補回真實資料功能後才可進入替換候選。
+
================================================================================
跨平台市場情報模組 (ADR-035 / 2026-05-06) [IN PROGRESS]
================================================================================
@@ -10,11 +43,35 @@
- Phase 2 schema 骨架:新增 `database/market_intel_models.py`,定義 `market_platforms`、`market_campaigns`、`market_campaign_snapshots`、`market_campaign_products`、`market_product_price_history`、`market_product_matches`、`market_crawler_runs`。
- Metadata 守門:`database/manager.py` 顯式 import market_intel models,`app.py` expected metadata tables 已同步。
- 安全開關:`.env.example` 補 `MARKET_INTEL_ENABLED=false`、`MARKET_INTEL_CRAWLER_ENABLED=false`、`MARKET_INTEL_WRITE_ENABLED=false`。
+ - Phase 3 read-only adapter 骨架:新增 `services/market_intel/adapters/`,目前註冊 MOMO 與 PChome,僅回傳 discovery plan,不發 HTTP request、不寫 DB、不掛 scheduler。
+ - Phase 4 手動 dry-run discovery:新增 `services/market_intel/discovery_runner.py` 與 `/api/market_intel/manual_discovery`。預設 `fetch=false` 只回 planned;`fetch=true` 也必須在 `MARKET_INTEL_ENABLED` 與 `MARKET_INTEL_CRAWLER_ENABLED` 同時開啟時才允許 HTTP,且仍不寫 DB、不掛 scheduler。
+ - Phase 5 parser 診斷層:新增 `services/market_intel/html_diagnostics.py`,手動 fetch 成功後只萃取 title、page_hash、link count、campaign link candidates,不建立正式 campaign/product。
+ - Phase 6 平台別 scorer:MOMO/PChome adapter 提供 URL/text 加權規則,diagnostics 同時輸出 generic_score 與 platform_score,仍只用於候選排序與人工診斷。
+ - Phase 7 confidence bands:diagnostics 將候選標成 `high` / `medium` / `low`,並輸出 `confidence_reason`;只作人工審核提示,不自動建立活動。
+ - Phase 8 candidate preview API:新增 `/api/market_intel/candidate_preview`,把本次 discovery diagnostics 候選合併排序,支援 `min_band` 與 `limit`,只供人工預覽,不入庫。
+ - Phase 9 UI preview panel:`templates/market_intel/disabled.html` 讀取 `/api/market_intel/candidate_preview?fetch=false`,顯示安全空狀態與 flags,不做自動外部 fetch。
+ - Phase 10 platform seed plan:新增 `/api/market_intel/platform_seed_plan`,只產生 adapter registry 對應的 `market_platforms` seed rows 與 gate 狀態,不寫 DB。
+ - Phase 11 Coupang read-only adapter:新增 Coupang 酷澎 adapter,以官方台灣站與火箭跨境入口作為公開 discovery 起點,預設不發 request、不寫 DB、不掛 scheduler。
+ - Phase 12 Shopee read-only adapter:新增 Shopee 蝦皮 adapter,以公開首頁與 Shopee Mall 入口作為 discovery 起點;不得登入、碰會員券、購物車、帳號池或反爬繞過。
+ - Phase 13 platform seed write guard:新增 `/api/market_intel/platform_seed_write_guard`,只回報寫入前 gate 與阻擋原因,預設永遠不寫 DB。
+ - Phase 14 platform seed writer dry-run:新增 `/api/market_intel/schema_smoke` 與 `/api/market_intel/platform_seed_writer_plan`,實際檢查 ORM metadata 並產生 parameterized upsert preview,不建立 session、不 commit。
+ - Phase 15 writer preview panel:`templates/market_intel/disabled.html` 讀取 `/api/market_intel/platform_seed_writer_plan`,顯示 schema smoke、upsert preview 與 blocked reasons,仍不寫 DB。
+ - Phase 16 deployment readiness panel:新增 `/api/market_intel/deployment_readiness` 與 UI「推版準備檢查」,明確回報尚未正式推版、尚未 commit/push、部署 SOP 與 production smoke 尚待人工執行。
+ - Phase 17 deployment handoff checklist:`/api/market_intel/deployment_readiness` 補人工步驟、備援方案、安全部署邊界、production smoke targets 與 `python backup_system.py` 備份要求;UI 直接顯示,不執行任何 git/部署/DB 操作;版本同步至 V10.87。
+ - Phase 18 write approval runbook:新增 `/api/market_intel/write_approval_runbook` 與 UI「正式寫入批准檢查」,列出 schema smoke、備份、migration review、feature flag、人工批准、rollback 等 gate;預設 `ready_for_real_write=false`、`database_session_created=false`、`database_commit_executed=false`;版本同步至 V10.88。
+ - Phase 19 migration blueprint:新增 `/api/market_intel/migration_blueprint` 與 UI「Schema migration 草案」,產生 `migrations/032_market_intel_core_schema.sql` 建議內容、migration apply command shape 與 seed writer command design;預設 `file_created=false`、`migration_executed=false`、`database_commit_executed=false`,且 forward SQL 只允許 additive DDL;版本同步至 V10.89。
+ - Phase 20 migration file draft:新增本地草稿檔 `migrations/032_market_intel_core_schema.sql`,blueprint API 會檢查檔案存在且內容與 blueprint 相符;仍然 `migration_executed=false`、`database_session_created=false`、`database_commit_executed=false`;版本同步至 V10.90。
+ - Phase 21 seed writer CLI skeleton:新增 `scripts/market_intel_seed_writer.py`、`services/market_intel/seed_writer_cli.py` 與 `/api/market_intel/seed_writer_cli_status`;CLI 可輸出 seed writer blocked plan,但即使帶 `--execute` 與 approval token 也會拒絕真寫入,維持 `database_session_created=false`、`database_commit_executed=false`;版本同步至 V10.94。
+ - Schema smoke:`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 七張 `market_*` tables。
+ - Desktop UI QA:本機只註冊 `market_intel_bp` 的 Flask harness 載入 `/market_intel`,確認 Phase 15、候選預覽、writer preview、安全 flags、點陣暖紙視覺正常,console error 0。
+ - API QA:`/api/market_intel/schema_smoke` 通過 7 張表與 `market_platforms` 必要欄位檢查;`/api/market_intel/platform_seed_writer_plan` 回傳 4 筆 dry-run upsert preview,`writes_executed=false`,四平台皆 `blocked_dry_run_only`。
+ - Narrow viewport UI QA:in-app browser 窄版畫面確認側欄收合、卡片不水平爆版、候選狀態顯示 `momo/pchome/coupang/shopee:planned`。
+ - 測試:新增 `tests/test_market_intel_skeleton.py`,確認預設 flags 全關、adapter 不允許 network/write/scheduler,手動 discovery / candidate preview 預設不發 request,parser/scorer/confidence 診斷只輸出 JSON,UI 只使用 `fetch=false`,platform seed plan 只讀且需 gate。
【下次待辦】
- - 補 marketplace 平台種子資料策略,但正式寫入仍需 feature flag 與 migration/smoke gate。
- - 新增 `services/market_intel/adapters/base.py` 與第一個 read-only MOMO/PChome discovery adapter。
- - 新增 market_intel schema smoke test,確認 fresh env metadata 表名一致。
+ - 補 UI preview panel 真 390px 截圖 QA;本輪 in-app browser 不支援直接設定 viewport,且 data URL iframe QA 被瀏覽器安全策略阻擋,不做繞過。
+ - 正式推版前需實際執行 worktree scope review、`python backup_system.py`、commit/push 目標變更、讀 deployment SOP 與 ADR-011,且只跑 `/health` 與市場情報頁 smoke。
+ - 下一步才可在明確批准後設計 seed writer 的真寫入 implementation gate;預設環境不得寫 DB,也不得執行 migration。
- 市場情報 UI 後續頁面必須沿用 V2 暖紙、暖墨、等寬數字與點陣風格,禁止複製巨型分析頁 template 模式。
================================================================================
diff --git a/app.py b/app.py
index 1c6b68a..4e8d405 100644
--- a/app.py
+++ b/app.py
@@ -96,7 +96,11 @@ except Exception as e:
# 🚩 系統版本定義 (備份與顯示用)
# 🚩 2026-05-01 V10.76: Move monthly analysis report onto V2 shell
-SYSTEM_VERSION = "V10.86"
+# 🚩 2026-05-07 V10.90: Market intelligence migration file draft
+# 🚩 2026-05-12 V10.92: Frontend V3 responsive QA + daily_sales/edm fixes
+# 🚩 2026-05-12 V10.93: Growth analysis mobile KPI readability polish
+# 🚩 2026-05-12 V10.94: Market intelligence seed writer CLI skeleton
+SYSTEM_VERSION = "V10.94"
# ==========================================
# 🔒 SQL Injection 防護函數
diff --git a/config.py b/config.py
index 6a548c1..d2257ca 100644
--- a/config.py
+++ b/config.py
@@ -237,11 +237,15 @@ def get_hermes_url():
"""Lazy 取得 Hermes 主機(每次呼叫都嘗試 resolve,內部有 120s cache)。
優先序:
- 1. env HERMES_URL(明確指定,向下相容)
+ 1. env HERMES_URL(僅接受 GCP-A/GCP-B/111)
2. resolve_ollama_host()(GCP 優先 / 111 備援)
3. 兜底 http://192.168.0.111:11434(防 ImportError)
"""
- env_val = os.getenv('HERMES_URL')
+ try:
+ from services.ollama_service import approved_ollama_env
+ env_val = approved_ollama_env('HERMES_URL')
+ except Exception:
+ env_val = os.getenv('HERMES_URL', '').strip()
if env_val:
return env_val
try:
@@ -253,9 +257,13 @@ def get_hermes_url():
def get_embedding_host():
"""Lazy 取得 Embedding 主機。
- 優先序:env EMBEDDING_HOST > get_hermes_url()(GCP 優先 / 111 備援)。
+ 優先序:env EMBEDDING_HOST(僅接受 GCP-A/GCP-B/111)> get_hermes_url()。
"""
- env_val = os.getenv('EMBEDDING_HOST')
+ try:
+ from services.ollama_service import approved_ollama_env
+ env_val = approved_ollama_env('EMBEDDING_HOST')
+ except Exception:
+ env_val = os.getenv('EMBEDDING_HOST', '').strip()
if env_val:
return env_val
return get_hermes_url()
@@ -263,10 +271,14 @@ def get_embedding_host():
def get_ollama_host():
"""Lazy 取得 Ollama 主機(一般 LLM 推理用)。
- 優先序:env OLLAMA_HOST > resolve_ollama_host()(GCP 優先 / 111 備援)。
+ 優先序:env OLLAMA_HOST(僅接受 GCP-A/GCP-B/111)> resolve_ollama_host()。
與舊 module-level OLLAMA_HOST 不同:本函數不會 freeze 結果,每次呼叫都重新 resolve。
"""
- env_val = os.getenv('OLLAMA_HOST')
+ try:
+ from services.ollama_service import approved_ollama_env
+ env_val = approved_ollama_env('OLLAMA_HOST')
+ except Exception:
+ env_val = os.getenv('OLLAMA_HOST', '').strip()
if env_val:
return env_val
try:
@@ -314,7 +326,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
-SYSTEM_VERSION = "V10.86"
+SYSTEM_VERSION = "V10.94"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示
diff --git a/docs/adr/ADR-036-fastapi-strangler-not-frontend-prerequisite.md b/docs/adr/ADR-036-fastapi-strangler-not-frontend-prerequisite.md
new file mode 100644
index 0000000..b00b570
--- /dev/null
+++ b/docs/adr/ADR-036-fastapi-strangler-not-frontend-prerequisite.md
@@ -0,0 +1,50 @@
+# ADR-036: FastAPI Strangler Migration,不作為前端 V3 前置條件
+
+Status: Accepted
+Date: 2026-05-12
+
+## Context
+
+前端 V3 視覺包已進入落地評估,同時出現「既然前端要一次到位,後端也應同步從 Flask 改為 FastAPI」的建議。
+
+現有系統的重資產集中在 Python:爬蟲、AI 學習鏈、Telegram bot、APScheduler、SQLAlchemy models、pgvector、AutoHeal 與多個 service 模組。FastAPI 若被定位為 HTTP layer 轉換,理論上可沿用大部分 Python 業務邏輯;但目前 `app.py`、`routes/` 與 Jinja 頁面仍承接大量 HTTP glue、template context、CSRF/session、錯誤處理與部分業務邏輯。
+
+同時,前端 V3 包內部分頁面仍有正式資料接線風險:若只為了視覺更新直接替換,就可能移除現有 PChome 比價、AI 挑品、歷史價格、AI 觀測台等真實功能,違反 CONSTITUTION 第 14.1 條。
+
+## Decision
+
+FastAPI 是可接受的中期目標,但不得作為前端 V3 落地的前置條件,也不得與前端視覺更新綁成同一個大爆炸工程。
+
+採用以下順序:
+
+1. 前端 V3 先在現有 Flask runtime 內落地,僅替換已確認接上真實資料、既有 route/API 與行為不退化的頁面。
+2. 對於會移除現有功能的 V3 prototype 頁,先拒絕替換,保留現行正式頁。
+3. 後端重構優先做 HTTP-agnostic service extraction:把 route 內資料組裝、查詢、匯出、狀態診斷逐步搬到 `services/`,讓 Flask route 變薄。
+4. FastAPI 僅能以 strangler pattern 漸進引入:新 API 或已抽乾的 API 可掛在新 app,經 OpenAPI contract、測試與 Nginx path split 驗證後再切流。
+5. 不採用 Node 重寫後端。Next.js 前端若需要型別,未來由 OpenAPI 產生 client/types,不以「前後端同語言」作為重寫理由。
+
+## Alternatives Considered
+
+### A. 前端 V3 與 FastAPI Phase 1 同時執行
+
+拒絕。這會同時改動 template/rendering、API boundary、auth/session、CSRF、Nginx、Docker、health check 與 deploy rollback。對目前生產系統而言,風險面過大,也會把原本可獨立驗收的 UI 工作變成跨層遷移。
+
+### B. 維持 Flask 永久不動
+
+拒絕作為長期結論。Flask 可繼續承接現有 Jinja/route,但 OpenAPI、型別 client、async API 與新前端資料邊界確實更適合用 FastAPI 承接。問題不是是否能換,而是何時、以什麼切面換。
+
+### C. Node/Next API 重寫
+
+拒絕。核心業務與自動化資產都在 Python,改 Node 等同重寫爬蟲、AI、排程、Telegram 與資料層,收益不足以抵銷風險。
+
+## Consequences
+
+- 前端 V3 可以立即繼續落地,但每頁必須通過真實資料與行為不退化檢查。
+- FastAPI migration 的第一步不是開新框架,而是清 route、補 contract、抽 services。
+- 若未來建立 `momo-fastapi` service,必須先有:
+ - endpoint inventory 與 owner mapping;
+ - auth/session/CSRF 或 token strategy;
+ - OpenAPI contract test;
+ - `/health`、production smoke、rollback plan;
+ - Nginx path split 只切已驗證 path。
+- Flask、Jinja 與 `templates/` 的刪除只能在所有對應頁面完成遷移並通過 smoke 後進行。
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 33c0f4c..097afe0 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -57,6 +57,7 @@
| [033](ADR-033-rag-three-guardrails.md) | RAG 治理三護欄 — Promotion Gate / Firecrawl 資源 / BGE-M3 一致性(Owen v5.0 鐵律) | Accepted | 2026-05-03 |
| [034](ADR-034-dynamic-model-router.md) | Caller × Context 動態 Model Router(短文 gemma3 / 複雜 SKU qwen3:14b / 重構 coder:32b) | Accepted | 2026-05-04 |
| [035](ADR-035-cross-platform-market-campaign-intelligence.md) | 跨平台市場活動情報系統 | Accepted | 2026-05-06 |
+| [036](ADR-036-fastapi-strangler-not-frontend-prerequisite.md) | FastAPI Strangler Migration,不作為前端 V3 前置條件 | Accepted | 2026-05-12 |
## 規範
diff --git a/docs/memory/README.md b/docs/memory/README.md
index 43e3e5d..78a27cb 100644
--- a/docs/memory/README.md
+++ b/docs/memory/README.md
@@ -22,6 +22,7 @@
| `project_phase3f_cleanup_roadmap.md` | ADR-017 執行矩陣與階段紅線 | 正在做 3f 模組化收尾時 |
| `code_modularization_inventory_20260430.md` | Python 大檔盤點、分層規範與拆分工作項目 | 新增功能、拆大檔、審查是否違反模組化治理時 |
| `schema_inventory_baseline.md` | DB 表分類與 drift 基線 | 要收斂 migration / ORM / raw SQL 真相時 |
+| `frontend_v3_handoff_20260512.md` | 前端 V3 守門落地、正式部署、UI/UX 響應式缺口與接續順序 | 新 session 接續前端 V3 全站 UI/UX、手機版確認、`/daily_sales` 或 `/edm` 修正與部署時 |
## 關聯 Guide
diff --git a/docs/memory/frontend_v3_handoff_20260512.md b/docs/memory/frontend_v3_handoff_20260512.md
new file mode 100644
index 0000000..485ee92
--- /dev/null
+++ b/docs/memory/frontend_v3_handoff_20260512.md
@@ -0,0 +1,142 @@
+# Frontend V3 Handoff 2026-05-12
+
+## 目的
+
+這份 handoff 給下一個 Codex session 接續「前端 V3 全站 UI/UX 與響應式修正」使用。
+
+## 已完成
+
+- 已閱讀並遵守專案入口規則:`AGENTS.md`、`CONSTITUTION.md`、ADR-011 部署紅線、部署 SOP。
+- 已評估 Claude Code 的「立刻 Flask → FastAPI」建議,結論改為:
+ - FastAPI 可作為 strangler migration 中期方向。
+ - 不作為前端 V3 落地前置條件。
+ - 不做大爆炸式後端重寫。
+- 已新增 `docs/adr/ADR-036-fastapi-strangler-not-frontend-prerequisite.md`,並更新 `docs/adr/README.md`。
+- 已把 V3 shell/tokens/page assets 落到 Flask runtime:
+ - `templates/ewoooc_base.html`
+ - `templates/components/_ewoooc_shell.html`
+ - `web/static/css/ewoooc-tokens.css`
+ - `web/static/css/ewoooc-shell.css`
+ - `web/static/css/ewoooc-dotmatrix.css`
+ - `web/static/css/ewoooc-tokens-v2-alias.css`
+ - 多個 `web/static/css/page-*.css`
+ - 多個 `web/static/js/page-*.js`
+- 已保留真實功能頁,拒絕直接用 `production_v3 3/templates/dashboard.html` 與 observability prototype 覆蓋正式頁,因為會移除 PChome、AI 挑品、歷史價格、觀測台真實資料。
+- 已修 `/sales_analysis` 首次進頁錯誤:
+ - `routes/sales_routes.py` 補 `category_data=cat_data`。
+ - `templates/sales_analysis.html` 對 chart JSON payload 補 default fallback,避免 Jinja `Undefined` 進 `tojson`。
+- 已同步版本:
+ - `app.py` `SYSTEM_VERSION = "V10.91"`
+ - `config.py` `SYSTEM_VERSION = "V10.91"`
+- 已更新 `TODO_NEXT_STEPS.txt`,記錄 V3 守門落地、正式推版與後續 FastAPI inventory。
+
+## 已部署到正式環境
+
+- 正式 URL:`https://mo.wooo.work`
+- 部署方式:白名單 tarball 同步 55 個 V3 runtime 檔案,再單獨同步 `config.py`。
+- 正式端只 recreate `momo-app`:
+ - `docker compose up -d --no-deps --force-recreate momo-app`
+- 未碰 `momo-db`。
+- 未使用 `--remove-orphans`。
+- 正式回滾備份:
+ - runtime files:`/tmp/codex_v3_predeploy_20260512_144835.tgz`
+ - config:`/tmp/config.py.pre_v3_20260512_145111`
+
+## 已通過驗證
+
+- 本機 focused pytest:`75 passed, 1 warning`。
+- `git diff --check` 通過。
+- `./scripts/quick_review.sh --check-observability-css` 通過。
+- `./scripts/quick_review.sh --observability-ui` 通過。
+- `./scripts/quick_review.sh --observability-qa --skip-production` 通過。
+- 正式:
+ - `https://mo.wooo.work/health` 回報 `V10.91`。
+ - `momo-pro-system`、`momo-scheduler`、`momo-telegram-bot`、`momo-db` 全部 healthy。
+ - `./scripts/quick_review.sh --observability-qa --base-url https://mo.wooo.work` 通過。
+ - V3 smoke 200:`/sales_analysis`、`/daily_sales`、`/monthly_summary_analysis`、`/ai_recommend`、`/auto_import`、`/vendor-stockout/*`、`/growth_analysis`、`/settings`、`/logs`、`/`。
+
+## 2026-05-12 V10.92 追更
+
+- 已完成 `/daily_sales` 本機 route 驗證與 responsive 修正。
+- 已完成 `/edm` 桌機外溢與 mobile responsive containment 修正。
+- 已將 V3 內容最大寬度調整為 `1600px`,改善寬螢幕活動頁與資料表使用空間。
+- 已移除 V3 主要頁面 CSS 中殘留的負字距。
+- 已新增全頁 responsive overflow 自動化:
+ - `scripts/check_v3_responsive_ui.js`
+- 本機全頁 responsive audit 已通過,覆蓋 23 個主要 route,desktop 1440 與 mobile 390 共 46 項皆 PASS。
+- 本機 observability QA 已通過:
+ - focused pytest `75 passed, 1 warning`
+ - `git diff --check`
+ - `scripts/check_v3_responsive_ui.js`
+- 已白名單部署到正式環境 `https://mo.wooo.work`,版本升至 `V10.92`。
+- 正式端只 recreate `momo-app`:
+ - `docker compose up -d --no-deps --force-recreate momo-app`
+- 未碰 `momo-db`,未使用 `--remove-orphans`。
+- 正式回滾備份:
+ - `/tmp/codex_v3_1092_predeploy_20260512_164549.tgz`
+- 正式驗證已通過:
+ - `/health` 回報 `V10.92`。
+ - `momo-pro-system`、`momo-scheduler`、`momo-telegram-bot`、`momo-db` 全部 healthy。
+ - `scripts/check_v3_responsive_ui.js` 對 `https://mo.wooo.work` 通過:23 route × desktop/mobile = 46/46 PASS。
+ - `./scripts/quick_review.sh --observability-qa --base-url https://mo.wooo.work` 通過。
+
+## 重要使用提醒
+
+- 不要用 `file://.../templates/*.html` 預覽 Jinja 模板。它需要 Flask route 注入 `url_for()`、context 與 session。
+- 正確本機預覽方式:
+ - 啟動 Flask:`LOGIN_PASSWORD=codex-local-password SECRET_KEY=codex-local-secret USE_POSTGRESQL=false .venv/bin/flask --app app:app run --host 127.0.0.1 --port 5003`
+ - 用瀏覽器開:`http://127.0.0.1:5003/daily_sales`、`http://127.0.0.1:5003/edm` 等 route。
+- 使用 Browser plugin 時,應導到 localhost route,不要開 template 檔。
+
+## 未完成
+
+- 使用者指出「很多頁面排版都有問題,且必須全部確認手機版」。
+- `/daily_sales`、`/edm`、全頁 responsive overflow 自動化與 V10.92 正式部署已完成。
+- 尚未完成所有頁面的人工 desktop/mobile 視覺逐頁設計審查;目前完成的是自動化 overflow guard 與重點頁抽看。
+- 若下一輪繼續 UI/UX,建議從「自動化已過但人工視覺仍需精修」的頁面開始,例如長表格密度、空狀態、文案層級與 mobile 互動細節。
+
+## 下一個 session 建議順序
+
+1. 啟動本機 Flask server,導 in-app browser 到目標 route。
+2. 用 desktop 與 mobile 寬度檢查:
+ - 1365/1440 desktop
+ - 390 mobile
+ - 不得有 body horizontal overflow。
+ - text 不得重疊或被裁切。
+ - 表格/日曆若需要橫向滾動,必須有明確容器與提示,不能讓整頁外溢。
+3. 每輪 UI 變更後跑全頁 responsive overflow guard:`scripts/check_v3_responsive_ui.js`,至少覆蓋:
+ - `/`
+ - `/edm`
+ - `/sales_analysis`
+ - `/daily_sales`
+ - `/monthly_summary_analysis`
+ - `/growth_analysis`
+ - `/ai_recommend`
+ - `/auto_import`
+ - `/vendor-stockout/`
+ - `/vendor-stockout/import`
+ - `/vendor-stockout/list`
+ - `/settings`
+ - `/logs`
+ - `/observability/*` 10 頁
+4. 全頁本機 QA 通過後,才按部署 SOP 白名單同步正式。
+
+## 當前工作樹注意事項
+
+- 工作樹很髒,混有多批未提交變更與大量 untracked 原型/文件/前端資料夾。
+- 不要整包 rsync 或整包 commit。
+- 不要 revert 使用者或其他 session 的變更。
+- 部署只能用白名單。
+- 特別避免同步:
+ - `.claude/`
+ - `production_v3*/`
+ - `frontend/`
+ - `MOMO Pro/uploads/`
+ - `data/dashboard_full_cache.pkl`
+ - 未確認的 docs/design dump
+
+## 已知本機/正式 warning
+
+- 本機 Python 3.9 / LibreSSL 會有 urllib3 warning。
+- 本機 SQLite 缺 `embedding_retry_queue` 時 OCLearn worker 會 warning。
+- 正式 compose 會警告 `momo-db` orphan,這是 ADR-011 已知狀態,禁止 `--remove-orphans`。
diff --git a/routes/sales_routes.py b/routes/sales_routes.py
index f82da9b..3106e17 100644
--- a/routes/sales_routes.py
+++ b/routes/sales_routes.py
@@ -330,6 +330,7 @@ def sales_analysis():
seasonality_data={'datasets': [], 'yLabels': [], 'xLabels': []},
bar_data=empty_data,
cat_data=empty_data,
+ category_data=empty_data,
price_dist_data=empty_data,
scatter_data=[],
bcg_data=[],
@@ -1199,6 +1200,7 @@ def sales_analysis():
seasonality_data=seasonality_data, # V-New: 傳遞淡旺季數據
bar_data=bar_data,
cat_data=cat_data,
+ category_data=cat_data,
price_dist_data=price_dist_data,
scatter_data=scatter_data,
bcg_data=bcg_data, # V-New: 傳遞 BCG 數據
@@ -1279,6 +1281,35 @@ def growth_analysis():
)
import time
+ def _render_growth_empty(message=None):
+ now_taipei = datetime.now(TAIPEI_TZ)
+ empty_chart_data = {
+ 'labels': [],
+ 'revenue': [],
+ 'profit': [],
+ 'orders': [],
+ 'aov': [],
+ 'mom': [],
+ 'yoy': [],
+ 'margin_rate': []
+ }
+ empty_kpi = {
+ 'ytd_revenue': 0,
+ 'ytd_growth': 0,
+ 'current_year': now_taipei.year,
+ 'recent_aov': 0,
+ 'total_orders': 0
+ }
+ return render_template('growth_analysis.html',
+ chart_data=empty_chart_data,
+ kpi=empty_kpi,
+ datetime_now=now_taipei.strftime('%Y-%m-%d %H:%M:%S'),
+ active_page='growth',
+ cache_hit=False,
+ cache_age=0,
+ is_empty_state=True,
+ empty_message=message)
+
try:
start_time = time.time()
@@ -1305,13 +1336,13 @@ def growth_analysis():
inspector = inspect(db.engine)
if table_name not in inspector.get_table_names():
- return f"尚未匯入業績資料 ({table_name})", 404
+ return _render_growth_empty(f"尚未匯入業績資料 ({table_name})")
req_cols = ['日期', '總業績', '訂單編號', '總成本']
df = safe_read_sql(table_name, columns=req_cols, engine=db.engine)
if df.empty:
- return f"資料表 {table_name} 為空", 404
+ return _render_growth_empty(f"資料表 {table_name} 為空")
df['dt'] = pd.to_datetime(df['日期'], errors='coerce')
df = df.dropna(subset=['dt'])
diff --git a/scripts/check_observability_ui.py b/scripts/check_observability_ui.py
index 18149bb..6e3ee11 100644
--- a/scripts/check_observability_ui.py
+++ b/scripts/check_observability_ui.py
@@ -96,15 +96,13 @@ REQUIRED_SHELL_SNIPPETS = [
"momo-nav-subtree",
"momo-nav-subtitle",
"momo-nav-sublink",
- "rgba(255, 248, 238, 0.72)",
- "linear-gradient(180deg",
]
FORBIDDEN_SHELL_PATTERNS = [
Rule(
"pure_black_sidebar",
re.compile(r"\.momo-sidebar\s*\{[^}]*background\s*:\s*(#000|black)\b", re.IGNORECASE | re.DOTALL),
- "側欄不得回退成純黑背景;必須維持暖深咖啡漸層。",
+ "側欄不得回退成純黑背景;必須維持暖色 token 化背景。",
),
Rule(
"low_contrast_sublink",
diff --git a/scripts/check_v3_responsive_ui.js b/scripts/check_v3_responsive_ui.js
new file mode 100644
index 0000000..63dc13b
--- /dev/null
+++ b/scripts/check_v3_responsive_ui.js
@@ -0,0 +1,159 @@
+#!/usr/bin/env node
+/**
+ * V3 responsive UI smoke.
+ *
+ * Checks the main Flask pages at desktop and mobile widths:
+ * - route renders without 4xx/5xx
+ * - route does not fall back to the login form
+ * - document body does not create page-level horizontal overflow
+ *
+ * Usage:
+ * LOGIN_PASSWORD=... BASE_URL=http://127.0.0.1:5003 \
+ * NODE_PATH=/path/to/node_modules node scripts/check_v3_responsive_ui.js
+ */
+
+const { chromium } = require('playwright');
+
+const BASE_URL = process.env.BASE_URL || 'http://127.0.0.1:5003';
+const LOGIN_PASSWORD = process.env.LOGIN_PASSWORD || 'codex-local-password';
+const CHROME_PATH = process.env.PLAYWRIGHT_CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+
+const ROUTES = [
+ '/',
+ '/edm',
+ '/sales_analysis',
+ '/daily_sales',
+ '/monthly_summary_analysis',
+ '/growth_analysis',
+ '/ai_recommend',
+ '/auto_import',
+ '/vendor-stockout/',
+ '/vendor-stockout/import',
+ '/vendor-stockout/list',
+ '/settings',
+ '/logs',
+ '/observability/overview',
+ '/observability/agent_orchestration',
+ '/observability/business_intel',
+ '/observability/host_health',
+ '/observability/ai_calls',
+ '/observability/budget',
+ '/observability/promotion_review',
+ '/observability/rag_queries',
+ '/observability/quality_trend',
+ '/observability/ppt_audit_history',
+];
+
+const VIEWPORTS = [
+ { name: 'desktop', width: 1440, height: 1000 },
+ { name: 'mobile', width: 390, height: 900 },
+];
+
+async function login(context) {
+ const page = await context.newPage();
+ await page.goto(`${BASE_URL}/login`, { waitUntil: 'domcontentloaded', timeout: 20000 });
+
+ const passwordInput = page.locator('input[name="password"]');
+ if ((await passwordInput.count()) === 0) {
+ await page.close();
+ return;
+ }
+
+ await passwordInput.fill(LOGIN_PASSWORD);
+ await Promise.all([
+ page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => null),
+ page.locator('button[type="submit"]').click(),
+ ]);
+ await page.close();
+}
+
+async function inspectRoute(page, route) {
+ const response = await page.goto(`${BASE_URL}${route}`, {
+ waitUntil: 'domcontentloaded',
+ timeout: 25000,
+ });
+ await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => null);
+
+ const metrics = await page.evaluate(() => {
+ const maxScroll = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth);
+ const client = document.documentElement.clientWidth;
+ const localWide = Array.from(document.querySelectorAll('body *'))
+ .filter((element) => {
+ const rect = element.getBoundingClientRect();
+ const style = getComputedStyle(element);
+ return rect.width > client + 1 && style.position !== 'fixed' && style.position !== 'absolute';
+ })
+ .slice(0, 4)
+ .map((element) => ({
+ tag: element.tagName,
+ className: String(element.className || ''),
+ width: Math.round(element.getBoundingClientRect().width),
+ }));
+
+ return {
+ title: document.title,
+ finalPath: `${location.pathname}${location.search}`,
+ login: Boolean(document.querySelector('form[action="/login"]')),
+ clientWidth: client,
+ scrollWidth: maxScroll,
+ overflow: maxScroll > client + 1,
+ localWide,
+ };
+ });
+
+ return {
+ route,
+ status: response ? response.status() : null,
+ ...metrics,
+ };
+}
+
+(async () => {
+ const browser = await chromium.launch({
+ headless: true,
+ executablePath: CHROME_PATH,
+ });
+
+ const failures = [];
+ for (const viewport of VIEWPORTS) {
+ const context = await browser.newContext({
+ viewport: { width: viewport.width, height: viewport.height },
+ });
+ await login(context);
+
+ for (const route of ROUTES) {
+ const page = await context.newPage();
+ try {
+ const result = await inspectRoute(page, route);
+ const failed = result.status >= 400 || result.login || result.overflow;
+ const line = [
+ failed ? 'FAIL' : 'PASS',
+ viewport.name,
+ route,
+ `status=${result.status}`,
+ `overflow=${result.overflow}`,
+ `scroll=${result.scrollWidth}`,
+ `client=${result.clientWidth}`,
+ `title=${result.title}`,
+ ].join(' ');
+ console.log(line);
+ if (failed) failures.push({ viewport: viewport.name, ...result });
+ } catch (error) {
+ const failure = { viewport: viewport.name, route, error: error.message.split('\n')[0] };
+ failures.push(failure);
+ console.log(`FAIL ${viewport.name} ${route} error=${failure.error}`);
+ } finally {
+ await page.close();
+ }
+ }
+
+ await context.close();
+ }
+
+ await browser.close();
+
+ if (failures.length > 0) {
+ console.error(JSON.stringify({ failures }, null, 2));
+ process.exit(1);
+ }
+})();
diff --git a/templates/ai_recommend.html b/templates/ai_recommend.html
index 5f42a62..57e6f23 100644
--- a/templates/ai_recommend.html
+++ b/templates/ai_recommend.html
@@ -2,1682 +2,448 @@
{% block title %}AI 智慧推薦 - WOOO TECH{% endblock %}
{% block extra_css %}
-
+
+
{% endblock %}
{% block ewooo_content %}
-
-
-
-
-
-
-
- AI 智慧推薦
-
- 根據資料庫商品分類、即時趨勢與 AI 生成紀錄,產出可追蹤的銷售文案。
-
-
-
-
- Ollama {% if ollama_status %}✓{% else %}✗{% endif %}
-
-
-
- Gemini {% if gemini_status %}✓{% else %}✗{% endif %}
-
-
-
-
-
-
-
+{# Runtime API calls live in page-ai-recommend.js: fetch('/api/ai/generate_copy', ...), fetch('/api/ai/web_search', ...), fetch('/api/ai/product_insights', ...), fetch('/api/ai/gemini_usage?days=30'). #}
+
+
+
+ {# ── Hero header ─────────────────────────────────── #}
+
+
+
+ AI 智慧推薦
+
+ 根據資料庫商品分類、即時趨勢與 AI 生成紀錄,產出可追蹤的銷售文案。
+
+
+
+ Ollama {{ '✓' if ollama_status else '✗' }}
+
+
+ Gemini {{ '✓' if gemini_status else '✗' }}
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 可從右側熱銷商品快速選取
-
-
-
+ {# ── 左側:文案生成 ────────────────────────────── #}
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Gemini 本月使用量
-
-
-
- 費用: $0.0000
- 請求: 0 次
- Token: 0
-
-
-
-
-
-
-
-
-
-
-
-
點選標籤加入/移除
-
- {% for category in product_categories[:12] %}
- {{ category }}
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {# 商品名稱 #}
+
+
+
+
+
+
+
+
+ 可從右側熱銷商品快速選取
+
+
-
-
-
+ {# 風格 / 引擎 / 模型 #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+ {# Gemini 用量面板 #}
+
+
+ Gemini 本月使用量
+
+
+
+ 費用: $0.0000
+ 請求: 0 次
+ Token: 0
+
+
+
+ {# 關鍵字+節日 #}
+
+
+
+
+
- {% for category in product_categories[:4] %}
-
{{ category }}
+
點選標籤加入/移除
+
+ {% for category in product_categories[:12] %}
+ {{ category }}
{% endfor %}
+
-
-
-
-
-
-
-
-
-
-
-
-
- 輸入商品名稱後點擊「分析商品」
- AI 會先搜尋最新網路資訊,再進行深度分析
+
+
+
+
+
+
+
+
+
+
+ {# 生成結果 #}
+
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {# AI 智慧搜尋 #}
+
+
+
+
+
+
+
+
+
+ {% for category in product_categories[:4] %}
+ {{ category }}
+ {% endfor %}
+
+
+
+
+
+ {# 商品洞察 #}
+
+
+
+
+ 輸入商品名稱後點擊「分析商品」
+ AI 會先搜尋最新網路資訊,再進行深度分析
+
+
+
+
+ {# ── 右側:市場資訊 tabs ──────────────────────── #}
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ {# 趨勢洞察 #}
+
+
+ {# 熱銷商品 #}
+
+
+ {# 排行榜 #}
+
+
+ {# 趨勢新聞 #}
+
+
+
-
+{# ── Help modal ──────────────────────────────────── #}
-
-
-
-
-
-
1
-
-
輸入商品名稱
-
可直接輸入或從右側熱銷商品點選
-
-
-
-
2
-
-
選擇文案風格
-
吸睛活潑、專業權威、溫馨感性、限時急迫
-
-
-
-
3
-
-
點擊生成文案
-
AI 會根據市場趨勢自動生成銷售文案
-
-
-
-
-
- 小技巧:使用「AI 智慧搜尋」可獲得更多市場洞察,
- 點選「商品洞察」可分析競品與市場定位。
-
-
+
+
+
+
+
+
+
2
+
選擇文案風格吸睛活潑、專業權威、溫馨感性、限時急迫
+
+
+
+
+
+ 小技巧:使用「AI 智慧搜尋」可獲得更多市場洞察,點選「商品洞察」可分析競品與市場定位。
+
+
+
-
-
-
+{# ── Loading overlay ─────────────────────────────── #}
+
{% endblock %}
{% block extra_js %}
-
-
-
-{% endblock extra_js %}
+
+{% endblock %}
diff --git a/templates/auto_import_index.html b/templates/auto_import_index.html
index dd609f8..182304c 100644
--- a/templates/auto_import_index.html
+++ b/templates/auto_import_index.html
@@ -1,711 +1,414 @@
-{% extends 'ewoooc_base.html' %}
-{% block title %}當日業績報表匯入 - EwoooC{% endblock %}
+{% extends "ewoooc_base.html" %}
-{% block extra_css %}
-
+{% block extra_head %}
+
+
{% endblock %}
-{% block ewooo_content %}
-
-
-
-
-
當日業績報表匯入
-
-
- 支援兩種匯入方式:Google Drive 自動匯入(每 30 分鐘檢查)或 手動上傳
-
-
-
+{% block content %}
+
+
-
-
-
-
-
+ {# ─────────── 頁首 ─────────── #}
+
-
-
-
- 系統每 30 分鐘自動檢查 Google Drive → 下載檔案 → 匯入資料庫 → 刪除雲端原檔
-
-
+ {# ─────────── 配置區 ─────────── #}
+
+
+
+ Google Drive 自動匯入配置
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
系統每 30 分鐘自動檢查 Google Drive → 下載檔案 → 匯入資料庫 → 刪除雲端原檔
-
-
-
-
-
-
-
-
每日業績快照
-
匯入格式:即時業績_當日_YYYYMMDD.xlsx(例如:即時業績_當日_20260113.xlsx)
-
資料將會累加寫入至 daily_sales_snapshot 資料表,並自動去重。
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- | ID |
- 檔案名稱 |
- 狀態 |
- 進度 |
- 成功/總 |
- 開始時間 |
- 完成時間 |
- 錯誤訊息 |
- 操作 |
-
-
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+ {# ─────────── 手動上傳 ─────────── #}
+
+
+
+
+
+
+
+
+
每日業績快照
+
+ 匯入格式:即時業績_當日_YYYYMMDD.xlsx(例:即時業績_當日_20260113.xlsx)
+
+
+ 資料將會累加寫入至 daily_sales_snapshot 資料表,並自動去重。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {# ─────────── 任務歷史 ─────────── #}
+
+
+
+ 匯入任務歷史
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ 檔案名稱 |
+ 狀態 |
+ 進度 |
+ 成功 / 總 |
+ 開始時間 |
+ 完成時間 |
+ 錯誤訊息 |
+ 操作 |
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
-{% block extra_js %}
-
+ // ───── 初始化 ─────
+ loadConfig();
+ loadJobs();
+ setInterval(loadJobs, 10000);
+
{% endblock %}
diff --git a/templates/components/_ewoooc_shell.html b/templates/components/_ewoooc_shell.html
index 97dfcf3..d39070a 100644
--- a/templates/components/_ewoooc_shell.html
+++ b/templates/components/_ewoooc_shell.html
@@ -1,13 +1,18 @@
{#
- EwoooC Frontend V2 shell.
-
- 使用方式:
- {% include 'components/_ewoooc_shell.html' %}
-
- 呼叫頁需提供 active_page;未提供時會以空字串處理。
+ EwoooC Sidebar v3.0 — Production
+ ─────────────────────────────────────────────────────────────
+ 變更重點:
+ 1. 移除原本 200 行 inline
{% set _scheduler = scheduler_stats|default({}) %}
{% set _momo_runs = _scheduler.get('momo_task', []) if _scheduler is mapping else [] %}
{% set _latest_run = _momo_runs[0] if _momo_runs else {} %}
@@ -191,10 +30,6 @@
{% set _added = _latest_run.get('new_records', _latest_run.get('added', '--')) if _latest_run is mapping else '--' %}
{% set _run_status = _latest_run.get('status', '') if _has_scheduler_data else '' %}
{% set _status_label = '尚無紀錄' if not _has_scheduler_data else ('最近成功' if _run_status in ['Success', 'success', 'SUCCESS'] else (_run_status or '已有紀錄')) %}
-{% set _next_run = next_run|default(None) %}
-{% set _session_username = session.get('username') if session is defined else None %}
-{% set _session_role = session.get('role') if session is defined else None %}
-{% set _is_logged_in = session.get('logged_in') if session is defined else false %}