BizGaze Connect: chat, meetings, recordings, mobile, directory + UI fixes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:15:29 +05:30
parent d045847a59
commit 27355cec76
21 changed files with 3952 additions and 208 deletions
+100 -5
View File
@@ -28,6 +28,11 @@
th,td{text-align:left;padding:.55rem .5rem;border-bottom:1px solid var(--line);}
.pill{font-size:.74rem;font-weight:600;padding:.15rem .55rem;border-radius:99px;}
.pill.on{background:#ecfdf3;color:#15803d;}
.pill.off{background:#fee2e2;color:var(--red);}
.reveal{margin-top:1rem;background:#f1f7ec;border:1px solid #cfe8bf;border-radius:10px;padding:.8rem 1rem;}
.reveal code{flex:1;word-break:break-all;background:#fff;border:1px solid var(--line);border-radius:8px;padding:.5rem .6rem;font-size:.85rem;}
.chk{display:flex;align-items:center;gap:.4rem;font-size:.85rem;}
.chk input{width:16px;height:16px;margin:0;accent-color:var(--blue);}
.hidden{display:none;}
.tabs{display:flex;gap:.5rem;margin-bottom:1.2rem;}
.tabs button{background:#eef1f6;color:var(--muted);font-weight:600;}
@@ -64,7 +69,9 @@
.profile .pmenu a{display:block;padding:.6rem .9rem;color:#1f2430;text-decoration:none;font-size:.9rem;cursor:pointer}
.profile .pmenu a:hover{background:#f1f5f9}
.profile .pmenu a.danger{color:#b91c1c;border-top:1px solid #eef1f6}
.ic{display:inline-block;vertical-align:middle}
</style>
<script src="/icons.js"></script>
</head>
<body>
<header>
@@ -188,21 +195,109 @@ async function dashboard(me) {
<div class="f"><span class="lbl">From</span><input id="fFrom" type="date"></div>
<div class="f"><span class="lbl">To</span><input id="fTo" type="date"></div>
<button id="fApply">Apply</button>
<button id="fExcel" class="mini" style="padding:.6rem .9rem"> Excel</button>
<button id="fPdf" class="mini" style="padding:.6rem .9rem"> PDF</button>
<button id="fExcel" class="mini" style="padding:.6rem .9rem">${ic('download',15)} Excel</button>
<button id="fPdf" class="mini" style="padding:.6rem .9rem">${ic('download',15)} PDF</button>
</div>
${IS_ADMIN ? '<input id="repSearch" class="srch" placeholder="Search by agent or ticket">' : ''}
<table id="report"><thead><tr><th>Date</th><th>Start time</th>${IS_ADMIN ? '<th>Agent</th>' : ''}<th>Ticket</th><th>Time spent</th><th>Recording / Transcript</th></tr></thead><tbody></tbody></table>
<div id="repPager" class="pager"></div>
<p id="repSummary" class="muted" style="margin-top:.6rem"></p>
</div>`);
</div>
${IS_ADMIN ? `
<div class="card" id="keysCard">
<h2>API keys <span class="muted" style="font-weight:400;font-size:.8rem;text-transform:none;letter-spacing:0">— let other systems read your data programmatically</span></h2>
<table id="keys"><thead><tr><th>Name</th><th>Scopes</th><th>Created</th><th>Last used</th><th>Status</th><th></th></tr></thead><tbody></tbody></table>
<div class="row" style="margin-top:1rem;flex-wrap:wrap;align-items:flex-end;gap:1rem">
<div><span class="lbl">Name</span><input id="kName" placeholder="e.g. Partner X" style="max-width:200px"></div>
<label class="chk"><input type="checkbox" id="kReport" checked> report:read</label>
<label class="chk"><input type="checkbox" id="kAudit"> audit:read</label>
<button id="kAdd">Generate key</button>
</div>
<div id="kOut"></div>
</div>
<div class="card" id="hooksCard">
<h2>Webhooks <span class="muted" style="font-weight:400;font-size:.8rem;text-transform:none;letter-spacing:0">— signed event callbacks to your systems</span></h2>
<table id="hooks"><thead><tr><th>Endpoint</th><th>Events</th><th>Status</th><th>Last delivery</th><th></th></tr></thead><tbody></tbody></table>
<div class="row" style="margin-top:1rem;flex-wrap:wrap;align-items:flex-end;gap:1rem">
<div style="flex:1;min-width:240px"><span class="lbl">Endpoint URL</span><input id="hUrl" placeholder="https://your-system.example.com/webhook"></div>
<label class="chk"><input type="checkbox" id="hStarted" checked> session.started</label>
<label class="chk"><input type="checkbox" id="hEnded" checked> session.ended</label>
<button id="hAdd">Add webhook</button>
</div>
<div id="hOut"></div>
</div>` : ''}`);
document.getElementById('fApply').onclick = loadReport;
document.getElementById('fExcel').onclick = exportExcel;
document.getElementById('fPdf').onclick = exportPdf;
if (IS_ADMIN) await populateAgentFilter();
await loadReport();
if (IS_ADMIN) {
document.getElementById('kAdd').onclick = createKey;
document.getElementById('hAdd').onclick = createHook;
await loadKeys();
await loadHooks();
}
}
// ---------- Integrations: API keys + webhooks (admin) ----------
function fmtTs(ms){ return ms ? new Date(ms).toLocaleString() : '—'; }
function revealBox(label, value, note){
return '<div class="reveal"><div class="lbl" style="margin:0 0 .3rem">'+esc(label)+' — copy now</div>'
+ '<div style="display:flex;gap:.5rem;align-items:center"><code id="revealVal">'+esc(value)+'</code>'
+ '<button class="mini" id="copyReveal">Copy</button></div>'
+ '<div class="muted" style="margin-top:.4rem;font-size:.78rem">'+esc(note)+'</div></div>';
}
function wireCopy(){ const b=document.getElementById('copyReveal'); if(!b)return; b.onclick=async()=>{ try{ await navigator.clipboard.writeText(document.getElementById('revealVal').textContent); }catch(_){} b.textContent='Copied'; setTimeout(()=>{b.textContent='Copy';},1500); }; }
async function loadKeys(){
let rows=[]; try{ rows = await api('/api/keys', null, 'GET'); }catch(e){ return; }
document.querySelector('#keys tbody').innerHTML = rows.length ? rows.map(k=>`
<tr style="${k.revoked?'opacity:.5':''}">
<td>${esc(k.name||'—')}</td>
<td class="muted">${esc(k.scopes||'')}</td>
<td>${fmtTs(k.created_at)}</td>
<td>${fmtTs(k.last_used_at)}</td>
<td>${k.revoked?'<span class="pill off">revoked</span>':'<span class="pill on">active</span>'}</td>
<td>${k.revoked?'':`<button class="mini danger" onclick="revokeKey('${k.id}')">Revoke</button>`}</td>
</tr>`).join('') : '<tr><td colspan=6 class="muted">No API keys yet.</td></tr>';
}
async function createKey(){
const scopes=[]; if(document.getElementById('kReport').checked)scopes.push('report:read'); if(document.getElementById('kAudit').checked)scopes.push('audit:read');
if(!scopes.length){ document.getElementById('kOut').innerHTML='<p class="muted">Select at least one scope.</p>'; return; }
try{
const r = await api('/api/keys', { name: document.getElementById('kName').value, scopes }, 'POST');
document.getElementById('kName').value='';
document.getElementById('kOut').innerHTML = revealBox('API key', r.key, "Send this to the integrator. It won't be shown again — revoke and re-issue if lost.");
wireCopy(); loadKeys();
}catch(e){ document.getElementById('kOut').innerHTML='<p class="muted">'+esc(e.message)+'</p>'; }
}
window.revokeKey = async (id)=>{ if(!confirm('Revoke this API key? Integrations using it will stop working.'))return; try{ await api('/api/keys/revoke',{id},'POST'); loadKeys(); }catch(e){} };
async function loadHooks(){
let rows=[]; try{ rows = await api('/api/webhooks', null, 'GET'); }catch(e){ return; }
document.querySelector('#hooks tbody').innerHTML = rows.length ? rows.map(h=>`
<tr style="${h.active?'':'opacity:.5'}">
<td style="max-width:280px;overflow:hidden;text-overflow:ellipsis" class="muted">${esc(h.url)}</td>
<td class="muted">${esc(h.events||'')}</td>
<td>${h.last_status==null?'<span class="muted">—</span>':(h.last_status?'<span class="pill on">ok</span>':'<span class="pill off">failing</span>')}</td>
<td>${fmtTs(h.last_at)}${h.last_error?' <span class="muted" title="'+esc(h.last_error)+'">'+ic('alertTriangle',13)+'</span>':''}</td>
<td><button class="mini danger" onclick="deleteHook('${h.id}')">Delete</button></td>
</tr>`).join('') : '<tr><td colspan=5 class="muted">No webhooks yet.</td></tr>';
}
async function createHook(){
const events=[]; if(document.getElementById('hStarted').checked)events.push('session.started'); if(document.getElementById('hEnded').checked)events.push('session.ended');
const url=document.getElementById('hUrl').value.trim();
if(!/^https?:\/\//i.test(url)){ document.getElementById('hOut').innerHTML='<p class="muted">Enter a valid http(s) URL.</p>'; return; }
if(!events.length){ document.getElementById('hOut').innerHTML='<p class="muted">Select at least one event.</p>'; return; }
try{
const r = await api('/api/webhooks', { url, events }, 'POST');
document.getElementById('hUrl').value='';
document.getElementById('hOut').innerHTML = revealBox('Signing secret', r.secret, 'Verify the X-BizGaze-Signature header (HMAC-SHA256 of the body) with this. Shown once.');
wireCopy(); loadHooks();
}catch(e){ document.getElementById('hOut').innerHTML='<p class="muted">'+esc(e.message)+'</p>'; }
}
window.deleteHook = async (id)=>{ if(!confirm('Delete this webhook?'))return; try{ await api('/api/webhooks/delete',{id},'POST'); loadHooks(); }catch(e){} };
const PER_PAGE = 5;
function pagerHTML(page, pages, total, fn){
if (total <= PER_PAGE) return total ? `<span>${total} total</span>` : '';
@@ -241,8 +336,8 @@ function reportRowHTML(r){
<td>${esc(r.ticket || 'Direct session')}</td>
<td>${r.ended_at ? fmtDuration(dur) : '<span class="pill on">in progress</span>'}</td>
<td>${[
r.recording ? `<a class="mini" style="text-decoration:none;display:inline-block;padding:.32rem .6rem;margin:1px" href="/recordings/${esc(r.recording)}" download> Video</a>` : '',
r.transcript ? `<a class="mini" style="text-decoration:none;display:inline-block;padding:.32rem .6rem;margin:1px" href="/transcripts/${esc(r.transcript)}" download> Text</a>` : ''
r.recording ? `<a class="mini" style="text-decoration:none;display:inline-block;padding:.32rem .6rem;margin:1px" href="/recordings/${esc(r.recording)}" download>${ic('download',14)} Video</a>` : '',
r.transcript ? `<a class="mini" style="text-decoration:none;display:inline-block;padding:.32rem .6rem;margin:1px" href="/transcripts/${esc(r.transcript)}" download>${ic('download',14)} Text</a>` : ''
].join('') || '<span class="muted">—</span>'}</td>
</tr>`;
}