From 3398c15a75951f7025d4d6e8b3e538709a7a42fb Mon Sep 17 00:00:00 2001 From: OoO Date: Thu, 30 Apr 2026 23:56:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=96=B0=E5=A2=9E=20EDM=20V2?= =?UTF-8?q?=20feature=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONSTITUTION.md | 2 +- app.py | 4 +- config.py | 2 +- docs/memory/history_logs.md | 1 + routes/edm_routes.py | 35 +- templates/edm_dashboard_v2.html | 747 +++++++++++++++++++++++++++++++ tests/test_frontend_v2_assets.py | 13 + 7 files changed, 790 insertions(+), 14 deletions(-) create mode 100644 templates/edm_dashboard_v2.html diff --git a/CONSTITUTION.md b/CONSTITUTION.md index fa985df..f0c1bb6 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -2,7 +2,7 @@ > 本文件定義專案開發的核心準則與不可違反的規範 > **建立日期**: 2026-01-12 -> **當前版本**: V10.35 (Frontend v2 web static assets mounted) +> **當前版本**: V10.36 (EDM dashboard v2 feature flag) > **最後更新**: 2026-04-30 --- diff --git a/app.py b/app.py index 21b2a03..29e8747 100644 --- a/app.py +++ b/app.py @@ -95,8 +95,8 @@ except Exception as e: sys_log.error(f"無法檢測磁碟空間: {e}") # 🚩 系統版本定義 (備份與顯示用) -# 🚩 2026-04-30 V10.35: Frontend v2 web static assets mounted -SYSTEM_VERSION = "V10.35" +# 🚩 2026-04-30 V10.36: EDM dashboard v2 feature flag +SYSTEM_VERSION = "V10.36" # ========================================== # 🔒 SQL Injection 防護函數 diff --git a/config.py b/config.py index 0867842..8364057 100644 --- a/config.py +++ b/config.py @@ -254,7 +254,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.35" +SYSTEM_VERSION = "V10.36" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index cb336c4..34ca92d 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -58,6 +58,7 @@ - **Frontend V2 Phase 0 assets**: 新增 `static/css/ewoooc-tokens.css`、`static/css/ewoooc-shell.css`、`templates/ewoooc_base.html` 與 `_ewoooc_shell.html`,先建立可重用 shell,不替換既有頁面;憲章補「真實資料與真實頁面」紅線,禁止假資料冒充完成。 - **Dashboard V2 feature flag**: `/` 預設仍走既有 `dashboard.html`,`/?ui=v2` 才渲染 `dashboard_v2.html`;新版頁沿用既有 dashboard 真實資料與篩選/排序參數,不建立 mock 商品或假 KPI。 - **Frontend V2 static mount**: `momo-app` 補 `./web/static:/app/web/static:ro`,確保 Flask `url_for('static', filename='css/ewoooc-*.css')` 會從實際 `STATIC_DIR=/app/web/static` 讀到 V2 CSS;保留 `./static:/app/static:ro` 供 local-dev Nginx/舊資產相容。 +- **EDM Dashboard V2 feature flag**: 五個活動看板入口預設仍走既有 `edm_dashboard.html`,加上 `?ui=v2` 才渲染 `edm_dashboard_v2.html`;新版頁沿用既有活動真實 `grouped_items/slot_stats/scheduler_stats`。 ### 2026-04-28~29:Phase 3e 重構大戰 + daily_sales cache 隱形 bug 根除 - **app.py 縮減 -10.8%**: 7,386 → 6,590 行,11 commits 全綠零 502。 diff --git a/routes/edm_routes.py b/routes/edm_routes.py index 3b4bded..fed91d4 100644 --- a/routes/edm_routes.py +++ b/routes/edm_routes.py @@ -321,7 +321,9 @@ def edm_dashboard(): scheduler_stats = load_scheduler_stats() now_taipei = datetime.now(TAIPEI_TZ) - return render_template('edm_dashboard.html', + template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html' + + return render_template(template_name, promo_pages=promo_pages, current_promo_page='edm', page_title='MOMO 限時搶購', @@ -337,7 +339,8 @@ def edm_dashboard(): current_sort=sort_by, current_order=order, slugify=slugify, - datetime_now=now_taipei.strftime('%Y-%m-%d %H:%M:%S')) + datetime_now=now_taipei.strftime('%Y-%m-%d %H:%M:%S'), + active_page='edm') except Exception as e: sys_log.error(f"EDM Dashboard 渲染錯誤: {e}") return f"系統錯誤: {e}" @@ -372,7 +375,9 @@ def festival_dashboard(): scheduler_stats = load_scheduler_stats() - return render_template('edm_dashboard.html', + template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html' + + return render_template(template_name, promo_pages=promo_pages, current_promo_page='festival', page_title=PAGE_NAME, @@ -386,7 +391,8 @@ def festival_dashboard(): scheduler_stats=scheduler_stats, current_sort=sort_by, current_order=order, - slugify=slugify) + slugify=slugify, + active_page='edm') except Exception as e: sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}") return f"系統錯誤: {e}" @@ -421,7 +427,9 @@ def mothers_day_dashboard(): scheduler_stats = load_scheduler_stats() - return render_template('edm_dashboard.html', + template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html' + + return render_template(template_name, promo_pages=promo_pages, current_promo_page='mothers_day', page_title=PAGE_NAME, @@ -435,7 +443,8 @@ def mothers_day_dashboard(): scheduler_stats=scheduler_stats, current_sort=sort_by, current_order=order, - slugify=slugify) + slugify=slugify, + active_page='edm') except Exception as e: sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}") return f"系統錯誤: {e}" @@ -470,7 +479,9 @@ def valentine_520_dashboard(): scheduler_stats = load_scheduler_stats() - return render_template('edm_dashboard.html', + template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html' + + return render_template(template_name, promo_pages=promo_pages, current_promo_page='valentine_520', page_title=PAGE_NAME, @@ -484,7 +495,8 @@ def valentine_520_dashboard(): scheduler_stats=scheduler_stats, current_sort=sort_by, current_order=order, - slugify=slugify) + slugify=slugify, + active_page='edm') except Exception as e: sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}") return f"系統錯誤: {e}" @@ -519,7 +531,9 @@ def labor_day_dashboard(): scheduler_stats = load_scheduler_stats() - return render_template('edm_dashboard.html', + template_name = 'edm_dashboard_v2.html' if request.args.get('ui') == 'v2' else 'edm_dashboard.html' + + return render_template(template_name, promo_pages=promo_pages, current_promo_page='labor_day', page_title=PAGE_NAME, @@ -533,7 +547,8 @@ def labor_day_dashboard(): scheduler_stats=scheduler_stats, current_sort=sort_by, current_order=order, - slugify=slugify) + slugify=slugify, + active_page='edm') except Exception as e: sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}") return f"系統錯誤: {e}" diff --git a/templates/edm_dashboard_v2.html b/templates/edm_dashboard_v2.html new file mode 100644 index 0000000..9813bb3 --- /dev/null +++ b/templates/edm_dashboard_v2.html @@ -0,0 +1,747 @@ +{% extends 'ewoooc_base.html' %} + +{% block title %}EwoooC 活動看板{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block ewooo_content %} +{% set current_endpoint = 'edm.' ~ current_promo_page ~ '_dashboard' %} +{% set current_stats = {'new': 0, 'up': 0, 'down': 0, 'delisted_last_run': 0, 'on_shelf': 0} %} +{% if active_tab in slot_stats %} + {% set current_stats = slot_stats[active_tab] %} +{% elif slot_stats %} + {% set current_stats = slot_stats.values() | list | first %} +{% endif %} +{% set task_key = 'festival_task' if current_promo_page == 'festival' else 'edm_task' %} +{% set run_list = scheduler_stats.get(task_key, []) %} +{% set latest_run = run_list[0] if run_list else None %} + +
+
+ +
+ +
+
+
+
+ CAMPAIGN + ID · {{ current_promo_page|upper }} +
+

