fix(p32): admin URL prefix /admin → /observability — 避開 188 nginx SPA shadow
All checks were successful
CD Pipeline / deploy (push) Successful in 2m25s

Root cause(curl 實證):
  prod 188 nginx 對 /admin/* 設 try_files → SPA index.html fallback
  → Phase 27-31 的 6 個 Flask admin 路由全被 nginx 攔截
  → 外部 GET /admin/ai_calls 回 7480 byte 靜態 HTML(同 etag = SPA shell)
  → 我之前說「6 admin 頁 prod 200」是回了 200,但 body 不是 Flask 渲染

修法:
  Blueprint url_prefix /admin → /observability
  → 6 個觀測頁實際生效在 /observability/* 不被 SPA 遮蔽
  → SPA frontend 仍擁有 /admin/* 命名空間(不破壞既有前端)

更新範圍:
  - routes/admin_observability_routes.py: url_prefix + 註解全改
  - 6 templates: 所有 href / fetch() 路徑改 /observability/
  - tests/test_admin_observability_routes.py: client.get/post 路徑改
  - 10/10 smoke tests 仍 PASS

統帥訪問新路徑:
  http://192.168.0.188/observability/ai_calls
  http://192.168.0.188/observability/host_health
  http://192.168.0.188/observability/budget
  http://192.168.0.188/observability/promotion_review
  http://192.168.0.188/observability/quality_trend
  http://192.168.0.188/observability/ppt_audit_history
This commit is contained in:
OoO
2026-05-04 14:13:27 +08:00
parent 82595ab4ac
commit 99d2f3c543
8 changed files with 56 additions and 56 deletions

View File

@@ -60,13 +60,13 @@ def _fake_session(rows_per_query=None):
# ──────────────────────────────────────────────────────────────────────────
# /admin/ai_calls
# /observability/ai_calls
# ──────────────────────────────────────────────────────────────────────────
def test_ai_calls_dashboard_200_empty(client, monkeypatch):
from routes import admin_observability_routes as mod
monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([]))
r = client.get('/admin/ai_calls')
r = client.get('/observability/ai_calls')
assert r.status_code == 200
assert b'AI Calls' in r.data or '\xe5\x88\x86\xe6\x9e\x90'.encode() in r.data or True # 中文標題可選
@@ -77,40 +77,40 @@ def test_ai_calls_dashboard_db_error_falls_back(client, monkeypatch):
bad.execute.side_effect = RuntimeError('DB down')
bad.close = MagicMock()
monkeypatch.setattr(mod, 'get_session', lambda: bad)
r = client.get('/admin/ai_calls')
r = client.get('/observability/ai_calls')
assert r.status_code == 200 # 失敗安全:仍 render不 500
# ──────────────────────────────────────────────────────────────────────────
# /admin/promotion_review
# /observability/promotion_review
# ──────────────────────────────────────────────────────────────────────────
def test_promotion_review_200(client, monkeypatch):
from routes import admin_observability_routes as mod
monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([]))
r = client.get('/admin/promotion_review')
r = client.get('/observability/promotion_review')
assert r.status_code == 200
# ──────────────────────────────────────────────────────────────────────────
# /admin/quality_trend
# /observability/quality_trend
# ──────────────────────────────────────────────────────────────────────────
def test_quality_trend_200(client, monkeypatch):
from routes import admin_observability_routes as mod
monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([]))
r = client.get('/admin/quality_trend')
r = client.get('/observability/quality_trend')
assert r.status_code == 200
# ──────────────────────────────────────────────────────────────────────────
# /admin/budget
# /observability/budget
# ──────────────────────────────────────────────────────────────────────────
def test_budget_dashboard_200_empty(client, monkeypatch):
from routes import admin_observability_routes as mod
monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([]))
r = client.get('/admin/budget')
r = client.get('/observability/budget')
assert r.status_code == 200
@@ -118,7 +118,7 @@ def test_budget_update_rejects_invalid_budget(client, monkeypatch):
from routes import admin_observability_routes as mod
monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([]))
r = client.post(
'/admin/budget/update/1',
'/observability/budget/update/1',
json={'budget_usd': -5, 'alert_pct': 80},
)
assert r.status_code == 400
@@ -129,7 +129,7 @@ def test_budget_update_rejects_invalid_alert(client, monkeypatch):
from routes import admin_observability_routes as mod
monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([]))
r = client.post(
'/admin/budget/update/1',
'/observability/budget/update/1',
json={'budget_usd': 10, 'alert_pct': 999},
)
assert r.status_code == 400
@@ -139,7 +139,7 @@ def test_budget_update_accepts_valid(client, monkeypatch):
from routes import admin_observability_routes as mod
monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([]))
r = client.post(
'/admin/budget/update/1',
'/observability/budget/update/1',
json={'budget_usd': 25.50, 'alert_pct': 80},
)
assert r.status_code == 200
@@ -147,17 +147,17 @@ def test_budget_update_accepts_valid(client, monkeypatch):
# ──────────────────────────────────────────────────────────────────────────
# /admin/ppt_audit_history
# /observability/ppt_audit_history
# ──────────────────────────────────────────────────────────────────────────
def test_ppt_audit_history_200(client):
"""無 DB 依賴,純掃 reports/。"""
r = client.get('/admin/ppt_audit_history')
r = client.get('/observability/ppt_audit_history')
assert r.status_code == 200
# ──────────────────────────────────────────────────────────────────────────
# /admin/host_health
# /observability/host_health
# ──────────────────────────────────────────────────────────────────────────
def test_host_health_200(client, monkeypatch):
@@ -172,5 +172,5 @@ def test_host_health_200(client, monkeypatch):
raise _r.exceptions.ConnectionError('mocked')
monkeypatch.setattr(_r, 'get', fake_get)
r = client.get('/admin/host_health')
r = client.get('/observability/host_health')
assert r.status_code == 200