From 605250619ccf7938e90967fa0d9c1f4f7c019e9f Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 12 May 2026 18:27:29 +0800 Subject: [PATCH] Frontend V3 responsive production update --- TODO_NEXT_STEPS.txt | 63 +- app.py | 6 +- config.py | 26 +- ...api-strangler-not-frontend-prerequisite.md | 50 + docs/adr/README.md | 1 + docs/memory/README.md | 1 + docs/memory/frontend_v3_handoff_20260512.md | 142 + routes/sales_routes.py | 35 +- scripts/check_observability_ui.py | 4 +- scripts/check_v3_responsive_ui.js | 159 + templates/ai_recommend.html | 2058 ++------- templates/auto_import_index.html | 1061 ++--- templates/components/_ewoooc_shell.html | 281 +- templates/daily_sales.html | 2214 ++------- templates/edm_dashboard_v2.html | 8 +- templates/ewoooc_base.html | 767 ++-- templates/growth_analysis.html | 379 +- templates/index.html | 432 +- templates/logs.html | 957 +--- templates/monthly_summary_analysis.html | 1897 ++------ templates/sales_analysis.html | 3950 ++++------------- templates/settings.html | 1799 +------- templates/vendor_stockout_import_v2.html | 668 +-- templates/vendor_stockout_index_v2.html | 644 +-- templates/vendor_stockout_list_v2.html | 625 +-- web/static/css/ewoooc-dotmatrix.css | 310 ++ web/static/css/ewoooc-shell.css | 497 ++- web/static/css/ewoooc-tokens-v2-alias.css | 38 + web/static/css/ewoooc-tokens.css | 704 +-- web/static/css/page-ai-recommend-bem.css | 181 + web/static/css/page-ai-recommend.css | 100 + web/static/css/page-auto-import-bem.css | 344 ++ web/static/css/page-auto-import.css | 373 ++ web/static/css/page-daily-sales.css | 788 ++++ web/static/css/page-dashboard.css | 271 ++ web/static/css/page-edm-bem.css | 465 ++ web/static/css/page-edm.css | 519 +++ web/static/css/page-growth-bem.css | 235 + web/static/css/page-growth.css | 76 + web/static/css/page-home.css | 253 ++ web/static/css/page-logs.css | 377 ++ web/static/css/page-monthly-summary-bem.css | 286 ++ web/static/css/page-monthly-summary.css | 168 + web/static/css/page-sales-analysis-bem.css | 530 +++ web/static/css/page-sales-analysis.css | 409 ++ web/static/css/page-settings.css | 480 ++ web/static/css/page-vendor-import.css | 307 ++ web/static/css/page-vendor-index.css | 304 ++ web/static/css/page-vendor-list.css | 288 ++ web/static/js/analysis-chart-theme.js | 489 +- web/static/js/page-ai-recommend.js | 1043 +++++ web/static/js/page-auto-import.js | 349 ++ web/static/js/page-daily-sales.js | 370 ++ web/static/js/page-dashboard.js | 234 + web/static/js/page-growth.js | 104 + web/static/js/page-logs.js | 191 + web/static/js/page-monthly-summary.js | 469 ++ web/static/js/page-sales-analysis.js | 550 +++ web/static/js/page-settings.js | 278 ++ web/static/js/page-vendor-import.js | 105 + 60 files changed, 15863 insertions(+), 14849 deletions(-) create mode 100644 docs/adr/ADR-036-fastapi-strangler-not-frontend-prerequisite.md create mode 100644 docs/memory/frontend_v3_handoff_20260512.md create mode 100644 scripts/check_v3_responsive_ui.js create mode 100644 web/static/css/ewoooc-dotmatrix.css create mode 100644 web/static/css/ewoooc-tokens-v2-alias.css create mode 100644 web/static/css/page-ai-recommend-bem.css create mode 100644 web/static/css/page-ai-recommend.css create mode 100644 web/static/css/page-auto-import-bem.css create mode 100644 web/static/css/page-auto-import.css create mode 100644 web/static/css/page-daily-sales.css create mode 100644 web/static/css/page-dashboard.css create mode 100644 web/static/css/page-edm-bem.css create mode 100644 web/static/css/page-edm.css create mode 100644 web/static/css/page-growth-bem.css create mode 100644 web/static/css/page-growth.css create mode 100644 web/static/css/page-home.css create mode 100644 web/static/css/page-logs.css create mode 100644 web/static/css/page-monthly-summary-bem.css create mode 100644 web/static/css/page-monthly-summary.css create mode 100644 web/static/css/page-sales-analysis-bem.css create mode 100644 web/static/css/page-sales-analysis.css create mode 100644 web/static/css/page-settings.css create mode 100644 web/static/css/page-vendor-import.css create mode 100644 web/static/css/page-vendor-index.css create mode 100644 web/static/css/page-vendor-list.css create mode 100644 web/static/js/page-ai-recommend.js create mode 100644 web/static/js/page-auto-import.js create mode 100644 web/static/js/page-daily-sales.js create mode 100644 web/static/js/page-dashboard.js create mode 100644 web/static/js/page-growth.js create mode 100644 web/static/js/page-logs.js create mode 100644 web/static/js/page-monthly-summary.js create mode 100644 web/static/js/page-sales-analysis.js create mode 100644 web/static/js/page-settings.js create mode 100644 web/static/js/page-vendor-import.js 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 '✗' }} + + +
+
- -
- -
-
-
-
文案生成
- 第 1 步:設定參數 -
-
-
- -
- -
- - - -
-
- 可從右側熱銷商品快速選取 - -
-
+ {# ── 左側:文案生成 ────────────────────────────── #} +
+
+
+
+
文案生成
+ 第 1 步:設定參數 +
+
+
- -
-
- - -
-
- - -
-
- - - - - -
-
- - - - - -
-
-

- -

-
-
- -
- 點選標籤加入/移除 -
- {% for category in product_categories[:12] %} - {{ category }} - {% endfor %} -
-
- -
- 近期節日(自動融入) -
-
-
-
-
-
- -
- - -
- -
-
+ {# 商品名稱 #} +
+ +
+ + + +
+
+ 可從右側熱銷商品快速選取 + +
- -
+ + {# 生成結果 #} + - -
- - - - -
- - - - -
-
-
-
-
熱銷商品參考
- 點選商品快速填入 -
-
-
- - - - -
- -
-
-
-
-
-
-
- - -
-
- -
-
-
-
-
COSME
- 日本美妝排行 -
- -
-
-
-
-
-
- -
-
-
-
-
mybest
- 專業評測 -
- -
-
-
-
-
-
-
-
- - -
-
-
-
-
-
趨勢新聞
- 點選新聞快速填入商品 -
- -
- - -
-
-

載入中...

-
-
-
+ {# AI 智慧搜尋 #} + + + {# 商品洞察 #} +
+
+
+
商品洞察分析
+ 結合網路搜尋的即時市場分析 +
+ +
+ +
+ 輸入商品名稱後點擊「分析商品」
+ AI 會先搜尋最新網路資訊,再進行深度分析 +
+
+
+ + {# ── 右側:市場資訊 tabs ──────────────────────── #} +
+ + +
+ {# 趨勢洞察 #} + + + {# 熱銷商品 #} +
+
+
+
+
熱銷商品參考
+ 點選商品快速填入 +
+
+
+ + + + +
+ +
+
+
+
+
+
+
+ + {# 排行榜 #} +
+
+
+
+
+
+
COSME
+ 日本美妝排行 +
+ +
+
+
+
+
+
+
+
+
+
+
mybest
+ 專業評測 +
+ +
+
+
+
+
+
+
+
+ + {# 趨勢新聞 #} +
+ +
+
+
- +{# ── Help modal ──────────────────────────────────── #}