V10.613 高可見頁面繁中化
All checks were successful
CD Pipeline / deploy (push) Successful in 1m14s

This commit is contained in:
OoO
2026-06-16 09:54:27 +08:00
parent 56ebba045b
commit 239b773288
10 changed files with 118 additions and 58 deletions

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.612"
SYSTEM_VERSION = "V10.613"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -1,8 +1,8 @@
# PChome 業績成長自動化作戰系統 — AI 競價情報模組 Single Source of Truth
> **最後更新**: 2026-06-16 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯PChome 後台業績匯入韌性已補強產品定位正名為「PChome 業績成長自動化作戰系統」外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢前台操作入口已建立
> **適用版本**: V10.612
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯PChome 後台業績匯入韌性已補強產品定位正名為「PChome 業績成長自動化作戰系統」外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢前台操作入口與高可見頁面繁中化守門已建立
> **適用版本**: V10.613
---
@@ -59,6 +59,7 @@
- V10.610 起 `/api/ai/pchome-growth/opportunities` 優先讀取 `external_offers` 的自動同步資料;只有新資料層缺資料時才 fallback 舊 `competitor_prices`。API stats 會回傳資料來源計數,方便確認作戰清單是否已走新資料層。
- V10.611 起 `/ai_intelligence` 是營運使用者主入口:頁首提供「今日作戰入口」,依序連到 PChome 成長作戰、補商品對應、MOMO 外部價格參考與外部報價預檢;作戰清單左側會直接顯示今日優先動作與資料來源摘要。
- V10.612 起 `/api/ai/icaim/dashboard` 的「MOMO 外部價格參考」表格也優先讀 `external_offers`,缺資料才 fallback `competitor_prices`;價差與風險改採 PChome 視角,正數代表 PChome 比 MOMO 外部參考價高。
- V10.613 起高可見前台頁面必須以繁體中文呈現程式碼審查、AI 自動化健康檢查、PPT 產線與商品看板操作標籤不得使用英文工程標題或簡體字;測試需防止頁面文案退回英文。
## 零之一、12 Agent 決策信封2026-05-24
@@ -169,11 +170,11 @@ SQL漏斗(~300筆)
- `/metrics` 匯出 `momo_ai_event_router_replay_total`
- `/metrics` 匯出 `momo_ai_autoheal_action_total``momo_ai_autoheal_duration_ms_count/sum/max`
- `/metrics` 在尚無事件時仍輸出 `momo_ai_*` zero-baseline series讓 Prometheus/Grafana 重啟後可立即看到 metric names。
- `/ai_automation_smoke` 提供登入後 smoke dashboard
- `/ai_automation_smoke` 提供登入後 AI 自動化健康檢查頁
- `/api/ai-automation/smoke` 提供 read-only JSON 狀態,不做外部網路呼叫。
- Smoke API 會將最近檢結果保存到 JSONLdashboard 顯示最近狀態趨勢。
- Smoke history 支援 JSONL 匯出、清理與每日 OK / Warning / Critical 摘要。
- Smoke 每日摘要支援手動 Telegram 推播,並由 `momo-scheduler` 每日 09:10 呼叫 `run_ai_smoke_daily_summary_task()`
- 健康檢查 API 會將最近檢結果保存到 JSONL頁面顯示最近狀態趨勢。
- 健康檢查歷史支援 JSONL 匯出、清理與每日「正常 / 注意 / 嚴重」摘要。
- 健康檢查每日摘要支援手動 Telegram 推播,並由 `momo-scheduler` 每日 09:10 呼叫 `run_ai_smoke_daily_summary_task()`
- Grafana provisioning 新增 `docker/grafana/provisioning/dashboards/json/ai-automation-overview.json`,觀測 EventRouter dispatch/latency、safe action、Telegram replay 與 AutoHeal action/duration。
- Active monitoring stack 使用 `monitoring/prometheus.yml``momo-app` job scrape `momo-pro-system:80/metrics`Prometheus container 需加入 `momo-network`
- Active Blackbox HTTP targets 必須探測 `/health`188 stack 目前 `https://mo.wooo.work/health``http://momo-pro-system:80/health`110 gateway stack 目前 `https://mo.wooo.work/health`),不可探測 Dashboard 首頁 `/`,避免監控流量觸發重型 DB 查詢。

View File

