Files
ewoooc/templates/admin/ppt_audit_history.html
ogt deb771d6f7
Some checks failed
CD Pipeline / deploy (push) Failing after 1m4s
fix: replace internal runtime terms in visible UI
2026-06-25 12:48:23 +08:00

823 lines
42 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-hero-grid">
<div class="ppt-hero-copy">
<div class="ppt-kicker"><i class="fas fa-search me-1"></i> PPT 視覺 QA 產線 · {{ report_month }} · {{ selected_report_type.label }}</div>
<h1 class="ppt-title">{{ operator_summary.headline }}</h1>
<p class="ppt-subtitle">先確認簡報可預覽、可審核,再把問題交給修復流程。</p>
{% if operator_summary.blocker_text %}
<p class="ppt-hero-note"><i class="fas fa-circle-info me-1" aria-hidden="true"></i>{{ operator_summary.blocker_text }}</p>
{% endif %}
<div class="ppt-hero-actions">
<a class="btn btn-primary btn-sm" href="{{ operator_summary.primary_anchor }}">{{ operator_summary.primary_action }}</a>
<a class="btn btn-outline-primary btn-sm" href="#ppt-production-center">定期產出矩陣</a>
{% if operator_summary.latest_deck.name %}
<a class="btn btn-outline-secondary btn-sm"
href="{{ url_for('admin_observability.ppt_audit_file', filename=operator_summary.latest_deck.name) }}"
target="_blank"
rel="noopener"
data-ppt-open-preview
data-ppt-filename="{{ operator_summary.latest_deck.name }}"
data-ppt-preview-title="最新簡報 · {{ operator_summary.latest_deck.name }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=operator_summary.latest_deck.name, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=operator_summary.latest_deck.name) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=operator_summary.latest_deck.name, action='download') }}">
<i class="fas fa-eye me-1"></i>預覽最新
</a>
{% endif %}
</div>
</div>
<aside class="ppt-hero-deck is-{{ operator_summary.status }}" aria-label="最新簡報狀態">
<div class="ppt-label">Latest Deck</div>
<strong>{{ operator_summary.latest_deck_label }}</strong>
<small>{{ operator_summary.latest_deck_meta }}</small>
<div class="ppt-hero-deck-run">
<span>{{ operator_summary.latest_run_label }}</span>
<small>{{ operator_summary.latest_run_meta }}</small>
</div>
</aside>
</div>
</section>
<section class="ppt-command ppt-command--compact" aria-label="PPT 產線關鍵狀態">
{% for signal in operator_summary.signals %}
<div class="ppt-signal is-{{ signal.status }}">
<div class="ppt-label">{{ signal.label }}</div>
<span class="ppt-value">{{ signal.value }}</span>
<small class="text-muted">{{ signal.meta }}</small>
</div>
{% endfor %}
</section>
{% if not vision_status.ready %}
<section class="ppt-diagnostic-strip" id="ppt-runtime-diagnostic">
<div>
<div class="ppt-label">視覺 QA 尚未就緒</div>
<strong>先補齊視覺 QA runtime再判斷簡報品質。</strong>
<small class="text-muted">{{ vision_status.summary }}</small>
</div>
<div class="ppt-diagnostic-checks" aria-label="視覺 QA runtime checklist">
{% for check in vision_status.readiness_checks %}
<div class="ppt-diagnostic-check is-{{ check.status }}">
<span>{{ check.label }}</span>
<strong>{{ check.value }}</strong>
<small>{{ check.detail }}</small>
</div>
{% endfor %}
</div>
<div class="ppt-diagnostic-actions">
<span class="ppt-run-status is-planned">{{ vision_status.ready_count }}/{{ vision_status.check_count }} 通過</span>
{% for action in vision_status.next_actions %}
<small>{{ action }}</small>
{% endfor %}
{% if vision_status.blockers %}
<small class="text-muted">阻擋:{{ vision_status.blockers|join('') }}</small>
{% endif %}
</div>
</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-aider-status{% if not aider_heal_active_count %} is-empty{% endif %}" data-ppt-aider-status aria-live="polite">
<div class="ppt-aider-status-main">
<span class="ppt-run-status is-planned"><i class="fas fa-wrench me-1" aria-hidden="true"></i>AiderHeal</span>
<div>
<strong data-ppt-aider-status-title>{% if aider_heal_active_count %}AiderHeal 執行中 · {{ aider_heal_active_count }}{% else %}AiderHeal 待命{% endif %}</strong>
<small data-ppt-aider-status-meta>{% if aider_heal_active_count %}修復完成後回報處理結果。{% else %}有問題的審核紀錄可直接派工。{% endif %}</small>
</div>
</div>
<div class="ppt-aider-job-list" data-ppt-aider-job-list>
{% for job in aider_heal_active_jobs[:3] %}
<div class="ppt-aider-job">
<code>{{ job.pptx_filename or 'manual' }}</code>
<span>{{ job.queued_at }}</span>
<small>{{ job.diagnosis }}</small>
</div>
{% endfor %}
</div>
</section>
<section class="ppt-vision-status is-{{ vision_audit_status.status }}" data-ppt-vision-status aria-live="polite">
<div class="ppt-vision-status-main">
<span class="ppt-run-status is-{{ 'ready' if vision_audit_status.status == 'completed' else 'planned' if vision_audit_status.status in ['idle', 'queued', 'running'] else 'error' }}">
<i class="fas fa-eye me-1" aria-hidden="true"></i>視覺檢查
</span>
<div>
<strong data-ppt-vision-status-title>{{ vision_audit_status.status_label }}</strong>
<small data-ppt-vision-status-meta>{{ vision_audit_status.message }}</small>
</div>
</div>
<div class="ppt-vision-status-list" data-ppt-vision-status-list>
<div class="ppt-vision-job is-runtime">
<span>執行環境</span>
<strong>{{ vision_status.status_label }}</strong>
<small>{{ vision_status.model }} · {{ vision_status.converter or '無轉檔器' }}</small>
</div>
{% if vision_audit_status.last_run %}
<div class="ppt-vision-job">
<span>{{ vision_audit_status.last_run.finished_at or vision_audit_status.last_run.started_at or vision_audit_status.last_run.queued_at }}</span>
<strong>{{ vision_audit_status.last_run.summary.audited_count }} 份 / {{ vision_audit_status.last_run.summary.total_issues }} 問題</strong>
<small>{% if vision_audit_status.last_run.summary.error_count %}執行錯誤 {{ vision_audit_status.last_run.summary.error_count }}{% else %}無執行錯誤{% endif %}</small>
</div>
{% else %}
<div class="ppt-vision-job">
<span>尚無紀錄</span>
<strong>待命</strong>
<small>啟動視覺 QA 後會顯示背景任務狀態。</small>
</div>
{% endif %}
</div>
</section>
<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-deck-workbench" id="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>
{% if files %}
<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>
{% else %}
<div class="ppt-empty ppt-deck-empty">
目前沒有符合 {{ report_month }} / {{ selected_report_type.label }} 的簡報檔案;可先切換報表類型,或在下方補齊定義簡報。
</div>
{% endif %}
</section>
<section class="ppt-health-board" aria-label="PPT 產線健康總覽">
<div class="ppt-health-main is-{{ pipeline_view.status }}">
<div class="ppt-label">產線健康度</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" id="ppt-action-queue" data-ppt-action-queue aria-label="PPT 工作隊列">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">工作隊列</div>
<h2 class="ppt-panel-title">接下來要處理的事</h2>
</div>
<small class="text-muted">把缺漏、預覽、視覺 QA 與產出紀錄集中成工作隊列</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', 'audit'] %}
<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" id="ppt-issue-board" aria-label="視覺問題追蹤">
<div class="ppt-workbench-head">
<div>
<div class="ppt-label">視覺問題</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>
{% if issue.report_type %}
<div class="ppt-action-foot">
<button class="btn btn-outline-warning btn-sm" type="button" data-ppt-generate-one data-report-type="{{ issue.report_type }}" data-report-label="{{ issue.pptx_filename }}">
<i class="fas fa-rotate me-1"></i>重跑同類簡報
</button>
</div>
{% endif %}
</article>
{% endfor %}
</div>
</section>
{% endif %}
<section class="ppt-panel mt-3"
id="ppt-production-center"
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">產線控制台</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">產出紀錄</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="定義簡報覆蓋明細">
<div class="ppt-coverage-list-head">
<span class="ppt-label">報表覆蓋矩陣</span>
<small>紀錄 / 預覽 / 視覺 QA / 交付</small>
</div>
{% for item in auto_generation_items %}
<div class="ppt-coverage-row is-{{ item.delivery_status }}">
<div class="ppt-coverage-main">
<strong>{{ item.label }}</strong>
<small>{{ item.target_label or '最新資料' }}{% if item.latest_file_mtime %} · {{ item.latest_file_mtime }}{% endif %}</small>
{% if item.latest_file_name %}
<small><code>{{ item.latest_file_name }}</code></small>
{% else %}
<small>{{ item.status_hint }}</small>
{% endif %}
{% if item.audit_summary %}
<small>{{ item.audit_summary[:90] }}</small>
{% endif %}
</div>
<div class="ppt-coverage-signals">
<span class="ppt-run-status is-{{ item.db_status }}">{{ item.db_label }}</span>
<span class="ppt-run-status is-{{ item.preview_status }}">{{ item.preview_label }}</span>
<span class="ppt-run-status is-{{ item.qa_status }}">{{ item.qa_label }}</span>
<span class="ppt-run-status is-{{ item.delivery_status }}">{{ item.delivery_label }}</span>
</div>
<div class="ppt-coverage-actions">
{% if item.can_preview %}
<a class="btn btn-outline-primary btn-sm"
href="{{ url_for('admin_observability.ppt_audit_file', filename=item.latest_file_name) }}"
target="_blank"
rel="noopener"
data-ppt-open-preview
data-ppt-filename="{{ item.latest_file_name }}"
data-ppt-preview-title="{{ item.label }} · {{ item.latest_file_name }}"
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=item.latest_file_name, action='pdf') }}"
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=item.latest_file_name) }}"
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=item.latest_file_name, action='download') }}">
<i class="fas fa-eye me-1"></i>預覽
</a>
{% endif %}
{% if item.can_prewarm %}
<button class="btn btn-outline-primary btn-sm" type="button" data-ppt-prewarm-preview data-ppt-filename="{{ item.latest_file_name }}">
<i class="fas fa-fire me-1"></i>預熱
</button>
{% endif %}
{% if item.can_regenerate and item.delivery_status in ['missing', 'error', 'partial'] %}
<button class="btn btn-outline-warning btn-sm" type="button" data-ppt-generate-one data-report-type="{{ item.key }}" data-report-label="{{ item.label }}">
<i class="fas fa-rotate me-1"></i>重跑
</button>
{% endif %}
</div>
</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 }} 會定期產出並保存紀錄;目前缺漏 {{ 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">產出紀錄</div>
<h3>最近產出</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">
本月尚無產出紀錄;先補排程或手動產出,再進行視覺 QA。
</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') %}
{% if r.report_type %}
<button class="btn btn-sm btn-outline-warning"
type="button"
data-ppt-generate-one
data-report-type="{{ r.report_type }}"
data-report-label="{{ r.pptx_filename }}">
<i class="fas fa-rotate me-1"></i>重跑
</button>
{% endif %}
<button class="btn btn-sm btn-outline-warning"
type="button"
data-ppt-aider-heal
data-ppt-filename="{{ r.pptx_filename }}"
data-ppt-error="{{ r.issue_summary or 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>先補視覺 QA 條件</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 %}