fix: lock pchome growth ui copy guardrails
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s

This commit is contained in:
ogt
2026-06-26 11:35:30 +08:00
parent 0808063133
commit 2144ef2102
14 changed files with 147 additions and 41 deletions

View File

@@ -141,6 +141,7 @@
- AI 自動化 Session SOP: `docs/guides/ai_automation_session_sop.md`
- Browse.sh 爬蟲診斷手冊: `docs/guides/browse_sh_crawler_playbook.md`
- Webcrumbs 共用 UI Runtime: `docs/guides/webcrumbs_shared_runtime.md`
- PChome 業績成長 UI/UX 守門: `docs/guides/pchome_growth_ui_ux_guardrails.md`
- 外部專業 Benchmark: `docs/guides/external_professional_benchmark.md`
- AI 競價情報 SOT: `docs/AI_INTELLIGENCE_MODULE_SOT.md`
- Agent 角色矩陣: `docs/guides/codex_agent_roles.md`

View File

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

View File

@@ -780,3 +780,5 @@ POSTGRES_HOST=momo-db
| 2026-06-26 | 靜態資源部署必須保持容器可讀 | V10.701 起部署 SOP 改用 Git 物件打包差異檔,並以測試檢查 `web/static` 檔案可被容器讀取,避免 macOS / iCloud 工作目錄權限造成正式 `/static/*` 500讓全站 CSS/JS 不再因檔案模式回歸而失效。 |
| 2026-06-26 | 治理頁也要用營運情境語,不得渲染 raw caller、服務代碼或錯誤日誌 | V10.702 起 AI 流量頁把 caller/provider/上下文改成「使用情境、建議路徑、作戰素材」;服務更新監控頁隱藏 branch/sha/error log/pod 名稱等工程細節,改用「更新流程、服務元件、診斷線索」語言;缺貨頁移除英文 Vendor Stockout。 |
| 2026-06-26 | 關鍵決策框架不得因資料不足整段消失 | V10.703 起 AI 流量頁的「情境 × 知識命中矩陣」改為永遠顯示;資料不足時顯示營運空狀態,避免使用者看不到頁面應該如何判讀。 |
| 2026-06-26 | 使用者頁不得把提交、分支、JSON/API 或 raw caller 當主訊息 | V10.704 起成本治理、AI 分工、AI 健康檢查與登入頁再收斂raw caller/server 改成使用情境與服務元件JSONL/API/PostgreSQL/Session/CSRF 等工程語改成檢查紀錄、健康檢查服務、資料服務與工作階段。 |
| 2026-06-26 | 全站 UI/UX 工作重點必須文件化並納入入口索引 | V10.704 起新增 `docs/guides/pchome_growth_ui_ux_guardrails.md` 並由 `AGENTS.md` 索引;所有前台頁面以「提升 PChome 業績、快速判斷、直接下一步」為共同目標,避免後續工作再偏回局部文案修補。 |

View File

@@ -0,0 +1,26 @@
# PChome Growth UI/UX Guardrails
本專案所有使用者可見頁面,都必須服務同一個產品目標:
> 讓營運者更快判斷如何提升 PChome 業績,並能直接採取下一步。
## 不可偏離的工作重點
1. 每個頁面都要有清楚的營運目的、目前狀態與下一步。
2. 使用者不應看到工程實作細節例如資料庫欄位、raw caller、commit、branch、JSON、API、endpoint、模型名稱或錯誤堆疊。
3. 比價、缺貨、匯入、AI 建議、觀測與服務健康頁,都要用同一套 PChome 業績成長語言:主推、守價、補比價、供貨風險、成長缺口、建議路徑、使用情境、服務元件。
4. 資料不足時不能整段消失,要顯示可理解的空狀態與下一步。
5. 不得把工作視窗溝通、部署交接、工程判斷或維護工作摘要搬到前台。
## 每次 UI/UX 修改的驗收
至少要完成:
- 本地測試:`tests/test_frontend_v2_assets.py`
- 相關業務測試:`tests/test_pchome_revenue_growth_service.py`
- 正式 smoke檢查 `/health` 版本、核心頁 HTTP 200、可見文案無 raw terms、靜態資源 HTTP 200
- 如果頁面有 PChome/MOMO 商品比較,必須能一眼看到兩平台價格與可同時開啟的外部賣場連結
## 判斷標準
如果頁面需要使用者理解「這是什麼欄位、哪個模型、哪個 pipeline、哪個 commit、哪個 API」才知道下一步這個 UI 就是不合格。

View File

