diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index ae06e92..48dd9d1 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -110,6 +110,7 @@ - V10.220 補 Phase 50 UI POST CSRF header:`manual_sample_review/evaluate` 保持 CSRF 保護,頁面 fetch 送出 `X-CSRFToken`,不豁免安全檢查。 - Phase 51 manual sample candidate handoff:新增 `/api/market_intel/manual_sample_review/candidate_handoff` POST 與 UI handoff 按鈕,將已通過審核的 sample result 轉成只讀候選活動 preview payload;不保存 handoff、不建立 review queue、不寫 market_*、不允許候選導入、不掛 scheduler;版本同步至 V10.222。 - Phase 52 manual sample candidate queue draft:新增 `services/market_intel/manual_sample_candidate_queue.py`、`/api/market_intel/manual_sample_review/candidate_queue_draft` POST 與 UI queue 草案按鈕,將 handoff 候選轉成只讀人工審核 queue draft;不建立正式 queue、不保存草案、不寫 market_*、不自動核准候選、不掛 scheduler;版本同步至 V10.223。 + - V10.224 補 PPT 報表覆蓋矩陣:`/observability/ppt_audit_history` 將每個定義簡報同列串起 DB 寫入、線上預覽、視覺 QA 與交付狀態,並提供預覽、預熱、重跑操作,避免只顯示「目標已產生」。 - Schema smoke:`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 八張 `market_*` tables。 - Desktop UI QA:本機只註冊 `market_intel_bp` 的 Flask harness 載入 `/market_intel`,確認 Phase 15、候選預覽、writer preview、安全 flags、點陣暖紙視覺正常,console error 0。 - API QA:`/api/market_intel/schema_smoke` 通過 7 張表與 `market_platforms` 必要欄位檢查;`/api/market_intel/platform_seed_writer_plan` 回傳 4 筆 dry-run upsert preview,`writes_executed=false`,四平台皆 `blocked_dry_run_only`。 diff --git a/config.py b/config.py index a797151..6d065b4 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.223" +SYSTEM_VERSION = "V10.224" 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 5e8e5ed..3b2f98a 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -2670,6 +2670,130 @@ def _build_ppt_pipeline_view(files, auto_generation, audit_stats, generation_run 'action_lanes': action_lanes, } + +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 [] + generation_runs = generation_runs or [] + audit_records = audit_records or [] + + file_by_name = {item.get('name'): item for item in files if item.get('name')} + latest_file_by_type = {} + for item in files: + report_type = item.get('report_type') + if report_type and report_type not in latest_file_by_type: + latest_file_by_type[report_type] = item + + latest_run_by_type = {} + for item in generation_runs: + report_type = item.get('report_type') + if report_type and report_type not in latest_run_by_type: + latest_run_by_type[report_type] = item + + latest_audit_by_file = {} + for item in audit_records: + filename = item.get('pptx_filename') + if filename and filename not in latest_audit_by_file: + latest_audit_by_file[filename] = item + + enriched = [] + for raw_item in auto_generation_items or []: + item = dict(raw_item) + report_type = item.get('key') or '' + latest_run = latest_run_by_type.get(report_type, {}) + candidate_file = latest_file_by_type.get(report_type, {}) + file_name = ( + item.get('latest_file_name') + or latest_run.get('file_name') + or candidate_file.get('name') + or '' + ) + file_item = file_by_name.get(file_name) or candidate_file or {} + audit = latest_audit_by_file.get(file_name, {}) + sources = set(item.get('sources') or []) + if file_item.get('source'): + sources.add(file_item.get('source')) + + file_exists = bool(file_item.get('file_exists') or ('filesystem' in sources and file_name)) + valid_ppt = bool(file_exists and (file_item.get('is_valid_ppt') is not False)) + db_backed = bool( + latest_run + or 'database' in sources + or file_item.get('source') in ('database', 'both') + ) + preview_cached = bool(valid_ppt and file_item.get('preview_cache_ready')) + audit_status = audit.get('audit_status') or '' + run_status = latest_run.get('status') or '' + + if latest_run and run_status == 'error': + db_status, db_label = 'error', 'DB 失敗' + elif db_backed: + db_status, db_label = 'ready', 'DB 已寫入' + else: + db_status, db_label = 'planned', '待 DB' + + if valid_ppt and preview_cached: + preview_status, preview_label = 'ready', 'PDF 快取' + elif valid_ppt: + preview_status, preview_label = 'partial', '可預覽' + elif file_name: + preview_status, preview_label = 'error', '不可預覽' + else: + preview_status, preview_label = 'planned', '待產檔' + + if audit_status == 'passed': + qa_status, qa_label = 'ready', 'QA 通過' + elif audit_status == 'failed': + qa_status, qa_label = 'error', 'QA 有問題' + elif audit_status == 'error': + qa_status, qa_label = 'error', 'QA 錯誤' + elif audit_status == 'skipped': + qa_status, qa_label = 'partial', 'QA 跳過' + elif valid_ppt: + qa_status, qa_label = 'planned', '待 QA' + else: + qa_status, qa_label = 'planned', '待產檔' + + if not item.get('ready'): + delivery_status, delivery_label = 'missing', '待產出' + elif latest_run and run_status == 'error': + delivery_status, delivery_label = 'error', '產出失敗' + elif file_name and not valid_ppt: + delivery_status, delivery_label = 'error', '檔案異常' + elif qa_status == 'error': + delivery_status, delivery_label = 'error', '需修復' + elif valid_ppt and db_backed and audit_status == 'passed': + delivery_status, delivery_label = 'ready', '可交付' + elif valid_ppt: + delivery_status, delivery_label = 'partial', '待驗收' + else: + delivery_status, delivery_label = item.get('status') or 'planned', item.get('status_label') or '待確認' + + item.update({ + 'latest_file_name': file_name, + 'latest_file_mtime': file_item.get('mtime') or item.get('latest_generated_at') or '', + 'latest_file_size_kb': file_item.get('size_kb'), + 'file_exists': file_exists, + 'is_valid_ppt': valid_ppt, + 'preview_cache_ready': preview_cached, + 'db_status': db_status, + 'db_label': db_label, + 'preview_status': preview_status, + 'preview_label': preview_label, + 'qa_status': qa_status, + 'qa_label': qa_label, + 'delivery_status': delivery_status, + 'delivery_label': delivery_label, + 'audit_summary': audit.get('issue_summary') or audit.get('error_msg') or '', + 'can_preview': valid_ppt and bool(file_name), + 'can_prewarm': valid_ppt and bool(file_name) and not preview_cached, + 'can_regenerate': bool(report_type), + }) + enriched.append(item) + + return enriched + + @admin_observability_bp.route('/ppt_audit_history') @login_required def ppt_audit_history(): @@ -3164,6 +3288,12 @@ def ppt_audit_history(): if item.get('file_exists') and item.get('is_valid_ppt') and item.get('name') ][:10] aider_heal_active_jobs = _list_ppt_aider_heal_active_jobs() + auto_generation_items = _enrich_ppt_coverage_items( + auto_generation.get('items', []), + files, + generation_runs, + audit_records, + ) return render_template( 'admin/ppt_audit_history.html', @@ -3183,7 +3313,7 @@ def ppt_audit_history(): vision_enabled=vision_enabled, vision_status=vision_status, auto_generation=auto_generation, - auto_generation_items=auto_generation.get('items', []), + auto_generation_items=auto_generation_items, auto_generation_missing_report_types=auto_generation.get('missing_report_types', []), generation_runs=generation_runs, pipeline_view=pipeline_view, diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html index a125e5a..94f4d3d 100644 --- a/templates/admin/ppt_audit_history.html +++ b/templates/admin/ppt_audit_history.html @@ -359,16 +359,56 @@
{{ item.latest_file_name }}
+ {% else %}
+ {{ item.status_hint }}
+ {% endif %}
+ {% if item.audit_summary %}
+ {{ item.audit_summary[:90] }}
+ {% endif %}
+