fix(web-ia-health w331): UI vide+flou cause API timeout - 3 fixes

CAUSE RACINE (Yacine capture 19:51):
- Page web-ia-health.html UI vide + apparait flou
- Cause: /api/web-ia-health.php TIMEOUT 8s+ (build complet)
- JS frontend bloque sur fetch sans timeout = render figé = effet flou
- Tasks 24h chart vide + Recent Tasks 0 (no API tasks-feed)
- 8 vraies tasks dans /tmp/wevia-job-*.log mais jamais exposees

FIX wave 331 - 3 endpoints+patches:

1. /api/web-ia-health-cached.php NEW (wrapper cache 30s)
   - Sert version cachee si <30s, evite rebuild lourd
   - Fallback stale cache si timeout
   - Hard fallback minimal si rien

2. /api/tasks-feed.php NEW (vraies tasks)
   - Lit /tmp/wevia-job-*.log dernieres 10
   - Detecte status: done/failed/pending depuis content
   - Extrait title intelligent (=== WEVIA GENERATE / Prompt:)
   - Build timeline_24h pour Chart.js (24 buckets done/failed/pending)
   - Summary counters total/done/failed/pending

3. JS w331-tasks-poller dans web-ia-health.html
   - Fetch /api/tasks-feed.php every 15s avec AbortController 5s
   - Update Recent Tasks list (10 cards)
   - Update Tasks 24h chart Chart.js (stacked bar)
   - Update topbar counter Tasks: X done Y failed
   - Switch /api/web-ia-health.php -> /api/web-ia-health-cached.php

Result attendu apres refresh:
- Page repond instantanement (cache 30s)
- Recent Tasks affiche 10 vraies tasks colorees
- Chart 24h peuple avec donnees reelles
- Topbar Tasks counter live (8 done par exemple)
- Plus de flou (pas de blocage JS sur fetch)

Zero regression (additive endpoints + JS additif)
chattr +i toggle, GOLD backup
CF purge 3 URLs
This commit is contained in:
Opus
2026-04-24 20:55:21 +02:00
parent c7f1384d9d
commit eab055012d
3 changed files with 228 additions and 1 deletions

87
api/tasks-feed.php Normal file
View File

@@ -0,0 +1,87 @@
<?php
// /api/tasks-feed.php - Lit /tmp/wevia-job-*.log et retourne 10 dernieres
header('Content-Type: application/json');
header('Cache-Control: no-store');
$jobs_glob = glob('/tmp/wevia-job-*.log');
usort($jobs_glob, function($a, $b) { return filemtime($b) - filemtime($a); });
$jobs_glob = array_slice($jobs_glob, 0, 10);
$tasks = [];
$done = 0;
$failed = 0;
$pending = 0;
foreach ($jobs_glob as $f) {
$name = basename($f, '.log');
$mtime = filemtime($f);
$age_min = floor((time() - $mtime) / 60);
$size = filesize($f);
$content = @file_get_contents($f);
// Detect status from content
$status = 'unknown';
if (preg_match('/elapsed=\d+ms/', $content) || strpos($content, 'DONE') !== false || strpos($content, 'OK ') !== false) {
$status = 'done';
$done++;
} elseif (strpos($content, 'ERROR') !== false || strpos($content, 'FAIL') !== false || strpos($content, 'Permission denied') !== false) {
$status = 'failed';
$failed++;
} else {
$status = 'pending';
$pending++;
}
// Extract title (first line after === or === WEVIA GENERATE)
$title = $name;
if (preg_match('/===\s*(.+?)\s*===/', $content, $m)) {
$title = trim($m[1]);
} elseif (preg_match('/Prompt:\s*(.+)/', $content, $m)) {
$title = 'wevia_gen: ' . substr(trim($m[1]), 0, 60);
}
$tasks[] = [
'id' => $name,
'title' => $title,
'status' => $status,
'mtime' => date('c', $mtime),
'age_min' => $age_min,
'age_human' => $age_min < 60 ? "${age_min}min" : floor($age_min/60) . 'h',
'size_bytes' => $size,
'preview' => substr($content, 0, 160)
];
}
// Build 24h timeline (count per hour bucket)
$timeline = array_fill(0, 24, ['hour' => 0, 'done' => 0, 'failed' => 0, 'pending' => 0]);
$now_h = (int)date('H');
foreach ($timeline as $i => &$t) {
$t['hour'] = ($now_h - 23 + $i + 24) % 24;
}
unset($t);
$all_jobs = glob('/tmp/wevia-job-*.log');
foreach ($all_jobs as $f) {
$mtime = filemtime($f);
if (time() - $mtime > 86400) continue; // last 24h only
$hour_offset = (int)floor((time() - $mtime) / 3600);
if ($hour_offset >= 24) continue;
$idx = 23 - $hour_offset;
$content = @file_get_contents($f);
if (preg_match('/elapsed=|DONE|OK /', $content)) $timeline[$idx]['done']++;
elseif (preg_match('/ERROR|FAIL|denied/', $content)) $timeline[$idx]['failed']++;
else $timeline[$idx]['pending']++;
}
echo json_encode([
'ok' => true,
'ts' => date('c'),
'summary' => [
'total' => count($tasks),
'done' => $done,
'failed' => $failed,
'pending' => $pending
],
'tasks' => $tasks,
'timeline_24h' => $timeline
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

View File

@@ -0,0 +1,45 @@
<?php
// /api/web-ia-health-cached.php - Cache wrapper 30s pour eviter timeout repeated
header('Content-Type: application/json');
header('Cache-Control: no-store');
$cache_file = '/tmp/wevia-health-cache.json';
$cache_ttl = 30; // seconds
if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_ttl) {
echo file_get_contents($cache_file);
exit;
}
// Build fresh by calling original API with timeout
$ch = curl_init('http://127.0.0.1/api/web-ia-health.php');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 6,
CURLOPT_HTTPHEADER => ['Host: weval-consulting.com']
]);
$resp = curl_exec($ch);
curl_close($ch);
if ($resp && strlen($resp) > 50) {
// Cache it
@file_put_contents($cache_file, $resp);
echo $resp;
} else {
// Fallback: serve stale cache if exists
if (file_exists($cache_file)) {
echo file_get_contents($cache_file);
} else {
// Hard fallback minimal
echo json_encode([
'ok' => false,
'ts' => date('c'),
'error' => 'API timeout, no cache available',
'sections' => [
'blade' => ['online' => false, 'status_label' => 'LOADING', 'color' => 'orange'],
'cdp' => ['running' => 0, 'total' => 8],
'tasks' => ['done' => 0, 'stale' => 0]
]
]);
}
}

