Files
ewoooc/templates/admin/ppt_audit_history.html
OoO 40d4d01007
All checks were successful
CD Pipeline / deploy (push) Successful in 58s
feat: add ppt file view/download actions in observability audit page
2026-05-15 14:11:04 +08:00

402 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "ewoooc_base.html" %}
{% block title %}PPT 視覺 QA 產線{% endblock %}
{% block ewooo_content %}
<style>
.ppt-hero, .ppt-panel, .ppt-table-shell {
border: 1px solid var(--obs-line);
border-radius: 26px;
background: var(--obs-card);
box-shadow: 0 16px 38px rgba(70, 46, 28, .08);
}
.ppt-hero {
padding: 1.4rem;
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));
}
.ppt-kicker {
color: var(--obs-accent);
font-size: .76rem;
letter-spacing: .13em;
text-transform: uppercase;
font-weight: 850;
}
.ppt-title {
margin: .45rem 0 .25rem;
font-family: 'Noto Sans TC', 'Inter', sans-serif;
font-size: var(--obs-title-size);
letter-spacing: -0.055em;
line-height: .98;
}
.ppt-subtitle {
color: var(--obs-muted);
max-width: 860px;
line-height: 1.7;
}
.ppt-command {
display: grid;
grid-template-columns: repeat(4, minmax(0,1fr));
gap: .75rem;
margin-top: 1rem;
}
.ppt-signal {
padding: .95rem;
border: 1px solid var(--obs-line);
border-radius: 20px;
background: rgba(255,255,255,.62);
}
.ppt-label {
color: var(--obs-muted);
font-size: .72rem;
letter-spacing: .1em;
text-transform: uppercase;
}
.ppt-value {
display: block;
margin-top: .28rem;
font-size: var(--obs-value-size);
font-weight: 880;
letter-spacing: -.045em;
}
.ppt-toolbar {
margin-top: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: .75rem;
flex-wrap: wrap;
}
.ppt-type-tabs {
display: flex;
flex-wrap: wrap;
gap: .45rem;
}
.ppt-type-chip {
display: inline-flex;
align-items: center;
gap: .35rem;
}
.ppt-grid {
display: grid;
grid-template-columns: minmax(0,1.2fr) minmax(330px,.8fr);
gap: 1rem;
margin-top: 1rem;
}
.ppt-stack { display: grid; gap: 1rem; }
.ppt-panel-head, .ppt-table-title {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
padding: 1.05rem 1.1rem .25rem;
}
.ppt-panel-title, .ppt-table-title h3 {
margin: .15rem 0 0;
font-size: 1.1rem;
font-weight: 850;
letter-spacing: -.025em;
}
.ppt-panel-body {
padding: 1rem 1.1rem 1.1rem;
}
.ppt-table-shell {
overflow: hidden;
margin-top: 1rem;
}
.ppt-table-shell .table {
min-width: 760px;
}
.ppt-mini-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0,1fr));
gap: .7rem;
}
.ppt-mini {
padding: .85rem;
border: 1px solid var(--obs-line);
border-radius: 18px;
background: rgba(255,255,255,.58);
}
.ppt-mini strong {
display: block;
margin-top: .24rem;
font-size: 1.35rem;
letter-spacing: -.04em;
}
.fix-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); }
.ppt-file-actions {
display: flex;
gap: .4rem;
flex-wrap: wrap;
}
.ppt-file-actions .btn {
display: inline-flex;
align-items: center;
gap: .3rem;
}
@media(max-width:1100px) {
.ppt-command, .ppt-mini-grid { grid-template-columns: repeat(2,minmax(0,1fr)); }
.ppt-grid { grid-template-columns: 1fr; }
}
@media(max-width:720px) {
.ppt-command { grid-template-columns: 1fr; }
}
</style>
{% import "admin/_observability_labels.html" as obs_label %}
{% set report_is_daily = report_type == 'daily' %}
<div class="container-fluid mt-3">
<section class="ppt-hero">
<div class="ppt-kicker"><i class="fas fa-search me-1"></i> PPT 視覺 QA 產線 · minicpm-v / AiderHeal / RAG 修法</div>
<h1 class="ppt-title">PPT 視覺 QA 產線</h1>
<p class="ppt-subtitle">這頁追蹤每份自動簡報是否通過視覺審核檔案產出、minicpm-v 審核、Telegram 推送、RAG 修法建議與 AiderHeal 自動修產生器。</p>
<div class="ppt-command">
<div class="ppt-signal">
<div class="ppt-label">視覺模型</div>
<span class="ppt-value {% if vision_enabled %}status-good{% else %}status-warn{% endif %}">{{ '啟用' if vision_enabled else '停用' }}</span>
<small class="text-muted">PPT_VISION_ENABLED</small>
</div>
<div class="ppt-signal">
<div class="ppt-label">{{ report_month }} {{ selected_report_type.label }}</div>
<span class="ppt-value">{{ files|length }}</span>
<small class="text-muted">檔案數</small>
</div>
<div class="ppt-signal">
<div class="ppt-label">審核紀錄</div>
<span class="ppt-value {% if report_is_daily and audit_30d_stats and audit_30d_stats.pass_rate >= 80 %}status-good{% elif report_is_daily and audit_30d_stats and audit_30d_stats.pass_rate >= 60 %}status-warn{% elif report_is_daily and audit_30d_stats %}status-bad{% else %}status-blue{% endif %}">
{% if report_is_daily %}{{ audit_30d_stats.total if audit_30d_stats else '—' }}{% else %}—{% endif %}
</span>
<small class="text-muted">{{ report_is_daily and '僅 daily' or '切到 daily 可查看' }}</small>
</div>
<div class="ppt-signal">
<div class="ppt-label">問題數</div>
<span class="ppt-value {% if report_is_daily and audit_30d_stats and audit_30d_stats.total_issues > 0 %}status-warn{% elif report_is_daily and audit_30d_stats %}status-good{% else %}status-blue{% endif %}">
{% if report_is_daily %}{{ audit_30d_stats.total_issues if audit_30d_stats else '—' }}{% else %}—{% endif %}
</span>
<small class="text-muted">視覺問題數</small>
</div>
</div>
</section>
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="ppt-toolbar">
<div class="d-flex align-items-center gap-2">
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin_observability.ppt_audit_history', month=prev_month_label, report_type=report_type) }}">
<i class="fas fa-angle-left me-1"></i>上個月
</a>
<span class="badge bg-light text-dark border py-2 px-3">{{ report_month }}</span>
{% if show_next_month %}
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin_observability.ppt_audit_history', month=next_month_label, report_type=report_type) }}">
下個月<i class="fas fa-angle-right ms-1"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" type="button" disabled>下個月<i class="fas fa-angle-right ms-1"></i></button>
{% endif %}
</div>
<div class="ppt-type-tabs">
{% for opt in report_type_options %}
<a class="btn btn-sm {% if report_type == opt.key %}btn-primary{% else %}btn-outline-primary{% endif %} ppt-type-chip" href="{{ url_for('admin_observability.ppt_audit_history', month=report_month, report_type=opt.key) }}">
{{ opt.label }}
</a>
{% endfor %}
</div>
</section>
<section class="ppt-grid">
<div class="ppt-stack">
<article class="ppt-table-shell">
<div class="ppt-table-title">
<div>
<div class="ppt-label">審核歷史</div>
<h3>視覺審核歷史({{ report_month }}</h3>
</div>
</div>
<div class="table-responsive">
{% if report_is_daily %}
<table class="table table-sm mb-0">
<thead class="table-light">
<tr><th>時間</th><th>檔名</th><th>結果</th><th class="text-end">問題</th><th class="text-end">信心</th><th class="text-end">耗時</th><th>錯誤</th><th>動作</th></tr>
</thead>
<tbody>
{% for r in audit_records %}
<tr>
<td><small>{{ r.audited_at }}</small></td>
<td><code>{{ r.pptx_filename }}</code></td>
<td>
{% if r.audit_status == 'passed' %}<span class="badge bg-success">通過</span>
{% elif r.audit_status == 'failed' %}<span class="badge bg-warning">有問題</span>
{% elif r.audit_status == 'error' %}<span class="badge bg-danger">錯誤</span>
{% elif r.audit_status == 'skipped' %}<span class="badge bg-secondary">跳過</span>
{% else %}<span class="badge bg-light text-dark">{{ r.audit_status }}</span>{% endif %}
</td>
<td class="text-end">{{ r.issues_count }}</td>
<td class="text-end">{{ "%.2f"|format(r.confidence) }}</td>
<td class="text-end">{{ r.duration_ms }}</td>
<td><small class="text-muted">{{ (r.error_msg or '')[:80] }}</small></td>
<td>{% if r.audit_status in ('failed','error') %}<button class="btn btn-sm btn-outline-warning" onclick="triggerAiderHeal({{ r.pptx_filename|tojson }}, {{ (r.error_msg or '')|tojson }})"><i class="fas fa-wrench me-1"></i>AiderHeal</button>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="8" class="text-center text-muted">目前無 daily 審核歷史;請確認 {{ report_month }} 是否已完成 22:00 排程。</td></tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-3">
<div class="ppt-empty p-3">
<div class="mb-2"><span class="badge bg-info text-dark">非每日型資料</span></div>
<p class="mb-2"><strong>只有「每日日報」會進入視覺審核流程。</strong></p>
<p class="mb-2">目前此頁只顯示每日以外的簡報檔案;若要追蹤視覺結果,請切到「每日日報」。</p>
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_history', month=report_month, report_type='daily') }}">
<i class="fas fa-calendar-day me-1"></i>切到每日日報
</a>
</div>
</div>
{% endif %}
</div>
</article>
<article class="ppt-table-shell">
<div class="ppt-table-title">
<div>
<div class="ppt-label">已產檔案</div>
<h3>{{ report_month }} {{ selected_report_type.label }}</h3>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr><th>檔名</th><th class="text-end">KB</th><th>修改時間</th><th>狀態</th><th>操作</th></tr>
</thead>
<tbody>
{% for f in files %}
<tr>
<td><code>{{ f.name }}</code></td>
<td class="text-end">{{ f.size_kb }}</td>
<td><small>{{ f.mtime }}</small></td>
<td><small class="text-muted">22:00 排程掃描</small></td>
<td>
<div class="ppt-file-actions">
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}" target="_blank" rel="noopener">
<i class="fas fa-file-powerpoint me-1"></i>開啟
</a>
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
<i class="fas fa-download me-1"></i>下載
</a>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center text-muted">本月無 {{ selected_report_type.label }} 簡報</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</article>
</div>
<aside class="ppt-stack">
{% if report_is_daily %}
{% if audit_30d_stats and audit_30d_stats.total > 0 %}
<article class="ppt-panel">
<div class="ppt-panel-head">
<div><div class="ppt-label">{{ report_month }} 審核分布</div><h2 class="ppt-panel-title">審核結果分布</h2></div>
</div>
<div class="ppt-panel-body">
<div class="obs-chart-frame"><canvas id="pptAuditPieChart"></canvas></div>
<div class="ppt-mini-grid mt-3">
<div class="ppt-mini"><span class="ppt-label">通過</span><strong class="status-good">{{ audit_30d_stats.passed }}</strong></div>
<div class="ppt-mini"><span class="ppt-label">失敗</span><strong class="{% if audit_30d_stats.failed > 0 %}status-warn{% endif %}">{{ audit_30d_stats.failed }}</strong></div>
<div class="ppt-mini"><span class="ppt-label">錯誤</span><strong class="{% if audit_30d_stats.error > 0 %}status-bad{% endif %}">{{ audit_30d_stats.error }}</strong></div>
<div class="ppt-mini"><span class="ppt-label">信心分</span><strong>{{ "%.2f"|format(audit_30d_stats.avg_confidence) }}</strong></div>
</div>
</div>
</article>
{% endif %}
{% if top_failure_files %}
<article class="ppt-panel">
<div class="ppt-panel-head">
<div><div class="ppt-label">失敗熱點</div><h2 class="ppt-panel-title">Top 失敗檔案</h2></div>
</div>
<div class="ppt-panel-body">
{% for f in top_failure_files %}<div class="fix-card"><code>{{ f.filename }}</code><div class="d-flex justify-content-between mt-1"><small class="text-muted">{{ f.last_audit }}</small><span class="badge bg-warning">{{ f.attempts }} 次</span></div><small class="text-muted">問題 {{ f.total_issues }}</small></div>{% endfor %}
</div>
</article>
{% endif %}
{% if not (audit_30d_stats and audit_30d_stats.total > 0) %}
<article class="ppt-panel">
<div class="ppt-panel-head">
<div><div class="ppt-label">審核摘要</div><h2 class="ppt-panel-title">本月審核狀態</h2></div>
</div>
<div class="ppt-panel-body">
<div class="ppt-empty p-3">
<p class="mb-1"><strong>尚未形成 daily 審核統計。</strong></p>
<p class="mb-0">排程完成後,這裡會顯示本月通過率、失敗檔案與修復建議。</p>
</div>
</div>
</article>
{% endif %}
{% endif %}
</aside>
</section>
{% if rag_fixes %}
<section class="ppt-panel mt-3">
<div class="ppt-panel-head">
<div><div class="ppt-label">RAG 修法建議</div><h2 class="ppt-panel-title">RAG 自動修法建議</h2></div>
</div>
<div class="ppt-panel-body">
{% for fix in rag_fixes %}
<div class="fix-card">
<strong><code>{{ fix.pptx_filename }}</code></strong><small class="text-muted ms-2">{{ fix.audited_at }}</small>
<div class="small status-bad mt-1">{{ fix.error_msg }}</div>
<ul class="list-unstyled mt-2 mb-0 small">
{% for h in fix.hits %}
<li class="mb-1">
<span class="badge bg-info me-1">{{ obs_label.insight(h.insight_type) }}</span>
<span class="badge bg-light text-dark me-1">相似度 {{ "%.2f"|format(h.similarity) }}</span>{{ h.content }}{% if h.content|length >= 200 %}…{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% if not vision_enabled %}
<div class="alert alert-info mt-3">
<strong>為什麼這頁空?</strong>
<ul class="mb-0 small mt-2">
<li>PPT_VISION_ENABLED=false</li>
<li>188 主機需安裝 LibreOffice</li>
<li>需 Ollama 拉取 minicpm-v 模型</li>
<li>啟用後每日 22:00 排程寫入 ppt_audit_results</li>
</ul>
</div>
{% elif files|length == 0 %}
<div class="alert alert-warning mt-3">
<strong>本月無資料</strong>
<ul class="mb-0 small mt-2">
<li>若為「每日日報」,檢查 {{ report_month }} 是否由 188 排程成功落盤。</li>
<li>若為「週報 / 月報 / 策略 / 競品 / 促銷」,請確認 Telegram 任務是否有對應產出。</li>
<li>可回到上方月份切換器看先前月份,或調整報表類型重新查詢。</li>
</ul>
</div>
{% endif %}
<p class="text-muted mt-3"><small><i class="fas fa-robot me-1"></i>Ollama 優先策略 v5.0 — PPT 視覺 QA 產線</small></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script><script>(function(){const stats={{ audit_30d_stats | default({}) | tojson }};const el=document.getElementById('pptAuditPieChart');if(!el||!stats.total)return;const data=[{label:'通過',value:stats.passed||0,color:'#4f8a5b'},{label:'失敗',value:stats.failed||0,color:'#b8792f'},{label:'錯誤',value:stats.error||0,color:'#b94b45'},{label:'跳過',value:stats.skipped||0,color:'#8b8077'}].filter(d=>d.value>0);if(!data.length)return;new Chart(el,{type:'doughnut',data:{labels:data.map(d=>d.label),datasets:[{data:data.map(d=>d.value),backgroundColor:data.map(d=>d.color),borderWidth:1,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:12}}}}}});})();
async function triggerAiderHeal(pptxFilename,errorMsg){if(!confirm(`觸發 AiderHeal 自動修復?\n\n檔案:${pptxFilename}\n錯誤:${(errorMsg||'').substring(0,200)}`))return;try{const r=await fetch('/observability/ppt_audit/trigger_aider_heal',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pptx_filename:pptxFilename,error_msg:errorMsg||''})});const d=await r.json();if(d.ok){alert(`✅ AiderHeal 已派出\n動作:${d.action||'—'}\n訊息:${d.message||''}`);}else{alert('❌ '+(d.error||d.message||'觸發失敗'));}}catch(e){console.warn('ppt_audit_trigger_aider_heal_failed',e);alert('操作暫時無法完成,請稍後再試或查看系統日誌。');}}
</script>
{% endblock %}