diff --git a/server/public/dashboard.html b/server/public/dashboard.html index e5f8e67..a759b4a 100644 --- a/server/public/dashboard.html +++ b/server/public/dashboard.html @@ -21,7 +21,7 @@ ::-webkit-scrollbar-thumb:hover{background:#aab6c8;} *{scrollbar-width:thin;scrollbar-color:#c7d0dd transparent;} body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;} - header{background:var(--blue);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center;} + header{background:var(--blue);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100;} /* #5: keep the top bar + profile visible while scrolling */ .brandrow{display:flex;align-items:center;gap:.6rem;} .logo{width:30px;height:30px;border-radius:8px;background:var(--brand);display:grid;place-items:center;font-weight:800;color:var(--blue);} .brand{font-weight:700;color:#fff;font-size:1.05rem;} .brand span.y{color:var(--brand);font-weight:700;} .brand span.tag{color:#8ea3cf;font-weight:500;font-size:.85rem;} @@ -77,7 +77,8 @@ /* #12: report table scrolls horizontally on small screens instead of overflowing. */ .table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%;} .table-scroll table{min-width:560px;} - .profile .pbtn .pav{width:28px;height:28px;border-radius:50%;background:var(--brand);color:var(--blue);display:grid;place-items:center;font-weight:800;font-size:.78rem} + .profile .pbtn .pav{position:relative;width:28px;height:28px;border-radius:50%;background:var(--brand);color:var(--blue);display:grid;place-items:center;font-weight:800;font-size:.78rem;overflow:hidden} + .profile .pbtn .pav img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover} .profile .pmenu{position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #e6e9ef;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.18);min-width:210px;overflow:hidden;z-index:5000;display:none} .profile .pmenu.open{display:block} .profile .pmenu .phead{padding:.7rem .9rem;border-bottom:1px solid #eef1f6} @@ -107,7 +108,7 @@ function initials(name){const p=String(name||'?').trim().split(/\s+/);return ((p function profileHTML(u){ const display=u.name||u.email; return '
' + + ''+pEsc(initials(display))+((u.avatarUrl||u.avatar_url)?'':'')+'' + '
' + '
'+pEsc(display)+'
'+pEsc(u.email)+(u.role?' · '+pEsc(u.role):'')+'
' + 'Home' @@ -244,8 +245,13 @@ async function dashboard(me) {

App installs — who installed the desktop / mobile app

+
UserPlatformVersionOSFirst seenLast seen
+
` : ''}`); document.getElementById('fApply').onclick = loadReport; document.getElementById('fExcel').onclick = exportExcel; @@ -260,13 +266,31 @@ async function dashboard(me) { await loadInstalls(); } } +let _installs=[], _installQ='', _installPage=0; const INSTALL_PAGE=10; async function loadInstalls(){ - let rows=[]; try{ rows=await fetch('/api/v1/admin/installs').then(r=>r.json()); }catch(_){} + try{ _installs=await fetch('/api/v1/admin/installs').then(r=>r.json()); }catch(_){ _installs=[]; } + if(!Array.isArray(_installs)) _installs=[]; + const s=document.getElementById('installSearch'); + if(s && !s._w){ s._w=1; s.oninput=()=>{ _installQ=s.value.trim().toLowerCase(); _installPage=0; renderInstalls(); }; } + _installPage=0; renderInstalls(); +} +function renderInstalls(){ const tb=document.querySelector('#installs tbody'); if(!tb) return; - const empty=document.getElementById('installsEmpty'); - if(!Array.isArray(rows)||!rows.length){ tb.innerHTML=''; if(empty) empty.style.display='block'; return; } + const empty=document.getElementById('installsEmpty'), pager=document.getElementById('installsPager'); + const q=_installQ; + const filtered=_installs.filter(r=>!q||[r.user_email,r.platform,r.app_version,r.os].some(v=>String(v||'').toLowerCase().includes(q))); + if(!filtered.length){ tb.innerHTML=''; if(empty){ empty.style.display='block'; empty.textContent=q?'No installs match your search.':'No installs recorded yet.'; } if(pager) pager.style.display='none'; return; } if(empty) empty.style.display='none'; - tb.innerHTML=rows.map(r=>`${esc(r.user_email||'—')}${esc(r.platform||'—')}${esc(r.app_version||'—')}${esc(r.os||'—')}${fmtTs(r.first_seen)}${fmtTs(r.last_seen)}`).join(''); + const pages=Math.ceil(filtered.length/INSTALL_PAGE); if(_installPage>=pages) _installPage=pages-1; + const page=filtered.slice(_installPage*INSTALL_PAGE, _installPage*INSTALL_PAGE+INSTALL_PAGE); + tb.innerHTML=page.map(r=>`${esc(r.user_email||'—')}${esc(r.platform||'—')}${esc(r.app_version||'—')}${esc(r.os||'—')}${fmtTs(r.first_seen)}${fmtTs(r.last_seen)}`).join(''); + if(pager){ pager.style.display=pages>1?'flex':'none'; + const cnt=document.getElementById('installsCount'); if(cnt) cnt.textContent=filtered.length+' install'+(filtered.length===1?'':'s'); + const pg=document.getElementById('installsPage'); if(pg) pg.textContent='Page '+(_installPage+1)+' / '+pages; + const prev=document.getElementById('installsPrev'), next=document.getElementById('installsNext'); + if(prev){ prev.disabled=_installPage<=0; prev.onclick=()=>{ if(_installPage>0){ _installPage--; renderInstalls(); } }; } + if(next){ next.disabled=_installPage>=pages-1; next.onclick=()=>{ if(_installPage