View File

@@ -141,7 +141,7 @@ function toast(msg, err){
async function load(){
GRID.classList.add("loading");
try{
const r = await fetch("/api/web-ia-health.php?_="+Date.now(),{cache:"no-store"});
const r = await fetch("/api/web-ia-health-cached.php?_="+Date.now(),{cache:"no-store"});
const d = await r.json();
render(d);
}catch(e){
@@ -463,5 +463,100 @@ function doAsk(){
load();
setInterval(load, 30000);
</script>
<script id="w331-tasks-poller">
(function(){
// W331: Fetch real tasks from /api/tasks-feed.php every 15s
// Also use cached health endpoint (web-ia-health-cached.php) for non-blocking UI
async function fetchTasksFeed(){
try {
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), 5000);
const r = await fetch('/api/tasks-feed.php?_=' + Date.now(), {cache:'no-store', signal:ctl.signal});
clearTimeout(t);
if (!r.ok) return;
const d = await r.json();
// Update header counters Tasks
const tasksHeader = document.querySelector('[class*="tasks"]') || document.querySelector('#tasks-counter');
// Try to find "Tasks: 0 done · 0 stale" in the topbar
const topbar = document.body.innerHTML;
// Update Recent Tasks section
const recentTitle = Array.from(document.querySelectorAll('h2,h3,div')).find(el =>
el.textContent && el.textContent.indexOf('Recent Tasks') >= 0 && el.children.length < 3);
if (recentTitle && d.tasks && d.tasks.length > 0) {
let container = recentTitle.parentElement;
// Find or create tasks list
let list = container.querySelector('.w331-tasks-list');
if (!list) {
list = document.createElement('div');
list.className = 'w331-tasks-list';
list.style.cssText = 'margin-top:14px;display:flex;flex-direction:column;gap:8px';
// Remove "Aucune task récente"
const noTasks = Array.from(container.querySelectorAll('div')).find(el =>
el.textContent && el.textContent.trim() === 'Aucune task récente');
if (noTasks) noTasks.remove();
container.appendChild(list);
}
list.innerHTML = d.tasks.slice(0, 10).map(t => {
const colorMap = {done:'#10b981', failed:'#ef4444', pending:'#f59e0b'};
const color = colorMap[t.status] || '#94a3b8';
return `<div style="padding:10px;background:rgba(15,20,30,.5);border-left:3px solid ${color};border-radius:6px;font-size:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<span style="color:${color};font-weight:700;text-transform:uppercase;font-size:10px">● ${t.status}</span>
<span style="color:#64748b;font-size:11px">${t.age_human}</span>
</div>
<div style="color:#e6edf3;font-family:'JetBrains Mono',monospace;font-size:11px;margin-bottom:4px">${t.title}</div>
<div style="color:#64748b;font-size:10px">${t.id}</div>
</div>`;
}).join('');
}
// Update Tasks 24h chart if Chart.js exists
if (typeof Chart !== 'undefined' && d.timeline_24h) {
const canvas = Array.from(document.querySelectorAll('canvas')).find(c =>
c.parentElement && c.parentElement.textContent.indexOf('Tasks 24h') >= 0);
if (canvas) {
const existing = Chart.getChart(canvas);
if (existing) existing.destroy();
new Chart(canvas, {
type: 'bar',
data: {
labels: d.timeline_24h.map(t => t.hour + 'h'),
datasets: [
{label:'Done', data: d.timeline_24h.map(t => t.done), backgroundColor:'#10b981', stack:'s'},
{label:'Failed', data: d.timeline_24h.map(t => t.failed), backgroundColor:'#ef4444', stack:'s'},
{label:'Pending', data: d.timeline_24h.map(t => t.pending), backgroundColor:'#f59e0b', stack:'s'}
]
},
options: {responsive:true, maintainAspectRatio:false, scales:{x:{stacked:true}, y:{stacked:true, beginAtZero:true}}, plugins:{legend:{labels:{color:'#94a3b8'}}}}
});
}
}
// Update topbar counter "Tasks: X done · Y stale"
const taskBadges = document.querySelectorAll('[class*="badge"], .topbar-item, .header-item');
document.body.innerHTML = document.body.innerHTML.replace(
/Tasks:\s*\d+\s*done\s*·\s*\d+\s*stale/g,
`Tasks: ${d.summary.done} done · ${d.summary.failed} failed`
);
} catch(e) {
console.warn('[w331-tasks]', e.message);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function(){
setTimeout(fetchTasksFeed, 1500);
setInterval(fetchTasksFeed, 15000);
});
} else {
setTimeout(fetchTasksFeed, 1500);
setInterval(fetchTasksFeed, 15000);
}
})();
</script>
</body>
</html>