From ffe0a0f512e9c693f0456624ada974f9b142129d Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 11:07:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=A4=96=E9=83=A8=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=85=A5=E5=8F=A3=E6=A9=8B=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 1 + app.py | 6 +- config.py | 2 +- routes/system_public_routes.py | 100 ++++++++++--- .../components/_analysis_report_tabs.html | 4 +- templates/components/_ewoooc_shell.html | 2 +- templates/components/_navbar.html | 4 +- templates/external_tool_status.html | 53 ++++++- tests/test_external_tool_entrypoints.py | 44 ++++++ web/static/css/page-external-tools.css | 139 ++++++++++++++++-- 10 files changed, 309 insertions(+), 46 deletions(-) create mode 100644 tests/test_external_tool_entrypoints.py diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index e5b0018..4ec6746 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.241 修正 `/metabase`、`/grist` 外部工具入口:全域導覽固定回 momo-pro 內部橋接頁,避免資料協作錯連 AwoooI;入口頁補路由狀態、設定診斷與可用替代分析入口,降低空白頁誤判。 - V10.221 補 `/observability/ppt_audit_history` AiderHeal 背景任務可見性:正在修復中的簡報會顯示於產線頁,並提供 JSON 狀態端點讓派工後即時刷新,避免重新整理後不知道是否已在修。 - V10.218 補 `/observability/ppt_audit_history` AiderHeal 去重鎖:同一份簡報已在背景修復時,再次點擊會回「已在執行中」,避免重複開 SSH / 模型 / git 修復流程。 - V10.217 讓 `/observability/ppt_audit_history` 的 AiderHeal 派工改為非阻塞背景任務:頁面立即回「已排入」,修復工作在背景執行,避免瀏覽器與 Gunicorn worker 等 SSH、模型與 git push 到超時。 diff --git a/app.py b/app.py index abb0234..ab31489 100644 --- a/app.py +++ b/app.py @@ -424,14 +424,12 @@ verify_metadata_tables() # ========================================== # 🔧 全域模板變數注入 (Context Processor) # ========================================== -from config import METABASE_URL, GRIST_URL - @app.context_processor def inject_global_vars(): """注入全域變數到所有模板""" return { - 'metabase_url': METABASE_URL, - 'grist_url': GRIST_URL, + 'metabase_url': '/metabase', + 'grist_url': '/grist', 'datetime_now': datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M:%S'), } diff --git a/config.py b/config.py index 5c44a0d..0edf552 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.240" +SYSTEM_VERSION = "V10.241" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/routes/system_public_routes.py b/routes/system_public_routes.py index 7fbe9ef..fdb71a7 100644 --- a/routes/system_public_routes.py +++ b/routes/system_public_routes.py @@ -8,12 +8,13 @@ import os import zipfile from datetime import datetime, timezone, timedelta +from urllib.parse import urlparse from flask import Blueprint, Response, jsonify, render_template, send_from_directory, url_for from sqlalchemy import text from auth import login_required -from config import BASE_DIR, DATABASE_TYPE, SYSTEM_VERSION +from config import BASE_DIR, DATABASE_TYPE, GRIST_URL, METABASE_URL, SYSTEM_VERSION from database.manager import DatabaseManager from database.models import Product, PriceRecord from services.json_storage import load_categories @@ -30,6 +31,81 @@ public_url = os.getenv('PUBLIC_URL', '服務啟動中...') STATIC_DIR = os.path.join(BASE_DIR, 'web/static') +def _safe_launch_url(configured_url, bridge_path): + """Return a browser-safe launch URL, keeping nav links inside momo-pro by default.""" + candidate = (configured_url or '').strip() + if not candidate or candidate.rstrip('/') == bridge_path.rstrip('/'): + return '' + if candidate.startswith('/') and not candidate.startswith('//'): + return candidate + + parsed = urlparse(candidate) + if parsed.scheme in {'http', 'https'} and parsed.hostname == 'mo.wooo.work': + if parsed.path.rstrip('/') == bridge_path.rstrip('/'): + return '' + return candidate + return '' + + +def _configured_url_label(configured_url): + candidate = (configured_url or '').strip() + if not candidate: + return '未設定' + parsed = urlparse(candidate) + if parsed.scheme in {'http', 'https'} and parsed.hostname != 'mo.wooo.work': + return f'{parsed.hostname}(已由入口攔截)' + return candidate + + +def _external_tool_payload(kind): + if kind == 'metabase': + launch_url = _safe_launch_url(METABASE_URL, '/metabase') + return { + 'key': 'metabase', + 'eyebrow': 'Analytics Bridge', + 'title': '自訂圖表入口', + 'status_label': '待接上 BI 服務' if not launch_url else '可開啟', + 'summary': '這裡是 momo-pro 的 Metabase 安全入口。導覽先停在系統內部,不再出現空白頁或錯站外跳。', + 'detail': '目前正式主機沒有啟用 Metabase / Grist 的 bi profile 容器,Gateway 也沒有可用 proxy;因此先提供可用的分析替代入口與設定診斷。', + 'launch_href': launch_url, + 'launch_label': '開啟 Metabase', + 'configured_label': _configured_url_label(METABASE_URL), + 'checks': [ + {'label': '導覽路由', 'value': '/metabase', 'state': 'ok'}, + {'label': '跨站保護', 'value': '已鎖在 momo-pro', 'state': 'ok'}, + {'label': 'BI 服務', 'value': '尚未接入 proxy', 'state': 'warn'}, + ], + 'actions': [ + {'label': '月份總表', 'href': '/monthly_summary_analysis', 'icon': 'fas fa-table'}, + {'label': '成長分析', 'href': '/growth_analysis', 'icon': 'fas fa-arrow-trend-up'}, + {'label': '當日業績', 'href': '/daily_sales', 'icon': 'fas fa-calendar-day'}, + ], + } + + launch_url = _safe_launch_url(GRIST_URL, '/grist') + return { + 'key': 'grist', + 'eyebrow': 'Data Collaboration', + 'title': '資料協作入口', + 'status_label': '錯鏈已攔截' if not launch_url else '可開啟', + 'summary': '資料協作入口已回到 momo-pro 內部,不會再連到 AwoooI 或其他專案站台。', + 'detail': 'Grist 的正式 momo-pro 專案隔離尚未接上 Gateway;在完成專屬服務前,本頁會提供資料作業入口與目前設定狀態。', + 'launch_href': launch_url, + 'launch_label': '開啟 Grist', + 'configured_label': _configured_url_label(GRIST_URL), + 'checks': [ + {'label': '導覽路由', 'value': '/grist', 'state': 'ok'}, + {'label': '錯站攔截', 'value': '已啟用', 'state': 'ok'}, + {'label': '協作服務', 'value': '尚未接入 proxy', 'state': 'warn'}, + ], + 'actions': [ + {'label': '月份總表', 'href': '/monthly_summary_analysis', 'icon': 'fas fa-table'}, + {'label': '雲端匯入', 'href': '/auto_import', 'icon': 'fas fa-download'}, + {'label': '業績分析', 'href': '/sales_analysis', 'icon': 'fas fa-chart-bar'}, + ], + } + + @system_public_bp.route('/favicon.ico') def favicon(): """使用既有品牌圖示回應瀏覽器預設 favicon 探測,避免全站 404 噪音。""" @@ -66,16 +142,7 @@ def metabase_status(): 'external_tool_status.html', active_page='metabase', system_version=SYSTEM_VERSION, - tool={ - 'key': 'metabase', - 'eyebrow': 'Analytics Bridge', - 'title': '自訂圖表入口', - 'status_label': '代理尚未接入', - 'summary': '正式入口已留在 momo-pro 內部,避免再落到 404 或空白頁。', - 'detail': 'Metabase 容器以 bi profile 管理;公開路由需由 Gateway / Nginx 接到 momo-metabase:3000 後才會切換為完整 BI 介面。', - 'primary_label': '回月份總表', - 'primary_href': '/monthly_summary_analysis', - }, + tool=_external_tool_payload('metabase'), ) @@ -88,16 +155,7 @@ def grist_status(): 'external_tool_status.html', active_page='grist', system_version=SYSTEM_VERSION, - tool={ - 'key': 'grist', - 'eyebrow': 'Data Collaboration', - 'title': '資料協作入口', - 'status_label': '錯鏈已攔截', - 'summary': '資料協作不再連到 grist.wooo.work,避免被轉往其他專案站台。', - 'detail': 'Grist 正式域名尚未完成 momo-pro 專案隔離;在完成 Gateway 綁定前,導覽會停在本頁狀態,不再跳出系統邊界。', - 'primary_label': '回月份總表', - 'primary_href': '/monthly_summary_analysis', - }, + tool=_external_tool_payload('grist'), ) diff --git a/templates/components/_analysis_report_tabs.html b/templates/components/_analysis_report_tabs.html index 73f7202..c951546 100644 --- a/templates/components/_analysis_report_tabs.html +++ b/templates/components/_analysis_report_tabs.html @@ -26,12 +26,12 @@ {% endif %} {% if metabase_url %} - Metabase + 自訂圖表 {% endif %} {% if grist_url %} - Grist + 資料協作 {% endif %} diff --git a/templates/components/_ewoooc_shell.html b/templates/components/_ewoooc_shell.html index 314d2ad..65309fd 100644 --- a/templates/components/_ewoooc_shell.html +++ b/templates/components/_ewoooc_shell.html @@ -13,7 +13,7 @@ {% 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 %} -{% set _analysis_pages = ['sales', 'daily_sales', 'monthly', 'growth'] %} +{% set _analysis_pages = ['sales', 'daily_sales', 'monthly', 'growth', 'metabase', 'grist'] %} {% set _obs_pages = [ 'obs_overview', 'obs_agent_orchestration', 'obs_business_intel', 'obs_host_health', 'obs_ai_calls', 'obs_budget', diff --git a/templates/components/_navbar.html b/templates/components/_navbar.html index b2208c6..fd0e183 100755 --- a/templates/components/_navbar.html +++ b/templates/components/_navbar.html @@ -202,14 +202,14 @@
  • - 自訂圖表 (Metabase) + 自訂圖表
  • {% endif %} {% if grist_url %}
  • - 資料協作 (Grist) + 資料協作
  • {% endif %} diff --git a/templates/external_tool_status.html b/templates/external_tool_status.html index 528f563..636faf2 100644 --- a/templates/external_tool_status.html +++ b/templates/external_tool_status.html @@ -10,7 +10,7 @@
    - + {{ tool.eyebrow }}

    {{ tool.title }}

    @@ -19,15 +19,60 @@ {{ tool.status_label }}
    +
    + {% for check in tool.checks %} +
    + {{ check.label }} + {{ check.value }} +
    + {% endfor %} +
    +
    路由狀態

    入口已由 momo-pro 接管

    {{ tool.detail }}

    +
    +
    +
    正式導覽
    +
    /{{ tool.key }}
    +
    +
    +
    目前設定
    +
    {{ tool.configured_label }}
    +
    +
    +
    版本
    +
    {{ system_version }}
    +
    +
    +
    +
    + {% if tool.launch_href %} + + {{ tool.launch_label }} + + {% endif %} + + 回分析報表 + +
    +
    + +
    +
    + 可用入口 +

    先回到已上線的分析工作流

    +
    +
    + {% for action in tool.actions %} + + + {{ action.label }} + + {% endfor %}
    - - {{ tool.primary_label }} -
    {% endblock %} diff --git a/tests/test_external_tool_entrypoints.py b/tests/test_external_tool_entrypoints.py new file mode 100644 index 0000000..2eed1ff --- /dev/null +++ b/tests/test_external_tool_entrypoints.py @@ -0,0 +1,44 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_metabase_and_grist_navigation_stays_inside_momo_pro(): + app_source = (ROOT / "app.py").read_text(encoding="utf-8") + shell = (ROOT / "templates/components/_ewoooc_shell.html").read_text(encoding="utf-8") + tabs = (ROOT / "templates/components/_analysis_report_tabs.html").read_text(encoding="utf-8") + legacy_nav = (ROOT / "templates/components/_navbar.html").read_text(encoding="utf-8") + + assert "'metabase_url': '/metabase'" in app_source + assert "'grist_url': '/grist'" in app_source + assert "['sales', 'daily_sales', 'monthly', 'growth', 'metabase', 'grist']" in shell + + combined = "\n".join([app_source, shell, tabs, legacy_nav]) + assert "grist.wooo.work" not in combined + assert "awoooi" not in combined.lower() + + +def test_external_tool_bridge_pages_are_diagnostic_not_blank(): + route_source = (ROOT / "routes/system_public_routes.py").read_text(encoding="utf-8") + template = (ROOT / "templates/external_tool_status.html").read_text(encoding="utf-8") + css = (ROOT / "web/static/css/page-external-tools.css").read_text(encoding="utf-8") + + assert "def _external_tool_payload(kind)" in route_source + assert "parsed.path.rstrip('/') == bridge_path.rstrip('/')" in route_source + assert "external-tool-checks" in template + assert "external-tool-diagnostics" in template + assert "external-tool-action-grid" in template + assert "尚未接入 proxy" in route_source + assert "已由入口攔截" in route_source + + assert "--momo-accent-rust" not in css + assert "--momo-accent-honey" not in css + + +def test_analysis_tabs_use_internal_tool_labels_without_external_icon(): + tabs = (ROOT / "templates/components/_analysis_report_tabs.html").read_text(encoding="utf-8") + + assert "自訂圖表" in tabs + assert "資料協作" in tabs + assert "fa-up-right-from-square" not in tabs diff --git a/web/static/css/page-external-tools.css b/web/static/css/page-external-tools.css index eb864d2..6e8bb28 100644 --- a/web/static/css/page-external-tools.css +++ b/web/static/css/page-external-tools.css @@ -5,7 +5,8 @@ } .external-tool-hero, -.external-tool-panel { +.external-tool-panel, +.external-tool-actions { background: radial-gradient(circle, rgba(45, 40, 32, 0.12) 1px, transparent 1.2px), var(--momo-bg-surface); @@ -20,7 +21,7 @@ align-items: flex-start; justify-content: space-between; gap: var(--momo-space-5, 24px); - padding: var(--momo-space-6, 32px); + padding: var(--momo-space-5, 24px); } .external-tool-eyebrow, @@ -28,7 +29,7 @@ display: inline-flex; align-items: center; gap: var(--momo-space-2, 8px); - color: var(--momo-accent-rust); + color: var(--momo-page-accent); font-size: var(--momo-text-body-sm); font-weight: var(--momo-font-weight-bold); letter-spacing: 0; @@ -44,11 +45,12 @@ } .external-tool-hero h1 { - font-size: 48px; + font-size: clamp(32px, 4vw, 48px); + line-height: 1.08; } .external-tool-panel h2 { - font-size: var(--momo-text-title-lg); + font-size: var(--momo-text-title, 17px); } .external-tool-hero p, @@ -56,19 +58,62 @@ max-width: 760px; margin: 0; color: var(--momo-text-secondary); - font-size: var(--momo-text-body-lg); + font-size: var(--momo-text-body, 14px); line-height: 1.8; } .external-tool-status { flex: 0 0 auto; - border: 1px solid var(--momo-accent-rust); + border: 1px solid var(--momo-page-accent); border-radius: var(--momo-radius-pill, 999px); - color: var(--momo-accent-rust); + color: var(--momo-page-accent); font-weight: var(--momo-font-weight-bold); padding: var(--momo-space-2, 8px) var(--momo-space-3, 12px); } +.external-tool-checks { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--momo-space-3, 12px); +} + +.external-tool-check { + min-height: 96px; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: var(--momo-space-3, 12px); + padding: var(--momo-space-4, 16px); + background: var(--momo-bg-surface); + border: 1px solid var(--momo-border-subtle); + border-left: 4px solid var(--momo-page-accent); + border-radius: var(--momo-radius-lg, 8px); +} + +.external-tool-check.is-ok { + border-left-color: var(--momo-success); +} + +.external-tool-check.is-warn { + border-left-color: var(--momo-warning); +} + +.external-tool-check span, +.external-tool-diagnostics dt { + color: var(--momo-text-muted); + font-size: var(--momo-text-body-sm); + font-weight: var(--momo-font-weight-bold); +} + +.external-tool-check strong, +.external-tool-diagnostics dd { + margin: 0; + color: var(--momo-text-primary); + font-family: var(--momo-font-mono); + font-size: var(--momo-text-body); + letter-spacing: 0; +} + .external-tool-panel { display: flex; align-items: center; @@ -77,13 +122,75 @@ padding: var(--momo-space-5, 24px); } -.external-tool-panel .btn { +.external-tool-diagnostics { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--momo-space-3, 12px); + margin: var(--momo-space-4, 16px) 0 0; +} + +.external-tool-diagnostics div { + padding: var(--momo-space-3, 12px); + background: rgba(255, 248, 238, 0.42); + border: 1px solid var(--momo-border-subtle); + border-radius: var(--momo-radius-md, 6px); +} + +.external-tool-panel__actions { + display: flex; + flex-direction: column; + gap: var(--momo-space-2, 8px); + flex: 0 0 auto; +} + +.external-tool-panel .btn, +.external-tool-action { display: inline-flex; align-items: center; gap: var(--momo-space-2, 8px); +} + +.external-tool-panel .btn { + justify-content: center; white-space: nowrap; } +.external-tool-actions { + padding: var(--momo-space-5, 24px); +} + +.external-tool-section-title h2 { + margin: var(--momo-space-2, 8px) 0 0; + color: var(--momo-text-primary); + font-size: var(--momo-text-title); + font-weight: var(--momo-font-weight-black); +} + +.external-tool-action-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--momo-space-3, 12px); + margin-top: var(--momo-space-4, 16px); +} + +.external-tool-action { + min-height: 64px; + padding: var(--momo-space-3, 12px); + color: var(--momo-text-primary); + background: var(--momo-bg-paper); + border: 1px solid var(--momo-border-subtle); + border-radius: var(--momo-radius-md, 6px); + font-weight: var(--momo-font-weight-bold); + text-decoration: none; + transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease; +} + +.external-tool-action:hover { + color: var(--momo-page-accent); + background: var(--momo-bg-surface); + border-color: var(--momo-border-strong); +} + @media (max-width: 720px) { .external-tool-hero, .external-tool-panel { @@ -92,7 +199,17 @@ padding: var(--momo-space-4, 16px); } - .external-tool-hero h1 { - font-size: 32px; + .external-tool-status { + align-self: flex-start; + } + + .external-tool-checks, + .external-tool-diagnostics, + .external-tool-action-grid { + grid-template-columns: 1fr; + } + + .external-tool-panel__actions { + width: 100%; } }