@@ -41,7 +41,7 @@ OBSERVABILITY_PAGES = (
"/observability/agent_orchestration",
"AI 分工矩陣",
"分工",
("AI 分工矩陣", "本地模型", "知識"),
("AI 分工矩陣", "建議路徑", "知識"),
),
ObservabilityPage(
"templates/admin/business_intel.html",
@@ -57,7 +57,7 @@ OBSERVABILITY_PAGES = (
"/observability/host_health",
"主機健康",
"主機",
("主機健康", "AI 模型", "自癒"),
("主機健康", "AI 建議服務", "自癒"),
),
ObservabilityPage(
"templates/admin/ai_calls_dashboard.html",

View File

@@ -8,6 +8,8 @@
.agent-hero{padding:clamp(1.2rem,2.4vw,2rem);background:radial-gradient(circle at 12% 14%,rgba(201,100,66,.18),transparent 24rem),radial-gradient(circle at 88% 8%,rgba(79,111,143,.14),transparent 22rem),linear-gradient(135deg,rgba(255,248,239,.98),rgba(255,255,255,.74))}.agent-kicker{color:var(--obs-accent);font-size:.76rem;letter-spacing:.13em;text-transform:uppercase;font-weight:850}.agent-title{margin:.45rem 0 .25rem;font-family: var(--momo-font-display, "Inter", "Noto Sans TC", system-ui, sans-serif);font-size:var(--obs-title-size);letter-spacing: 0;line-height:.98}.agent-subtitle{color:var(--obs-muted);max-width:870px;line-height:1.7}.agent-filter{display:flex;gap:.55rem;flex-wrap:wrap;margin-top:1rem;padding:.8rem;border:1px solid var(--obs-line);border-radius:20px;background:rgba(255,255,255,.58)}.agent-command{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.75rem;margin-top:1rem}.agent-signal{padding:.95rem;border:1px solid var(--obs-line);border-radius:20px;background:rgba(255,255,255,.62)}.agent-label{color:var(--obs-muted);font-size:.72rem;letter-spacing:.1em;text-transform:uppercase}.agent-value{display:block;margin-top:.28rem;font-size:var(--obs-value-size);font-weight:880;letter-spacing: 0}.agent-grid{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(330px,.8fr);gap:1rem;margin-top:1rem}.agent-stack{display:grid;gap:1rem}.agent-panel-head,.agent-table-title{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;padding:1.05rem 1.1rem .25rem}.agent-panel-title,.agent-table-title h3{margin:.15rem 0 0;font-size:1.1rem;font-weight:850;letter-spacing: 0}.agent-panel-body{padding:1rem 1.1rem 1.1rem}.agent-table-shell{overflow:hidden;margin-top:1rem}.agent-card{padding:.9rem;border:1px solid var(--obs-line);border-radius:20px;background:rgba(255,255,255,.58);margin-bottom:.75rem}.agent-card-top{display:flex;justify-content:space-between;gap:.8rem;align-items:start}.agent-meter{height:7px;border-radius:999px;background:rgba(86,64,48,.1);overflow:hidden;margin-top:.65rem}.agent-meter span{display:block;height:100%;background:var(--obs-accent)}.rec-card{padding:.85rem;border:1px solid var(--obs-line);border-radius:18px;background:rgba(255,255,255,.58);margin-bottom:.7rem}.status-good{color:var(--obs-green)}.status-warn{color:var(--obs-amber)}.status-bad{color:var(--obs-red)}.status-blue{color:var(--obs-blue)}@media(max-width:1100px){.agent-command{grid-template-columns:repeat(2,minmax(0,1fr))}.agent-grid{grid-template-columns:1fr}}@media(max-width:720px){.agent-command{grid-template-columns:1fr}}
</style>
{% import "admin/_observability_labels.html" as obs_label %}
<div class="container-fluid mt-3">
<section class="agent-hero"><div class="agent-kicker"><i class="fas fa-network-wired me-1"></i> AI 分工指揮台 · {{ hours }} 小時視窗</div><h1 class="agent-title">AI 分工指揮台</h1><p class="agent-subtitle">確認 AI 分工、建議路徑、知識命中與工具編排是否支撐業績決策。</p><form method="get" class="agent-filter"><select name="hours" class="form-select form-select-sm" onchange="this.form.submit()">{% for h in [1,6,24,72,168] %}<option value="{{ h }}" {% if hours == h %}selected{% endif %}>{% if h < 24 %}過去 {{ h }} 小時{% else %}過去 {{ h//24 }} {% endif %}</option>{% endfor %}</select></form>{% if overall %}<div class="agent-command"><div class="agent-signal"><div class="agent-label">呼叫總量</div><span class="agent-value">{{ "{:,}".format(overall.total_calls) }}</span><small class="text-muted">{{ "{:,}".format(overall.total_tokens) }} 用量</small></div><div class="agent-signal"><div class="agent-label">主力路徑占比</div><span class="agent-value status-good">{{ "%.0f"|format(overall.local_pct) }}%</span><small class="text-muted">{{ "{:,}".format(overall.local_calls) }} 次主力呼叫</small></div><div class="agent-signal"><div class="agent-label">付費成本</div><span class="agent-value {% if overall.total_cost > 0 %}status-warn{% else %}status-good{% endif %}">${{ "%.2f"|format(overall.total_cost) }}</span><small class="text-muted">{{ "{:,}".format(overall.paid_calls) }} 次付費呼叫</small></div><div class="agent-signal"><div class="agent-label">知識命中率</div><span class="agent-value status-blue">{{ "%.0f"|format(overall.rag_rate) }}%</span><small class="text-muted">{{ "{:,}".format(overall.rag_hits) }} 次命中</small></div></div>{% endif %}</section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
@@ -22,7 +24,7 @@
</section>
{% if recommendations %}<section class="agent-panel mt-3"><div class="agent-panel-head"><div><div class="agent-label">策略規則</div><h2 class="agent-panel-title">編排策略自動建議</h2></div></div><div class="agent-panel-body">{% for r in recommendations %}<div class="rec-card"><span class="badge {% if r.severity == 'high' %}bg-danger{% elif r.severity == 'med' %}bg-warning{% else %}bg-info{% endif %} me-1">{{ r.severity|upper }}</span><strong>{{ r.agent }}</strong><div class="small mt-1"><i class="fas fa-search me-1"></i><strong>發現:</strong>{{ r.finding }}</div><div class="small text-muted"><i class="fas fa-arrow-right me-1"></i><strong>建議:</strong>{{ r.suggestion }}</div></div>{% endfor %}</div></section>{% endif %}
{% if mcp_matrix %}<section class="agent-table-shell"><div class="agent-table-title"><div><div class="agent-label">工具服務明細</div><h3>工具服務 × 呼叫端工作量</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>工具服務</th><th>呼叫端</th><th class="text-end">工具呼叫</th><th class="text-end">快取</th><th class="text-end">快取率</th><th class="text-end">成本</th></tr></thead><tbody>{% for m in mcp_matrix %}<tr><td><code>{{ m.server }}</code></td><td><code>{{ m.caller }}</code></td><td class="text-end">{{ "{:,}".format(m.calls) }}</td><td class="text-end">{{ m.cache_hits }}</td><td class="text-end"><span class="{% if m.cache_rate >= 50 %}status-good{% elif m.cache_rate >= 20 %}status-warn{% endif %}">{{ "%.0f"|format(m.cache_rate) }}%</span></td><td class="text-end">${{ "%.4f"|format(m.cost) }}</td></tr>{% endfor %}</tbody></table></div></section>{% endif %}
{% if mcp_matrix %}<section class="agent-table-shell"><div class="agent-table-title"><div><div class="agent-label">工具協作明細</div><h3>工具協作 × 使用情境</h3></div></div><div class="table-responsive"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>工具服務</th><th>使用情境</th><th class="text-end">工具協作</th><th class="text-end">快取</th><th class="text-end">快取率</th><th class="text-end">成本</th></tr></thead><tbody>{% for m in mcp_matrix %}<tr><td><span>服務元件</span></td><td><span>{{ obs_label.caller(m.caller) }}</span></td><td class="text-end">{{ "{:,}".format(m.calls) }}</td><td class="text-end">{{ m.cache_hits }}</td><td class="text-end"><span class="{% if m.cache_rate >= 50 %}status-good{% elif m.cache_rate >= 20 %}status-warn{% endif %}">{{ "%.0f"|format(m.cache_rate) }}%</span></td><td class="text-end">${{ "%.4f"|format(m.cost) }}</td></tr>{% endfor %}</tbody></table></div></section>{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>AI 分工指揮台</small></p>
</div>
{% endblock %}

View File

@@ -56,7 +56,7 @@
<div class="gov-stack">
<article class="gov-table-shell">
<div class="gov-table-title"><div><div class="gov-label">預算線</div><h3>預算線與節流狀態</h3></div></div>
<div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th>週期</th><th>供應商</th><th class="text-end">已花費</th><th>預算</th><th>閾值</th><th class="text-end">使用率</th><th>狀態</th><th>動作</th></tr></thead><tbody>{% for r in rows %}<tr {% if r.throttled %}class="table-danger"{% elif r.ratio >= 0.8 %}class="table-warning"{% endif %}><td><span class="badge bg-secondary">{{ r.period }}</span></td><td><code>{{ obs_label.provider(r.provider) }}</code></td><td class="text-end">${{ "%.2f"|format(r.spent) }}</td><td><input type="number" step="0.01" min="0.01" value="{{ "%.2f"|format(r.budget_usd) }}" class="form-control form-control-sm budget-input" data-budget-id="{{ r.id }}" ></td><td><input type="number" min="1" max="100" value="{{ r.alert_pct }}" class="form-control form-control-sm alert-input" data-budget-id="{{ r.id }}" ></td><td class="text-end"><strong class="{% if r.ratio >= 1.10 %}status-bad{% elif r.ratio >= 0.8 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(r.ratio * 100) }}%</strong></td><td>{% if r.throttled %}<span class="badge bg-danger">已節流</span>{% elif r.ratio >= 0.8 %}<span class="badge bg-warning">接近上限</span>{% else %}<span class="badge bg-success">正常</span>{% endif %}</td><td><button class="btn btn-primary btn-sm save-budget-btn" data-budget-id="{{ r.id }}" onclick="saveBudget({{ r.id }})"><i class="fas fa-save me-1"></i>儲存</button></td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">無預算資料(需先跑 migrations/025</td></tr>{% endfor %}</tbody></table></div>
<div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th>週期</th><th>建議路徑</th><th class="text-end">已花費</th><th>預算</th><th>預警線</th><th class="text-end">使用率</th><th>狀態</th><th>動作</th></tr></thead><tbody>{% for r in rows %}<tr {% if r.throttled %}class="table-danger"{% elif r.ratio >= 0.8 %}class="table-warning"{% endif %}><td><span class="badge bg-secondary">{{ r.period }}</span></td><td><span>{{ obs_label.provider(r.provider) }}</span></td><td class="text-end">${{ "%.2f"|format(r.spent) }}</td><td><input type="number" step="0.01" min="0.01" value="{{ "%.2f"|format(r.budget_usd) }}" class="form-control form-control-sm budget-input" data-budget-id="{{ r.id }}" ></td><td><input type="number" min="1" max="100" value="{{ r.alert_pct }}" class="form-control form-control-sm alert-input" data-budget-id="{{ r.id }}" ></td><td class="text-end"><strong class="{% if r.ratio >= 1.10 %}status-bad{% elif r.ratio >= 0.8 %}status-warn{% else %}status-good{% endif %}">{{ "%.0f"|format(r.ratio * 100) }}%</strong></td><td>{% if r.throttled %}<span class="badge bg-danger">已節流</span>{% elif r.ratio >= 0.8 %}<span class="badge bg-warning">接近上限</span>{% else %}<span class="badge bg-success">正常</span>{% endif %}</td><td><button class="btn btn-primary btn-sm save-budget-btn" data-budget-id="{{ r.id }}" onclick="saveBudget({{ r.id }})"><i class="fas fa-save me-1"></i>儲存</button></td></tr>{% else %}<tr><td colspan="8" class="text-center text-muted">尚未建立預算線。</td></tr>{% endfor %}</tbody></table></div>
</article>
{% if cost_trend_30d %}
@@ -70,7 +70,7 @@
{% endif %}
{% if top_cost_callers %}
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">燃燒率</div><h2 class="gov-panel-title">Top 5 燒錢呼叫端</h2></div></div><div class="gov-panel-body">{% set max_cost = (top_cost_callers | map(attribute='cost') | max) or 1 %}{% for c in top_cost_callers %}<div class="gov-mini mb-2"><div class="d-flex justify-content-between"><code>{{ c.caller }}</code><strong>${{ "%.2f"|format(c.cost) }}</strong></div><div class="progress mt-2 obs-progress-xs"><div class="progress-bar" style="width: {{ (c.cost / max_cost * 100) | round | int }}%;"></div></div><small class="text-muted">{{ "{:,}".format(c.calls) }} 次呼叫 · {{ "{:,}".format(c.tokens) }} 用量</small></div>{% endfor %}</div></article>
<article class="gov-panel"><div class="gov-panel-head"><div><div class="gov-label">成本集中</div><h2 class="gov-panel-title">Top 5 成本使用情境</h2></div></div><div class="gov-panel-body">{% set max_cost = (top_cost_callers | map(attribute='cost') | max) or 1 %}{% for c in top_cost_callers %}<div class="gov-mini mb-2"><div class="d-flex justify-content-between"><span>{{ obs_label.caller(c.caller) }}</span><strong>${{ "%.2f"|format(c.cost) }}</strong></div><div class="progress mt-2 obs-progress-xs"><div class="progress-bar" style="width: {{ (c.cost / max_cost * 100) | round | int }}%;"></div></div><small class="text-muted">{{ "{:,}".format(c.calls) }} 次 · {{ "{:,}".format(c.tokens) }} 用量</small></div>{% endfor %}</div></article>
{% endif %}
</aside>
</section>

View File

@@ -138,7 +138,7 @@
<i class="fas fa-sync-alt me-2"></i>重新檢查
</button>
<a class="btn btn-outline-primary mt-3 ms-lg-2" href="/api/ai-automation/smoke/history/export">
<i class="fas fa-file-export me-2"></i>匯出 JSONL
<i class="fas fa-file-export me-2"></i>下載檢查紀錄
</a>
<button id="clearHistoryBtn" class="btn btn-outline-warning mt-3 ms-lg-2">
<i class="fas fa-broom me-2"></i>清理趨勢
@@ -234,7 +234,7 @@ function renderSmoke(data) {
${badge(item.status)}
</div>
<p class="text-muted">${escapeHtml(item.summary)}</p>
<div class="detail-box">${escapeHtml(JSON.stringify(item.details || {}, null, 2))}</div>
<div class="detail-box">已保留診斷細節,請由維護者查看。</div>
</div>
</div>
`).join('');
@@ -282,15 +282,15 @@ async function loadSmoke() {
btn.querySelector('i').classList.add('fa-spin');
try {
const res = await fetchWithCSRF('/api/ai-automation/smoke');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.ok) throw new Error('smoke-unavailable');
renderSmoke(await res.json());
} catch (err) {
document.getElementById('overallStatus').innerHTML = badge('critical');
document.getElementById('checkGrid').innerHTML = `
<div class="card smoke-card">
<div class="card-body">
<h5>健康檢查 API</h5>
<p class="text-danger">讀取失敗${escapeHtml(err.message)}</p>
<h5>健康檢查服務</h5>
<p class="text-danger">讀取失敗,請稍後重試。</p>
</div>
</div>`;
} finally {

View File

@@ -289,7 +289,7 @@
<!-- Review Steps -->
<div class="card">
<div class="card-header">審查流程
<span id="pipelineId" style="font-size:11px;color:var(--muted);margin-left:auto;font-family:monospace"></span>
<span id="pipelineId" style="font-size:11px;color:var(--muted);margin-left:auto;"></span>
</div>
<div class="card-body">
<div class="pipeline" id="stepsContainer">
@@ -315,11 +315,10 @@
</div>
</div>
<!-- Commit Info -->
<div class="card">
<div class="card-header">部署證據</div>
<div class="card-header">上線證據</div>
<div class="card-body" id="commitInfo" style="font-size:13px;color:var(--muted);line-height:1.8;">
<span style="color:var(--muted)">等待下次部署觸發...</span>
<span style="color:var(--muted)">等待下次上線檢查...</span>
</div>
</div>
@@ -490,17 +489,15 @@ function renderEA(ea, autoFix) {
</div>`;
}
// ── Commit info ───────────────────────────────────────────────────
// ── 上線證據 ─────────────────────────────────────────────────────
function renderCommitInfo(state) {
const el = document.getElementById('commitInfo');
if (!state.commit_sha) return;
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>` : '';
const changedCount = (state.changed_files||[]).length;
el.innerHTML = `
<div><b>提交</b> <code>${state.commit_sha.slice(0,8)}</code></div>
<div><b>分支</b> <code>${state.branch||'?'}</code></div>
<div><b>狀態</b> 已收到上線檢查資料</div>
<div><b>模式</b> ${DEPLOY_TYPE_LABEL[state.deploy_type] || state.deploy_type || '同步部署'}</div>
<div><b>變更</b> ${files} ${more}</div>`;
<div><b>變更</b> ${changedCount} 項待檢查</div>`;
}
// ── Status bar ────────────────────────────────────────────────────
@@ -514,7 +511,7 @@ function renderStatusBar(state) {
const msgs = {
running: `⟳ <b>流程執行中</b> — 第 ${state.current_step}/5 步`,
completed: `✅ <b>程式碼審查完成</b> — ${state.message||''}`,
error: `❌ <b>流程失敗</b> — ${escHtml(state.message||'')}`,
error: `❌ <b>流程需確認</b> — 請查看風險清單`,
skipped: `⏭ <b>已略過</b> — ${escHtml(state.message||'')}`,
};
el.innerHTML = msgs[state.status] || '';
@@ -533,10 +530,10 @@ function renderHistory(items) {
const sev = h.severity_summary || {};
return `<div class="hist-item" onclick="loadHistoryItem(${idx})" data-idx="${idx}">
<div style="display:flex;justify-content:space-between">
<span class="hist-sha">${h.commit_sha}</span>
<span class="hist-sha">上線檢查</span>
<span style="font-size:11px;color:var(--muted)">${h.created_at.slice(0,16).replace('T',' ')}</span>
</div>
<div class="hist-meta">🌿 ${h.branch}${(h.changed_files||[]).length} 檔案${h.auto_fix?' • 🔧 已自動修復':''}</div>
<div class="hist-meta">${(h.changed_files||[]).length} 項變更${h.auto_fix?' • 已完成修復流程':''}</div>
<div class="hist-sev">
${sev.critical?`<span style="background:rgba(248,81,73,.2);color:var(--red)">🔴 ${sev.critical}</span>`:''}
${sev.high?`<span style="background:rgba(210,153,34,.2);color:var(--orange)">🟠 ${sev.high}</span>`:''}
@@ -555,18 +552,16 @@ function loadHistoryItem(idx) {
el.style.borderColor = i === idx ? 'var(--blue)' : '';
});
renderSeverity(h.severity_summary);
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>` : '';
const changedCount = (h.changed_files||[]).length;
document.getElementById('commitInfo').innerHTML = `
<div><b>提交</b> <code>${h.commit_sha}</code></div>
<div><b>分支</b> <code>${h.branch||'?'}</code></div>
<div><b>狀態</b> 已完成上線檢查</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);
<div><b>變更</b> ${changedCount} 項已檢查</div>`;
document.getElementById('pipelineId').textContent = '最近一次';
const sBar = document.getElementById('statusBar');
sBar.style.display = 'block';
sBar.className = 'completed';
sBar.innerHTML = `✅ <b>歷史記錄</b> — 提交 ${h.commit_sha}${h.auto_fix ? ' 🔧 已自動修復' : ''}`;
sBar.innerHTML = `✅ <b>歷史記錄</b> — 已完成上線檢查${h.auto_fix ? ',並完成修復流程' : ''}`;
renderFindings(h.findings || []);
renderArchitectureReport(h.openclaw_report || '');
renderEA(h.ea_decision || {}, h.auto_fix || false);
@@ -593,7 +588,7 @@ async function poll() {
renderArchitectureReport(state.openclaw_report);
renderEA(state.ea_decision, state.auto_fix_triggered);
renderCommitInfo(state);
document.getElementById('pipelineId').textContent = (state.pipeline_id||'').slice(-14);
document.getElementById('pipelineId').textContent = state.pipeline_id ? '執行中' : '';
// 每 3s 輪詢running/ 30sidle
const interval = state.status === 'running' ? 3000 : 30000;

View File

@@ -282,9 +282,9 @@
<h1>營運數據工作台</h1>
<p>登入後先看 PChome 業績、價差、缺貨與 AI 建議。</p>
<div class="login-meta">
<span class="login-pill"><i class="fas fa-shield-halved"></i>CSRF 防護</span>
<span class="login-pill"><i class="fas fa-clock"></i>Session 2h</span>
<span class="login-pill"><i class="fas fa-database"></i>PostgreSQL</span>
<span class="login-pill"><i class="fas fa-shield-halved"></i>安全登入</span>
<span class="login-pill"><i class="fas fa-clock"></i>工作階段</span>
<span class="login-pill"><i class="fas fa-database"></i>資料服務</span>
</div>
</div>
<div class="login-footer d-none d-md-block">

View File

@@ -64,6 +64,9 @@ def test_high_visibility_pages_use_traditional_chinese_labels():
page_paths = [
"templates/code_review.html",
"templates/ai_automation_smoke.html",
"templates/login.html",
"templates/admin/budget.html",
"templates/admin/agent_orchestration.html",
"templates/admin/ppt_audit_history.html",
"templates/admin/host_health.html",
"templates/dashboard_v2.html",
@@ -84,8 +87,29 @@ def test_high_visibility_pages_use_traditional_chinese_labels():
assert "最新更新流程" in combined
assert "執行環境正常" in combined
assert "視覺檢查" in combined
assert "下載檢查紀錄" in combined
assert "健康檢查服務" in combined
assert "資料服務" in combined
assert "Top 5 成本使用情境" in combined
assert "工具協作 × 使用情境" in combined
forbidden_visible_text = [
"工作視窗",
"Codex",
"Claude",
"推到 Gitea",
"本輪已完成",
"剛剛修正",
"後續 session",
"JSONL",
"健康檢查 API",
"JSON.stringify(item.details",
"PostgreSQL",
"CSRF 防護",
"Session 2h",
"燒錢呼叫端",
"<code>{{ m.caller }}</code>",
"<code>{{ m.server }}</code>",
"AI Code Review",
"Smoke Dashboard",
"FOUR-AGENT CONTROL PLANE",
@@ -166,7 +190,8 @@ def test_frontend_v2_syncs_latest_momo_pro_prototype_tokens_and_shell():
tokens = (ROOT / "web/static/css/ewoooc-tokens.css").read_text(encoding="utf-8")
shell = (ROOT / "web/static/css/ewoooc-shell.css").read_text(encoding="utf-8")
assert "MOMO Pro × Nothing × Claude" in tokens
assert "MOMO Pro × minimal grid × operational clarity" in tokens
assert "Claude" not in tokens
assert "--momo-warm-caramel" in tokens
assert "--momo-tag-honey-bg" in tokens
assert "--momo-text-body" in tokens
@@ -624,6 +649,11 @@ def test_utility_pages_keep_operator_copy_professional():
host_health = (ROOT / "templates/admin/host_health.html").read_text(encoding="utf-8")
cicd_dashboard = (ROOT / "templates/cicd_dashboard.html").read_text(encoding="utf-8")
observability_js = (ROOT / "web/static/js/observability-charts.js").read_text(encoding="utf-8")
budget = (ROOT / "templates/admin/budget.html").read_text(encoding="utf-8")
agent_orchestration = (ROOT / "templates/admin/agent_orchestration.html").read_text(encoding="utf-8")
ai_automation = (ROOT / "templates/ai_automation_smoke.html").read_text(encoding="utf-8")
code_review = (ROOT / "templates/code_review.html").read_text(encoding="utf-8")
login = (ROOT / "templates/login.html").read_text(encoding="utf-8")
combined = "\n".join([
ppt_history,
ppt_preview,
@@ -637,6 +667,11 @@ def test_utility_pages_keep_operator_copy_professional():
host_health,
cicd_dashboard,
observability_js,
budget,
agent_orchestration,
ai_automation,
code_review,
login,
])
assert "簡報線上預覽" in ppt_preview
@@ -652,8 +687,30 @@ def test_utility_pages_keep_operator_copy_professional():
assert "供貨風險" in stockout_index
assert "無服務資料 / 未連線" in host_health
assert "部署檢查已排入背景處理" in observability_js
assert "Top 5 成本使用情境" in budget
assert "工具協作 × 使用情境" in agent_orchestration
assert "下載檢查紀錄" in ai_automation
assert "健康檢查服務" in ai_automation
assert "上線證據" in code_review
assert "資料服務" in login
forbidden = [
"工作視窗",
"Codex",
"Claude",
"推到 Gitea",
"本輪已完成",
"剛剛修正",
"後續 session",
"JSONL",
"健康檢查 API",
"JSON.stringify(item.details",
"PostgreSQL",
"CSRF 防護",
"Session 2h",
"燒錢呼叫端",
"<code>{{ m.caller }}</code>",
"<code>{{ m.server }}</code>",
"PPT Online Preview",
"Preview unavailable",
"原始 PPTX",

View File

@@ -812,9 +812,16 @@ def test_governance_and_low_frequency_pages_avoid_engineering_status_copy():
"templates/external_tool_status.html": ["共用預覽"],
"templates/market_intel/disabled.html": ["市場情報", "操作入口"],
"templates/brand_assets.html": ["EwoooC 品牌資產庫", "品牌資產混用"],
"templates/code_review.html": ["部署守門與程式碼審查", "業績流程上線前檢查", "部署證據"],
"templates/code_review.html": ["部署守門與程式碼審查", "業績流程上線前檢查", "上線證據"],
}
forbidden = [
"工作視窗",
"Codex",
"Claude",
"推到 Gitea",
"本輪已完成",
"剛剛修正",
"後續 session",
"MIGRATED PAGE",
"V3 READY",
"Growth workflow active",
@@ -904,10 +911,14 @@ def test_visible_operations_pages_hide_internal_runtime_terms():
"templates/vendor_stockout_vendor_management_v2.html": ["匯入供應商窗口名單", "確認窗口清單"],
"templates/vendor_stockout_import_v2.html": ["會先停止匯入", "處理缺貨清單"],
"templates/admin/ppt_audit_history.html": ["產出紀錄", "最近產出", "保存紀錄"],
"templates/admin/agent_orchestration.html": ["AI 分工指揮台", "建議路徑、工具與知識命中矩陣", "工具服務明細"],
"templates/admin/agent_orchestration.html": ["AI 分工指揮台", "建議路徑、工具與知識命中矩陣", "工具協作明細", "工具協作 × 使用情境"],
"templates/admin/ai_calls_dashboard.html": ["用量", "最近作戰素材", "情境 × 知識命中矩陣"],
"templates/admin/observability_overview.html": ["用量", "知識與工具矩陣"],
"templates/cicd_dashboard.html": ["最新更新流程", "更新歷史", "修復服務", "查看更新紀錄"],
"templates/admin/budget.html": ["Top 5 成本使用情境", "尚未建立預算線"],
"templates/ai_automation_smoke.html": ["下載檢查紀錄", "健康檢查服務"],
"templates/login.html": ["安全登入", "工作階段", "資料服務"],
"templates/code_review.html": ["上線證據", "已收到上線檢查資料"],
}
forbidden_by_path = {
"templates/ai_recommend.html": ["權杖:", "AI 模型", "分析模型", "AI 路徑", "Gemini 備援", "Ollama 主路徑"],
@@ -920,10 +931,22 @@ def test_visible_operations_pages_hide_internal_runtime_terms():
"templates/vendor_stockout_vendor_management_v2.html": ["拖曳檔案到此處或點擊選擇檔案", "查看廠商清單"],
"templates/vendor_stockout_import_v2.html": ["API 會拒絕匯入", "查看缺貨清單"],
"templates/admin/ppt_audit_history.html": ["DB 紀錄", "DB / 預覽", "寫入 DB", "DB 產出紀錄", "資料庫快取", "本月尚無 DB"],
"templates/admin/agent_orchestration.html": ["Agent 指揮矩陣", "四 Agent 矩陣", "LLM × MCP × RAG 編排矩陣", "權杖"],
"templates/admin/agent_orchestration.html": [
"Agent 指揮矩陣",
"四 Agent 矩陣",
"LLM × MCP × RAG 編排矩陣",
"權杖",
"<code>{{ m.caller }}</code>",
"<code>{{ m.server }}</code>",
"呼叫端工作量",
],
"templates/admin/ai_calls_dashboard.html": ["權杖量", "權杖/次", ">權杖<", "Agent 上下文", "RAG × MCP"],
"templates/admin/observability_overview.html": ["權杖量", "RAG × MCP"],
"templates/cicd_dashboard.html": ["Pipeline Flow", "Pipeline History", "完整修復", "一鍵修復", "重啟 Registry", "舊叢集"],
"templates/admin/budget.html": ["燒錢呼叫端", "migrations/025", "<code>{{ c.caller }}</code>"],
"templates/ai_automation_smoke.html": ["JSONL", "健康檢查 API", "JSON.stringify(item.details"],
"templates/login.html": ["CSRF 防護", "Session 2h", "PostgreSQL"],
"templates/code_review.html": ["<b>提交</b>", "<b>分支</b>", "提交 ${h.commit_sha}", "🌿 ${h.branch}", "<code>${state.commit_sha"],
}
for path, markers in expected.items():

View File

@@ -1,7 +1,7 @@
/**
* EwoooC Design Token v3.0 — Production
* ─────────────────────────────────────────────────────────────
* DNA: MOMO Pro × Nothing × Claude
* DNA: MOMO Pro × minimal grid × operational clarity
* 變更重點(相對 v2.x
* 1. 全站去除 linear-gradientradial-gradientbox-shadow blur改為單色 + 1px 線條
* 2. 五大導航群組各自 accent避免「全站焦糖橘」造成主次不分

View File

@@ -10,7 +10,7 @@
ollama_secondary: '備援建議路徑',
ollama_111: '第三建議路徑',
gemini: '雲端備援',
claude: 'Claude',
claude: '雲端審查備援',
nim: '快速雲端建議',
openrouter: '外部備援路徑',
nim_via_elephant: '雲端加速備援'