@@ -226,3 +226,10 @@
- 表格 stats 新增 `competitor_data_source_counts`,前端 footer 顯示「自動同步資料層 / 舊比價快取」各幾筆;每列狀態會顯示「自動同步」或「舊資料」。
- 價差與高風險統計改採 PChome 視角:正數代表 PChome 比 MOMO 外部參考價高,才列入需檢查價格。
- 下一步:把 Hermes / ElephantAlpha / AI product pick 等後端價格分析逐步改讀 `external_offers`,讓告警與頁面使用同一份資料來源。
## 16. 2026-06-16 V10.613 高可見頁面繁中化守門
- 使用者要求所有內容與頁面必須使用繁體中文工程內部變數、CSS class、API key 名稱可保留英文,但使用者可見標題、按鈕、狀態與說明不得用英文工程語。
- 已把 `/code-review/` 顯示文字改成「AI 程式碼審查」「流程進度」「程式碼審查完成」,並將 OpenClaw 模型顯示改成「Ollama 優先」,避免前台誤以為 Gemini 是主路徑。
- 已把 AI 自動化健康檢查頁改為白話繁中命名,狀態顯示改成「正常 / 注意 / 嚴重 / 產生時間」。
- 已把 PPT 觀測台與商品看板高可見英文標籤改成「產線健康度 / 工作隊列 / 視覺問題 / 產線控制台 / 覆蓋率流程」,並新增測試防回歸。

View File

@@ -233,7 +233,7 @@
</section>
<section class="ppt-health-board" aria-label="PPT 產線健康總覽">
<div class="ppt-health-main is-{{ pipeline_view.status }}">
<div class="ppt-label">Pipeline Health</div>
<div class="ppt-label">產線健康度</div>
<h2>{{ pipeline_view.title }}</h2>
<p>{{ pipeline_view.message }}</p>
<div class="ppt-health-facts">
@@ -260,7 +260,7 @@
<section class="ppt-action-queue" id="ppt-action-queue" data-ppt-action-queue aria-label="PPT 工作隊列">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">Action Queue</div>
<div class="ppt-label">工作隊列</div>
<h2 class="ppt-panel-title">接下來要處理的事</h2>
</div>
<small class="text-muted">把缺漏、預覽、視覺 QA、DB 寫入集中成工作隊列</small>
@@ -315,7 +315,7 @@
<section class="ppt-issue-board" id="ppt-issue-board" aria-label="視覺問題追蹤">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">Vision Findings</div>
<div class="ppt-label">視覺問題</div>
<h2 class="ppt-panel-title">視覺問題追蹤</h2>
</div>
<div class="ppt-issue-metrics">
@@ -368,7 +368,7 @@
data-report-types="{{ auto_generation_missing_report_types | join(',') }}">
<div class="ppt-panel-head">
<div>
<div class="ppt-label">Production Command Center</div>
<div class="ppt-label">產線控制台</div>
<h2 class="ppt-panel-title">簡報產線控制台</h2>
</div>
<div class="ppt-panel-actions">

View File

