danielle2035's picture
Update templates/logs.html
7181271 verified
Raw
History Blame Contribute Delete
27.6 kB
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrackIQ Logs</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root{--bg:#f0f4f8;--surface:#fff;--surface2:#f6f8fb;--border:rgba(0,0,0,0.07);--shadow:0 2px 16px rgba(0,0,0,0.09);--shadow-lg:0 8px 36px rgba(0,0,0,0.13);--text:#0f1923;--text2:#5a6a7a;--text3:#9aaab8;--accent:#880000;--accent2:#660000;--accent-light:rgba(136,0,0,0.07);--sidebar-w:60px;--radius:14px;--radius-sm:9px;}
[data-theme="dark"]{--bg:#0c1117;--surface:#151c25;--surface2:#1c2531;--border:rgba(255,255,255,0.07);--shadow:0 2px 16px rgba(0,0,0,0.45);--shadow-lg:0 8px 36px rgba(0,0,0,0.55);--text:#e8edf2;--text2:#7a8fa0;--text3:#3a4a5a;}
*{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0}
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);font-size:13px;display:flex;min-height:100vh}
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200}
.sb-logo{width:34px;height:34px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none}
.sb-sep{width:28px;height:1px;background:var(--border);margin:4px 0}
.sb-btn{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:15px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative}
.sb-btn:hover,.sb-btn.active{background:var(--accent-light);color:var(--accent)}
.sb-btn .tip{position:absolute;left:46px;top:50%;transform:translateY(-50%);background:var(--text);color:var(--surface);font-size:11px;padding:3px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none}.sb-btn:hover .tip{opacity:1}
.sb-spacer{flex:1}.page-wrap{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column}
.topbar{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 22px}
.topbar-l,.topbar-r{display:flex;align-items:center;gap:10px}
.brand-name{font-size:15px;font-weight:600}.brand-name b{color:var(--accent)}.sep{color:var(--border);font-size:18px;margin:0 2px}
.lang-group{display:flex;background:var(--surface2);border-radius:6px;padding:2px;border:1px solid var(--border)}
.lang-btn{font-size:11px;font-weight:500;padding:3px 8px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--text2);font-family:'DM Sans',sans-serif}.lang-btn.active{background:var(--accent);color:white}
.icon-btn{width:30px;height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:12px}
main{flex:1;padding:16px 18px;max-width:1440px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:12px}
.sum-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
.sc,.card,.tcard{background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow)}
.sc{padding:13px 15px}.sc-l{font-size:11px;color:var(--text2);margin-bottom:5px}.sc-v{font-size:22px;font-weight:300}.sc-s{font-size:10px;color:var(--text3);margin-top:3px}
.card{padding:14px 16px}.filter-row{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap}.fg{display:flex;flex-direction:column;gap:4px}.fg label{font-size:11px;color:var(--text2)}
.fin{padding:8px 10px;border:1px solid var(--border);border-radius:7px;font-size:12px;background:var(--surface2);color:var(--text);font-family:'DM Sans',sans-serif;outline:none;min-width:160px}.fin:focus{border-color:var(--accent)}
.f-sp{flex:1}
.btn{display:flex;align-items:center;gap:5px;border:none;border-radius:7px;padding:8px 13px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif}
.btn-exp{background:var(--accent);color:white}.btn-exp:hover{background:var(--accent2)}
.save-opt{display:flex;align-items:center;gap:10px;width:100%;padding:9px 14px;border:none;background:none;cursor:pointer;font-size:12px;color:var(--text);font-family:'DM Sans',sans-serif;text-align:left}.save-opt:hover{background:var(--surface2)}
.btn-clr{background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
.tcard{overflow:hidden}.twrap{overflow:auto}
table{width:100%;border-collapse:collapse}
thead th{padding:10px 12px;text-align:left;font-size:11px;font-weight:500;color:var(--text2);background:var(--surface2);border-bottom:1px solid var(--border);white-space:nowrap}
tbody tr{border-bottom:1px solid var(--border)} tbody tr:last-child{border-bottom:none}
tbody td{padding:10px 12px;font-size:12px;vertical-align:middle}
.mono{font-family:'DM Mono',monospace}
.pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--surface2);border-radius:999px;padding:3px 8px}
.dot{width:8px;height:8px;border-radius:50%}
.empty{padding:38px;text-align:center;color:var(--text3)}
@media(max-width:1100px){.sum-row{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:760px){.sum-row{grid-template-columns:1fr}}
</style>
</head>
<body>
<nav class="sidebar">
<a href="/" class="sb-logo"><i class="fas fa-satellite-dish"></i></a>
<div class="sb-sep"></div>
<a href="/home" class="sb-btn"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
<a href="/dashboard" class="sb-btn"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
<a href="/logs" class="sb-btn active"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
<div class="sb-spacer"></div>
<button class="sb-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon"></i><span class="tip">Theme</span></button>
</nav>
<div class="page-wrap">
<header class="topbar">
<div class="topbar-l">
<span class="brand-name">Track<b>IQ</b></span>
<span class="sep">|</span>
<span style="font-size:13px;font-weight:500;" id="t-title">Detection Logs</span>
</div>
<div class="topbar-r">
<div class="lang-group">
<button class="lang-btn active" onclick="setLang('en',this)">EN</button>
<button class="lang-btn" onclick="setLang('fr',this)">FR</button>
<button class="lang-btn" onclick="setLang('wo',this)">WO</button>
</div>
<button class="icon-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon2"></i></button>
</div>
</header>
<main>
<div class="sum-row">
<div class="sc"><div class="sc-l" id="t-s1">Detections</div><div class="sc-v" id="sumDetections">0</div><div class="sc-s" id="sumDetectionsSub">Real rows from generated videos</div></div>
<div class="sc"><div class="sc-l" id="t-s2">Scenes</div><div class="sc-v" id="sumScenes">0</div><div class="sc-s" id="sumScenesSub">Processed analyses</div></div>
<div class="sc"><div class="sc-l" id="t-s3">Classes</div><div class="sc-v" id="sumClasses">0</div><div class="sc-s" id="sumClassesSub">Distinct detected labels</div></div>
<div class="sc"><div class="sc-l" id="t-s4">Latest video</div><div class="sc-v" id="sumLatest">-</div><div class="sc-s" id="sumLatestSub">Most recent analyzed clip</div></div>
</div>
<div class="card">
<div class="filter-row">
<div class="fg"><label id="t-search">Search video or scene</label><input id="searchIn" class="fin" oninput="applyFilters()"></div>
<div class="fg"><label id="t-scene">Scene</label><select id="sceneF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
<div class="fg"><label id="t-type">Class</label><select id="classF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
<div class="fg"><label id="t-direction">Direction</label><select id="dirF" class="fin" onchange="applyFilters()"><option value="">All</option><option>up</option><option>down</option><option>left</option><option>right</option></select></div>
<div class="f-sp"></div>
<button class="btn btn-clr" onclick="clearFilters()"><i class="fas fa-rotate-left"></i> <span id="t-clear">Clear</span></button>
<button class="btn btn-clr" onclick="clearAllLogs()" style="color:#ef4444;border-color:rgba(239,68,68,0.4);" title="Delete all logs"><i class="fas fa-trash"></i></button>
<div style="position:relative">
<button class="btn btn-exp" onclick="toggleSaveMenu(event)"><i class="fas fa-cloud-arrow-up"></i> <span id="t-export">Save Logs</span> <i class="fas fa-chevron-down" style="font-size:9px;margin-left:2px"></i></button>
<div id="saveMenu" style="display:none;position:absolute;right:0;top:calc(100% + 6px);background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-lg);min-width:240px;z-index:999;overflow:hidden">
<div style="padding:10px 14px 6px;font-size:11px;color:var(--text2);font-weight:500;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
<label style="font-size:11px;color:var(--text2)">Group name:</label>
<input id="groupNameIn" value="trackiq" style="flex:1;padding:4px 7px;border:1px solid var(--border);border-radius:5px;font-size:11px;background:var(--surface2);color:var(--text);outline:none" placeholder="e.g. session1">
</div>
<span style="color:var(--text3);font-size:10px">File: <span id="csvPreview">trackiq_all.csv</span></span>
</div>
<button class="save-opt" onmousedown="event.stopPropagation();saveLocal()"><i class="fas fa-download" style="color:#3b82f6"></i> Download locally</button>
<button class="save-opt" onmousedown="event.stopPropagation();showGithubPanel()"><i class="fab fa-github" style="color:#e2e8f0"></i> Push to GitHub</button>
<button class="save-opt" onmousedown="event.stopPropagation();saveDrive()"><i class="fab fa-google-drive" style="color:#34a853"></i> Save to Google Drive</button>
</div>
</div>
<!-- GitHub panel -->
<div id="githubPanel" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:none;align-items:center;justify-content:center">
<div style="background:var(--surface);border-radius:14px;padding:22px 24px;width:420px;box-shadow:var(--shadow-lg)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<span style="font-weight:600;font-size:14px"><i class="fab fa-github"></i> Push to GitHub</span>
<button onclick="closeGithubPanel()" style="background:none;border:none;color:var(--text2);cursor:pointer;font-size:16px"><i class="fas fa-times"></i></button>
</div>
<div style="display:flex;flex-direction:column;gap:9px">
<div class="fg"><label>Personal Access Token (PAT)</label><input id="ghToken" class="fin" type="password" placeholder="ghp_xxxxxxxxxxxx"></div>
<div class="fg"><label>Repository (owner/repo)</label><input id="ghRepo" class="fin" placeholder="username/trackiq-logs"></div>
<div class="fg"><label>Branch</label><input id="ghBranch" class="fin" value="main" placeholder="main"></div>
<div class="fg"><label>Path in repo</label><input id="ghPath" class="fin" placeholder="logs/" value="logs/"></div>
</div>
<div id="ghStatus" style="margin-top:10px;font-size:12px;color:var(--text2);min-height:18px"></div>
<div style="display:flex;gap:8px;margin-top:14px;justify-content:flex-end">
<button class="btn btn-clr" onclick="closeGithubPanel()">Cancel</button>
<button class="btn btn-exp" onclick="pushToGithub()"><i class="fab fa-github"></i> Push CSV</button>
</div>
</div>
</div>
</div>
</div>
<div class="tcard">
<div class="twrap">
<table>
<thead>
<tr>
<th id="th-video">Video</th>
<th id="th-scene">Scene</th>
<th id="th-time">Time</th>
<th id="th-frame">Frame</th>
<th id="th-track">Track</th>
<th id="th-class">Class</th>
<th id="th-confidence">Confidence</th>
<th id="th-direction">Direction</th>
<th id="th-bbox">Bounding Box</th>
<th id="th-position">Center</th>
</tr>
</thead>
<tbody id="tbody">
<tr><td colspan="9" class="empty">Loading real logs...</td></tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
<script>
let allRows = [];
let filteredRows = [];
function toggleTheme(){
const d=document.documentElement;
const dark=d.getAttribute('data-theme')==='dark';
d.setAttribute('data-theme',dark?'light':'dark');
['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className=dark?'fas fa-moon':'fas fa-sun';});
try{localStorage.setItem('tiq_theme',dark?'light':'dark');}catch(e){}
}
const TL = {
en:{title:'Detection Logs',s1:'Detections',s2:'Scenes',s3:'Classes',s4:'Latest video',search:'Search video or scene',scene:'Scene',type:'Class',direction:'Direction',clear:'Clear',export:'Save Logs',video:'Video',time:'Time',frame:'Frame',track:'Track',class:'Class',confidence:'Confidence',bbox:'Bounding Box',position:'Center'},
fr:{title:'Logs de détection',s1:'Détections',s2:'Scènes',s3:'Classes',s4:'Dernière vidéo',search:'Recherche vidéo ou scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Effacer',export:'Sauvegarder',video:'Vidéo',time:'Temps',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Boîte englobante',position:'Centre'},
wo:{title:'Logs yi',s1:'Réy',s2:'Scène yi',s3:'Classe yi',s4:'Video mujj',search:'Seet video walla scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Baň',export:'Sauvegarder',video:'Video',time:'Waxtu',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Bounding Box',position:'Centre'}
};
function setLang(l, btn){
document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
const t = TL[l];
const ids = {title:'t-title',s1:'t-s1',s2:'t-s2',s3:'t-s3',s4:'t-s4',search:'t-search',scene:'t-scene',type:'t-type',direction:'t-direction',clear:'t-clear',export:'t-export',video:'th-video',time:'th-time',frame:'th-frame',track:'th-track',class:'th-class',confidence:'th-confidence',bbox:'th-bbox',position:'th-position'};
Object.entries(ids).forEach(([k,id])=>{const el=document.getElementById(id);if(el)el.textContent=t[k];});
document.getElementById('th-scene').textContent = t.scene;
document.getElementById('th-direction').textContent = t.direction;
try{localStorage.setItem('tiq_lang',l);}catch(e){}
}
async function loadLogs(){
try{
// /api/logs/rows peut ne pas exister sur certaines versions — fallback sur /api/logs
let allRowsData = [];
const rowsRes = await fetch('/api/logs/rows');
if(rowsRes.ok){
allRowsData = await rowsRes.json();
}
// Si pas de rows détaillées, construire depuis /api/logs (résumé par scène)
const scenesRes = await fetch('/api/logs');
const scenes = await scenesRes.json();
if(!allRowsData.length && scenes.length){
// Fallback : une ligne par classe par scène
scenes.forEach(s=>{
Object.entries(s.unique_counts||{}).forEach(([cls,cnt])=>{
allRowsData.push({
scene_id: s.scene_id, scene_name: s.scene_name||s.scene_id,
video_name: s.video_name||'', class_name: cls,
confidence: 0, frame_id: 0, timestamp_s: 0,
track_id: 0, x1:0, y1:0, x2:0, y2:0, cx:0, cy:0,
frame_width:0, frame_height:0,
direction:'', speed_px_s:0, crossed_line:false
});
});
});
}
allRows = allRowsData;
fillFilters(scenes);
fillSummary(scenes, allRows);
applyFilters();
}catch(err){
const tb=document.getElementById('tbody');
tb.innerHTML = '<tr><td colspan="10" class="empty">Unable to load logs.</td></tr>';
console.warn('[TrackIQ] logs:', err);
}
}
function fillFilters(scenes){
const sceneF = document.getElementById('sceneF');
const classF = document.getElementById('classF');
sceneF.innerHTML = '<option value="">All</option>' + scenes.map(s=>`<option value="${s.scene_id}">${s.video_name || s.scene_id}</option>`).join('');
const classes = [...new Set(allRows.map(r=>r.class_name).filter(Boolean))].sort();
classF.innerHTML = '<option value="">All</option>' + classes.map(c=>`<option value="${c}">${c}</option>`).join('');
}
function fillSummary(scenes, rows){
document.getElementById('sumDetections').textContent = rows.length.toLocaleString();
document.getElementById('sumScenes').textContent = scenes.length.toLocaleString();
document.getElementById('sumClasses').textContent = new Set(rows.map(r=>r.class_name).filter(Boolean)).size.toLocaleString();
document.getElementById('sumLatest').textContent = scenes[scenes.length-1]?.video_name || '-';
}
function applyFilters(){
const search = document.getElementById('searchIn').value.toLowerCase();
const scene = document.getElementById('sceneF').value;
const cls = document.getElementById('classF').value;
const dir = document.getElementById('dirF').value;
filteredRows = allRows.filter(r =>
(!search || (r.video_name || '').toLowerCase().includes(search) || (r.scene_id || '').toLowerCase().includes(search)) &&
(!scene || r.scene_id === scene) &&
(!cls || r.class_name === cls) &&
(!dir || r.direction === dir)
);
renderRows();
}
async function clearAllLogs(){
if(!confirm('Delete all logs? This cannot be undone.')) return;
try{
// Essaie d'abord /api/logs/clear, sinon DELETE par scène
const r = await fetch('/api/logs/clear',{method:'DELETE'});
if(!r.ok) throw new Error('clear failed');
location.reload();
}catch(e){
try{
const scenes = await fetch('/api/logs').then(r=>r.json());
await Promise.all(scenes.map(s=>fetch(`/api/logs/${s.scene_id}`,{method:'DELETE'})));
location.reload();
}catch(e2){ alert('Error: '+e2.message); }
}
}
function clearFilters(){
['searchIn','sceneF','classF','dirF'].forEach(id=>{document.getElementById(id).value='';});
applyFilters();
}
function renderRows(){
const tb = document.getElementById('tbody');
if(!filteredRows.length){
tb.innerHTML = '<tr><td colspan="10" class="empty">No real detections found for this filter.</td></tr>';
return;
}
const colors = {Vehicle:'#3b82f6',Motorcycle:'#10b981',Truck:'#f59e0b',Bus:'#8b5cf6',Human:'#ec4899',Bicycle:'#06b6d4'};
tb.innerHTML = '';
filteredRows.forEach(r=>{
const tr = document.createElement('tr');
tr.innerHTML = `
<td title="${r.video_name || ''}">${(r.video_name || '-').slice(0,28)}</td>
<td class="mono">${r.scene_id}</td>
<td class="mono">${Number(r.timestamp_s || 0).toFixed(2)}s</td>
<td class="mono">${r.frame_id}</td>
<td class="mono">#${r.track_id}</td>
<td><span class="pill"><span class="dot" style="background:${colors[r.class_name] || '#888'}"></span>${r.class_name || '-'}</span></td>
<td class="mono">${Math.round((r.confidence || 0) * 100)}%</td>
<td>${r.direction || ''}</td>
<td class="mono" style="font-size:11px">(${r.x1},${r.y1})→(${r.x2},${r.y2})</td>
<td class="mono">(${r.cx}, ${r.cy})</td>
`;
tb.appendChild(tr);
});
}
/* ── Helpers CSV ─────────────────────────────────────────────── */
function getGroupName(){ return (document.getElementById('groupNameIn')?.value||'trackiq').trim().replace(/\s+/g,'_')||'trackiq'; }
function getCsvFilename(){
const g=getGroupName();
const scenes=[...new Set(filteredRows.map(r=>r.scene_id))];
return `${g}_${scenes.length===1?scenes[0]:'all'}.csv`;
}
function buildCsvContent(){
// Same schema as SCHEMA_EXAMPLE.csv.
const hdr = 'frame,timestamp_sec,scene_name,group_id,video_name,track_id,class_name,confidence,bbox_x1,bbox_y1,bbox_x2,bbox_y2,cx,cy,frame_width,frame_height,crossed_line,direction,speed_px_s\n';
const g = getGroupName();
const rows = filteredRows.map(r => [
r.frame??r.frame_id??'', r.timestamp_sec??r.timestamp_s??'',
r.scene_name||r.scene_id||'',
r.group_id||g,
`"${(r.video_name||'').replace(/"/g,'""')}"`,
r.track_id??'', r.class_name||'', r.confidence??'',
r.bbox_x1??r.x1??'', r.bbox_y1??r.y1??'', r.bbox_x2??r.x2??'', r.bbox_y2??r.y2??'',
r.cx??'', r.cy??'',
r.frame_width??'', r.frame_height??'',
r.crossed_line??false,
r.direction||'',
r.speed_px_s??0
].join(',')).join('\n');
return hdr + rows;
}
function updateCsvPreview(){
const el = document.getElementById('csvPreview');
if(el) el.textContent = getCsvFilename();
}
/* ── Save menu ───────────────────────────────────────────────── */
function toggleSaveMenu(e){
e.stopPropagation();
const m = document.getElementById('saveMenu');
m.style.display = m.style.display==='none'?'block':'none';
updateCsvPreview();
}
document.addEventListener('click', (e)=>{
const m=document.getElementById('saveMenu');
if(m && !e.target.closest('#saveMenu') && !e.target.closest('.btn-exp'))
m.style.display='none';
});
/* ── 1. Download local ───────────────────────────────────────── */
function saveLocal(){
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([buildCsvContent()], {type:'text/csv'}));
a.download = getCsvFilename();
a.click();
document.getElementById('saveMenu').style.display='none';
}
/* ── 2. GitHub push ──────────────────────────────────────────── */
function showGithubPanel(){
document.getElementById('saveMenu').style.display='none';
const p = document.getElementById('githubPanel');
p.style.display='flex';
updateCsvPreview();
}
function closeGithubPanel(){ document.getElementById('githubPanel').style.display='none'; }
async function pushToGithub(){
const token = document.getElementById('ghToken').value.trim();
const repo = document.getElementById('ghRepo').value.trim();
const branch = document.getElementById('ghBranch').value.trim() || 'main';
const path = (document.getElementById('ghPath').value.trim().replace(/\/+$/,'')||'logs') + '/' + getCsvFilename();
const status = document.getElementById('ghStatus');
if(!token||!repo){ status.textContent='⚠ Token and repo are required.'; return; }
status.textContent='Pushing…';
const content = btoa(unescape(encodeURIComponent(buildCsvContent())));
// Check if file exists (to get SHA for update)
let sha = null;
try{
const check = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{headers:{Authorization:`token ${token}`}});
if(check.ok){ const d=await check.json(); sha=d.sha; }
}catch(e){}
try{
const body = {message:`TrackIQ logs export — ${getCsvFilename()}`, content, branch};
if(sha) body.sha = sha;
const res = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{
method:'PUT', headers:{Authorization:`token ${token}`,'Content-Type':'application/json'},
body: JSON.stringify(body)
});
if(res.ok){ status.innerHTML='<span style="color:#10b981">✓ Pushed successfully!</span>'; }
else{ const e=await res.json(); status.innerHTML=`<span style="color:#ef4444">Error: ${e.message}</span>`; }
}catch(err){ status.innerHTML=`<span style="color:#ef4444">Network error: ${err.message}</span>`; }
}
/* ── 3. Google Drive ─────────────────────────────────────────── */
async function saveDrive(){
document.getElementById('saveMenu').style.display='none';
// Uses Google Drive File Picker API via OAuth implicit flow
const CLIENT_ID = ''; // User must fill their own GCP OAuth client ID
if(!CLIENT_ID){
const ok = confirm('To save to Google Drive you need a Google OAuth Client ID.\n\nWould you like instructions on how to set it up?\n\n(For now the file will be downloaded locally as a fallback)');
if(ok){ window.open('https://developers.google.com/drive/api/guides/about-auth','_blank'); }
saveLocal();
return;
}
// Full Drive upload with OAuth token
const scope = 'https://www.googleapis.com/auth/drive.file';
const redirect = location.origin + location.pathname;
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(redirect)}&response_type=token&scope=${encodeURIComponent(scope)}`;
const popup = window.open(authUrl,'_blank','width=500,height=600');
// Listen for token in URL hash after redirect
const interval = setInterval(()=>{
try{
if(popup.closed){ clearInterval(interval); return; }
const hash = popup.location.hash;
if(hash && hash.includes('access_token')){
clearInterval(interval); popup.close();
const token = new URLSearchParams(hash.replace('#','')).get('access_token');
uploadToDrive(token);
}
}catch(e){}
},500);
}
async function uploadToDrive(token){
const csv = buildCsvContent();
const fname = getCsvFilename();
const meta = JSON.stringify({name:fname,mimeType:'text/csv'});
const form = new FormData();
form.append('metadata', new Blob([meta],{type:'application/json'}));
form.append('file', new Blob([csv], {type:'text/csv'}));
try{
const res = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',{
method:'POST', headers:{Authorization:`Bearer ${token}`}, body:form
});
if(res.ok){ alert(`"${fname}" saved to Google Drive!`); }
else{ const e=await res.json(); alert('Drive error: '+e.error?.message); }
}catch(err){ alert('Drive upload failed: '+err.message); }
}
function exportFiltered(){ saveLocal(); } // legacy alias
(function(){
try{
const th=localStorage.getItem('tiq_theme');
if(th){
document.documentElement.setAttribute('data-theme',th);
if(th==='dark'){['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className='fas fa-sun';});}
}
const lg=localStorage.getItem('tiq_lang');
if(lg&&lg!=='en'){
const btn=document.querySelector(`.lang-btn[onclick*="'${lg}'"]`);
if(btn)setLang(lg,btn);
}
}catch(e){}
loadLogs();
// Update CSV filename preview when group name changes
document.addEventListener('input', e=>{ if(e.target.id==='groupNameIn') updateCsvPreview(); });
})();
</script>
</body>
</html>