This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.221 補 `/observability/ppt_audit_history` AiderHeal 背景任務可見性:正在修復中的簡報會顯示於產線頁,並提供 JSON 狀態端點讓派工後即時刷新,避免重新整理後不知道是否已在修。
|
||||
- V10.218 補 `/observability/ppt_audit_history` AiderHeal 去重鎖:同一份簡報已在背景修復時,再次點擊會回「已在執行中」,避免重複開 SSH / 模型 / git 修復流程。
|
||||
- V10.217 讓 `/observability/ppt_audit_history` 的 AiderHeal 派工改為非阻塞背景任務:頁面立即回「已排入」,修復工作在背景執行,避免瀏覽器與 Gunicorn worker 等 SSH、模型與 git push 到超時。
|
||||
- V10.216 修正 `/observability/ppt_audit_history` 的 AiderHeal 派工斷點:失敗簡報即使只有 `issues_found` 診斷摘要也能一鍵送修,並修正 `execute_code_fix` 參數與 dict 回傳解析,避免按鈕 400/500。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.220"
|
||||
SYSTEM_VERSION = "V10.221"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -36,7 +36,12 @@ admin_observability_bp = Blueprint(
|
||||
)
|
||||
|
||||
_PPT_AIDER_HEAL_LOCK = threading.Lock()
|
||||
_PPT_AIDER_HEAL_ACTIVE = set()
|
||||
_PPT_AIDER_HEAL_ACTIVE = {}
|
||||
|
||||
|
||||
def _list_ppt_aider_heal_active_jobs():
|
||||
with _PPT_AIDER_HEAL_LOCK:
|
||||
return [dict(job) for job in _PPT_AIDER_HEAL_ACTIVE.values()]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1839,16 +1844,27 @@ def ppt_audit_trigger_aider_heal():
|
||||
'issue_summary': issue_summary[:500],
|
||||
}
|
||||
heal_key = pptx_filename or diagnosis[:160] or 'manual'
|
||||
queued_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
active_job = {
|
||||
'key': heal_key,
|
||||
'pptx_filename': pptx_filename,
|
||||
'target_file': 'services/ppt_generator.py',
|
||||
'queued_at': queued_at,
|
||||
'diagnosis': diagnosis[:160],
|
||||
}
|
||||
with _PPT_AIDER_HEAL_LOCK:
|
||||
if heal_key in _PPT_AIDER_HEAL_ACTIVE:
|
||||
existing_job = dict(_PPT_AIDER_HEAL_ACTIVE.get(heal_key) or active_job)
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'status': 'already_running',
|
||||
'action': 'CODE_FIX',
|
||||
'message': '這份簡報的 AiderHeal 已在背景執行中,請等 Telegram/Gitea/CD 結果回報。',
|
||||
'target_file': 'services/ppt_generator.py',
|
||||
'active_count': len(_PPT_AIDER_HEAL_ACTIVE),
|
||||
'job': existing_job,
|
||||
}), 202
|
||||
_PPT_AIDER_HEAL_ACTIVE.add(heal_key)
|
||||
_PPT_AIDER_HEAL_ACTIVE[heal_key] = active_job
|
||||
|
||||
def _heal_worker():
|
||||
try:
|
||||
@@ -1871,7 +1887,7 @@ def ppt_audit_trigger_aider_heal():
|
||||
)
|
||||
finally:
|
||||
with _PPT_AIDER_HEAL_LOCK:
|
||||
_PPT_AIDER_HEAL_ACTIVE.discard(heal_key)
|
||||
_PPT_AIDER_HEAL_ACTIVE.pop(heal_key, None)
|
||||
|
||||
thread_key = ''.join(ch for ch in pptx_filename if ch.isalnum())[:24] or 'manual'
|
||||
threading.Thread(
|
||||
@@ -1886,11 +1902,24 @@ def ppt_audit_trigger_aider_heal():
|
||||
'action': 'CODE_FIX',
|
||||
'message': 'AiderHeal 已排入背景執行;完成後會由 Telegram/Gitea/CD 結果回報。',
|
||||
'target_file': 'services/ppt_generator.py',
|
||||
'active_count': len(_list_ppt_aider_heal_active_jobs()),
|
||||
'job': active_job,
|
||||
}), 202
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500
|
||||
|
||||
|
||||
@admin_observability_bp.route('/ppt_audit/aider_heal_status')
|
||||
@login_required
|
||||
def ppt_audit_aider_heal_status():
|
||||
jobs = _list_ppt_aider_heal_active_jobs()
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'active_count': len(jobs),
|
||||
'jobs': jobs,
|
||||
})
|
||||
|
||||
|
||||
@admin_observability_bp.route('/ppt_audit/generate_missing', methods=['POST'])
|
||||
@login_required
|
||||
def ppt_audit_generate_missing():
|
||||
@@ -3134,6 +3163,7 @@ def ppt_audit_history():
|
||||
for item in files
|
||||
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()
|
||||
|
||||
return render_template(
|
||||
'admin/ppt_audit_history.html',
|
||||
@@ -3160,6 +3190,8 @@ def ppt_audit_history():
|
||||
vision_audit_filenames=vision_audit_filenames,
|
||||
issue_items=issue_items,
|
||||
issue_digest=issue_digest,
|
||||
aider_heal_active_jobs=aider_heal_active_jobs,
|
||||
aider_heal_active_count=len(aider_heal_active_jobs),
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +66,25 @@
|
||||
{% 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-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) }}">
|
||||
|
||||
@@ -524,6 +524,7 @@ def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch)
|
||||
from services import aider_heal_executor as svc
|
||||
|
||||
captured = {}
|
||||
mod._PPT_AIDER_HEAL_ACTIVE.clear()
|
||||
|
||||
def fake_execute_code_fix(**kwargs):
|
||||
captured.update(kwargs)
|
||||
@@ -563,6 +564,7 @@ def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch)
|
||||
assert 'ocbot_daily_20260517.pptx' in captured['error_message']
|
||||
assert '圖表被切掉' in captured['error_message']
|
||||
assert captured['context']['issue_summary'] == 'S1: 圖表被切掉:右側圖例超出邊界'
|
||||
assert mod._PPT_AIDER_HEAL_ACTIVE == {}
|
||||
|
||||
|
||||
def test_ppt_audit_trigger_aider_heal_dedupes_same_file(client, monkeypatch):
|
||||
@@ -610,6 +612,68 @@ def test_ppt_audit_trigger_aider_heal_dedupes_same_file(client, monkeypatch):
|
||||
mod._PPT_AIDER_HEAL_ACTIVE.clear()
|
||||
|
||||
|
||||
def test_ppt_audit_aider_heal_status_reports_active_jobs(client, monkeypatch):
|
||||
"""背景 AiderHeal 已派工時,狀態端點要能讓頁面重新整理後看見執行中。"""
|
||||
from routes import admin_observability_routes as mod
|
||||
from services import aider_heal_executor as svc
|
||||
|
||||
def fake_execute_code_fix(**_kwargs):
|
||||
return {'success': True, 'message': '不應在此測試執行'}
|
||||
|
||||
class HoldingThread:
|
||||
def __init__(self, target, **_kwargs):
|
||||
self.target = target
|
||||
|
||||
def start(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(svc, 'execute_code_fix', fake_execute_code_fix)
|
||||
monkeypatch.setattr(mod.threading, 'Thread', HoldingThread)
|
||||
mod._PPT_AIDER_HEAL_ACTIVE.clear()
|
||||
|
||||
payload = {
|
||||
'pptx_filename': 'ocbot_daily_20260517.pptx',
|
||||
'issue_summary': 'S1: 圖表被切掉:右側圖例超出邊界',
|
||||
}
|
||||
first = client.post('/observability/ppt_audit/trigger_aider_heal', json=payload)
|
||||
status = client.get('/observability/ppt_audit/aider_heal_status')
|
||||
|
||||
try:
|
||||
assert first.status_code == 202
|
||||
data = status.get_json()
|
||||
assert status.status_code == 200
|
||||
assert data['ok'] is True
|
||||
assert data['active_count'] == 1
|
||||
assert data['jobs'][0]['pptx_filename'] == 'ocbot_daily_20260517.pptx'
|
||||
assert data['jobs'][0]['target_file'] == 'services/ppt_generator.py'
|
||||
assert '圖表被切掉' in data['jobs'][0]['diagnosis']
|
||||
finally:
|
||||
mod._PPT_AIDER_HEAL_ACTIVE.clear()
|
||||
|
||||
|
||||
def test_ppt_audit_history_shows_active_aider_heal_jobs(client):
|
||||
"""PPT 產線頁要直接顯示正在背景修復的檔案。"""
|
||||
from routes import admin_observability_routes as mod
|
||||
|
||||
mod._PPT_AIDER_HEAL_ACTIVE.clear()
|
||||
mod._PPT_AIDER_HEAL_ACTIVE['ocbot_daily_20260517.pptx'] = {
|
||||
'key': 'ocbot_daily_20260517.pptx',
|
||||
'pptx_filename': 'ocbot_daily_20260517.pptx',
|
||||
'target_file': 'services/ppt_generator.py',
|
||||
'queued_at': '2026-05-18 14:42:00',
|
||||
'diagnosis': 'S1: 圖表被切掉:右側圖例超出邊界',
|
||||
}
|
||||
try:
|
||||
r = client.get('/observability/ppt_audit_history')
|
||||
html = r.get_data(as_text=True)
|
||||
assert r.status_code == 200
|
||||
assert 'AiderHeal 執行中' in html
|
||||
assert 'ocbot_daily_20260517.pptx' in html
|
||||
assert '圖表被切掉' in html
|
||||
finally:
|
||||
mod._PPT_AIDER_HEAL_ACTIVE.clear()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# /observability/host_health
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -73,6 +73,81 @@
|
||||
font-weight: var(--momo-font-weight-bold, 700);
|
||||
}
|
||||
|
||||
.ppt-aider-status {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 0.36fr) minmax(0, 0.64fr);
|
||||
gap: var(--momo-space-3, 12px);
|
||||
align-items: stretch;
|
||||
margin-top: var(--momo-space-3, 12px);
|
||||
padding: var(--momo-space-3, 12px);
|
||||
border: 1px solid rgba(184, 121, 47, 0.32);
|
||||
border-radius: var(--momo-radius-lg, 8px);
|
||||
background:
|
||||
radial-gradient(circle, rgba(184, 121, 47, 0.1) 1px, transparent 1.2px),
|
||||
rgba(255, 248, 239, 0.72);
|
||||
background-size: 10px 10px, auto;
|
||||
}
|
||||
|
||||
.ppt-aider-status.is-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ppt-aider-status-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--momo-space-3, 12px);
|
||||
}
|
||||
|
||||
.ppt-aider-status-main strong,
|
||||
.ppt-aider-status-main small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ppt-aider-status-main strong {
|
||||
color: var(--obs-ink);
|
||||
font-size: var(--momo-text-body, 14px);
|
||||
font-weight: var(--momo-font-weight-black, 800);
|
||||
}
|
||||
|
||||
.ppt-aider-status-main small {
|
||||
color: var(--obs-muted);
|
||||
}
|
||||
|
||||
.ppt-aider-job-list {
|
||||
display: grid;
|
||||
gap: var(--momo-space-2, 8px);
|
||||
}
|
||||
|
||||
.ppt-aider-job {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 0.42fr) minmax(130px, 0.22fr) minmax(0, 0.36fr);
|
||||
gap: var(--momo-space-2, 8px);
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
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, 255, 255, 0.54);
|
||||
}
|
||||
|
||||
.ppt-aider-job code,
|
||||
.ppt-aider-job span,
|
||||
.ppt-aider-job small {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ppt-aider-job code {
|
||||
color: var(--obs-ink);
|
||||
font-family: var(--momo-font-mono);
|
||||
}
|
||||
|
||||
.ppt-aider-job span,
|
||||
.ppt-aider-job small {
|
||||
color: var(--obs-muted);
|
||||
font-size: var(--momo-text-caption, 12px);
|
||||
}
|
||||
|
||||
.ppt-command {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
@@ -952,6 +1027,7 @@ body.ppt-preview-open {
|
||||
}
|
||||
|
||||
.ppt-diagnostic-strip,
|
||||
.ppt-aider-status,
|
||||
.ppt-health-board,
|
||||
.ppt-pipeline-layout {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1027,4 +1103,8 @@ body.ppt-preview-open {
|
||||
.ppt-preview-frame-wrap iframe {
|
||||
min-height: calc(100vh - 168px);
|
||||
}
|
||||
|
||||
.ppt-aider-job {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,6 +604,9 @@
|
||||
statusNode.classList.remove('is-working');
|
||||
statusNode.textContent = data.message || 'AiderHeal 已排入背景執行,完成後會由 Telegram/Gitea/CD 結果回報。';
|
||||
}
|
||||
if (window.refreshPptAiderHealStatus) {
|
||||
window.refreshPptAiderHealStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('ppt_audit_trigger_aider_heal_failed', error);
|
||||
if (triggerButton) {
|
||||
@@ -617,6 +620,45 @@
|
||||
}
|
||||
};
|
||||
|
||||
function renderPptAiderHealStatus(payload) {
|
||||
const panel = document.querySelector('[data-ppt-aider-status]');
|
||||
if (!panel) return;
|
||||
const jobs = Array.isArray(payload && payload.jobs) ? payload.jobs : [];
|
||||
const count = Number((payload && payload.active_count) || jobs.length || 0);
|
||||
panel.classList.toggle('is-empty', count <= 0);
|
||||
const title = panel.querySelector('[data-ppt-aider-status-title]');
|
||||
const meta = panel.querySelector('[data-ppt-aider-status-meta]');
|
||||
const list = panel.querySelector('[data-ppt-aider-job-list]');
|
||||
if (title) title.textContent = count > 0 ? `AiderHeal 執行中 · ${count}` : 'AiderHeal 待命';
|
||||
if (meta) {
|
||||
meta.textContent = count > 0
|
||||
? '修復完成後會由 Telegram/Gitea/CD 回報。'
|
||||
: '有問題的審核紀錄可直接一鍵派工。';
|
||||
}
|
||||
if (list) {
|
||||
list.innerHTML = jobs.slice(0, 3).map(job => `
|
||||
<div class="ppt-aider-job">
|
||||
<code>${escapeHtml(job.pptx_filename || 'manual')}</code>
|
||||
<span>${escapeHtml(job.queued_at || '')}</span>
|
||||
<small>${escapeHtml(job.diagnosis || '')}</small>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
window.refreshPptAiderHealStatus = async function refreshPptAiderHealStatus() {
|
||||
try {
|
||||
const response = await fetch('/observability/ppt_audit/aider_heal_status', {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.ok) return;
|
||||
renderPptAiderHealStatus(data);
|
||||
} catch (error) {
|
||||
console.warn('ppt_audit_aider_heal_status_failed', error);
|
||||
}
|
||||
};
|
||||
|
||||
function initPptAutoGeneration() {
|
||||
const panel = document.querySelector('[data-ppt-auto-generation]');
|
||||
const pageStatus = document.querySelector('[data-ppt-auto-status]');
|
||||
|
||||
Reference in New Issue
Block a user