From 09b57f7afa6e2ae06f76b5f824a4a24ee939e2eb Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sun, 15 Mar 2026 09:52:44 +0000 Subject: [PATCH] Offer GitHub sign-in in the frontend The load dialog shows the GitHub account state with sign-in, sign-out and repository-selection links. A clone that needs authorisation shows a Sign in with GitHub or Grant repository access button, and the repository is resumed automatically on return from GitHub. --- web/app.js | 38 +++++++++++++++++++++++++++++++++----- web/index.html | 6 ++++-- web/styles.css | 3 +++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/web/app.js b/web/app.js index 6f3ec19..912c39c 100644 --- a/web/app.js +++ b/web/app.js @@ -709,7 +709,7 @@ async function uploadLocal(name,chosen){ } $('#githubButton').addEventListener('click',startGithub);$('#githubInput').addEventListener('keydown',event=>{if(event.key==='Enter'){event.preventDefault();startGithub();}}); -async function startGithub(){const url=$('#githubInput').value.trim();$('#githubError').textContent='';if(!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/?(?:\.git)?$/.test(url)){ $('#githubError').textContent='Enter a public GitHub owner/repository URL.';return;}dialog.close();showProgress('Cloning repository','Connecting to GitHub…',0,0);try{const response=await apiFetch(apiUrl('/api/jobs'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({kind:'github',url,name:url.split('/').filter(Boolean).pop()?.replace(/\.git$/,'')})});const payload=await response.json();if(!response.ok)throw new Error(payload.error||'Could not start indexing');watchJob(payload.jobId);}catch(error){showError(error.message);}} +async function startGithub(){const url=$('#githubInput').value.trim();$('#githubError').textContent='';if(!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/?(?:\.git)?$/.test(url)){ $('#githubError').textContent='Enter a GitHub owner/repository URL.';return;}lastGithubUrl=url;dialog.close();showProgress('Cloning repository','Connecting to GitHub…',0,0);try{const response=await apiFetch(apiUrl('/api/jobs'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({kind:'github',url,name:url.split('/').filter(Boolean).pop()?.replace(/\.git$/,'')})});const payload=await response.json();if(!response.ok)throw new Error(payload.error||'Could not start indexing');watchJob(payload.jobId);}catch(error){showError(error.message);}} let activeJobStream=null; async function watchJob(jobId){ @@ -720,7 +720,7 @@ async function watchJob(jobId){ for await(const job of progressEvents(response)){ if(controller.signal.aborted)return; showProgress(job.phase==='ready'?'Landscape ready':'Indexing codebase',job.message,job.completed,job.total); - if(job.error){showError(job.error);return;} + if(job.error){showError(job.error,job.authRequired);return;} if(job.snapshotId){ const scene=await apiFetch(apiUrl(`/api/snapshots/${job.snapshotId}/scene`),{signal:controller.signal}); if(!scene.ok)throw new Error('Could not load the landscape.'); @@ -732,8 +732,36 @@ async function watchJob(jobId){ }catch(error){if(!controller.signal.aborted)showError(error.message);} finally{if(activeJobStream===controller)activeJobStream=null;controller.abort();} } -function showProgress(title,message,completed,total){$('#progressCard').hidden=false;$('#progressTitle').textContent=title;$('#progressMessage').textContent=message||'';const percent=total?Math.max(3,Math.round(completed/total*100)):12;$('#progressFill').style.width=`${percent}%`;$('#progressFill').style.background='';$('#progressCount').textContent=total?`${format(completed)} / ${format(total)} · ${percent}%`:'Working…';} -function showError(message){$('#progressCard').hidden=false;$('#progressTitle').textContent='Could not build landscape';$('#progressMessage').textContent=message;$('#progressFill').style.width='100%';$('#progressFill').style.background='var(--danger)';$('#progressCount').textContent='Check the repository or folder and try again.';} +function showProgress(title,message,completed,total){$('#progressCard').hidden=false;$('#progressAction').hidden=true;$('#progressTitle').textContent=title;$('#progressMessage').textContent=message||'';const percent=total?Math.max(3,Math.round(completed/total*100)):12;$('#progressFill').style.width=`${percent}%`;$('#progressFill').style.background='';$('#progressCount').textContent=total?`${format(completed)} / ${format(total)} · ${percent}%`:'Working…';} +function showError(message,authRequired){$('#progressCard').hidden=false;showAuthAction(authRequired);$('#progressTitle').textContent='Could not build landscape';$('#progressMessage').textContent=message;$('#progressFill').style.width='100%';$('#progressFill').style.background='var(--danger)';$('#progressCount').textContent='Check the repository or folder and try again.';} + +// Private repositories: sign in (or grant the GitHub App access), then resume the same repository on return. +let lastGithubUrl='',githubStatus={}; +function showAuthAction(authRequired){ + const action=$('#progressAction'),href=authRequired==='signin'?apiUrl('/api/github/login'):authRequired==='install'?githubStatus.installUrl:''; + action.hidden=!href;if(!href)return; + action.href=href;action.textContent=authRequired==='signin'?'Sign in with GitHub':'Grant repository access'; +} +$('#progressAction').addEventListener('click',()=>{try{sessionStorage.setItem('codenav.pendingRepo',lastGithubUrl);}catch{}}); +$('#githubAccount').addEventListener('click',event=>{if(event.target.closest('a'))try{sessionStorage.setItem('codenav.pendingRepo',$('#githubInput').value.trim());}catch{}}); +async function loadGithubStatus(){ + try{const response=await apiFetch(apiUrl('/api/github/status'));if(response.ok)githubStatus=await response.json();}catch{} + const account=$('#githubAccount');account.hidden=!githubStatus.configured;account.replaceChildren();if(account.hidden)return; + if(githubStatus.connected){ + const signOut=Object.assign(document.createElement('button'),{type:'button',textContent:'Sign out'}); + signOut.addEventListener('click',async()=>{await apiFetch(apiUrl('/api/github/logout'),{method:'POST'}).catch(()=>{});loadGithubStatus();}); + account.append(`Signed in to GitHub as ${githubStatus.login} · `,signOut); + if(githubStatus.installUrl)account.append(' · ',Object.assign(document.createElement('a'),{href:githubStatus.installUrl,textContent:'Choose repositories'})); + }else account.append('Private repository? ',Object.assign(document.createElement('a'),{href:apiUrl('/api/github/login'),textContent:'Sign in with GitHub'})); +} +async function resumeAfterGithub(){ + const params=new URLSearchParams(location.search),outcome=params.get('github')||(params.get('setup_action')?'installed':''); + if(!outcome)return; + window.history.replaceState(null,'',location.pathname); + let pending='';try{pending=sessionStorage.getItem('codenav.pendingRepo')||'';sessionStorage.removeItem('codenav.pendingRepo');}catch{} + if(outcome==='denied'||outcome==='error'){showError(outcome==='denied'?'GitHub sign-in was cancelled.':'GitHub sign-in failed. Please try again.');return;} + if(pending){$('#githubInput').value=pending;startGithub();} +} async function checkBackend(){ const indicator=$('.live-dot'); @@ -773,4 +801,4 @@ switchTab('inspector'); new ResizeObserver(()=>{dirty=true;}).observe(viewport); $('#settingsButton').title='Workspace settings';$('#settingsButton').setAttribute('aria-label','Workspace settings'); $('#settingsButton svg').innerHTML=''; -renderer.setData([]);fitScene();renderInspector();renderHistory();requestAnimationFrame(animate);checkBackend(); +renderer.setData([]);fitScene();renderInspector();renderHistory();requestAnimationFrame(animate);checkBackend();loadGithubStatus().then(resumeAfterGithub); diff --git a/web/index.html b/web/index.html index b3f8b1e..e99f55c 100644 --- a/web/index.html +++ b/web/index.html @@ -60,6 +60,7 @@
+