@@ -1,6 +1,6 @@
{% extends 'ewoooc_base.html' %}
{% block title %}AI 自動化 Smoke Dashboard - EwoooC{% endblock %}
{% block title %}AI 自動化健康檢查 - EwoooC{% endblock %}
{% block extra_css %}
<style>
@@ -127,8 +127,8 @@
<div class="smoke-hero mb-4">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 position-relative" style="z-index: 1;">
<div>
<span class="smoke-pill mb-3"><i class="fas fa-robot"></i> FOUR-AGENT CONTROL PLANE</span>
<h1 class="fw-bold mb-2">AI 自動化 Smoke Dashboard</h1>
<span class="smoke-pill mb-3"><i class="fas fa-robot"></i>四 Agent 控制面</span>
<h1 class="fw-bold mb-2">AI 自動化健康檢查</h1>
<p class="mb-0 text-muted">快速確認 EventRouter、AutoHeal、NemoTron、OpenClaw 與 ElephantAlpha 的閉環狀態。</p>
</div>
<div class="text-lg-end">
@@ -151,17 +151,17 @@
</div>
<div class="row g-3 mb-4" id="summaryCards">
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">OK</div><div class="h3 mb-0" id="okCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">Warning</div><div class="h3 mb-0" id="warningCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">Critical</div><div class="h3 mb-0" id="criticalCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">Generated</div><div class="fs-6 fw-semibold" id="generatedAt">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">正常</div><div class="h3 mb-0" id="okCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">注意</div><div class="h3 mb-0" id="warningCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">嚴重</div><div class="h3 mb-0" id="criticalCount">-</div></div></div></div>
<div class="col-md-3"><div class="card smoke-card"><div class="card-body"><div class="text-muted small">產生時間</div><div class="fs-6 fw-semibold" id="generatedAt">-</div></div></div></div>
</div>
<div class="card smoke-card mb-4">
<div class="card-body">
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 mb-3">
<div>
<h5 class="mb-1">最近 Smoke 趨勢</h5>
<h5 class="mb-1">最近健康檢查趨勢</h5>
<div class="text-muted small">每次 API 快檢會保存一筆精簡紀錄,保留最近 200 筆。</div>
</div>
<div class="text-muted small" id="historySummary">等待資料...</div>
@@ -175,7 +175,7 @@
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 mb-3">
<div>
<h5 class="mb-1">每日摘要</h5>
<div class="text-muted small">依最近保存紀錄彙整,方便快速觀察是否有連續 warning / critical</div>
<div class="text-muted small">依最近保存紀錄彙整,方便快速觀察是否有連續注意或嚴重狀態</div>
</div>
</div>
<div class="table-responsive">
@@ -183,10 +183,10 @@
<thead>
<tr>
<th>日期</th>
<th>OK</th>
<th>Warning</th>
<th>Critical</th>
<th>Total</th>
<th>正常</th>
<th>注意</th>
<th>嚴重</th>
<th>總計</th>
</tr>
</thead>
<tbody id="dailySummaryRows">
@@ -203,7 +203,7 @@
{% block extra_js %}
<script>
function badge(status) {
const label = {ok: 'OK', warning: 'WARNING', critical: 'CRITICAL'}[status] || status;
const label = {ok: '正常', warning: '注意', critical: '嚴重'}[status] || status;
return `<span class="status-badge status-${status}">${label}</span>`;
}
@@ -244,7 +244,7 @@ function renderTrend(history) {
const recent = history.recent || [];
const counts = history.counts || {ok: 0, warning: 0, critical: 0};
document.getElementById('historySummary').textContent =
`OK ${counts.ok || 0} / Warning ${counts.warning || 0} / Critical ${counts.critical || 0}`;
`正常 ${counts.ok || 0} / 注意 ${counts.warning || 0} / 嚴重 ${counts.critical || 0}`;
const strip = document.getElementById('trendStrip');
if (!recent.length) {
@@ -289,7 +289,7 @@ async function loadSmoke() {
document.getElementById('checkGrid').innerHTML = `
<div class="card smoke-card">
<div class="card-body">
<h5>Smoke API</h5>
<h5>健康檢查 API</h5>
<p class="text-danger">讀取失敗:${escapeHtml(err.message)}</p>
</div>
</div>`;
@@ -301,7 +301,7 @@ async function loadSmoke() {
document.getElementById('refreshBtn').addEventListener('click', loadSmoke);
document.getElementById('clearHistoryBtn').addEventListener('click', async () => {
if (!confirm('確定要清理 AI Smoke 趨勢紀錄?這只會刪除本頁 JSONL history,不會影響 DB 或事件資料。')) {
if (!confirm('確定要清理 AI 健康檢查趨勢紀錄?這只會刪除本頁健康檢查紀錄檔,不會影響 DB 或事件資料。')) {
return;
}
const btn = document.getElementById('clearHistoryBtn');
@@ -310,7 +310,7 @@ document.getElementById('clearHistoryBtn').addEventListener('click', async () =>
const res = await fetchWithCSRF('/api/ai-automation/smoke/history/clear', {method: 'POST'});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
showToast(`已清理 ${data.cleared || 0} Smoke 趨勢紀錄`, 'success');
showToast(`已清理 ${data.cleared || 0}健康檢查趨勢紀錄`, 'success');
await loadSmoke();
} catch (err) {
showToast(`清理失敗:${err.message}`, 'error');
@@ -325,7 +325,7 @@ document.getElementById('sendSummaryBtn').addEventListener('click', async () =>
const res = await fetchWithCSRF('/api/ai-automation/smoke/daily-summary/send', {method: 'POST'});
const data = await res.json();
if (!res.ok) throw new Error((data.telegram?.errors || []).join(', ') || `HTTP ${res.status}`);
showToast(`AI Smoke 每日摘要已推播:sent=${data.telegram?.sent || 0}`, 'success');
showToast(`AI 健康檢查每日摘要已推播:已送出 ${data.telegram?.sent || 0}`, 'success');
} catch (err) {
showToast(`推播失敗:${err.message}`, 'error');
} finally {

View File

@@ -1,6 +1,6 @@
{% extends "ewoooc_base.html" %}
{% block title %}AI Code Review - EwoooC{% endblock %}
{% block title %}AI 程式碼審查 - EwoooC{% endblock %}
{% block extra_css %}
<style>
@@ -276,9 +276,9 @@
<!-- Top Bar -->
<div class="topbar">
<span>🔍</span>
<h1>AI Code Review</h1>
<span class="badge">EwoooC · Post-Deploy Pipeline</span>
<div id="liveDot" class="live-dot idle" title="Pipeline 狀態"></div>
<h1>AI 程式碼審查</h1>
<span class="badge">EwoooC · 部署後檢查流程</span>
<div id="liveDot" class="live-dot idle" title="流程狀態"></div>
</div>
<div class="layout">
@@ -288,16 +288,16 @@
<!-- Pipeline Steps -->
<div class="card">
<div class="card-header">🤖 Pipeline 進度
<div class="card-header">🤖 流程進度
<span id="pipelineId" style="font-size:11px;color:var(--muted);margin-left:auto;font-family:monospace"></span>
</div>
<div class="card-body">
<div class="pipeline" id="stepsContainer">
<div class="step" id="step-1"><div class="step-num">1</div><div class="step-info"><div class="step-name">讀取變更檔案</div><div class="step-agent">system</div></div></div>
<div class="step" id="step-1"><div class="step-num">1</div><div class="step-info"><div class="step-name">讀取變更檔案</div><div class="step-agent">系統</div></div></div>
<div class="step" id="step-2"><div class="step-num">2</div><div class="step-info"><div class="step-name">Hermes 程式碼掃描</div><div class="step-agent">Hermes · hermes3:latest</div></div></div>
<div class="step" id="step-3"><div class="step-num">3</div><div class="step-info"><div class="step-name">OpenClaw 架構評估</div><div class="step-agent">OpenClaw · Gemini 2.5</div></div></div>
<div class="step" id="step-3"><div class="step-num">3</div><div class="step-info"><div class="step-name">OpenClaw 架構評估</div><div class="step-agent">OpenClaw · Ollama 優先</div></div></div>
<div class="step" id="step-4"><div class="step-num">4</div><div class="step-info"><div class="step-name">Elephant Alpha 決策</div><div class="step-agent">Elephant Alpha · 100B</div></div></div>
<div class="step" id="step-5"><div class="step-num">5</div><div class="step-info"><div class="step-name">NemoTron 行動派遣</div><div class="step-agent">NemoTron · Dispatcher</div></div></div>
<div class="step" id="step-5"><div class="step-num">5</div><div class="step-info"><div class="step-name">NemoTron 行動派遣</div><div class="step-agent">NemoTron · 派遣器</div></div></div>
</div>
</div>
</div>
@@ -307,10 +307,10 @@
<div class="card-header">📊 問題嚴重度分佈</div>
<div class="card-body">
<div class="sev-grid">
<div class="sev-cell sev-critical"><div class="num" id="cnt-critical"></div><div class="lbl">🔴 CRITICAL</div></div>
<div class="sev-cell sev-high"><div class="num" id="cnt-high"></div><div class="lbl">🟠 HIGH</div></div>
<div class="sev-cell sev-medium"><div class="num" id="cnt-medium"></div><div class="lbl">🟡 MEDIUM</div></div>
<div class="sev-cell sev-low"><div class="num" id="cnt-low"></div><div class="lbl">🟢 LOW</div></div>
<div class="sev-cell sev-critical"><div class="num" id="cnt-critical"></div><div class="lbl">🔴 最高</div></div>
<div class="sev-cell sev-high"><div class="num" id="cnt-high"></div><div class="lbl">🟠 </div></div>
<div class="sev-cell sev-medium"><div class="num" id="cnt-medium"></div><div class="lbl">🟡 </div></div>
<div class="sev-cell sev-low"><div class="num" id="cnt-low"></div><div class="lbl">🟢 </div></div>
</div>
</div>
</div>
@@ -354,7 +354,7 @@
</div>
<div class="card-body" style="padding:0">
<div id="findingsTable">
<div class="empty"><div class="icon">🔍</div><div>等待 Code Review 完成...</div></div>
<div class="empty"><div class="icon">🔍</div><div>等待程式碼審查完成...</div></div>
</div>
</div>
</div>
@@ -392,6 +392,9 @@
{% block extra_js %}
<script>
const SEV_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
const SEV_LABEL = { CRITICAL: '最高', HIGH: '高', MEDIUM: '中', LOW: '低' };
const PRIORITY_LABEL = { critical: '最高', high: '高', medium: '中', low: '低' };
const DEPLOY_TYPE_LABEL = { sync: '同步部署', rebuild: '重建部署', manual: '手動觸發' };
let _polling = null;
let _lastPipelineId = null;
@@ -452,7 +455,7 @@ function renderFindings(findings) {
<thead><tr><th>嚴重度</th><th>類型</th><th>檔案 / 位置</th><th>問題說明</th><th>修復建議</th></tr></thead>
<tbody>${sorted.map(f => `
<tr>
<td><span class="badge-sev badge-${f.severity}">${f.severity}</span></td>
<td><span class="badge-sev badge-${f.severity}">${SEV_LABEL[f.severity] || f.severity || '?'}</span></td>
<td><span class="badge-type">${f.type||'?'}</span></td>
<td><code>${(f.file||'').split('/').pop()}</code><br><span style="font-size:11px;color:var(--muted)">${f.line_hint||''}</span></td>
<td>${escHtml(f.description||'')}</td>
@@ -474,11 +477,12 @@ function renderEA(ea, autoFix) {
}
const priColors = { critical: 'var(--red)', high: 'var(--orange)', medium: 'var(--yellow)', low: 'var(--green)' };
const col = priColors[ea.priority] || 'var(--blue)';
const priorityLabel = PRIORITY_LABEL[ea.priority] || ea.priority || '待確認';
const fixFiles = (ea.fix_files||[]).map(f=>`<code>${f}</code>`).join(' ');
document.getElementById('eaDecision').innerHTML = `
<div class="ea-box ${ea.priority}">
<div style="display:flex;align-items:center;gap:10px">
<div class="ea-priority" style="color:${col}">${ea.priority.toUpperCase()}</div>
<div class="ea-priority" style="color:${col}">${priorityLabel}</div>
<div>${autoFix ? '🔧 <b>自動修復已觸發AiderHeal</b>' : (ea.human_review_needed ? '👁 <b>需人工審查</b>' : '✅ <b>無需修復</b>')}</div>
</div>
<div class="ea-reasoning">${escHtml(ea.reasoning||'')}</div>
@@ -493,9 +497,9 @@ function renderCommitInfo(state) {
const files = (state.changed_files||[]).slice(0,5).map(f=>`<code>${f.split('/').pop()}</code>`).join(' ');
const more = (state.changed_files||[]).length > 5 ? `<span style="color:var(--muted)">+${state.changed_files.length-5}</span>` : '';
el.innerHTML = `
<div><b>Commit</b> <code>${state.commit_sha.slice(0,8)}</code></div>
<div><b>Branch</b> <code>${state.branch||'?'}</code></div>
<div><b>模式</b> ${state.deploy_type||'sync'}</div>
<div><b>提交</b> <code>${state.commit_sha.slice(0,8)}</code></div>
<div><b>分支</b> <code>${state.branch||'?'}</code></div>
<div><b>模式</b> ${DEPLOY_TYPE_LABEL[state.deploy_type] || state.deploy_type || '同步部署'}</div>
<div><b>變更</b> ${files} ${more}</div>`;
}
@@ -508,9 +512,9 @@ function renderStatusBar(state) {
el.style.display = 'block';
el.className = state.status;
const msgs = {
running: `⟳ <b>Pipeline 執行中</b> — Step ${state.current_step}/5`,
completed: `✅ <b>Code Review 完成</b> — ${state.message||''}`,
error: `❌ <b>Pipeline 失敗</b> — ${escHtml(state.message||'')}`,
running: `⟳ <b>流程執行中</b> — ${state.current_step}/5`,
completed: `✅ <b>程式碼審查完成</b> — ${state.message||''}`,
error: `❌ <b>流程失敗</b> — ${escHtml(state.message||'')}`,
skipped: `⏭ <b>已略過</b> — ${escHtml(state.message||'')}`,
};
el.innerHTML = msgs[state.status] || '';
@@ -554,15 +558,15 @@ function loadHistoryItem(idx) {
const files = (h.changed_files||[]).slice(0,5).map(f=>`<code>${f.split('/').pop()}</code>`).join(' ');
const more = (h.changed_files||[]).length > 5 ? `<span style="color:var(--muted)">+${h.changed_files.length-5}</span>` : '';
document.getElementById('commitInfo').innerHTML = `
<div><b>Commit</b> <code>${h.commit_sha}</code></div>
<div><b>Branch</b> <code>${h.branch||'?'}</code></div>
<div><b>提交</b> <code>${h.commit_sha}</code></div>
<div><b>分支</b> <code>${h.branch||'?'}</code></div>
<div><b>時間</b> ${h.created_at.slice(0,16).replace('T',' ')}</div>
<div><b>變更</b> ${files} ${more}</div>`;
document.getElementById('pipelineId').textContent = (h.pipeline_id||'').slice(-14);
const sBar = document.getElementById('statusBar');
sBar.style.display = 'block';
sBar.className = 'completed';
sBar.innerHTML = `✅ <b>歷史記錄</b> — Commit ${h.commit_sha}${h.auto_fix ? ' 🔧 已自動修復' : ''}`;
sBar.innerHTML = `✅ <b>歷史記錄</b> — 提交 ${h.commit_sha}${h.auto_fix ? ' 🔧 已自動修復' : ''}`;
renderFindings(h.findings || []);
renderOpenClaw(h.openclaw_report || '');
renderEA(h.ea_decision || {}, h.auto_fix || false);

View File

@@ -351,7 +351,7 @@
</li>
<li>
<a class="dropdown-item" href="/ai_automation_smoke">
<i class="fas fa-heartbeat me-2"></i>AI 自動化 Smoke
<i class="fas fa-heartbeat me-2"></i>AI 自動化健康檢查
</a>
</li>
</ul>

View File

@@ -97,7 +97,7 @@
</div>
<div class="dashboard-decision-workbench" aria-label="提升決策支援覆蓋率工作流">
<div class="dashboard-decision-workbench__head">
<span class="dashboard-backfill-label momo-mono">COVERAGE WORKFLOW</span>
<span class="dashboard-backfill-label momo-mono">覆蓋率流程</span>
<strong>提升決策支援覆蓋率</strong>
<em>先處理最能轉成可決策資料的隊列,避免盲目降低門檻。</em>
</div>

View File

@@ -403,11 +403,11 @@ def test_ppt_audit_history_shows_ppt_schedule_and_db_runs(client, monkeypatch):
assert text in html
assert 'ppt_generation_runs' in html
assert '每日日報' in html
assert 'Pipeline Health' in html
assert '產線健康度' in html
assert '排程節奏' in html
assert 'DB 寫入' in html
assert '線上預覽' in html
assert 'Action Queue' in html
assert '工作隊列' in html
assert '接下來要處理的事' in html
assert '異常優先' in html
assert '產線錯誤:缺少資料來源' in html

View File

@@ -50,6 +50,54 @@ def test_frontend_v2_shell_uses_real_runtime_context():
assert all(marker not in combined for marker in forbidden_markers)
def test_high_visibility_pages_use_traditional_chinese_labels():
page_paths = [
"templates/code_review.html",
"templates/ai_automation_smoke.html",
"templates/admin/ppt_audit_history.html",
"templates/dashboard_v2.html",
"templates/components/_navbar.html",
]
combined = "\n".join((ROOT / path).read_text(encoding="utf-8") for path in page_paths)
assert "AI 程式碼審查" in combined
assert "等待程式碼審查完成" in combined
assert "AI 自動化健康檢查" in combined
assert "四 Agent 控制面" in combined
assert "產線健康度" in combined
assert "工作隊列" in combined
assert "覆蓋率流程" in combined
assert "NemoTron · 派遣器" in combined
assert "同步部署" in combined
forbidden_visible_text = [
"AI Code Review",
"Smoke Dashboard",
"FOUR-AGENT CONTROL PLANE",
"Warning</",
"Critical</",
"Generated</",
"Action Queue",
"Pipeline Health",
"Vision Findings",
"Production Command Center",
"COVERAGE WORKFLOW",
"OpenClaw · Gemini 2.5",
"等待 Code Review 完成",
"Pipeline 執行中",
"Pipeline 失敗",
"Step ${state.current_step}",
"AI 自動化 Smoke",
"NemoTron · Dispatcher",
"<b>Commit</b>",
"<b>Branch</b>",
"${f.severity}</span>",
"ea.priority.toUpperCase",
]
for marker in forbidden_visible_text:
assert marker not in combined
def test_topbar_observability_indicator_is_cached_and_timeout_bounded():
base_js = (ROOT / "web/static/js/ewoooc-base.js").read_text(encoding="utf-8")
observability_route = (ROOT / "routes/admin_observability_routes.py").read_text(encoding="utf-8")