From d2d6bcd2631aec8edbbeeb91bdc41ca6eb0453e6 Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 11:37:18 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=95=B4=20PPT=20=E8=A6=96=E8=A6=BA?= =?UTF-8?q?=20QA=20=E7=94=A2=E7=B7=9A=E9=A6=96=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- routes/admin_observability_routes.py | 116 ++++++++++++++++ services/ppt_auto_generation_service.py | 2 +- templates/admin/ppt_audit_history.html | 93 +++++++------ tests/test_admin_observability_routes.py | 2 +- tests/test_ppt_auto_generation_service.py | 2 +- web/static/css/page-ppt-audit-history.css | 154 +++++++++++++++++++++- 8 files changed, 323 insertions(+), 49 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index bbb3e9f..d6888f3 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.244 重整 `/observability/ppt_audit_history` 首屏資訊階層:改成簡報操作摘要、最新可預覽簡報、下一步動作與可橫向捲動報表類型 rail;產線覆蓋矩陣改為下方驗收明細,避免一進頁只看到大量「產線狀態」。 - V10.242 修正 `/metabase`、`/grist` 外部工具入口:全域導覽固定回 momo-pro 內部橋接頁,避免資料協作錯連其他專案站;入口頁補路由狀態、設定診斷與可用替代分析入口,降低空白頁誤判。 - V10.221 補 `/observability/ppt_audit_history` AiderHeal 背景任務可見性:正在修復中的簡報會顯示於產線頁,並提供 JSON 狀態端點讓派工後即時刷新,避免重新整理後不知道是否已在修。 - V10.218 補 `/observability/ppt_audit_history` AiderHeal 去重鎖:同一份簡報已在背景修復時,再次點擊會回「已在執行中」,避免重複開 SSH / 模型 / git 修復流程。 diff --git a/config.py b/config.py index e8edfca..2d12e54 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.242" +SYSTEM_VERSION = "V10.244" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index 4310ce1..1ada1b0 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -2683,6 +2683,113 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run } +def _build_ppt_operator_summary(files, auto_generation, pipeline_view, vision_status, audit_stats, generation_runs): + """Build first-screen operator copy that prioritizes deck work over raw pipeline states.""" + files = files or [] + auto_generation = auto_generation or {} + pipeline_view = pipeline_view or {} + vision_status = vision_status or {} + audit_stats = audit_stats or {} + generation_runs = generation_runs or [] + + latest_preview = next( + ( + item for item in files + if item.get('file_exists') and item.get('is_valid_ppt') and item.get('name') + ), + None, + ) + issue_count = int(audit_stats.get('total_issues') or 0) if audit_stats else 0 + missing_count = int(auto_generation.get('missing_count') or 0) + valid_preview_count = int(pipeline_view.get('valid_preview_count') or 0) + cached_preview_count = int(pipeline_view.get('cached_preview_count') or 0) + audit_total = int(pipeline_view.get('audit_total') or 0) + run_error_count = int(pipeline_view.get('run_error_count') or 0) + broken_file_count = int(pipeline_view.get('broken_file_count') or 0) + blockers = vision_status.get('blockers') or [] + + if run_error_count or broken_file_count: + status = 'error' + headline = '先處理異常,再放行簡報' + message = f'目前有 {run_error_count} 筆產出失敗、{broken_file_count} 份檔案不可預覽,建議先看 Action Queue。' + primary_action = '查看待處理' + primary_anchor = '#ppt-action-queue' + elif missing_count: + status = 'partial' + headline = '定期簡報尚未全數補齊' + message = f'本期還有 {missing_count} 類定義簡報缺漏,可手動補齊或等待排程寫入 DB。' + primary_action = '補齊缺漏' + primary_anchor = '#ppt-production-center' + elif not vision_status.get('ready'): + status = 'partial' + headline = '簡報可管理,視覺 QA 待啟用' + message = 'PPT 產出與預覽入口仍可用;視覺模型、LibreOffice 或模型檔需補齊後才會自動審核。' + primary_action = '查看就緒檢查' + primary_anchor = '#ppt-runtime-diagnostic' + elif issue_count: + status = 'partial' + headline = '有視覺問題待回放' + message = f'本期視覺 QA 發現 {issue_count} 個問題,請從問題追蹤或審核歷史回放檢查。' + primary_action = '查看問題' + primary_anchor = '#ppt-issue-board' + else: + status = 'ready' if valid_preview_count else 'planned' + headline = '簡報工作台待命' + message = '最新簡報、PDF 預覽、DB 寫入與視覺 QA 都集中在同一頁追蹤。' + primary_action = '查看簡報' + primary_anchor = '#ppt-deck-workbench' + + latest_run = generation_runs[0] if generation_runs else {} + latest_deck_label = latest_preview.get('name') if latest_preview else '尚無可預覽 PPTX' + latest_deck_meta = ( + f"{latest_preview.get('mtime') or '時間未知'} · " + f"{latest_preview.get('size_kb') if latest_preview.get('size_kb') is not None else '—'} KB · " + f"{'PDF 已快取' if latest_preview and latest_preview.get('preview_cache_ready') else '首次開啟轉檔'}" + if latest_preview else + '請先補齊本期簡報或切換月份 / 報表類型' + ) + + return { + 'status': status, + 'headline': headline, + 'message': message, + 'primary_action': primary_action, + 'primary_anchor': primary_anchor, + 'latest_deck': latest_preview or {}, + 'latest_deck_label': latest_deck_label, + 'latest_deck_meta': latest_deck_meta, + 'latest_run_label': latest_run.get('report_label') or latest_run.get('report_type') or '尚無 DB run', + 'latest_run_meta': latest_run.get('started_at') or '等待下一次排程寫入', + 'blocker_text': ';'.join(blockers[:2]) if blockers else '', + 'signals': [ + { + 'label': '可預覽簡報', + 'value': valid_preview_count, + 'meta': f'{cached_preview_count} 份 PDF 快取', + 'status': 'ready' if valid_preview_count else 'planned', + }, + { + 'label': '待補齊定義', + 'value': missing_count, + 'meta': f"{auto_generation.get('ready_count', 0)}/{auto_generation.get('total', 0)} 已覆蓋", + 'status': 'ready' if missing_count == 0 and auto_generation.get('total') else 'partial', + }, + { + 'label': '視覺 QA', + 'value': audit_total if audit_total else '待跑', + 'meta': '已就緒' if vision_status.get('ready') else 'runtime 待確認', + 'status': 'ready' if vision_status.get('ready') and not issue_count else 'partial', + }, + { + 'label': '視覺問題', + 'value': issue_count, + 'meta': '需回放' if issue_count else '目前無待處理', + 'status': 'partial' if issue_count else 'ready', + }, + ], + } + + def _enrich_ppt_coverage_items(auto_generation_items, files, generation_runs, audit_records): """Join coverage rows with file, DB run, preview and QA state for the UI matrix.""" files = files or [] @@ -3312,6 +3419,14 @@ def ppt_audit_history(): vision_status=vision_status, audit_records=audit_records, ) + operator_summary = _build_ppt_operator_summary( + files=files, + auto_generation=auto_generation, + pipeline_view=pipeline_view, + vision_status=vision_status, + audit_stats=audit_30d_stats, + generation_runs=generation_runs, + ) vision_audit_filenames = [ item.get('name') for item in files @@ -3348,6 +3463,7 @@ def ppt_audit_history(): auto_generation_missing_report_types=auto_generation.get('missing_report_types', []), generation_runs=generation_runs, pipeline_view=pipeline_view, + operator_summary=operator_summary, vision_audit_filenames=vision_audit_filenames, issue_items=issue_items, issue_digest=issue_digest, diff --git a/services/ppt_auto_generation_service.py b/services/ppt_auto_generation_service.py index b149ccc..fde3ed8 100644 --- a/services/ppt_auto_generation_service.py +++ b/services/ppt_auto_generation_service.py @@ -597,7 +597,7 @@ def get_defined_report_coverage( exact_count = exact_counts[job.report_type] if exact_count > 0: status = "ready" - status_label = "目標已產生" + status_label = "已產出" status_hint = "檔案參數與本期定義相符。" elif count > 0: status = "partial" diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html index 1ed1950..c8b2620 100644 --- a/templates/admin/ppt_audit_history.html +++ b/templates/admin/ppt_audit_history.html @@ -11,45 +11,55 @@
-
PPT 視覺 QA 產線 · minicpm-v / AiderHeal / RAG 修法
-

PPT 視覺 QA 產線

-

這頁追蹤每份自動簡報是否通過視覺審核:檔案產出、minicpm-v 審核、Telegram 推送、RAG 修法建議與 AiderHeal 自動修產生器。

-
-
-
視覺模型
- {{ '啟用' if vision_enabled else '停用' }} - minicpm-v + LibreOffice +
+
+
PPT 視覺 QA 產線 · {{ report_month }} · {{ selected_report_type.label }}
+

{{ operator_summary.headline }}

+

{{ operator_summary.message }}

+ {% if operator_summary.blocker_text %} +

{{ operator_summary.blocker_text }}

+ {% endif %} +
+ {{ operator_summary.primary_action }} + 定期產出矩陣 + {% if operator_summary.latest_deck.name %} + + 預覽最新 + + {% endif %} +
-
-
{{ report_month }} {{ selected_report_type.label }}
- {{ files|length }} - 檔案數 -
-
-
審核紀錄
- - {{ audit_30d_stats.total if audit_30d_stats else '—' }} - - {{ selected_report_type.label }} -
-
-
問題數
- - {{ audit_30d_stats.total_issues if audit_30d_stats else '—' }} - - 視覺問題數 -
-
-
定義覆蓋
- - {{ auto_generation.ready_count }}/{{ auto_generation.total }} - - 自動簡報產線 + +
+
+ {% for signal in operator_summary.signals %} +
+
{{ signal.label }}
+ {{ signal.value }} + {{ signal.meta }}
+ {% endfor %}
{% if not vision_status.ready %} -
+
視覺 QA 尚未就緒
目前不是模型能力問題,而是執行環境尚未完整開啟。 @@ -145,8 +155,7 @@ {% endfor %}
- {% if files %} -
+
Preview Workbench
@@ -165,6 +174,7 @@ {% endif %}
+ {% if files %}
{% for f in files[:4] %}
@@ -210,8 +220,12 @@
{% endfor %}
+ {% else %} +
+ 目前沒有符合 {{ report_month }} / {{ selected_report_type.label }} 的簡報檔案;可先切換報表類型,或在下方補齊定義簡報。 +
+ {% endif %}
- {% endif %}
Pipeline Health
@@ -238,7 +252,7 @@ {% endfor %}
-
+
Action Queue
@@ -293,7 +307,7 @@
{% if issue_items %} -
+
Vision Findings
@@ -336,6 +350,7 @@
{% endif %}
diff --git a/tests/test_admin_observability_routes.py b/tests/test_admin_observability_routes.py index 04d6132..af41c8c 100644 --- a/tests/test_admin_observability_routes.py +++ b/tests/test_admin_observability_routes.py @@ -393,7 +393,7 @@ def test_ppt_audit_history_coverage_matrix_joins_db_preview_qa(client, monkeypat 'target_label': '2026/05/17', 'ready': True, 'status': 'ready', - 'status_label': '目標已產生', + 'status_label': '已產出', 'status_hint': '檔案參數與本期定義相符。', 'sources': ['database', 'filesystem'], 'latest_generated_at': '2026-05-17 20:31', diff --git a/tests/test_ppt_auto_generation_service.py b/tests/test_ppt_auto_generation_service.py index c27258f..903ba4d 100644 --- a/tests/test_ppt_auto_generation_service.py +++ b/tests/test_ppt_auto_generation_service.py @@ -101,7 +101,7 @@ def test_coverage_marks_ready_from_database(monkeypatch): by_key = {item["key"]: item for item in result["items"]} assert by_key["daily"]["ready"] is True assert by_key["daily"]["status"] == "ready" - assert by_key["daily"]["status_label"] == "目標已產生" + assert by_key["daily"]["status_label"] == "已產出" assert by_key["monthly"]["ready"] is True assert by_key["weekly"]["ready"] is False assert by_key["weekly"]["status"] == "missing" diff --git a/web/static/css/page-ppt-audit-history.css b/web/static/css/page-ppt-audit-history.css index 4edff65..2cd6a7f 100644 --- a/web/static/css/page-ppt-audit-history.css +++ b/web/static/css/page-ppt-audit-history.css @@ -11,10 +11,21 @@ padding: var(--momo-space-5, 24px); background: radial-gradient(circle, rgba(45, 40, 32, 0.12) 1px, transparent 1.2px), - linear-gradient(135deg, rgba(255, 248, 239, 0.98), rgba(255, 255, 255, 0.78)); + linear-gradient(135deg, rgba(255, 248, 239, 0.98), rgba(250, 247, 240, 0.78)); background-size: 12px 12px, auto; } +.ppt-hero-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(280px, 0.34fr); + gap: var(--momo-space-5, 24px); + align-items: stretch; +} + +.ppt-hero-copy { + min-width: 0; +} + .ppt-kicker { color: var(--obs-accent); font-size: var(--momo-text-caption, 12px); @@ -36,6 +47,83 @@ line-height: 1.7; } +.ppt-hero-note { + margin: var(--momo-space-2, 8px) 0 0; + color: var(--obs-amber); + font-size: var(--momo-text-body, 14px); + font-weight: var(--momo-font-weight-bold, 700); +} + +.ppt-hero-actions { + display: flex; + align-items: center; + gap: var(--momo-space-2, 8px); + flex-wrap: wrap; + margin-top: var(--momo-space-4, 16px); +} + +.ppt-hero-actions .btn { + display: inline-flex; + align-items: center; + gap: var(--momo-space-1, 4px); +} + +.ppt-hero-deck { + display: grid; + align-content: space-between; + gap: var(--momo-space-3, 12px); + min-height: 180px; + padding: var(--momo-space-4, 16px); + border: 1px solid var(--obs-line); + border-left: 5px solid var(--obs-blue); + border-radius: var(--momo-radius-lg, 8px); + background: + radial-gradient(circle, rgba(45, 40, 32, 0.08) 1px, transparent 1.2px), + rgba(250, 247, 240, 0.68); + background-size: 10px 10px, auto; +} + +.ppt-hero-deck.is-ready { + border-left-color: var(--obs-green); +} + +.ppt-hero-deck.is-partial, +.ppt-hero-deck.is-planned { + border-left-color: var(--obs-amber); +} + +.ppt-hero-deck.is-error { + border-left-color: var(--obs-red); +} + +.ppt-hero-deck strong { + display: block; + color: var(--obs-ink); + font-family: var(--momo-font-mono, "IBM Plex Mono", monospace); + font-size: var(--momo-text-title, 18px); + line-height: 1.35; + overflow-wrap: anywhere; +} + +.ppt-hero-deck small, +.ppt-hero-deck-run { + color: var(--obs-muted); + font-size: var(--momo-text-caption, 12px); + line-height: 1.5; +} + +.ppt-hero-deck-run { + display: grid; + gap: var(--momo-space-1, 4px); + padding-top: var(--momo-space-3, 12px); + border-top: 1px solid var(--obs-line); +} + +.ppt-hero-deck-run span { + color: var(--obs-ink); + font-weight: var(--momo-font-weight-bold, 700); +} + .ppt-diagnostic-strip { display: grid; grid-template-columns: minmax(220px, 0.65fr) minmax(320px, 1fr) minmax(260px, 0.8fr); @@ -289,13 +377,31 @@ margin-top: var(--momo-space-4, 16px); } +.ppt-command--compact { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + .ppt-signal { padding: var(--momo-space-3, 12px); border: 1px solid var(--obs-line); + border-left: 4px solid var(--obs-blue); border-radius: var(--momo-radius-lg, 8px); background: rgba(255, 255, 255, 0.62); } +.ppt-signal.is-ready { + border-left-color: var(--obs-green); +} + +.ppt-signal.is-partial, +.ppt-signal.is-planned { + border-left-color: var(--obs-amber); +} + +.ppt-signal.is-error { + border-left-color: var(--obs-red); +} + .ppt-label { color: var(--obs-muted); font-size: var(--momo-text-caption, 12px); @@ -313,16 +419,21 @@ .ppt-toolbar { margin-top: var(--momo-space-4, 16px); - display: flex; - justify-content: space-between; - align-items: center; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: start; gap: var(--momo-space-3, 12px); - flex-wrap: wrap; } .ppt-type-tabs { display: flex; - flex-wrap: wrap; + min-width: 0; + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; + padding-bottom: var(--momo-space-1, 4px); + scrollbar-width: thin; + flex-wrap: nowrap; gap: var(--momo-space-2, 8px); } @@ -330,6 +441,8 @@ display: inline-flex; align-items: center; gap: var(--momo-space-1, 4px); + flex: 0 0 auto; + white-space: nowrap; } .ppt-deck-workbench { @@ -344,6 +457,10 @@ box-shadow: var(--momo-shadow-md, 0 16px 38px rgba(70, 46, 28, 0.08)); } +.ppt-deck-empty { + padding: var(--momo-space-4, 16px); +} + .ppt-workbench-head { display: flex; align-items: flex-start; @@ -660,6 +777,14 @@ body.ppt-preview-open { box-shadow: var(--momo-shadow-md, 0 16px 38px rgba(70, 46, 28, 0.08)); } +#ppt-deck-workbench, +#ppt-action-queue, +#ppt-issue-board, +#ppt-production-center, +#ppt-runtime-diagnostic { + scroll-margin-top: 88px; +} + .ppt-issue-board { margin-top: var(--momo-space-4, 16px); padding: var(--momo-space-4, 16px); @@ -1222,6 +1347,10 @@ body.ppt-preview-open { } @media (max-width: 1180px) { + .ppt-hero-grid { + grid-template-columns: 1fr; + } + .ppt-command { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -1273,6 +1402,10 @@ body.ppt-preview-open { } @media (max-width: 760px) { + .ppt-hero { + padding: var(--momo-space-4, 16px); + } + .ppt-command, .ppt-auto-grid, .ppt-mini-grid, @@ -1285,6 +1418,15 @@ body.ppt-preview-open { grid-template-columns: 1fr; } + .ppt-toolbar { + grid-template-columns: 1fr; + } + + .ppt-type-tabs { + margin-inline: calc(var(--momo-space-2, 8px) * -1); + padding-inline: var(--momo-space-2, 8px); + } + .ppt-panel-head, .ppt-panel-actions, .ppt-table-title,