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

@@ -114,11 +114,11 @@
<p class="text-muted mt-2"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — Admin Observability
| <a href="/admin/promotion_review">Promotion Review</a>
| <a href="/admin/quality_trend">Quality Trend</a>
| <a href="/admin/host_health">Host Health</a>
| <a href="/admin/budget">Budget</a>
| <a href="/admin/ppt_audit_history">PPT Audit</a>
| <a href="/observability/promotion_review">Promotion Review</a>
| <a href="/observability/quality_trend">Quality Trend</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
</small></p>
</div>
{% endblock %}

View File

@@ -72,9 +72,9 @@
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — Budget Manager
| <a href="/admin/ai_calls">AI Calls</a>
| <a href="/admin/host_health">Host Health</a>
| <a href="/admin/promotion_review">Promotion Review</a>
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/promotion_review">Promotion Review</a>
</small></p>
</div>
@@ -85,7 +85,7 @@ async function saveBudget(id) {
const btn = document.querySelector(`.save-budget-btn[data-budget-id="${id}"]`);
btn.disabled = true; btn.innerText = '⏳';
try {
const r = await fetch(`/admin/budget/update/${id}`, {
const r = await fetch(`/observability/budget/update/${id}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({

View File

@@ -114,11 +114,11 @@
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — Host Health Dashboard
| <a href="/admin/ai_calls">AI Calls</a>
| <a href="/admin/promotion_review">Promotion Review</a>
| <a href="/admin/quality_trend">Quality Trend</a>
| <a href="/admin/budget">Budget</a>
| <a href="/admin/ppt_audit_history">PPT Audit</a>
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/promotion_review">Promotion Review</a>
| <a href="/observability/quality_trend">Quality Trend</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
</small></p>
</div>
{% endblock %}

View File

@@ -50,9 +50,9 @@
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — PPT Audit History
| <a href="/admin/ai_calls">AI Calls</a>
| <a href="/admin/host_health">Host Health</a>
| <a href="/admin/budget">Budget</a>
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
</small></p>
</div>
{% endblock %}

View File

@@ -54,11 +54,11 @@
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — PromotionGate Web 審核頁
| <a href="/admin/ai_calls">AI Calls</a>
| <a href="/admin/quality_trend">Quality Trend</a>
| <a href="/admin/host_health">Host Health</a>
| <a href="/admin/budget">Budget</a>
| <a href="/admin/ppt_audit_history">PPT Audit</a>
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/quality_trend">Quality Trend</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
</small></p>
</div>
@@ -66,7 +66,7 @@
async function approveEpisode(id, btn) {
btn.disabled = true; btn.innerText = '⏳ 處理中...';
try {
const r = await fetch(`/admin/promotion_review/approve/${id}`, {method: 'POST'});
const r = await fetch(`/observability/promotion_review/approve/${id}`, {method: 'POST'});
const d = await r.json();
if (d.ok) {
const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);
@@ -87,7 +87,7 @@ async function rejectEpisode(id, btn) {
if (!confirm(`拒絕 Episode #${id}?此筆將永不晉升(保留在 learning_episodes 不刪除)`)) return;
btn.disabled = true; btn.innerText = '⏳ 處理中...';
try {
const r = await fetch(`/admin/promotion_review/reject/${id}`, {method: 'POST'});
const r = await fetch(`/observability/promotion_review/reject/${id}`, {method: 'POST'});
const d = await r.json();
if (d.ok) {
const card = document.querySelector(`.episode-card[data-episode-id="${id}"]`);

View File

@@ -94,11 +94,11 @@
<p class="text-muted mt-3"><small>
🤖 Operation Ollama-First v5.0 / Phase 29 — Caller Quality Trend
| <a href="/admin/ai_calls">AI Calls</a>
| <a href="/admin/promotion_review">Promotion Review</a>
| <a href="/admin/host_health">Host Health</a>
| <a href="/admin/budget">Budget</a>
| <a href="/admin/ppt_audit_history">PPT Audit</a>
| <a href="/observability/ai_calls">AI Calls</a>
| <a href="/observability/promotion_review">Promotion Review</a>
| <a href="/observability/host_health">Host Health</a>
| <a href="/observability/budget">Budget</a>
| <a href="/observability/ppt_audit_history">PPT Audit</a>
</small></p>
</div>
{% endblock %}