{{ page_title }}

+
+
+
活動時段
+
{{ activity_time }}
+
+
+
最後更新
+
{{ last_update }}
+
+
+
商品總數
+
{{ total_edm_products | number_format }}
+
+
+ +
+ {% if current_promo_page == 'edm' %} + + + {% elif current_promo_page == 'festival' %} + + {% endif %} +
+
+
+ + +
+ +
+ + {% if slot_stats %} +
+ {% for slot, stats in slot_stats.items() %} + {% set slot_id = slugify(slot) %} + + {% endfor %} +
+ {% else %} +
+
目前沒有可顯示的活動時段資料
+
+ {% endif %} +
+ + {% if slot_stats %} +
+ {% for slot, stats in slot_stats.items() %} + {% set slot_id = slugify(slot) %} + {% set items = grouped_items.get(slot, []) %} +
+
+
+ 03 + 商品列表 + {{ items|length }} 筆 +
+ 新品 {{ stats.get('new', 0) }} + 漲價 {{ stats.get('up', 0) }} + 降價 {{ stats.get('down', 0) }} + 下架 {{ stats.get('delisted_last_run', 0) }} +
+
+
+ + + + + + + + + + + {% for item in items %} + + + + + + + {% else %} + + + + {% endfor %} + +
分類 + {% set next_order_name = 'asc' if current_sort == 'name' and current_order == 'desc' else 'desc' %} + 商品資訊 + + {% set next_order_price = 'asc' if current_sort == 'price' and current_order == 'desc' else 'desc' %} + 價格 + + {% if current_promo_page == 'edm' %} + {% set next_order_qty = 'asc' if current_sort == 'remain_qty' and current_order == 'desc' else 'desc' %} + 倒數組數 + {% else %} + 狀態 + {% endif %} +
+ {{ item.main_category or '未分類' }} + +
+ {% if item.image_url %} + {{ item.name }} + {% else %} +
無圖
+ {% endif %} +
+ {{ item.name }} +
ID {{ item.i_code }}
+
+ {% if item.status_change == 'NEW' %} + NEW + {% elif item.status_change == 'PRICE_DOWN' %} + 降價 + {% elif item.status_change == 'PRICE_UP' %} + 漲價 + {% elif item.status_change in ['DELISTED', 'SLOT_END'] %} + 下架 + {% endif %} + {% if item.discount_text %} + {{ item.discount_text }} + {% endif %} +
+
+
+
+ {% if item.previous_price and item.price and item.previous_price != item.price %} + {% set diff = item.price - item.previous_price %} + {% if diff < 0 %} +
▼ {{ (diff|abs) | number_format }}
+ {% else %} +
▲ {{ diff | number_format }}
+ {% endif %} +
${{ item.previous_price | number_format }}
+
${{ item.price | number_format }}
+ {% elif item.price is not none %} + ${{ item.price | number_format }} + {% else %} + -- + {% endif %} +
+ {% if current_promo_page == 'edm' %} + {% if item.remain_qty is not none %} + 剩 {{ item.remain_qty | number_format }} 組 + {% else %} + -- + {% endif %} + {% else %} + {{ item.status_change or '活動中' }} + {% endif %} +
+
此時段目前沒有商品資料
+
+
+
+
+ {% endfor %} +
+ {% endif %} +
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index bebafe0..4e55f25 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -50,3 +50,16 @@ def test_dashboard_v2_is_feature_flagged_and_uses_real_dashboard_data(): assert "{% for item in items %}" in dashboard assert "mockProducts" not in dashboard assert "假商品" not in dashboard + + +def test_edm_dashboard_v2_is_feature_flagged_and_uses_real_campaign_data(): + route_source = (ROOT / "routes/edm_routes.py").read_text(encoding="utf-8") + template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8") + + assert route_source.count("request.args.get('ui') == 'v2'") == 5 + assert "template_name = 'edm_dashboard_v2.html'" in route_source + assert "{% for slot, stats in slot_stats.items() %}" in template + assert "{% for item in items %}" in template + assert "scheduler_stats.get(task_key, [])" in template + assert "mock" not in template.lower() + assert "假商品" not in template