This commit is contained in:
@@ -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 停用原因與更精簡的控制台式排版。
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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' }}"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user