優化 PPT 產線工作隊列
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
OoO
2026-05-18 19:13:50 +08:00
parent 8f6b3a4b41
commit b5511e818f
6 changed files with 247 additions and 6 deletions

View File

@@ -4,6 +4,7 @@
================================================================================
【已完成】
- V10.196 補 `/observability/ppt_audit_history` Action Queue把待補齊、可預覽、視覺 QA、DB 寫入集中成工作隊列,讓使用者不用在多張卡與表格間找下一個處理點。
- V10.194 重整 `/observability/ppt_audit_history` 產線資訊階層:新增 Pipeline Health、五段式流程階段、排程/覆蓋/DB/預覽/視覺 QA 狀態摘要,讓「已產生」改為可判斷的目標產生、其他版本、待排程補齊等狀態。
- V10.192 補 `/observability/ppt_audit_history` 最近可預覽簡報 workbench最新 4 份 PPT 直接在控制台下方提供線上預覽與下載,降低使用者找檔案的操作成本;完整檔案清單仍保留在下方表格。
- V10.190 補 `/observability/ppt_audit_file/<filename>` 站內線上預覽PPTX 由 LibreOffice 轉 PDF 快取後以 iframe 預覽,保留原始 PPTX 下載Dockerfile 加 `libreoffice-impress`compose 預設啟用 `PPT_VISION_ENABLED=true`PPT 產線頁新增視覺 QA 停用原因與更精簡的控制台式排版。

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.195"
SYSTEM_VERSION = "V10.196"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -2229,13 +2229,14 @@ def budget_update(budget_id: int):
# /observability/ppt_audit_history — Phase 29 PPT 視覺審核歷史
# ─────────────────────────────────────────────────────────────────────────────
def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_runs, vision_status):
def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_runs, vision_status, audit_records=None):
"""Compose page-level PPT pipeline health so the template stays declarative."""
files = files or []
auto_generation = auto_generation or {}
audit_stats = audit_stats or {}
generation_runs = generation_runs or []
vision_status = vision_status or {}
audit_records = audit_records or []
def _as_int(value):
try:
@@ -2345,6 +2346,88 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run
'status': qa_status,
},
]
missing_items = [
item for item in auto_generation.get('items', [])
if not item.get('ready')
]
preview_items = [
item for item in files
if item.get('file_exists') and item.get('is_valid_ppt')
]
audit_attention = [
item for item in audit_records
if item.get('audit_status') in ('failed', 'error')
]
action_lanes = [
{
'key': 'missing',
'label': '待補齊',
'status': 'partial' if missing_items else 'ready',
'count': len(missing_items),
'empty_text': '目前定義簡報都已對齊本期目標。',
'entries': [
{
'title': item.get('label') or item.get('key') or '未命名簡報',
'meta': item.get('target_label') or '最新資料',
'detail': item.get('status_hint') or item.get('status_label') or '等待排程補齊',
'status_label': item.get('status_label') or '待補齊',
}
for item in missing_items[:4]
],
},
{
'key': 'preview',
'label': '可預覽',
'status': 'ready' if preview_items else 'planned',
'count': len(preview_items),
'empty_text': '目前沒有可線上預覽的 PPTX 檔案。',
'entries': [
{
'title': item.get('name') or '未命名檔案',
'meta': item.get('mtime') or '時間未知',
'detail': f"{item.get('size_kb') if item.get('size_kb') is not None else ''} KB · {item.get('source') or 'filesystem'}",
'status_label': '線上預覽',
'filename': item.get('name'),
}
for item in preview_items[:4]
],
},
{
'key': 'audit',
'label': '視覺 QA',
'status': 'error' if audit_attention else ('ready' if audit_total else 'planned'),
'count': len(audit_attention) if audit_attention else audit_total,
'empty_text': '目前沒有需要處理的視覺 QA 失敗紀錄。',
'entries': [
{
'title': item.get('pptx_filename') or '未命名檔案',
'meta': item.get('audited_at') or '時間未知',
'detail': item.get('error_msg') or f"問題 {item.get('issues_count', 0)} 個,信心 {item.get('confidence', 0):.2f}",
'status_label': '需修復' if item.get('audit_status') == 'failed' else '需排查',
'filename': item.get('pptx_filename'),
}
for item in audit_attention[:4]
],
},
{
'key': 'database',
'label': 'DB 寫入',
'status': 'error' if run_error_count else ('ready' if generation_runs else 'planned'),
'count': len(generation_runs),
'empty_text': '本月尚未看到 ppt_generation_runs 寫入紀錄。',
'entries': [
{
'title': item.get('report_label') or item.get('report_type') or '未知簡報',
'meta': item.get('started_at') or '時間未知',
'detail': f"{item.get('schedule_label') or '手動'} · {item.get('target_label') or '最新資料'}",
'status_label': item.get('status_label') or item.get('status') or '未知',
'filename': item.get('file_name') or '',
}
for item in generation_runs[:4]
],
},
]
return {
'status': health_status,
@@ -2363,6 +2446,7 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run
'latest_run': latest_run,
'latest_file': latest_file,
'stages': stages,
'action_lanes': action_lanes,
}
@admin_observability_bp.route('/ppt_audit_history')
@@ -2728,6 +2812,7 @@ def ppt_audit_history():
audit_stats=audit_30d_stats,
generation_runs=generation_runs,
vision_status=vision_status,
audit_records=audit_records,
)
return render_template(

View File

@@ -153,6 +153,46 @@
{% 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'] %}
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_observability.ppt_audit_file', filename=item.filename) }}" target="_blank" rel="noopener">
<i class="fas fa-eye me-1"></i>預覽
</a>
{% endif %}
</div>
</div>
{% else %}
<div class="ppt-action-empty">{{ lane.empty_text }}</div>
{% endfor %}
</div>
</article>
{% endfor %}
</div>
</section>
<section class="ppt-panel mt-3"
data-ppt-auto-generation
data-auto-start="{{ 'true' if auto_generation.can_auto_start else 'false' }}"

View File

@@ -184,10 +184,18 @@ def test_ppt_audit_history_shows_ppt_schedule_and_db_runs(client, monkeypatch):
])
monkeypatch.setattr(svc, 'get_defined_report_coverage', lambda **_kw: {
'enabled': True,
'items': [],
'missing_report_types': [],
'missing_count': 0,
'ready_count': len(svc.DEFINED_REPORT_TYPES),
'items': [{
'key': 'daily',
'label': '每日日報',
'target_label': '2026/05/17',
'ready': False,
'status': 'missing',
'status_label': '待排程補齊',
'status_hint': '尚未找到符合定義的檔案或 DB 紀錄。',
}],
'missing_report_types': ['daily'],
'missing_count': 1,
'ready_count': len(svc.DEFINED_REPORT_TYPES) - 1,
'total': len(svc.DEFINED_REPORT_TYPES),
'last_run': None,
'cadences': cadences,
@@ -219,6 +227,9 @@ def test_ppt_audit_history_shows_ppt_schedule_and_db_runs(client, monkeypatch):
assert '排程節奏' in html
assert 'DB 寫入' in html
assert '線上預覽' in html
assert 'Action Queue' in html
assert '接下來要處理的事' in html
assert '待排程補齊' in html
def test_ppt_audit_history_shows_recent_preview_workbench(client, monkeypatch, tmp_path):

View File

@@ -318,6 +318,105 @@
overflow-wrap: anywhere;
}
.ppt-action-queue {
margin-top: var(--momo-space-4, 16px);
padding: var(--momo-space-4, 16px);
border: 1px solid var(--obs-line);
border-radius: var(--momo-radius-lg, 8px);
background:
radial-gradient(circle, rgba(45, 40, 32, 0.08) 1px, transparent 1.2px),
rgba(255, 255, 255, 0.5);
background-size: 10px 10px, auto;
box-shadow: var(--momo-shadow-md, 0 16px 38px rgba(70, 46, 28, 0.08));
}
.ppt-action-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--momo-space-3, 12px);
}
.ppt-action-lane {
display: grid;
gap: var(--momo-space-3, 12px);
align-content: start;
min-height: 246px;
padding: var(--momo-space-3, 12px);
border: 1px solid var(--obs-line);
border-radius: var(--momo-radius-lg, 8px);
background: rgba(255, 255, 255, 0.58);
border-top: 4px solid var(--obs-blue);
}
.ppt-action-lane.is-ready {
border-top-color: var(--obs-green);
}
.ppt-action-lane.is-partial,
.ppt-action-lane.is-planned {
border-top-color: var(--obs-amber);
}
.ppt-action-lane.is-error {
border-top-color: var(--obs-red);
}
.ppt-action-lane-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--momo-space-2, 8px);
}
.ppt-action-lane-head strong {
color: var(--obs-ink);
font-size: var(--momo-text-headline, 22px);
line-height: 1;
}
.ppt-action-list {
display: grid;
gap: var(--momo-space-2, 8px);
}
.ppt-action-item,
.ppt-action-empty {
padding: var(--momo-space-2, 8px);
border: 1px solid rgba(86, 64, 48, 0.12);
border-radius: var(--momo-radius-md, 6px);
background: rgba(255, 248, 239, 0.54);
}
.ppt-action-item strong {
display: block;
color: var(--obs-ink);
font-size: var(--momo-text-body, 14px);
line-height: 1.35;
overflow-wrap: anywhere;
}
.ppt-action-item small,
.ppt-action-item p,
.ppt-action-empty {
color: var(--obs-muted);
font-size: var(--momo-text-caption, 12px);
line-height: 1.5;
}
.ppt-action-item p {
margin: var(--momo-space-1, 4px) 0 0;
overflow-wrap: anywhere;
}
.ppt-action-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--momo-space-2, 8px);
flex-wrap: wrap;
margin-top: var(--momo-space-2, 8px);
}
.ppt-grid {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(330px, 0.8fr);
@@ -651,6 +750,10 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ppt-action-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ppt-deck-rail {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -670,6 +773,7 @@
.ppt-mini-grid,
.ppt-deck-rail,
.ppt-stage-grid,
.ppt-action-grid,
.ppt-coverage-score,
.ppt-coverage-list {
grid-template-columns: 1fr;