823 lines
42 KiB
HTML
823 lines
42 KiB
HTML
{% 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">{{ operator_summary.message }}</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>目前不是模型能力問題,而是執行環境尚未完整開啟。</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 %}修復完成後會由 Telegram/Gitea/CD 回報。{% 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、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', '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">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="定義簡報覆蓋明細">
|
||
<div class="ppt-coverage-list-head">
|
||
<span class="ppt-label">報表覆蓋矩陣</span>
|
||
<small>DB / 預覽 / 視覺 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 }} 會定期產出並寫入 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') %}
|
||
{% 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>為什麼這頁空?</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 %}
|