Spaces:
Sleeping
Sleeping
| /** | |
| * trackiq-api.js v5 | |
| * home.html est autonome — ce fichier gère dashboard et logs | |
| */ | |
| const PAGE=(()=>{ | |
| const p=location.pathname; | |
| if(p==='/'||p.includes('index')) return 'index'; | |
| if(p.includes('home')) return 'home'; | |
| if(p.includes('dashboard')) return 'dashboard'; | |
| if(p.includes('logs')) return 'logs'; | |
| return 'unknown'; | |
| })(); | |
| /* ══ DASHBOARD ══ */ | |
| if(PAGE==='dashboard' && !window.__TRACKIQ_INLINE_DASHBOARD__){ | |
| window.addEventListener('load',()=>{ | |
| loadDashboard(); | |
| setInterval(loadDashboard, 5000); // toutes les 5s, pas 6s | |
| }); | |
| async function loadDashboard(){ | |
| try{ | |
| // ── Charge stats backend ── | |
| const res = await fetch('/api/dashboard/stats'); | |
| const data = await res.json(); | |
| const uc = data.global_unique_counts || {}; | |
| const scenes = data.scenes || []; | |
| const total = Object.values(uc).reduce((a,b)=>a+b,0); | |
| // ── Metric card 1 : total objets ── | |
| const m1 = document.getElementById('mv1'); | |
| if(m1) m1.textContent = total || '—'; | |
| // ── Metric card 2 : nb scènes ── | |
| const m2 = document.getElementById('mv2'); | |
| if(m2) m2.textContent = scenes.length || '—'; | |
| // ── Metric card 3 : FPS de la dernière analyse ── | |
| const lastScene = scenes[0]; | |
| const m3 = document.getElementById('mv3'); | |
| if(m3) m3.textContent = lastScene?.fps ? lastScene.fps.toFixed(1) : '—'; | |
| // ── Metric card 4 : durée dernière analyse ── | |
| const m4 = document.getElementById('mv4'); | |
| if(m4) m4.textContent = lastScene?.duration_s ? lastScene.duration_s+'s' : '—'; | |
| // ── Scene summary (se met à jour maintenant !) ── | |
| if(lastScene){ | |
| const ls = document.getElementById('liveSource'); | |
| const lsm = document.getElementById('liveSourceMeta'); | |
| if(ls) ls.textContent = lastScene.video_name || lastScene.scene_id || 'Unknown'; | |
| if(lsm) lsm.textContent = `${lastScene.duration_s||0}s analyzed`; | |
| const lo = document.getElementById('liveObjects'); | |
| if(lo) lo.textContent = lastScene.total || 0; | |
| const lf = document.getElementById('liveFps'); | |
| if(lf) lf.textContent = lastScene.fps ? lastScene.fps.toFixed(1) : '—'; | |
| const ld = document.getElementById('liveDuration'); | |
| if(ld) ld.textContent = lastScene.duration_s ? lastScene.duration_s+'s' : '—'; | |
| } | |
| // ── Pie chart Object Type Distribution ── | |
| if(Object.keys(uc).length && typeof updatePie === 'function'){ | |
| updatePie(uc); | |
| } | |
| // ── Live stats (depuis webcam stats si dispo, sinon dernière analyse) ── | |
| try{ | |
| const wcRes = await fetch('/api/webcam/stats'); | |
| if(wcRes.ok){ | |
| const wc = await wcRes.json(); | |
| const det = document.getElementById('sbdet'); | |
| const acc = document.getElementById('sbacc'); | |
| if(det) det.textContent = wc.detections ?? 0; | |
| if(acc) acc.textContent = wc.frame ? Math.round(wc.frame) : '—'; | |
| // Montre le panneau live seulement si webcam active | |
| if(wc.frame > 0){ | |
| const note = document.getElementById('livePanelNote'); | |
| if(note) note.style.display='none'; | |
| } | |
| } | |
| }catch(e){} | |
| // ── Charts flow (vraies données timeline) ── | |
| if(window.fch && lastScene?.timeline?.length > 1){ | |
| const tl = lastScene.timeline; | |
| const step = Math.max(1,Math.floor(tl.length/20)); | |
| const lbl=[],objDat=[]; | |
| for(let i=0;i<tl.length;i+=step){ | |
| const pt=tl[i]; | |
| lbl.push(pt.ts.toFixed(0)+'s'); | |
| const cnt=Object.entries(pt) | |
| .filter(([k])=>!['frame','ts'].includes(k)) | |
| .reduce((a,[,v])=>a+(v||0),0); | |
| objDat.push(cnt); | |
| } | |
| window.fch.data.labels = lbl; | |
| window.fch.data.datasets[0].data = objDat; | |
| // FPS dataset : valeur constante (fps de la scène) | |
| window.fch.data.datasets[1].data = lbl.map(()=>lastScene.fps||0); | |
| window.fch.update('none'); | |
| } | |
| // ── Chart classification (vraies classes) ── | |
| if(window.tch && Object.keys(uc).length){ | |
| const COLORS=['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#06b6d4','#f97316']; | |
| window.tch.data.labels = Object.keys(uc); | |
| window.tch.data.datasets[0].data = Object.values(uc); | |
| window.tch.data.datasets[0].backgroundColor = Object.keys(uc).map((_,i)=>COLORS[i%COLORS.length]); | |
| window.tch.update('none'); | |
| } | |
| // ── Heatmap depuis vraies positions (déjà gérée par loadHeatmap) ── | |
| }catch(err){ console.warn('[TrackIQ] Dashboard fetch:',err); } | |
| } | |
| } | |
| /* ══ LOGS ══ */ | |
| if(PAGE==='logs'){ | |
| let _rows=[]; | |
| window.addEventListener('load',async()=>{ | |
| try{ | |
| const res=await fetch('/api/logs'); | |
| const scenes=await res.json(); | |
| if(!scenes.length)return; | |
| _rows=[]; | |
| scenes.forEach(s=>{ | |
| Object.entries(s.unique_counts||{}).forEach(([cls,cnt])=>{ | |
| _rows.push({date:s.generated_at?new Date(s.generated_at).toLocaleString():'—', | |
| scene:s.scene_id,video:s.video_name||'—',cls,cnt,total:s.total_unique||0,sid:s.scene_id}); | |
| }); | |
| }); | |
| const scvs=document.querySelectorAll('.sc-v'); | |
| if(scvs[0]) scvs[0].textContent=scenes.reduce((a,s)=>a+(s.total_unique||0),0); | |
| if(scvs[1]) scvs[1].textContent=scenes.length; | |
| renderTable(_rows); | |
| hookFilters(scenes); | |
| const expBtn=document.querySelector('.btn-exp'); | |
| if(expBtn) expBtn.onclick=()=>scenes.forEach((s,i)=>setTimeout(()=>window.open(`/api/logs/${s.scene_id}/csv`),i*300)); | |
| }catch(err){console.warn('[TrackIQ] Logs:',err);} | |
| }); | |
| function renderTable(rows){ | |
| const tbody=document.getElementById('tbody');if(!tbody)return; | |
| if(!rows.length){tbody.innerHTML=`<tr><td colspan="7" style="text-align:center;padding:30px;color:var(--text3);">No data yet — run an analysis first</td></tr>`;return;} | |
| const DOT={'Vehicle':'#3b82f6','Motorcycle':'#10b981','Truck':'#f59e0b','Bus':'#8b5cf6','Human':'#ec4899','Person':'#ec4899','Bicycle':'#06b6d4'}; | |
| const EMO={'Vehicle':'🚗','Motorcycle':'🏍','Truck':'🚚','Bus':'🚌','Human':'🚶','Person':'🚶','Bicycle':'🚲'}; | |
| tbody.innerHTML=''; | |
| rows.forEach(r=>{ | |
| const tr=document.createElement('tr'); | |
| tr.innerHTML=`<td><div class="thumb">${EMO[r.cls]||'📦'}</div></td> | |
| <td style="font-size:11px;color:var(--text2);">${r.date}</td> | |
| <td style="font-size:11px;color:var(--text2);">${r.scene}</td> | |
| <td style="font-size:11px;" title="${r.video}">${r.video.length>18?r.video.slice(0,18)+'…':r.video}</td> | |
| <td><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${DOT[r.cls]||'#888'};margin-right:4px;"></span>${r.cls}</td> | |
| <td><strong>${r.cnt}</strong></td> | |
| <td><a href="/api/logs/${r.sid}/csv" style="color:var(--accent);font-size:11px;"><i class="fas fa-download"></i> CSV</a></td>`; | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| function hookFilters(scenes){ | |
| const sIn=document.getElementById('sIn'); | |
| const camF=document.getElementById('camF'); | |
| const typeF=document.getElementById('typeF'); | |
| if(typeF){ | |
| const cls=[...new Set(_rows.map(r=>r.cls))]; | |
| typeF.innerHTML='<option value="">All</option>'+cls.map(c=>`<option>${c}</option>`).join(''); | |
| } | |
| if(camF) camF.innerHTML='<option value="">All</option>'+ | |
| scenes.map(s=>`<option value="${s.scene_id}">${s.video_name||s.scene_id}</option>`).join(''); | |
| const apply=()=>{ | |
| const s=(sIn?.value||'').toLowerCase(),sc=camF?.value||'',tp=typeF?.value||''; | |
| renderTable(_rows.filter(r=>(!s||r.video.toLowerCase().includes(s)||r.scene.toLowerCase().includes(s))&&(!sc||r.scene===sc)&&(!tp||r.cls===tp))); | |
| }; | |
| if(sIn) sIn.oninput=apply; | |
| if(camF) camF.onchange=apply; | |
| if(typeF) typeF.onchange=apply; | |
| } | |
| } | |
| console.log(`[TrackIQ v5] page=${PAGE}`); | |