Files
ewoooc/templates/admin/ppt_audit_history.html
OoO dddafc579b
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
強化 PPT 視覺問題追蹤
2026-05-19 00:16:21 +08:00

690 lines
35 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 %}
<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 audit_30d_stats and audit_30d_stats.pass_rate >= 80 %}status-good{% elif audit_30d_stats and audit_30d_stats.pass_rate >= 60 %}status-warn{% elif audit_30d_stats %}status-bad{% else %}status-blue{% endif %}">
{{ audit_30d_stats.total if audit_30d_stats else '—' }}
</span>
<small class="text-muted">{{ selected_report_type.label }}</small>
</div>
<div class="ppt-signal">
<div class="ppt-label">問題數</div>
<span class="ppt-value {% if audit_30d_stats and audit_30d_stats.total_issues > 0 %}status-warn{% elif audit_30d_stats %}status-good{% else %}status-blue{% endif %}">
{{ audit_30d_stats.total_issues if audit_30d_stats else '—' }}
</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>
<div class="ppt-workbench-actions">
<small class="text-muted">最新 {{ files[:4]|length }} 份,直接線上預覽或下載原始 PPTX</small>
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-run-vision {% if not vision_status.ready or not vision_audit_filenames %}disabled{% endif %}>
<i class="fas fa-eye me-1"></i>立即視覺 QA
</button>
{% if pipeline_view.uncached_preview_count > 0 %}
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-prewarm-all>
<i class="fas fa-fire me-1"></i>預熱本頁 PDF
<span class="badge bg-light text-dark border ms-1" data-ppt-prewarm-count>{{ pipeline_view.uncached_preview_count }}</span>
</button>
{% endif %}
</div>
</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 %}
{% if f.file_exists and f.is_valid_ppt %}
{% if f.preview_cache_ready %}<span class="status-blue" data-ppt-preview-state="{{ f.name }}">PDF 快取</span>{% else %}<span class="status-warn" data-ppt-preview-state="{{ f.name }}">首次轉檔</span>{% endif %}
{% 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"
data-ppt-open-preview
data-ppt-filename="{{ f.name }}"
data-ppt-preview-title="{{ selected_report_type.label }} · {{ f.name }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
<i class="fas fa-eye me-1"></i>線上預覽
</a>
{% if not f.preview_cache_ready %}
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-prewarm-preview data-ppt-filename="{{ f.name }}">
<i class="fas fa-fire me-1"></i>預熱 PDF
</button>
{% endif %}
{% 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.cached_preview_count }}</strong> 份 PDF 快取</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-action-queue" data-ppt-action-queue aria-label="PPT 工作隊列">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">Action Queue</div>
<h2 class="ppt-panel-title">接下來要處理的事</h2>
</div>
<small class="text-muted">把缺漏、預覽、視覺 QA、DB 寫入集中成工作隊列</small>
</div>
<div class="ppt-action-grid">
{% for lane in pipeline_view.action_lanes %}
<article class="ppt-action-lane is-{{ lane.status }}">
<div class="ppt-action-lane-head">
<span class="ppt-label">{{ lane.label }}</span>
<strong>{{ lane.count }}</strong>
</div>
<div class="ppt-action-list">
{% for item in lane.entries %}
<div class="ppt-action-item">
<div>
<strong>{{ item.title }}</strong>
<small>{{ item.meta }}</small>
<p>{{ item.detail }}</p>
</div>
<div class="ppt-action-foot">
<span class="ppt-run-status is-{{ lane.status }}">{{ item.status_label }}</span>
{% if item.filename and lane.key in ['preview', 'database', 'triage'] %}
<a class="btn btn-outline-primary btn-sm"
href="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename) }}"
target="_blank"
rel="noopener"
data-ppt-open-preview
data-ppt-filename="{{ item.filename }}"
data-ppt-preview-title="{{ item.title }} · {{ item.filename }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename, action='download') }}">
<i class="fas fa-eye me-1"></i>預覽
</a>
{% endif %}
{% if item.can_regenerate %}
<button class="btn btn-outline-warning btn-sm" type="button" data-ppt-generate-one data-report-type="{{ item.report_type }}" data-report-label="{{ item.title }}">
<i class="fas fa-rotate me-1"></i>重跑
</button>
{% endif %}
</div>
</div>
{% else %}
<div class="ppt-action-empty">{{ lane.empty_text }}</div>
{% endfor %}
</div>
</article>
{% endfor %}
</div>
</section>
{% if issue_items %}
<section class="ppt-issue-board" aria-label="視覺問題追蹤">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">Vision Findings</div>
<h2 class="ppt-panel-title">視覺問題追蹤</h2>
</div>
<div class="ppt-issue-metrics">
<span><strong>{{ issue_digest.total }}</strong> 問題</span>
<span><strong>{{ issue_digest.files }}</strong> 檔案</span>
<span><strong>{{ issue_digest.error_count }}</strong> 高優先</span>
<span><strong>{{ issue_digest.latest_audit or '—' }}</strong> 最近審核</span>
</div>
</div>
<div class="ppt-issue-grid">
{% for issue in issue_items[:8] %}
<article class="ppt-issue-card is-{{ issue.status }}">
<div class="ppt-issue-top">
<span class="ppt-run-status is-{{ 'error' if issue.status == 'error' else 'planned' }}">{{ issue.slide_label }}</span>
<span class="ppt-label">{{ issue.category }}</span>
</div>
<strong><code>{{ issue.pptx_filename }}</code></strong>
<p>{{ issue.text }}</p>
<div class="ppt-action-foot">
<small class="text-muted">{{ issue.audited_at }}</small>
<a class="btn btn-outline-primary btn-sm"
href="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename) }}"
target="_blank"
rel="noopener"
data-ppt-open-preview
data-ppt-filename="{{ issue.pptx_filename }}"
data-ppt-preview-title="問題回放 · {{ issue.pptx_filename }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename, action='download') }}">
<i class="fas fa-eye me-1"></i>回放
</a>
</div>
</article>
{% endfor %}
</div>
</section>
{% endif %}
<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>
<div class="ppt-panel-actions">
<button class="btn btn-sm btn-outline-primary" type="button" data-ppt-run-vision {% if not vision_status.ready or not vision_audit_filenames %}disabled{% endif %}>
<i class="fas fa-eye me-1" aria-hidden="true"></i>立即視覺 QA
</button>
<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>
<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 }} 類。視覺 QA 可立即補跑,或等待每日 22:00 排程。
{% 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"
data-ppt-open-preview
data-ppt-filename="{{ run.file_name }}"
data-ppt-preview-title="{{ run.report_label }} · {{ run.file_name }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=run.file_name, action='download') }}">
<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">
<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.issue_summary or r.error_msg or '')[:180] }}</small></td>
<td>
{% if r.pptx_filename %}
<a class="btn btn-sm btn-outline-primary"
href="{{ url_for('admin_observability.ppt_audit_file', filename=r.pptx_filename) }}"
target="_blank"
rel="noopener"
data-ppt-open-preview
data-ppt-filename="{{ r.pptx_filename }}"
data-ppt-preview-title="審核回放 · {{ r.pptx_filename }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=r.pptx_filename, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=r.pptx_filename) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=r.pptx_filename, action='download') }}">
<i class="fas fa-eye me-1"></i>回放
</a>
{% endif %}
{% 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">目前無 {{ selected_report_type.label }} 審核歷史;可按「立即視覺 QA」補跑或等待每日 22:00 排程。</td></tr>
{% endfor %}
</tbody>
</table>
</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 %}
{% if f.file_exists and f.is_valid_ppt %}
<div>
<small class="{{ 'status-blue' if f.preview_cache_ready else 'status-warn' }}" data-ppt-preview-state="{{ f.name }}">
{% if f.preview_cache_ready %}
PDF 預覽快取已建立{% if f.preview_cache_mtime %} · {{ f.preview_cache_mtime }}{% endif %}
{% else %}
PDF 預覽尚未快取,首次開啟會自動轉檔
{% endif %}
</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"
data-ppt-open-preview
data-ppt-filename="{{ f.name }}"
data-ppt-preview-title="{{ selected_report_type.label }} · {{ f.name }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=f.name) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=f.name, action='download') }}">
<i class="fas fa-eye me-1"></i>線上預覽
</a>
{% if not f.preview_cache_ready %}
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-prewarm-preview data-ppt-filename="{{ f.name }}">
<i class="fas fa-fire me-1"></i>預熱 PDF
</button>
{% endif %}
{% 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 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">排程或立即視覺 QA 完成後,這裡會顯示本月通過率、失敗檔案與修復建議。</p>
</div>
</div>
</article>
{% 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>
<div class="ppt-preview-modal" data-ppt-preview-modal hidden aria-hidden="true">
<div class="ppt-preview-backdrop" data-ppt-preview-close></div>
<section class="ppt-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="pptPreviewTitle">
<header class="ppt-preview-head">
<div>
<div class="ppt-label">Inline PDF Preview</div>
<h2 id="pptPreviewTitle" data-ppt-preview-modal-title>簡報線上預覽</h2>
<small class="text-muted" data-ppt-preview-filename>尚未選擇檔案</small>
</div>
<div class="ppt-preview-actions">
<a class="btn btn-outline-secondary btn-sm" href="#" target="_blank" rel="noopener" data-ppt-preview-open-page>
<i class="fas fa-up-right-from-square me-1"></i>開新頁
</a>
<a class="btn btn-outline-secondary btn-sm" href="#" data-ppt-preview-download>
<i class="fas fa-download me-1"></i>下載
</a>
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-preview-close aria-label="關閉預覽">
<i class="fas fa-xmark me-1"></i>關閉
</button>
</div>
</header>
<div class="ppt-preview-frame-wrap">
<div class="ppt-preview-loading" data-ppt-preview-loading>正在載入 PDF 預覽</div>
<iframe data-ppt-preview-frame title="PPT PDF 線上預覽" loading="lazy"></iframe>
</div>
</section>
</div>
<template id="obs-ppt-audit-data">{{ audit_30d_stats | default({}) | tojson }}</template>
<template id="obs-ppt-audit-filenames">{{ vision_audit_filenames | 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 %}