feat: BizGaze Connect home, BizGaze login, modular backend, /api/v1
User-facing - New post-login home (/home): chat rail + Share/Connect (embedded) + Meeting; login lives here when logged out - Landing: "Log in with BizGaze" + no-login screen share - Console replaced by a role-scoped Dashboard (/dashboard): admins see all team sessions, others see only their own; stats + CSV/PDF export - Recordings saved as MP4 (H.264/AAC) with WebM fallback; old .webm still downloadable - Fix: duplicate "Sign in" on the login card Auth / integration - BizGaze as identity provider: /api/login validates against BIZGAZE_LOGIN_URL (env-gated) and provisions a local user - Phase 2 start: /api/v1 alias for all /api routes; Authorization: Bearer accepted across HTTP + WS; login returns a token (for native desktop/mobile clients) Backend refactor (Phase 1, behavior-preserving) - Split server.js into config/lib/session/presence/routes/static/signaling + repos (data-access) + bizgaze (service) - All SQL behind repos.js, tenant-scoped (tenantId == team_id for now) - e2e updated to current flow (21/21 pass before and after) Docs: ARCHITECTURE.md (target architecture + phased plan), CLAUDE.md repo layout, .env.example BIZGAZE_LOGIN_URL Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,20 @@
|
||||
.pwwrap input{padding-right:2.7rem;}
|
||||
.eye{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;margin:0;}
|
||||
.eye:hover{color:var(--blue);}
|
||||
#homeLink{position:fixed;top:14px;left:16px;z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15);}
|
||||
.formerr{color:#b91c1c;font-weight:600;font-size:.88rem;margin-top:.9rem;min-height:1.1em;text-align:left;}
|
||||
.formerr.show{display:flex;align-items:center;gap:.5rem;background:#fee2e2;border:1px solid #fca5a5;border-radius:9px;padding:.6rem .75rem;animation:errShake .35s;}
|
||||
.formerr.show::before{content:"⚠";font-size:1rem;}
|
||||
@keyframes errShake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-5px)}40%,80%{transform:translateX(5px)}}
|
||||
/* Embedded inside the home shell: hide own chrome (the shell provides it). */
|
||||
html.embed .topbar{display:none!important;}
|
||||
html.embed #homeLink{display:none!important;}
|
||||
html.embed #video{height:100vh!important;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script>
|
||||
<a href="/home" id="homeLink">← Home</a>
|
||||
<div class="topbar" id="topbar">
|
||||
<div class="brandrow"><img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">BizGaze <span>Support</span></div></div>
|
||||
<div class="agentchip" id="agentChip"></div>
|
||||
@@ -60,7 +71,10 @@ const IS_MOBILE=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|M
|
||||
let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers&&IS_MOBILE)ICE=c;}).catch(()=>{});}catch(_){}
|
||||
async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/console">Console / Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
// When embedded in the home shell, tell the parent when a session is live so the
|
||||
// rail can show a "return here" indicator.
|
||||
function bzcSession(active){try{if(window.parent&&window.parent!==window)window.parent.postMessage({type:'bzc-session',flow:'connect',active:!!active},location.origin);}catch(_){}}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/home">Home</a><a href="/dashboard">Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
|
||||
function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
|
||||
makeBrandClickable();
|
||||
@@ -99,13 +113,14 @@ function renderLogin(){
|
||||
<span class="lbl">Password</span><div class="pwwrap"><input id="pw" type="password" placeholder="password"><button type="button" class="eye" data-for="pw" aria-label="Show password"></button></div>
|
||||
<label style="display:flex;align-items:center;gap:.5rem;margin:.7rem 0;color:var(--ink);font-size:.9rem;cursor:pointer"><input type="checkbox" id="rememberMe" style="width:18px;height:18px;accent-color:var(--blue);margin:0"> Remember me on this device</label>
|
||||
<button class="btn" id="loginBtn" style="width:100%">Sign in</button>
|
||||
<div class="status err" id="err"></div>`;
|
||||
<div class="formerr" id="err"></div>`;
|
||||
{
|
||||
const doSignIn=async()=>{
|
||||
const errEl=document.getElementById('err'); errEl.textContent=''; errEl.classList.remove('show');
|
||||
try{
|
||||
await api('/api/login',{email:document.getElementById('email').value,password:document.getElementById('pw').value,remember:(document.getElementById('rememberMe')||{}).checked||false});
|
||||
me=await api('/api/me',null,'GET'); renderAgent();
|
||||
}catch(e){ document.getElementById('err').textContent=e.message; }
|
||||
}catch(e){ errEl.textContent=/invalid credentials/i.test(e.message)?'Incorrect email or password. Please try again.':e.message; errEl.classList.add('show'); }
|
||||
};
|
||||
document.getElementById('loginBtn').onclick=doSignIn;
|
||||
onEnter(['email','pw'], doSignIn);
|
||||
@@ -173,11 +188,13 @@ function renderWaiting(){
|
||||
}
|
||||
|
||||
function renderEnded(msg){
|
||||
bzcSession(false);
|
||||
try{ stopRecording(); }catch(_){}
|
||||
removeSessionUI();
|
||||
if(pc){ try{pc.close();}catch(e){} pc=null; }
|
||||
video.style.display='none'; bar.classList.remove('show');
|
||||
topbar.style.display='flex'; wrap.style.display='grid';
|
||||
{ const hl=document.getElementById('homeLink'); if(hl && !document.documentElement.classList.contains('embed')) hl.style.display=''; }
|
||||
card.innerHTML=`
|
||||
<h1>Session ended</h1>
|
||||
<div class="sub">${esc(msg)}</div>
|
||||
@@ -251,12 +268,17 @@ function startRecording(){
|
||||
const mixed=new MediaStream();
|
||||
mixed.addTrack(remote.getVideoTracks()[0]);
|
||||
dest.stream.getAudioTracks().forEach(t=>mixed.addTrack(t));
|
||||
let mime='video/webm;codecs=vp8,opus'; if(!(window.MediaRecorder&&MediaRecorder.isTypeSupported(mime))) mime='video/webm';
|
||||
// Prefer MP4 (H.264/AAC) — playable by most tools (Windows Media Player, QuickTime,
|
||||
// WhatsApp, etc.). Fall back to WebM only if the browser can't record MP4.
|
||||
const REC_TYPES=['video/mp4;codecs=avc1.42E01E,mp4a.40.2','video/mp4;codecs=h264,aac','video/mp4','video/webm;codecs=vp8,opus','video/webm'];
|
||||
let mime='video/webm'; for(const t of REC_TYPES){ if(window.MediaRecorder&&MediaRecorder.isTypeSupported(t)){ mime=t; break; } }
|
||||
const recExt = mime.indexOf('mp4')!==-1 ? 'mp4' : 'webm';
|
||||
const recBlobType = mime.indexOf('mp4')!==-1 ? 'video/mp4' : 'video/webm';
|
||||
recChunks=[];
|
||||
mediaRecorder=new MediaRecorder(mixed,{mimeType:mime});
|
||||
mediaRecorder.ondataavailable=(e)=>{ if(e.data&&e.data.size) recChunks.push(e.data); };
|
||||
mediaRecorder.onstop=async()=>{
|
||||
try{ const blob=new Blob(recChunks,{type:'video/webm'}); if(blob.size) await fetch('/api/recording?sessionId='+encodeURIComponent(sessionId),{method:'POST',headers:{'Content-Type':'video/webm'},body:blob}); }catch(_){}
|
||||
try{ const blob=new Blob(recChunks,{type:recBlobType}); if(blob.size) await fetch('/api/recording?sessionId='+encodeURIComponent(sessionId)+'&ext='+recExt,{method:'POST',headers:{'Content-Type':recBlobType},body:blob}); }catch(_){}
|
||||
try{recCtx&&recCtx.close();}catch(_){} recCtx=null;
|
||||
};
|
||||
mediaRecorder.start(1000);
|
||||
@@ -277,6 +299,8 @@ function stopRecording(){
|
||||
function _btn(id,svg,label,bg){const b=document.createElement('button');b.id=id;b.innerHTML='<span style="display:inline-flex">'+svg+'</span><span>'+label+'</span>';b.style.cssText='display:inline-flex;align-items:center;gap:7px;border:none;border-radius:12px;padding:.62rem 1rem;font-weight:600;font-size:.9rem;cursor:pointer;color:#fff;background:'+bg+';transition:background .15s,transform .08s';b.onmouseenter=()=>b.style.transform='translateY(-1px)';b.onmouseleave=()=>b.style.transform='none';return b;}
|
||||
function buildBar(){
|
||||
if(document.getElementById('sessionBar'))return;
|
||||
{ const hl=document.getElementById('homeLink'); if(hl) hl.style.display='none'; }
|
||||
bzcSession(true);
|
||||
const bar=document.createElement('div'); bar.id='sessionBar';
|
||||
bar.style.cssText='position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:2147483000;display:flex;gap:10px;align-items:center;background:rgba(15,23,42,.94);padding:8px 12px;border-radius:16px;box-shadow:0 10px 28px rgba(0,0,0,.35)';
|
||||
const mic=_btn('micBtn',SVG_MIC,'Mic','#2563eb');
|
||||
|
||||
Reference in New Issue
Block a user