feat(dashboard): "App installs" view + compact top-right Download button

- dashboard.html: admin "App installs" card — table of user · platform · version · OS ·
  first/last seen, from GET /api/v1/admin/installs (loadInstalls).
- index.html: replaced the long inline link with a compact white "Download app" button in
  the blue top header (top-right); hidden when already inside the desktop app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:16:38 +05:30
parent 899770ed02
commit 9ff2ef5d48
2 changed files with 21 additions and 2 deletions
+14
View File
@@ -235,6 +235,11 @@ async function dashboard(me) {
<button id="hAdd">Add webhook</button> <button id="hAdd">Add webhook</button>
</div> </div>
<div id="hOut"></div> <div id="hOut"></div>
</div>
<div class="card" id="installsCard">
<h2>App installs <span class="muted" style="font-weight:400;font-size:.8rem;text-transform:none;letter-spacing:0">— who installed the desktop / mobile app</span></h2>
<div class="table-scroll"><table id="installs"><thead><tr><th>User</th><th>Platform</th><th>Version</th><th>OS</th><th>First seen</th><th>Last seen</th></tr></thead><tbody></tbody></table></div>
<p id="installsEmpty" class="muted" style="margin-top:.6rem;display:none">No installs recorded yet.</p>
</div>` : ''}`); </div>` : ''}`);
document.getElementById('fApply').onclick = loadReport; document.getElementById('fApply').onclick = loadReport;
document.getElementById('fExcel').onclick = exportExcel; document.getElementById('fExcel').onclick = exportExcel;
@@ -246,8 +251,17 @@ async function dashboard(me) {
document.getElementById('hAdd').onclick = createHook; document.getElementById('hAdd').onclick = createHook;
await loadKeys(); await loadKeys();
await loadHooks(); await loadHooks();
await loadInstalls();
} }
} }
async function loadInstalls(){
let rows=[]; try{ rows=await fetch('/api/v1/admin/installs').then(r=>r.json()); }catch(_){}
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; }
if(empty) empty.style.display='none';
tb.innerHTML=rows.map(r=>`<tr><td>${esc(r.user_email||'—')}</td><td>${esc(r.platform||'—')}</td><td>${esc(r.app_version||'—')}</td><td>${esc(r.os||'—')}</td><td>${fmtTs(r.first_seen)}</td><td>${fmtTs(r.last_seen)}</td></tr>`).join('');
}
// ---------- Integrations: API keys + webhooks (admin) ---------- // ---------- Integrations: API keys + webhooks (admin) ----------
function fmtTs(ms){ return ms ? new Date(ms).toLocaleString() : '—'; } function fmtTs(ms){ return ms ? new Date(ms).toLocaleString() : '—'; }
+6 -1
View File
@@ -10,6 +10,9 @@
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;min-height:100vh;display:flex;flex-direction:column;} body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;min-height:100vh;display:flex;flex-direction:column;}
header{background:var(--blue);padding:.85rem 1.5rem;display:flex;justify-content:space-between;align-items:center;} header{background:var(--blue);padding:.85rem 1.5rem;display:flex;justify-content:space-between;align-items:center;}
.brandrow{display:flex;align-items:center;gap:.7rem;} .brandrow{display:flex;align-items:center;gap:.7rem;}
.hdr-right{display:flex;align-items:center;gap:.9rem;}
.dl-btn{display:inline-flex;align-items:center;gap:.4rem;background:#fff;color:var(--blue);padding:.45rem .85rem;border-radius:9px;font-size:.85rem;font-weight:700;text-decoration:none;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.12);}
.dl-btn:hover{background:var(--brand);color:var(--blue);}
.brand{font-weight:700;color:#fff;font-size:1.1rem;} .brand span{color:var(--brand);font-weight:600;} .brand{font-weight:700;color:#fff;font-size:1.1rem;} .brand span{color:var(--brand);font-weight:600;}
.signin{color:#dbe4f5;text-decoration:none;font-size:.9rem;border:1px solid #46598c;border-radius:8px;padding:.45rem 1rem;} .signin{color:#dbe4f5;text-decoration:none;font-size:.9rem;border:1px solid #46598c;border-radius:8px;padding:.45rem 1rem;}
.signin:hover{background:var(--blue-d);} .signin:hover{background:var(--blue-d);}
@@ -49,7 +52,10 @@
<img src="/logo.png" alt="" style="height:40px;width:auto;max-width:170px;border-radius:8px;object-fit:contain;background:#fff;padding:4px 10px" onerror="this.style.display='none'"> <img src="/logo.png" alt="" style="height:40px;width:auto;max-width:170px;border-radius:8px;object-fit:contain;background:#fff;padding:4px 10px" onerror="this.style.display='none'">
<div class="brand">Biz <span>Connect</span></div> <div class="brand">Biz <span>Connect</span></div>
</div> </div>
<div class="hdr-right">
<a class="dl-btn" id="dlWin" href="/download/windows" title="Download the Windows desktop app"><span data-ic="download" data-sz="15"></span> Download app</a>
<div id="authArea"></div> <div id="authArea"></div>
</div>
</header> </header>
<div class="wrap"> <div class="wrap">
<div class="inner"> <div class="inner">
@@ -67,7 +73,6 @@
<!-- Team member path BELOW: log in to the full app. Stub SSO -> /home for now. --> <!-- Team member path BELOW: log in to the full app. Stub SSO -> /home for now. -->
<div class="divider" style="margin-top:1.6rem">BizGaze team member?</div> <div class="divider" style="margin-top:1.6rem">BizGaze team member?</div>
<a class="ssobtn" id="ssoBtn" href="/home"><span class="bmark">B</span> Log in with BizGaze</a> <a class="ssobtn" id="ssoBtn" href="/home"><span class="bmark">B</span> Log in with BizGaze</a>
<a class="dl-win" id="dlWin" href="/download/windows" style="display:inline-flex;align-items:center;gap:.5rem;margin-top:1rem;color:var(--blue);font-size:.88rem;text-decoration:none;font-weight:600"><span data-ic="download" data-sz="16"></span> Download the Windows desktop app</a>
</div> </div>
</div> </div>
<footer>© BizGaze · Remote Support</footer> <footer>© BizGaze · Remote Support</footer>