修正外部工具入口橋接
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-19 11:07:33 +08:00
parent 8301f0ac7e
commit ffe0a0f512
10 changed files with 309 additions and 46 deletions

View File

@@ -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 到超時。

6
app.py
View File

@@ -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'),
}

View File

@@ -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 # 用於模板顯示

View File

@@ -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'),
)

View File

@@ -26,12 +26,12 @@
{% endif %}
{% if metabase_url %}
<a class="analysis-report-tab is-external {% if _analysis_active == 'metabase' %}is-active{% endif %}" href="{{ metabase_url }}">
<i class="fas fa-chart-pie"></i>Metabase <i class="fas fa-up-right-from-square"></i>
<i class="fas fa-chart-pie"></i>自訂圖表
</a>
{% endif %}
{% if grist_url %}
<a class="analysis-report-tab is-external {% if _analysis_active == 'grist' %}is-active{% endif %}" href="{{ grist_url }}">
<i class="fas fa-table"></i>Grist <i class="fas fa-up-right-from-square"></i>
<i class="fas fa-table-cells"></i>資料協作
</a>
{% endif %}
</nav>

View File

@@ -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',

View File

@@ -202,14 +202,14 @@
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{{ metabase_url }}">
<i class="fas fa-chart-pie me-2"></i>自訂圖表 (Metabase)
<i class="fas fa-chart-pie me-2"></i>自訂圖表
</a>
</li>
{% endif %}
{% if grist_url %}
<li>
<a class="dropdown-item" href="{{ grist_url }}">
<i class="fas fa-table me-2"></i>資料協作 (Grist)
<i class="fas fa-table-cells me-2"></i>資料協作
</a>
</li>
{% endif %}

View File

@@ -10,7 +10,7 @@
<section class="external-tool-hero">
<div>
<div class="external-tool-eyebrow">
<i class="fas fa-link" aria-hidden="true"></i>
<i class="fas fa-plug-circle-check" aria-hidden="true"></i>
{{ tool.eyebrow }}
</div>
<h1>{{ tool.title }}</h1>
@@ -19,15 +19,60 @@
<span class="external-tool-status">{{ tool.status_label }}</span>
</section>
<section class="external-tool-checks" aria-label="入口檢查">
{% for check in tool.checks %}
<article class="external-tool-check is-{{ check.state }}">
<span>{{ check.label }}</span>
<strong>{{ check.value }}</strong>
</article>
{% endfor %}
</section>
<section class="external-tool-panel">
<div class="external-tool-panel__body">
<span class="external-tool-kicker">路由狀態</span>
<h2>入口已由 momo-pro 接管</h2>
<p>{{ tool.detail }}</p>
<dl class="external-tool-diagnostics">
<div>
<dt>正式導覽</dt>
<dd>/{{ tool.key }}</dd>
</div>
<div>
<dt>目前設定</dt>
<dd>{{ tool.configured_label }}</dd>
</div>
<div>
<dt>版本</dt>
<dd>{{ system_version }}</dd>
</div>
</dl>
</div>
<div class="external-tool-panel__actions">
{% if tool.launch_href %}
<a class="btn btn-primary" href="{{ tool.launch_href }}">
<i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i>{{ tool.launch_label }}
</a>
{% endif %}
<a class="btn btn-outline-primary" href="/monthly_summary_analysis">
<i class="fas fa-arrow-left" aria-hidden="true"></i>回分析報表
</a>
</div>
</section>
<section class="external-tool-actions" aria-label="可用替代入口">
<div class="external-tool-section-title">
<span class="external-tool-kicker">可用入口</span>
<h2>先回到已上線的分析工作流</h2>
</div>
<div class="external-tool-action-grid">
{% for action in tool.actions %}
<a class="external-tool-action" href="{{ action.href }}">
<i class="{{ action.icon }}" aria-hidden="true"></i>
<span>{{ action.label }}</span>
</a>
{% endfor %}
</div>
<a class="btn btn-primary" href="{{ tool.primary_href }}">
<i class="fas fa-arrow-left" aria-hidden="true"></i>{{ tool.primary_label }}
</a>
</section>
</main>
{% endblock %}

View File

@@ -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

View File

@@ -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%;
}
}