Files
ewoooc/templates/admin/ppt_audit_history.html
OoO ebbf7bc063
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
優化 PPT 產線健康總覽
2026-05-18 19:00:18 +08:00

498 lines
24 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 extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-ppt-audit-history.css') }}">
{% endblock %}
{% block ewooo_content %}
{% 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">minicpm-v + LibreOffice</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 class="ppt-signal">
<div class="ppt-label">定義覆蓋</div>
<span class="ppt-value {% if auto_generation.missing_count == 0 and auto_generation.total > 0 %}status-good{% elif auto_generation.missing_count > 0 %}status-warn{% else %}status-blue{% endif %}">
{{ auto_generation.ready_count }}/{{ auto_generation.total }}
</span>
<small class="text-muted">自動簡報產線</small>
</div>
</div>
</section>
{% if not vision_status.ready %}
<section class="ppt-diagnostic-strip">
<div>
<div class="ppt-label">視覺 QA 尚未就緒</div>
<strong>目前不是模型能力問題,而是執行環境尚未完整開啟。</strong>
</div>
<div class="ppt-diagnostic-items">
<span>PPT_VISION_ENABLED={{ vision_status.env_value }}</span>
<span>模型 {{ vision_status.model }}</span>
<span>{{ 'LibreOffice OK' if vision_status.converter_ready else '缺 LibreOffice' }}</span>
</div>
{% if vision_status.blockers %}
<small class="text-muted">{{ vision_status.blockers|join('') }}</small>
{% endif %}
</section>
{% endif %}
{% 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>
{% if files %}
<section class="ppt-deck-workbench" aria-label="最近可預覽簡報">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">Preview Workbench</div>
<h2 class="ppt-panel-title">最近可預覽簡報</h2>
</div>
<small class="text-muted">最新 {{ files[:4]|length }} 份,直接線上預覽或下載原始 PPTX</small>
</div>
<div class="ppt-deck-rail">
{% for f in files[:4] %}
<article class="ppt-deck-card {% if not f.file_exists or not f.is_valid_ppt %}is-disabled{% endif %}">
<div class="ppt-deck-meta">
<span class="ppt-label">{{ selected_report_type.label }}</span>
<span>{{ f.mtime }}</span>
</div>
<h3>{{ f.name }}</h3>
<div class="ppt-deck-facts">
<span>{{ f.size_kb if f.size_kb is not none else '—' }} KB</span>
<span>{{ f.source }}</span>
{% if f.file_exists and f.is_valid_ppt %}<span class="status-good">可預覽</span>{% else %}<span class="status-bad">需回補</span>{% endif %}
</div>
<div class="ppt-file-actions">
{% if f.file_exists and f.is_valid_ppt %}
<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-eye me-1"></i>線上預覽
</a>
{% endif %}
{% if f.file_exists %}
<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>
{% endif %}
</div>
</article>
{% endfor %}
</div>
</section>
{% endif %}
<section class="ppt-health-board" aria-label="PPT 產線健康總覽">
<div class="ppt-health-main is-{{ pipeline_view.status }}">
<div class="ppt-label">Pipeline Health</div>
<h2>{{ pipeline_view.title }}</h2>
<p>{{ pipeline_view.message }}</p>
<div class="ppt-health-facts">
<span><strong>{{ pipeline_view.ready_count }}/{{ pipeline_view.total_count }}</strong> 定義覆蓋</span>
<span><strong>{{ pipeline_view.valid_preview_count }}</strong> 份可預覽</span>
<span><strong>{{ pipeline_view.audit_total }}</strong> 筆視覺 QA</span>
</div>
</div>
<div class="ppt-stage-grid">
{% for stage in pipeline_view.stages %}
<article class="ppt-stage-card is-{{ stage.status }}">
<div class="ppt-stage-icon"><i class="fas fa-{{ stage.icon }}" aria-hidden="true"></i></div>
<div>
<div class="ppt-label">{{ stage.label }}</div>
<strong>{{ stage.value }}</strong>
<small>{{ stage.meta }}</small>
<p>{{ stage.detail }}</p>
</div>
</article>
{% endfor %}
</div>
</section>
<section class="ppt-panel mt-3"
data-ppt-auto-generation
data-auto-start="{{ 'true' if auto_generation.can_auto_start else 'false' }}"
data-report-types="{{ auto_generation_missing_report_types | join(',') }}">
<div class="ppt-panel-head">
<div>
<div class="ppt-label">Production Command Center</div>
<h2 class="ppt-panel-title">簡報產線控制台</h2>
</div>
<button class="btn btn-sm btn-outline-primary" type="button" data-ppt-generate-missing {% if not auto_generation.enabled or auto_generation.missing_count == 0 %}disabled{% endif %}>
<i class="fas fa-wand-magic-sparkles me-1" aria-hidden="true"></i>補齊缺漏簡報
</button>
</div>
<div class="ppt-panel-body">
<div class="ppt-pipeline-layout">
<div class="ppt-cadence-rail" aria-label="PPT 定期產出節奏">
{% for cadence in auto_generation.cadences %}
<article class="ppt-cadence-tile is-{{ cadence.status }}">
<div class="ppt-cadence-top">
<span class="ppt-label">{{ cadence.label }}</span>
<strong>{{ cadence.schedule_text }}</strong>
</div>
<div class="ppt-cadence-meter" aria-hidden="true">
<span style="width: {{ cadence.progress_pct }}%"></span>
</div>
<div class="ppt-cadence-meta">
<span>{{ cadence.status_label }}</span>
<span>{{ cadence.coverage_text }}</span>
</div>
<p class="ppt-cadence-gate">{{ cadence.description }}</p>
{% if cadence.missing_count > 0 %}
<small class="text-muted">待補:{{ cadence.missing_report_labels[:3]|join('、') }}{% if cadence.missing_report_labels|length > 3 %} 等 {{ cadence.missing_report_labels|length }} 類{% endif %}</small>
{% else %}
<small class="status-good">{{ cadence.status_hint }}</small>
{% endif %}
</article>
{% endfor %}
</div>
<div class="ppt-coverage-board">
<div class="ppt-coverage-score">
<div>
<span class="ppt-label">定義簡報</span>
<strong>{{ auto_generation.ready_count }}/{{ auto_generation.total }}</strong>
<small class="text-muted">目前缺漏 {{ auto_generation.missing_count }} 類</small>
</div>
<div>
<span class="ppt-label">DB 紀錄</span>
<strong>{{ generation_runs|length }}</strong>
<small class="text-muted">本月最近寫入</small>
</div>
<div>
<span class="ppt-label">線上預覽</span>
<strong>PDF</strong>
<small class="text-muted">PPTX 轉檔快取</small>
</div>
</div>
<div class="ppt-coverage-list" aria-label="定義簡報覆蓋明細">
{% for item in auto_generation_items %}
<div class="ppt-coverage-row">
<div>
<strong>{{ item.label }}</strong>
<small>{{ item.target_label or '最新資料' }}{% if item.latest_generated_at %} · {{ item.latest_generated_at }}{% endif %}</small>
<small>{{ item.status_hint }}</small>
</div>
<span class="ppt-run-status is-{{ item.status }}">
{{ item.status_label }}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="ppt-auto-status small text-muted mt-3" data-ppt-auto-status>
{% if auto_generation.enabled %}
{{ auto_generation.cadence_summary }} 會定期產出並寫入 DB目前缺漏 {{ auto_generation.missing_count }} 類。
{% else %}
PPT_AUTO_GENERATION_ENABLED=false已停用自動補齊。
{% endif %}
</div>
<div class="ppt-run-log mt-3">
<div class="ppt-run-log-head">
<div>
<div class="ppt-label">DB 產出紀錄</div>
<h3>最近寫入 ppt_generation_runs</h3>
</div>
<small class="text-muted">產出檔案、參數、狀態會同步保留在資料庫</small>
</div>
{% if generation_runs %}
<div class="ppt-run-list">
{% for run in generation_runs %}
<div class="ppt-run-row">
<span class="ppt-run-kind">{{ run.schedule_label }}</span>
<strong>{{ run.report_label }}</strong>
<span class="ppt-run-target">{{ run.target_label or '最新資料' }}</span>
<span class="ppt-run-status is-{{ run.status }}">{{ run.status_label }}</span>
<small class="text-muted">{{ run.started_at }}{% if run.finished_at %} → {{ run.finished_at }}{% endif %}</small>
{% if run.file_name %}
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name) }}" target="_blank" rel="noopener">
<i class="fas fa-eye me-1"></i>預覽
</a>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="ppt-empty ppt-run-empty">
目前查無本月 DB 產出紀錄;下一次自動排程或手動補齊後,會寫入 ppt_generation_runs 並顯示在這裡。
</div>
{% endif %}
</div>
</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"
type="button"
data-ppt-aider-heal
data-ppt-filename="{{ r.pptx_filename }}"
data-ppt-error="{{ r.error_msg or '' }}">
<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 if f.size_kb is not none else '—' }}</td>
<td><small>{{ f.mtime }}</small></td>
<td>
{% if f.source == 'database' %}
{% if f.file_exists %}
{% if f.is_valid_ppt %}
<span class="text-success">資料庫快取 + 檔案可預覽</span>
{% else %}
<span class="status-bad">資料庫快取 + 檔案損毀,建議重跑</span>
{% endif %}
{% else %}
<span class="text-muted">資料庫快取(檔案未落盤)</span>
{% endif %}
{% elif f.source == 'both' %}
{% if f.is_valid_ppt %}
<span class="text-success">檔案 + 資料庫</span>
{% else %}
<span class="status-bad">檔案 + 資料庫,檔案損毀</span>
{% endif %}
{% else %}
{% if f.is_valid_ppt %}
<span class="text-success">22:00 掃描落盤,可預覽</span>
{% else %}
<span class="status-bad">22:00 掃描落盤,檔案損毀</span>
{% endif %}
{% endif %}
{% if f.file_error %}
<div><small class="text-muted">{{ f.file_error }}</small></div>
{% endif %}
</td>
<td>
<div class="ppt-file-actions">
{% if f.file_exists %}
{% if f.is_valid_ppt %}
<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-eye me-1"></i>線上預覽
</a>
{% else %}
<span class="small status-bad">檔案不可預覽</span>
{% endif %}
<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>
{% else %}
<span class="small text-muted">本機暫無檔案,請先至來源回補</span>
{% endif %}
</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>
<template id="obs-ppt-audit-data">{{ audit_30d_stats | default({}) | tojson }}</template>
<script src="{{ url_for('static', filename='js/analysis-chart-theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/observability-charts.js') }}"></script>
{% endblock %}