diff --git a/scripts/check_observability_ui.py b/scripts/check_observability_ui.py
index 781f306..55fdf30 100644
--- a/scripts/check_observability_ui.py
+++ b/scripts/check_observability_ui.py
@@ -65,7 +65,7 @@ TEMPLATE_RULES = [
Rule(
"raw_error_copy",
re.compile(
- r"(查詢失敗:|ProgrammingError|UndefinedError|Traceback|Internal Server Error|relation\s+"|relation\s+\")"
+ r"(查詢失敗:|ProgrammingError|UndefinedError|Traceback|Internal Server Error|relation\s+"|relation\s+\"|alert\(['\"]Error[::]|載入錯誤:\$\{e\}|unknown)"
),
"不得把 SQL/Jinja exception 或 raw failure 文案直接顯示給使用者。",
),
diff --git a/templates/admin/ai_calls_dashboard.html b/templates/admin/ai_calls_dashboard.html
index c0c63fb..10f6b88 100644
--- a/templates/admin/ai_calls_dashboard.html
+++ b/templates/admin/ai_calls_dashboard.html
@@ -143,6 +143,6 @@
const el = document.getElementById('hourlyTrendChart'); if (!el || !labels.length) return;
new Chart(el, { data: { labels, datasets: [ { type: 'line', label: '呼叫數', data: calls, borderColor: '#c96442', backgroundColor: 'rgba(201,100,66,.12)', tension: .35, fill: true, yAxisID: 'y' }, { type: 'line', label: '錯誤', data: errors, borderColor: '#b94b45', backgroundColor: 'rgba(185,75,69,.1)', tension: .35, yAxisID: 'y' }, { type: 'bar', label: '成本 USD', data: costs, backgroundColor: 'rgba(184,121,47,.38)', borderColor: '#b8792f', yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, scales: { y: { beginAtZero: true, title: { display: true, text: '次數' } }, y1: { position: 'right', beginAtZero: true, grid: { drawOnChartArea: false }, title: { display: true, text: 'USD' } } } } });
})();
-async function triggerCodeReview() { if (!confirm('觸發 Code Review Pipeline?\n\n會對最新 commit 跑 5 step 審查,背景執行。')) return; try { const r = await fetch('/observability/ai_calls/trigger_code_review', {method: 'POST'}); const d = await r.json(); if (d.ok) { alert(`✅ ${d.message}\n\nPipeline ID: ${d.pipeline_id}\nCommit: ${d.commit_sha}\n變更檔案: ${d.changed_files_count} 個`); } else { alert('❌ ' + (d.error || '觸發失敗')); } } catch (e) { alert('Error: ' + e); } }
+async function triggerCodeReview() { if (!confirm('觸發 Code Review Pipeline?\n\n會對最新 commit 跑 5 step 審查,背景執行。')) return; try { const r = await fetch('/observability/ai_calls/trigger_code_review', {method: 'POST'}); const d = await r.json(); if (d.ok) { alert(`✅ ${d.message}\n\nPipeline ID: ${d.pipeline_id}\nCommit: ${d.commit_sha}\n變更檔案: ${d.changed_files_count} 個`); } else { alert('❌ ' + (d.error || '觸發失敗')); } } catch (e) { console.warn('code_review_trigger_failed', e); alert('操作暫時無法完成,請稍後再試或查看系統日誌。'); } }
{% endblock %}
diff --git a/templates/admin/budget.html b/templates/admin/budget.html
index 52e7b5f..893f3c2 100644
--- a/templates/admin/budget.html
+++ b/templates/admin/budget.html
@@ -89,7 +89,7 @@
{% endblock %}
diff --git a/templates/admin/business_intel.html b/templates/admin/business_intel.html
index d2667cf..ca499a4 100644
--- a/templates/admin/business_intel.html
+++ b/templates/admin/business_intel.html
@@ -423,7 +423,7 @@
{% set ns.conf_total = ns.conf_total + ((r.avg_confidence or 0) * (r.count or 0)) %}
{% endfor %}
{% for v in verdict_stats %}
- {% set label = v.verdict or 'unknown' %}
+ {% set label = v.verdict or '未分類' %}
{% set ns.verdict_total = ns.verdict_total + (v.count or 0) %}
{% if label == 'effective' or label == 'success' or label == 'positive' %}
{% set ns.effective = ns.effective + (v.count or 0) %}
@@ -517,7 +517,7 @@
{% for r in rec_by_strategy %}
- {{ r.strategy or 'unknown' }}
+ {{ r.strategy or '未分類策略' }}
{{ r.count }}建議數
{{ '%.0f'|format((r.avg_confidence or 0) * 100) }}%信心
@@ -629,7 +629,7 @@
| Verdict | Count | Avg Δ |
{% for v in verdict_stats %}
- | {{ v.verdict or 'unknown' }} | {{ v.count }} | {{ '%.1f'|format(v.avg_delta or 0) }}% |
+ | {{ v.verdict or '未分類' }} | {{ v.count }} | {{ '%.1f'|format(v.avg_delta or 0) }}% |
{% endfor %}
@@ -712,7 +712,7 @@
new Chart(canvas, {
type: 'doughnut',
data: {
- labels: verdictRows.map(row => row.verdict || 'unknown'),
+ labels: verdictRows.map(row => row.verdict || '未分類'),
datasets: [{
data: verdictRows.map(row => row.count || 0),
backgroundColor: ['#2f8f6b', '#c96442', '#f1b45a', '#6d4b3f', '#d9a06f'],
diff --git a/templates/admin/host_health.html b/templates/admin/host_health.html
index 1b5eaac..b649c41 100644
--- a/templates/admin/host_health.html
+++ b/templates/admin/host_health.html
@@ -178,7 +178,7 @@
if (!el || !data.length) return;
new Chart(el, { type: 'line', data: { labels: data.map(d => d.date), datasets: [{ data: data.map(d => d.rate), borderColor: '#c96442', backgroundColor: 'rgba(201,100,66,.14)', borderWidth: 2, fill: true, tension: .35, pointRadius: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { min: 0, max: 100, ticks: { callback: v => v + '%' } } } } });
})();
-async function togglePlaybook(id, name) { if (!confirm(`切換 Playbook 「${name}」狀態?`)) return; try { const r = await fetch(`/observability/playbooks/toggle/${id}`, {method: 'POST'}); const d = await r.json(); if (d.ok) { alert(`✅ ${d.message}`); window.location.reload(); } else { alert('❌ ' + (d.error || '切換失敗')); } } catch (e) { alert('Error: ' + e); } }
-async function triggerAutoHeal(hostLabel) { if (!confirm(`觸發 AutoHeal?\n\n主機:${hostLabel}`)) return; try { const r = await fetch('/observability/host_health/trigger_autoheal', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({host_label: hostLabel}) }); const d = await r.json(); if (d.ok) { alert(`✅ AutoHeal 已派出\n動作:${d.action || '—'}\n訊息:${d.message || ''}`); window.location.reload(); } else { alert('❌ ' + (d.error || d.message || '觸發失敗')); } } catch (e) { alert('Error: ' + e); } }
+async function togglePlaybook(id, name) { if (!confirm(`切換 Playbook 「${name}」狀態?`)) return; try { const r = await fetch(`/observability/playbooks/toggle/${id}`, {method: 'POST'}); const d = await r.json(); if (d.ok) { alert(`✅ ${d.message}`); window.location.reload(); } else { alert('❌ ' + (d.error || '切換失敗')); } } catch (e) { console.warn('playbook_toggle_failed', e); alert('操作暫時無法完成,請稍後再試或查看系統日誌。'); } }
+async function triggerAutoHeal(hostLabel) { if (!confirm(`觸發 AutoHeal?\n\n主機:${hostLabel}`)) return; try { const r = await fetch('/observability/host_health/trigger_autoheal', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({host_label: hostLabel}) }); const d = await r.json(); if (d.ok) { alert(`✅ AutoHeal 已派出\n動作:${d.action || '—'}\n訊息:${d.message || ''}`); window.location.reload(); } else { alert('❌ ' + (d.error || d.message || '觸發失敗')); } } catch (e) { console.warn('host_autoheal_failed', e); alert('操作暫時無法完成,請稍後再試或查看系統日誌。'); } }
{% endblock %}
diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html
index af2cc75..7b7316f 100644
--- a/templates/admin/ppt_audit_history.html
+++ b/templates/admin/ppt_audit_history.html
@@ -29,6 +29,6 @@
{% endblock %}
diff --git a/templates/admin/promotion_review.html b/templates/admin/promotion_review.html
index 5ea27dc..ad4f460 100644
--- a/templates/admin/promotion_review.html
+++ b/templates/admin/promotion_review.html
@@ -101,7 +101,7 @@
{% endblock %}
diff --git a/templates/admin/rag_queries.html b/templates/admin/rag_queries.html
index f1efb9a..18909ff 100644
--- a/templates/admin/rag_queries.html
+++ b/templates/admin/rag_queries.html
@@ -40,7 +40,7 @@
{% endblock %}