#!/usr/bin/env python3 """V136 - Make V135 KPI banner clickable: opens inline modal with broken URLs detail Modal: positioned fixed, shows top 10 broken with code/url/ms, dismissible by click outside or Escape. """ import sys src, dst = sys.argv[1], sys.argv[2] with open(src, "r", encoding="utf-8") as f: c = f.read() if "V136-HEALTH-MODAL" in c: print("ALREADY", file=sys.stderr) sys.exit(0) # Change the span to add cursor:pointer and id + click handler. It's already id=v135-kpi-live # Wrap in a click-capable element # Find the span line old_span = 'All-IA Hub · consolidation 84 dashboards' new_span = 'All-IA Hub · consolidation 84 dashboards' if old_span not in c: print("span anchor missing", file=sys.stderr) sys.exit(1) c = c.replace(old_span, new_span, 1) # Add modal HTML + JS logic. Insert modal markup at the END of the xnav div anchor_xnav_close = ' ''' + tabs_marker if tabs_marker in c: c = c.replace(tabs_marker, modal_html, 1) # Add JS functions before the V135-KPI-BANNER IIFE js_marker = "/* V135-KPI-BANNER: load screens-health summary" js_add = '''/* V136-HEALTH-MODAL: show/hide drill-down modal with broken URL list */ function __v136ShowHealthModal(){ const modal = document.getElementById('v136-health-modal'); const content = document.getElementById('v136-modal-content'); const summary = document.getElementById('v136-modal-summary'); if (!modal || !content) return; modal.style.display = 'flex'; content.textContent = 'Fetching /api/screens-health.json ...'; fetch('/api/screens-health.json', {cache: 'no-store'}).then(r => r.ok ? r.json() : null).then(d => { if (!d || !d.counts) { content.textContent = 'Pas de data health disponible.'; return; } const c = d.counts; const total = d.total || 0; summary.textContent = 'Scan ' + (d.generated_at || '') + ' · Total ' + total + ' URLs · UP ' + (c.UP||0) + ' · SLOW ' + (c.SLOW||0) + ' · BROKEN ' + (c.BROKEN||0) + ' · DOWN ' + (c.DOWN||0) + ' · PHANTOM ' + (c.PHANTOM||0); const byUrl = d.by_url || {}; const broken = []; const slow = []; for (const url in byUrl) { const info = byUrl[url]; if (info.status === 'BROKEN' || info.status === 'DOWN') broken.push({url, ...info}); else if (info.status === 'SLOW') slow.push({url, ...info}); } broken.sort((a,b) => (b.code||0) - (a.code||0)); slow.sort((a,b) => (b.ms||0) - (a.ms||0)); let html = ''; if (broken.length) { html += '
🚫 BROKEN/DOWN (' + broken.length + '):
'; broken.slice(0, 30).forEach(b => { html += '
' + '' + (b.status||'?') + ' ' + '' + (b.code||'?') + ' ' + '' + b.url + ' ' + '' + (b.ms||'?') + 'ms' + '
'; }); } else { html += '
✅ No broken URLs
'; } if (slow.length) { html += '
📤 SLOW top 10 (>2s):
'; slow.slice(0, 10).forEach(s => { html += '
' + 'SLOW ' + '' + s.url + ' ' + '' + (s.ms||'?') + 'ms' + '
'; }); } content.innerHTML = html; }).catch(err => { content.textContent = 'Erreur fetch: ' + err; }); } function __v136HideHealthModal(){ const modal = document.getElementById('v136-health-modal'); if (modal) modal.style.display = 'none'; } /* Escape key closes modal */ document.addEventListener('keydown', function(e){ if (e.key === 'Escape') { const modal = document.getElementById('v136-health-modal'); if (modal && modal.style.display !== 'none') __v136HideHealthModal(); } }); ''' + js_marker if js_marker in c: c = c.replace(js_marker, js_add, 1) with open(dst, "w", encoding="utf-8") as f: f.write(c) print(f"OK size={len(c)}", file=sys.stderr)