From fcb8a67cd4f8e201e18b7bc3917af3af174a042b Mon Sep 17 00:00:00 2001 From: matcha Date: Sun, 4 Oct 2026 02:57:28 +0000 Subject: [PATCH] Finish Persims consolidation --- old/app.js | 458 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 old/app.js diff --git a/old/app.js b/old/app.js new file mode 100644 index 0000000..f726f36 --- /dev/null +++ b/old/app.js @@ -0,0 +1,458 @@ +import { + session as loadSession, + repositories as loadRepositoryOptions, + listUser, + listRepository, + createConnection, + rotateConnection, + revokeConnection, + deleteConnection +} from './store.js'; + +const $=id=>document.getElementById(id); +const userList=$('userList'),userState=$('userState'),repoList=$('repoList'),repoState=$('repoState'); +const repoSelect=$('repoSelect'),composerRepo=$('composerRepo'),scrim=$('composerScrim'); +const nextButton=$('nextButton'),backButton=$('backButton'),saveButton=$('saveButton'),composerState=$('composerState'); + +const providers={ + Gitea:{type:'PAT',account:'Username',accountPlaceholder:'Your Gitea username',value:'Personal access token',scopes:['Repositories','Issues','Wiki']}, + Cloudflare:{type:'API token',account:'Account or label',accountPlaceholder:'Optional account or label',value:'API token',scopes:['DNS','Domains','Account']}, + Fly:{type:'Token',account:'Account or label',accountPlaceholder:'Optional account or label',value:'Access token',scopes:['Apps','Machines','Deploys']}, + Other:{type:'',account:'Account or label',accountPlaceholder:'Optional account or label',value:'Password, key, or token',scopes:[]} +}; + +const ALL_REPOS='__all__'; +let repos=[]; +let flow={step:1,provider:'',vault_kind:'',repository:''}; +let authPopup=null; +let authPoll=null; + +function node(tag,text='',cls=''){ + const el=document.createElement(tag); + if(cls)el.className=cls; + el.textContent=text; + return el; +} +function fmt(value){ + if(!value)return '—'; + const d=new Date(value); + return Number.isNaN(d.getTime())?String(value):d.toLocaleString(); +} +function setState(el,message,kind=''){ + el.textContent=message; + el.className='state'+(kind?' '+kind:''); +} +function normalizeSecretName(value){ + let name=String(value||'').trim().toUpperCase().replace(/[^A-Z0-9_]+/g,'_').replace(/^_+|_+$/g,''); + if(/^\d/.test(name))name='P_'+name; + return name.slice(0,100); +} +async function reloadFor(item){ + if((item.vault_kind||'user')==='user')return loadUserVault(); + if(repoSelect.value===ALL_REPOS||repoSelect.value===item.repository)return loadRepoVault(); +} + +function renderList(target,items,emptyText){ + target.replaceChildren(); + if(!items.length){ + target.append(node('div',emptyText,'empty')); + return; + } + for(const item of items){ + const card=node('article','', 'connection'); + const top=node('div','', 'cardtop'),left=node('div',''); + left.append(node('div','🍊 '+(item.secret_name||item.name||item.provider||'Connection'),'title')); + left.append(node('div',[item.provider,item.account,item.credential_type].filter(Boolean).join(' · '),'account')); + const status=node('div',item.available?'active ✓':'revoked','badge '+(item.available?'active':'revoked')); + top.append(left,status);card.append(top); + + card.append( + node('div',item.vault_kind==='repository'?'📦 '+item.repository:'👤 My vault','where'), + node('div','••••••••••••••••','mask') + ); + + const scopes=node('div',''); + for(const scope of item.scopes||[])scopes.append(node('span',scope,'scope')); + if(!(item.scopes||[]).length)scopes.append(node('span','No capabilities recorded','meta')); + card.append(scopes,node('div','Updated '+fmt(item.updated_at)+' · Expires '+(item.expires||'—'),'meta')); + if(item.notes)card.append(node('div',item.notes,'meta')); + + const actions=node('div','', 'actions'); + const rotate=node('button',item.available?'Rotate':'Add new value'); + const revoke=node('button','Revoke'); + const remove=node('button','Delete','danger'); + rotate.type=revoke.type=remove.type='button';revoke.disabled=!item.available; + actions.append(rotate,revoke,remove);card.append(actions); + + const rotateBox=node('div','', 'rotatebox'); + const value=document.createElement('input'); + value.type='password';value.autocomplete='new-password';value.spellcheck=false;value.placeholder='New value'; + const save=node('button','Save','primary');save.type='button'; + rotateBox.append(value,save);card.append(rotateBox); + + rotate.onclick=()=>{rotateBox.classList.toggle('open');if(rotateBox.classList.contains('open'))value.focus()}; + save.onclick=async()=>{ + if(!value.value)return; + save.disabled=true; + try{await rotateConnection(item,value.value);value.value='';await reloadFor(item)} + catch(error){alert(error.message)} + finally{save.disabled=false} + }; + revoke.onclick=async()=>{ + if(!confirm('Revoke this connection? Its metadata will remain.'))return; + revoke.disabled=true; + try{await revokeConnection(item);await reloadFor(item)} + catch(error){alert(error.message)} + finally{revoke.disabled=false} + }; + remove.onclick=async()=>{ + if(!confirm('Delete this Persimmon and its stored value?'))return; + remove.disabled=true; + try{await deleteConnection(item);await reloadFor(item)} + catch(error){alert(error.message)} + finally{remove.disabled=false} + }; + target.append(card); + } +} + +function setSignedOut(){ + $('userIdentity').textContent='Signed out'; + $('signOutButton').hidden=true; + $('addButton').hidden=true; +} + +function setSignedIn(login){ + $('userIdentity').textContent=login?'Signed in as '+login:'Signed in'; + $('signOutButton').hidden=false; + $('addButton').hidden=false; +} + +async function loadIdentity(){ + try{ + const data=await loadSession(); + setSignedIn(data.login||''); + return true; + }catch(error){ + if(error.status===401){setSignedOut();return false} + $('userIdentity').textContent='Identity unavailable'; + return false; + } +} + +async function loadUserVault(){ + setState(userState,'Loading…'); + try{ + const data=await listUser(); + const items=data.connections||[]; + renderList(userList,items,'No Persimmons yet. Use + Add to store your first useful connection.'); + setState(userState,items.length?items.length+' in your vault':'','good'); + }catch(error){ + if(error.status===401){ + const wrap=node('div','', 'empty emptyCta'); + const signIn=node('button','Sign in to use your vault.','ctaOutline'); + signIn.type='button'; + signIn.onclick=beginSignIn; + wrap.append(signIn); + userList.replaceChildren(wrap); + setState(userState,''); + return; + } + const wrap=node('div','', 'empty emptyCta'); + wrap.append(node('div','Unable to load your vault.','meta')); + const retry=node('button','Retry','ctaOutline'); + retry.type='button'; + retry.onclick=refreshEverything; + wrap.append(retry); + userList.replaceChildren(wrap); + setState(userState,'Unavailable','bad'); + } +} + +async function loadRepoVault(){ + const repository=repoSelect.value; + repoList.replaceChildren(); + if(!repository){setState(repoState,'Choose a repository to see its Persimmons.');return} + + if(repository===ALL_REPOS){ + if(!repos.length){ + renderList(repoList,[],'No writable repository vaults are available.'); + setState(repoState,'0 writable repositories','good'); + return; + } + setState(repoState,'Loading all repository vaults…'); + const items=[]; + const failed=[]; + const queue=[...repos]; + const workers=Array.from({length:Math.min(6,queue.length)},async()=>{ + while(queue.length){ + const repo=queue.shift(); + try{ + const data=await listRepository(repo.full_name); + items.push(...(data.connections||[])); + }catch(error){ + failed.push({repository:repo.full_name,error}); + } + } + }); + await Promise.all(workers); + items.sort((a,b)=>String(b.updated_at||b.created_at||'').localeCompare(String(a.updated_at||a.created_at||''))); + renderList(repoList,items,'No Persimmons in your repository vaults yet.'); + if(failed.length){ + setState(repoState,items.length+' Persimmon'+(items.length===1?'':'s')+' across '+repos.length+' repos · '+failed.length+' vault'+(failed.length===1?'':'s')+' unavailable','bad'); + }else{ + setState(repoState,items.length+' Persimmon'+(items.length===1?'':'s')+' across '+repos.length+' repository vault'+(repos.length===1?'':'s'),'good'); + } + return; + } + + setState(repoState,'Loading…'); + try{ + const data=await listRepository(repository); + const items=data.connections||[]; + renderList(repoList,items,'No Persimmons in this repository yet.'); + setState(repoState,items.length?items.length+' in '+repository:'Empty vault','good'); + }catch(error){ + repoList.replaceChildren(node('div','Unable to load this repository vault.','empty')); + setState(repoState,'Unavailable','bad'); + } +} + +function fillRepoSelect(select,current='',includeAll=false){ + select.replaceChildren(); + if(includeAll)select.append(new Option('All repository vaults',ALL_REPOS)); + else select.append(new Option('Choose a repository…','')); + for(const repo of repos)select.append(new Option(repo.full_name,repo.full_name)); + if(includeAll){ + select.value=current&&repos.some(x=>x.full_name===current)?current:ALL_REPOS; + }else if(current&&repos.some(x=>x.full_name===current)){ + select.value=current; + } +} +async function loadRepositories(){ + setState(repoState,'Discovering writable repositories…'); + try{ + const data=await loadRepositoryOptions(); + repos=data.repositories||[]; + repoSelect.disabled=false; + fillRepoSelect(repoSelect,repoSelect.value,true); + fillRepoSelect(composerRepo,flow.repository,false); + await loadRepoVault(); + }catch(error){ + repos=[];fillRepoSelect(repoSelect,'',true);fillRepoSelect(composerRepo,'',false); + if(error.status===401){ + repoList.replaceChildren(); + repoSelect.disabled=true; + setState(repoState,''); + return; + } + repoSelect.disabled=false; + setState(repoState,'Repository discovery unavailable.','bad'); + } +} + +function stopAuthPoll(){ + if(authPoll){clearInterval(authPoll);authPoll=null} +} + +function openAuthWindow(){ + const width=520,height=680; + const left=Math.max(0,(window.screenX||0)+((window.outerWidth||screen.width)-width)/2); + const top=Math.max(0,(window.screenY||0)+((window.outerHeight||screen.height)-height)/2); + authPopup=window.open( + 'https://teahub.xyz/user/login?redirect_to=%2Ftea%2Fchat%2Fpersimmons%2Fauth-complete', + 'persimmons-auth', + 'popup=yes,width='+width+',height='+height+',left='+Math.round(left)+',top='+Math.round(top) + ); + if(!authPopup){ + setState(userState,'Pop-up blocked. Allow pop-ups and try again.','bad'); + return; + } + setState(userState,'Waiting for sign-in…'); + authPopup.focus(); + + stopAuthPoll(); + const started=Date.now(); + authPoll=setInterval(async()=>{ + try{ + const data=await loadSession(); + if(data&&data.login){ + stopAuthPoll(); + try{authPopup&&authPopup.close()}catch{} + authPopup=null; + setSignedIn(data.login); + setState(userState,''); + await Promise.all([loadRepositories(),loadUserVault()]); + return; + } + }catch(error){ + if(error.status!==401){ + setState(userState,'Sign-in check unavailable. Try again.','bad'); + } + } + + if(Date.now()-started>120000){ + stopAuthPoll(); + authPopup=null; + setState(userState,'Sign-in timed out. Try again.'); + } + },700); +} + +async function beginSignIn(){ + setState(userState,'Checking sign-in…'); + try{ + const data=await loadSession(); + if(data&&data.login){ + setSignedIn(data.login); + setState(userState,''); + await Promise.all([loadRepositories(),loadUserVault()]); + return; + } + }catch(error){ + if(error.status!==401){ + setState(userState,'Sign-in check unavailable. Try again.','bad'); + return; + } + } + setState(userState,''); + openAuthWindow(); +} + +const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms)); + +async function waitForSignedOut(){ + for(let attempt=0;attempt<15;attempt++){ + await sleep(200); + try{ + await loadSession(); + }catch(error){ + if(error.status===401)return true; + } + } + return false; +} + +async function signOut(){ + const button=$('signOutButton'); + button.disabled=true; + setState(userState,'Signing out…'); + try{ + await fetch('https://teahub.xyz/user/logout',{ + credentials:'include', + mode:'no-cors', + cache:'no-store' + }); + if(!await waitForSignedOut())throw new Error('Sign out did not complete. Try again.'); + setSignedOut(); + await Promise.all([loadRepositories(),loadUserVault()]); + }catch(error){ + setState(userState,'Sign out failed. Try again.','bad'); + }finally{ + button.disabled=false; + } +} + +function resetComposer(){ + flow={step:1,provider:'',vault_kind:'',repository:''}; + document.querySelectorAll('.choice').forEach(x=>x.classList.remove('selected')); + $('connectionName').value='';$('accountInput').value='';$('valueInput').value=''; + $('expiresInput').value='';$('notesInput').value='';$('typeInput').value='Password'; + $('composerRepoField').hidden=true;composerRepo.value=''; + setState(composerState,'');showStep(1); +} +function openComposer(){resetComposer();scrim.hidden=false} +function closeComposer(){$('valueInput').value='';scrim.hidden=true} +function showStep(step){ + flow.step=step; + $('step1').hidden=step!==1;$('step2').hidden=step!==2;$('step3').hidden=step!==3; + backButton.hidden=step===1;nextButton.hidden=step===3;saveButton.hidden=step!==3; + $('stepSummary').textContent={1:'What are you connecting?',2:'Where should it be available?',3:'Add only what this connection needs.'}[step]; + updateNext();if(step===3)prepareDetails(); +} +function updateNext(){ + if(flow.step===1)nextButton.disabled=!flow.provider; + if(flow.step===2)nextButton.disabled=!flow.vault_kind||(flow.vault_kind==='repository'&&!composerRepo.value); +} +function selectChoice(selector,clicked){ + document.querySelectorAll(selector).forEach(x=>x.classList.toggle('selected',x===clicked)); +} + +document.querySelectorAll('.provider').forEach(button=>{ + button.onclick=()=>{flow.provider=button.dataset.provider;selectChoice('.provider',button);updateNext()}; +}); +document.querySelectorAll('.vault').forEach(button=>{ + button.onclick=()=>{ + flow.vault_kind=button.dataset.vault;selectChoice('.vault',button); + $('composerRepoField').hidden=flow.vault_kind!=='repository'; + if(flow.vault_kind!=='repository'){flow.repository='';composerRepo.value=''} + updateNext(); + }; +}); +composerRepo.onchange=()=>{flow.repository=composerRepo.value;updateNext()}; + +function prepareDetails(){ + const cfg=providers[flow.provider]; + $('connectionName').placeholder=flow.provider==='Other'?'e.g. MY_PASSWORD':flow.provider.toUpperCase()+'_TOKEN'; + $('accountLabel').textContent=cfg.account;$('accountInput').placeholder=cfg.accountPlaceholder; + $('valueLabel').textContent=cfg.value;$('typeField').hidden=flow.provider!=='Other'; + if(flow.provider!=='Other')$('typeInput').value=cfg.type; + $('scopeChoices').replaceChildren();$('scopeField').hidden=!cfg.scopes.length; + cfg.scopes.forEach((scope,index)=>{ + const wrap=node('span','', 'check'),input=document.createElement('input'),label=document.createElement('label'); + input.type='checkbox';input.id='scope_'+index;input.value=scope; + label.htmlFor=input.id;label.textContent=scope;wrap.append(input,label);$('scopeChoices').append(wrap); + }); +} +const selectedScopes=()=>[...$('scopeChoices').querySelectorAll('input:checked')].map(x=>x.value); + +nextButton.onclick=()=>{ + if(flow.step===1&&flow.provider)return showStep(2); + if(flow.step===2&&flow.vault_kind&&(flow.vault_kind!=='repository'||composerRepo.value)){ + flow.repository=composerRepo.value;return showStep(3); + } +}; +backButton.onclick=()=>showStep(Math.max(1,flow.step-1)); + +saveButton.onclick=async()=>{ + const value=$('valueInput').value; + const name=normalizeSecretName($('connectionName').value); + if(!name){setState(composerState,'Name this Persimmon first.','bad');$('connectionName').focus();return} + if(!value){setState(composerState,'Enter the new value.','bad');return} + $('connectionName').value=name; + const cfg=providers[flow.provider]; + const metadata={ + name, + provider:flow.provider, + account:$('accountInput').value.trim(), + credential_type:flow.provider==='Other'?$('typeInput').value:cfg.type, + scopes:selectedScopes(), + vault_kind:flow.vault_kind, + expires:$('expiresInput').value.trim(), + notes:$('notesInput').value.trim() + }; + if(flow.vault_kind==='repository')metadata.repository=flow.repository; + saveButton.disabled=true;setState(composerState,'Saving…'); + try{ + await createConnection(metadata,value); + $('valueInput').value=''; + const savedVault=flow.vault_kind,savedRepo=flow.repository; + closeComposer();await loadUserVault(); + if(savedVault==='repository'){repoSelect.value=savedRepo;await loadRepoVault()} + }catch(error){setState(composerState,error.message,'bad')} + finally{saveButton.disabled=false} +}; + +async function refreshEverything(){ + await Promise.all([loadIdentity(),loadRepositories(),loadUserVault()]); +} + +$('connectionName').addEventListener('blur',()=>{$('connectionName').value=normalizeSecretName($('connectionName').value)}); +$('addButton').onclick=openComposer;$('closeComposer').onclick=closeComposer; +$('refreshAll').onclick=refreshEverything;$('signOutButton').onclick=signOut;repoSelect.onchange=loadRepoVault; +scrim.addEventListener('click',event=>{if(event.target===scrim)closeComposer()}); +document.addEventListener('keydown',event=>{ + if(event.key==='Escape'&&!scrim.hidden)closeComposer(); +}); + +refreshEverything();