Files
safetynet/apps/browser/renderer.js
T
2026-10-13 10:34:48 +02:00

199 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const $=id=>document.getElementById(id);let panel=false,vaultPanel=false,lastState,vaultState,vaultBusy=false,vaultRevision=0;
let sidebarOpen=false,startPageOpen=false,overviewOpen=false,tabs=[],activeTab=null,groupFilter='',navigationRevision=0,pendingNavigation=null,lastLayoutKey='',lastTabsKey='',lastLibraryKey='',nativeTabsRevision=-1;
const preference=(key,fallback)=>{try{return JSON.parse(localStorage.getItem(`sbrowser.${key}`))??fallback;}catch{return fallback;}};
const remember=(key,value)=>{try{localStorage.setItem(`sbrowser.${key}`,JSON.stringify(value));}catch{}};
const safeURL=value=>{try{const u=new URL(value);if(!['https:','http:'].includes(u.protocol)||u.username||u.password)return null;u.search='';u.hash='';return u.href;}catch{return null;}};
const metadata=list=>Array.isArray(list)?list.slice(0,100).filter(v=>v&&safeURL(v.url)&&typeof v.title==='string').map(v=>({url:safeURL(v.url),title:v.title.slice(0,160)})):[];
let bookmarks=[],readingList=metadata(preference('reading-list',[])),groups=preference('groups',[]);groups=Array.isArray(groups)?groups.filter(v=>typeof v==='string'&&v.length<=40).slice(0,20):[];
function currentTab(){return tabs.find(t=>t.id===activeTab)||{id:null,url:'',title:'New Tab',group:'',startPage:true};}
async function updateChrome(){
document.body.classList.toggle('sidebar-open',sidebarOpen);if($('library'))$('library').hidden=!sidebarOpen;if($('start-page'))$('start-page').hidden=!startPageOpen||overviewOpen||!lastState?.ready;if($('overview'))$('overview').hidden=!overviewOpen||!lastState?.ready;
for(const id of ['bookmark','reading-add','share-url'])if($(id))$(id).disabled=!lastState?.ready||startPageOpen||overviewOpen||!currentTab().url||(id==='bookmark'&&(!sectionEditable('library')||!lastState?.bookmarks?.allowUserAdd))||(id==='reading-add'&&!sectionEditable('library'));
const height=Math.ceil($('chrome')?.getBoundingClientRect().bottom||104),layout={height,sidebarOpen,startPageOpen:startPageOpen&&!!lastState?.ready,overviewOpen:overviewOpen&&!!lastState?.ready,toolsOpen:!!($('tools-menu')&&!$('tools-menu').hidden)||!!$('bookmark-editor')?.open},key=JSON.stringify(layout);document.documentElement.style.setProperty('--chrome-height',`${height}px`);if(window.safetynet.setChromeLayout&&key!==lastLayoutKey){lastLayoutKey=key;try{await call('setChromeLayout',layout);}catch(e){lastLayoutKey='';throw e;}}
}
async function navigateTab(value){
const token=++navigationRevision,tab=currentTab();if(!tab.id)throw Error('No managed tab is available');pendingNavigation={token,tabId:tab.id};overviewOpen=false;
try{const result=await call('navigate',{url:value,tabId:tab.id});if(token===navigationRevision)show(result);}
finally{if(pendingNavigation?.token===token)pendingNavigation=null;}
}
// Native WebContents own identity, history and lifetime. Switching never reloads a URL.
async function selectTab(id){if(!tabs.some(t=>t.id===id))return;navigationRevision++;overviewOpen=false;show(await call('activateTab',id));$('address').value=currentTab().startPage?'':currentTab().url;await updateChrome();}
async function newTab(){navigationRevision++;overviewOpen=false;show(await call('createTab',{group:groupFilter}));$('address').value=currentTab().startPage?'':currentTab().url;await updateChrome();$('address').focus();}
async function closeTab(id){if(!tabs.some(t=>t.id===id))return;navigationRevision++;const previous=activeTab;show(await call('closeTab',id));if(previous!==activeTab)$('address').value=currentTab().startPage?'':currentTab().url;await updateChrome();}
function button(text,fn,cls=''){const b=document.createElement('button');b.type='button';b.className=cls;b.textContent=text;b.onclick=task(fn);return b;}
function renderTabs(){
const strip=$('tab-strip');if(!strip)return;const key=JSON.stringify({tabs,activeTab,groupFilter,ready:!!lastState?.ready});if(key===lastTabsKey)return;lastTabsKey=key;strip.replaceChildren();for(const tab of tabs.filter(t=>!groupFilter||t.group===groupFilter)){
const el=document.createElement('div');el.className=`browser-tab${tab.id===activeTab?' active':''}`;el.setAttribute('role','tab');el.setAttribute('aria-selected',String(tab.id===activeTab));el.title=tab.url||'New Tab';
const select=button(tab.title||'New Tab',()=>selectTab(tab.id),'tab-label'),close=button('×',()=>closeTab(tab.id),'tab-close');close.setAttribute('aria-label',`Close ${tab.title}`);select.disabled=!lastState?.ready;close.disabled=!lastState?.ready;el.dataset.tabId=tab.id;el.append(select,close);strip.append(el);}
if($('overview-grid')){$('overview-grid').replaceChildren();for(const tab of tabs){const card=button('',()=>selectTab(tab.id),'overview-card');const title=document.createElement('strong'),url=document.createElement('span');title.textContent=tab.title;url.textContent=safeURL(tab.url)||'Favorites and a fresh start';card.append(title,url);$('overview-grid').append(card);}}
}
function renderLinks(id,items){if(!$(id))return;$(id).replaceChildren();for(const item of items){const b=button('',()=>navigateTab(item.url),'favorite-card');const initial=document.createElement('span'),label=document.createElement('strong');initial.className='favorite-icon';initial.textContent=new URL(item.url).hostname.charAt(0).toUpperCase();label.textContent=item.title||new URL(item.url).hostname;if(item.managed){const badge=document.createElement('small');badge.textContent='Company';b.append(badge);}b.title=item.url;b.append(initial,label);$(id).append(b);}}
let bookmarkEditingId=null;
async function openBookmarkEditor(item){
requireSection('library',true);const rules=lastState?.bookmarks;
if(item?.managed||!(item?rules?.allowUserEdit:rules?.allowUserAdd))throw Error('This bookmark is managed by your administrator');
bookmarkEditingId=item?.id||null;$('bookmark-editor-title').textContent=item?'Edit bookmark':'Add bookmark';
$('bookmark-title').value=(item?.title||lastState?.pageTitle||'New bookmark').slice(0,120);$('bookmark-url').value=item?.url||safeURL(lastState?.url)||'';$('bookmark-folder').value=item?.folder||'';
$('bookmark-editor-error').textContent='';$('bookmark-editor').showModal();await updateChrome();$('bookmark-title').focus();
}
async function mutateBookmark(action,bookmark,id){
const snapshot=lastState;const result=await call('mutateBookmarks',{action,...(bookmark?{bookmark}:{}),...(id?{id}:{}),expectedRevision:snapshot.bookmarks.personal.revision,policyVersion:snapshot.policyVersion});show({...snapshot,...result});
}
function renderBookmarkLibrary(){
const target=$('library-bookmarks');target.replaceChildren();
for(const [managed,label] of [[true,'Company bookmarks (managed)'],[false,'Your bookmarks']]){
const heading=document.createElement('p');heading.className='bookmark-section-label';heading.textContent=label;target.append(heading);
let folder=null;for(const item of bookmarks.filter(item=>!!item.managed===managed).sort((a,b)=>(a.folder||'').localeCompare(b.folder||''))){
if(item.folder&&item.folder!==folder){const group=document.createElement('small');group.textContent=item.folder;target.append(group);}folder=item.folder;
const row=document.createElement('div');row.className='bookmark-row';const link=button(item.title,()=>navigateTab(item.url),'bookmark-link');link.title=item.url;row.append(link);
if(!managed&&sectionEditable('library')){if(lastState?.bookmarks?.allowUserEdit){const edit=button('Edit',()=>openBookmarkEditor(item),'bookmark-action');edit.setAttribute('aria-label',`Edit ${item.title}`);row.append(edit);}if(lastState?.bookmarks?.allowUserDelete){const remove=button('Remove',()=>mutateBookmark('delete',null,item.id),'bookmark-action');remove.setAttribute('aria-label',`Remove ${item.title}`);row.append(remove);}}
target.append(row);
}
}
const legacy=metadata(preference('bookmarks',[]));
if(legacy.length&&sectionVisible('library')){const heading=document.createElement('p');heading.className='bookmark-section-label';heading.textContent='Previous local bookmarks';target.append(heading);const note=document.createElement('small');note.textContent='These existing links remain local. Add them to your bookmarks to sync them when company policy permits.';target.append(note);for(const item of legacy){const link=button(item.title,()=>navigateTab(item.url),'bookmark-link');link.title=item.url;target.append(link);}}
}
$('bookmark-cancel').onclick=()=>$('bookmark-editor').close();
$('bookmark-editor').addEventListener('close',()=>updateChrome().catch(()=>{}));
$('bookmark-form').onsubmit=async event=>{event.preventDefault();const submit=$('bookmark-save');submit.disabled=true;try{
requireSection('library',true);await mutateBookmark(bookmarkEditingId?'update':'add',{id:bookmarkEditingId||crypto.randomUUID(),title:$('bookmark-title').value,url:$('bookmark-url').value,folder:$('bookmark-folder').value});$('bookmark-editor').close();
}catch(e){$('bookmark-editor-error').textContent=e.message;}finally{submit.disabled=false;}};
function renderLibrary(){
const key=JSON.stringify({bookmarks,bookmarkPermissions:lastState?.bookmarks,settings:lastState?.settings,readingList,groups,tabs,activeTab});if(key===lastLibraryKey)return;lastLibraryKey=key;
const favorites=sectionVisible('library')?bookmarks.slice(0,12):[];renderLinks('favorites-grid',favorites);renderLinks('library-favorites',favorites);renderBookmarkLibrary();renderLinks('library-reading',readingList);
if($('library-groups')){$('library-groups').replaceChildren();$('library-groups').append(button('All tabs',()=>{groupFilter='';renderTabs();},'group-item'));for(const group of groups)$('library-groups').append(button(group,()=>{groupFilter=group;renderTabs();},'group-item'));}
if($('favorites-empty'))$('favorites-empty').hidden=!!favorites.length;
if($('library-tabs')){$('library-tabs').replaceChildren();for(const tab of tabs)$('library-tabs').append(button(tab.title,()=>selectTab(tab.id),`library-tab${tab.id===activeTab?' active':''}`));}
renderLinks('recent-list',tabs.filter(t=>t.url).slice(-5).map(t=>({url:safeURL(t.url),title:t.title})).filter(t=>t.url));
}
const systemAppearance=window.matchMedia('(prefers-color-scheme: dark)');
const systemIsDark=()=>window.matchMedia('(prefers-color-scheme: dark)').matches;
const accentPresets={blue:{base:'#0969da',light:'#0969da',dark:'#75baff'},teal:{base:'#007d76',light:'#00736d',dark:'#6cdbca'},violet:{base:'#7042c1',light:'#7042c1',dark:'#c5a4ff'}};
function applyAppearance(){
// Appearance comes exclusively from the privileged process, including unlocked saved choices.
const appearance=lastState?.settings?.appearance||{mode:'system',accent:'blue',tabMode:'separate'},theme=appearance.mode==='system'?'auto':appearance.mode,mode=appearance.tabMode;
const effective=theme==='auto'?(systemIsDark()?'dark':'light'):theme,savedAccent=appearance.accent,accent=typeof savedAccent==='string'&&Object.hasOwn(accentPresets,savedAccent)?savedAccent:'blue';
document.documentElement.dataset.theme=theme;document.documentElement.dataset.effectiveTheme=effective;document.documentElement.dataset.accent=accent;document.documentElement.dataset.tabMode=mode==='compact'?'compact':'separate';
document.body.dataset.theme=effective;document.body.dataset.tabMode=document.documentElement.dataset.tabMode;document.documentElement.style.colorScheme=effective;
document.documentElement.style.setProperty('--accent',accentPresets[accent].base);document.documentElement.style.setProperty('--accent-text',accentPresets[accent][effective]);
if($('theme-mode'))$('theme-mode').value=theme;if($('tab-mode'))$('tab-mode').value=document.documentElement.dataset.tabMode;if($('accent-mode'))$('accent-mode').value=accent;
if($('theme-status'))$('theme-status').textContent=theme==='auto'?`System · ${effective==='dark'?'Dark':'Light'}. Changes follow your device.`:`${effective==='dark'?'Dark':'Light'} theme · browser workspace only.`;
updateChrome().catch(()=>{});
}
systemAppearance.addEventListener('change',()=>{if((!lastState?.settings||lastState.settings.appearance.mode==='system'))applyAppearance();});
function refreshSystemAppearance(){if((!lastState?.settings||lastState.settings.appearance.mode==='system')&&document.body.dataset.theme!==(systemIsDark()?'dark':'light'))applyAppearance();}
window.addEventListener('focus',refreshSystemAppearance);
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refreshSystemAppearance();});
// Some Windows compositor/theme transitions update the query without a change event.
setInterval(refreshSystemAppearance,1000);
// Hidden controls are removed from keyboard navigation as well as visual presentation.
// Native IPC repeats every check: DOM visibility is never the security boundary.
function sectionVisible(name){const settings=lastState?.settings;return !!settings?.available&&(settings.visible||['library','ai','passwords','documentScan','agents'].includes(name))&&!settings.hiddenSections.includes(name);}
function sectionEditable(name){return sectionVisible(name)&&(name!=='library'||lastState.settings.visible)&&!lastState.settings.lockedSections.includes(name);}
function requireSection(name,edit=false){if(!(edit?sectionEditable(name):sectionVisible(name)))throw Error('This action is managed by your administrator.');}
function applyManagedSettings(){
const settings=lastState?.settings,visible=!!settings?.available&&settings.visible;
$('tools-toggle').hidden=!visible;$('tools-toggle').disabled=!visible;
if(!visible){$('tools-menu').hidden=true;$('tools-toggle').setAttribute('aria-expanded','false');}
const sections={appearance:['appearance-section'],library:['bookmark','reading-add','sidebar-toggle'],ai:['ai-toggle','sidebar'],passwords:['passwords'],documentScan:['inspect'],agents:['agents']};
for(const [name,ids] of Object.entries(sections))for(const id of ids){const el=$(id);if(!el)continue;el.hidden=!sectionVisible(name);if(!sectionVisible(name)||(['library','ai','passwords','documentScan','agents'].includes(name)&&!sectionEditable(name)))el.disabled=true;}
for(const id of ['theme-mode','accent-mode','tab-mode'])$(id).disabled=!sectionEditable('appearance');
for(const id of ['bookmark','reading-add','group-add','group-name'])if($(id))$(id).disabled=!sectionEditable('library')||!lastState?.ready||(id==='bookmark'&&!lastState?.bookmarks?.allowUserAdd);
$('send').disabled=!lastState?.aiEnabled||!sectionEditable('ai');$('prompt').disabled=!sectionEditable('ai');
for(const [quick,original,name,enabled] of [['quick-ai','ai-toggle','ai',lastState?.aiEnabled],['quick-passwords','passwords','passwords',lastState?.ready&&lastState?.passwordManagerEnabled!==false],['quick-inspect','inspect','documentScan',lastState?.documentScanEnabled],['quick-agents','agents','agents',lastState?.agentsEnabled]]){const el=$(quick);el.hidden=visible||!sectionVisible(name)||!enabled;el.disabled=!sectionEditable(name)||!enabled;el.setAttribute('aria-expanded',$(original).getAttribute('aria-expanded')||'false');}
$('settings-managed').hidden=!sectionVisible('appearance')||sectionEditable('appearance');
if(!sectionVisible('library')){sidebarOpen=false;$('sidebar-toggle').setAttribute('aria-expanded','false');}
if(!sectionEditable('ai')&&panel)closeAI().catch(()=>{});
if(!sectionEditable('passwords')&&vaultPanel)closeVault().catch(()=>{});
applyAppearance();
}
async function call(method,...args){const r=await window.safetynet[method](...args);if(!r.ok)throw Error(r.error);return r.data;}
function clearVaultFields(){for(const id of ['vault-master','vault-confirm','vault-password','vault-origin','vault-label','vault-username'])$(id).value='';}
async function closeVault(){vaultRevision++;vaultPanel=false;$('vault').hidden=true;$('passwords').setAttribute('aria-expanded','false');clearVaultFields();$('vault-list').replaceChildren();$('vault-add').open=false;await call('vaultLock');await call('toggleVault',false);}
async function closeAI(){panel=false;$('ai').hidden=true;for(const id of ['sidebar','ai-toggle'])$(id).setAttribute('aria-expanded','false');await call('toggleAI',false);}
async function openAI(){requireSection('ai',true);if(vaultPanel)await closeVault();panel=true;$('ai').hidden=false;for(const id of ['sidebar','ai-toggle'])$(id).setAttribute('aria-expanded','true');await call('toggleAI',true);}
function show(s){
// A delayed invoke response must not replace a newer tab switch or navigation event.
if(Number.isSafeInteger(s.tabsRevision)&&s.tabsRevision<nativeTabsRevision)return;
if(Array.isArray(s.tabs)){nativeTabsRevision=s.tabsRevision??nativeTabsRevision;tabs=s.tabs;activeTab=s.activeTabId;startPageOpen=!!currentTab().startPage;if(groupFilter&&currentTab().group!==groupFilter)groupFilter=currentTab().group;}
document.body.classList.toggle('window-maximized',!!s.windowMaximized);
lastState=s;bookmarks=[...(s.bookmarks?.managed||[]).map(item=>({...item,managed:true})),...(s.bookmarks?.personal?.items||[])];if(!s.ready||!sectionEditable('library')||($('bookmark-editor').open&&!(bookmarkEditingId?s.bookmarks?.allowUserEdit:s.bookmarks?.allowUserAdd)))$('bookmark-editor').close();const brand=s.brand.name==='SafetyNet'?'S Browser':s.brand.name;$('brand').textContent=brand;document.title=brand;$('managed').textContent=s.ready?(s.loginCheckPending?'Login check required':`Managed · policy ${s.policyVersion}`):'Access paused';$('managed').parentElement.classList.toggle('paused',!s.ready);
$('welcome').hidden=s.ready;$('enrollment').hidden=s.enrolled;$('message').textContent=s.message;
if(document.activeElement!==$('address'))$('address').value=startPageOpen?'':s.url||'';
$('managed').title=s.message;$('address-form').querySelector('.address-lock').src=s.url?.startsWith('https:')?'icons/lock-keyhole.svg':'icons/globe.svg';
$('ai-toggle').disabled=!s.aiEnabled;$('sidebar').disabled=!s.ready;$('send').disabled=!s.aiEnabled;
$('agents').disabled=!s.agentsEnabled;$('inspect').disabled=!s.documentScanEnabled;$('passwords').disabled=!s.ready||s.passwordManagerEnabled===false;
$('inspect').title=s.documentScanEnabled?'Inspect a document locally':'Document analysis is disabled by company policy';
$('back').disabled=!s.ready||!currentTab().canGoBack;$('forward').disabled=!s.ready||!currentTab().canGoForward;$('reload').disabled=!s.ready||currentTab().startPage;$('address').disabled=!(s.canNavigate??s.ready);
if(vaultPanel&&s.vaultLocked===true&&vaultState?.locked!==true){vaultRevision++;clearVaultFields();$('vault-list').replaceChildren();$('vault-open').hidden=true;vaultState={...vaultState,locked:true};refreshVault().catch(()=>{});}
if(vaultPanel&&s.passwordManagerEnabled===false)closeVault().catch(()=>{});
if(!s.ready){if(panel)closeAI().catch(()=>{});if(vaultPanel)closeVault().catch(()=>{});clearVaultFields();}
document.documentElement.style.setProperty('--page-tint',/^#[a-f0-9]{6}$/i.test(s.pageColor||'')?s.pageColor:'#b8c7dd');
if($('start-greeting'))$('start-greeting').textContent='A fresh place to begin';
if($('sidebar-toggle'))$('sidebar-toggle').disabled=!s.ready;
if($('new-tab'))$('new-tab').disabled=!s.ready;if($('tab-overview'))$('tab-overview').disabled=!s.ready;
applyManagedSettings();updateChrome().catch(()=>{});renderTabs();renderLibrary();
}
function task(fn){return async event=>{event?.preventDefault();$('error').textContent='';try{await fn();}catch(e){$('error').textContent=e.message;$('managed').textContent=e.message;}};}
function vaultTask(fn){return async event=>{event?.preventDefault();if(vaultBusy)return;vaultBusy=true;$('vault-error').textContent='';try{await fn();}catch{$('vault-error').textContent='The vault action could not be completed. Check your password and company policy.';}finally{vaultBusy=false;$('vault-master').value='';$('vault-confirm').value='';}};}
function input(){return {server:$('server').value,code:$('code').value};}
$('enroll').onclick=task(async()=>{$('enroll').disabled=true;try{show(await call('enroll',input()));$('code').value='';}finally{$('enroll').disabled=false;}});
$('sso').onclick=task(async()=>{$('sso').disabled=true;try{show(await call('sso',input()));}finally{$('sso').disabled=false;}});
$('address-form').onsubmit=task(async()=>navigateTab($('address').value));
for(const method of ['back','forward','reload'])$(method).onclick=task(()=>call(method));
$('ai-toggle').onclick=$('sidebar').onclick=task(async()=>{if(panel)await closeAI();else await openAI();});
$('ai-close').onclick=task(closeAI);
$('inspect').onclick=task(async()=>{
requireSection('documentScan',true);$('inspect').disabled=true;try{const result=await call('inspectFile');if(!result)return;await openAI();entry(`Inspection: ${result.name}\nDecision: ${result.decision}\nDetector: ${result.ruleId||'No match'}\nType: ${result.contentType}\nComplete coverage: ${result.coverage.complete}\n${result.model?`Model: ${result.model}`:''}\nAnalyzed locally. This result does not authorize a website upload.`);}finally{show(await call('state'));}
});
function entry(text){const empty=$('conversation').querySelector('.empty-chat');empty?.remove();const e=document.createElement('div');e.className='entry';e.textContent=text;$('conversation').append(e);e.scrollIntoView();}
$('agents').onclick=task(async()=>{requireSection('agents',true);const connection=await call('agentConnection');await openAI();entry(`Local agent API\nEndpoint: ${connection.address}\nEncrypted pairing file: ${connection.credentialPath}\nUse the trusted local helper. Every action follows company policy.`);});
$('ai-form').onsubmit=task(async()=>{const prompt=$('prompt').value;if(!prompt.trim())return;entry(`You: ${prompt}`);$('send').disabled=true;try{const result=await call('ai',prompt);entry(result.answer);$('prompt').value='';}catch(e){entry(e.message);}finally{$('send').disabled=!lastState?.aiEnabled;}});
async function refreshVault(){
const revision=++vaultRevision;const status=await call('vaultStatus');if(!vaultPanel||revision!==vaultRevision)return;const previous=vaultState;vaultState=status;
$('vault-access').hidden=!status.enabled||!status.locked&&status.exists;$('vault-open').hidden=!status.enabled||status.locked||!status.exists;
$('vault-access-submit').textContent=status.exists?'Unlock vault':'Create vault';$('vault-master').autocomplete=status.exists?'current-password':'new-password';
$('vault-confirm').hidden=status.exists;$('vault-confirm-label').hidden=status.exists;$('vault-confirm').required=!status.exists&&status.enabled;
$('master-label').textContent=status.exists?'Master password':'Choose a master password';
$('vault-description').textContent=!status.enabled?'Password storage is disabled by your administrator.':status.locked||!status.exists?'Your encrypted logins stay on this device. Unlock with your master password.':`Unlocked · locks after ${status.idleLockSeconds} seconds of inactivity.`;
$('vault-add').hidden=!status.allowSaving;$('vault-save-submit').disabled=!status.allowSaving;$('vault-generate').disabled=!status.allowSaving;
if(!status.enabled||status.locked||!status.exists){if(!status.enabled||previous?.locked===false)clearVaultFields();$('vault-list').replaceChildren();return;}
const entries=await call('vaultList');if(!vaultPanel||revision!==vaultRevision)return;$('vault-list').replaceChildren();
if(!entries.length){const empty=document.createElement('div');empty.className='empty-vault';empty.textContent='No saved logins yet.';$('vault-list').append(empty);}
for(const item of entries){const card=document.createElement('div');card.className='login-card';const label=document.createElement('strong');label.textContent=item.label||item.origin;const origin=document.createElement('p');origin.textContent=item.origin;const username=document.createElement('p');username.textContent=item.username||'No username';const buttons=document.createElement('div');buttons.className='login-buttons';const fill=document.createElement('button');fill.textContent='Fill login';fill.disabled=!status.allowAutofill;fill.onclick=vaultTask(async()=>{await call('vaultFill',item.id);$('vault-error').textContent='Login filled in the current page.';});const remove=document.createElement('button');remove.className='secondary';remove.textContent='Delete';remove.disabled=!status.allowSaving;remove.onclick=vaultTask(async()=>{await call('vaultDelete',item.id);await refreshVault();});buttons.append(fill,remove);card.append(label,origin,username,buttons);$('vault-list').append(card);}
}
$('passwords').onclick=vaultTask(async()=>{requireSection('passwords',true);if(vaultPanel){await closeVault();return;}await closeAI();vaultPanel=true;$('vault').hidden=false;$('passwords').setAttribute('aria-expanded','true');await call('toggleVault',true);await refreshVault();});
$('vault-close').onclick=vaultTask(closeVault);
$('vault-access').onsubmit=vaultTask(async()=>{const exists=vaultState?.exists,master=$('vault-master').value;if(!exists&&master!==$('vault-confirm').value){$('vault-error').textContent='The master passwords do not match.';return;}await call(exists?'vaultUnlock':'vaultCreate',master);$('vault-master').value='';$('vault-confirm').value='';await refreshVault();});
$('vault-lock').onclick=vaultTask(async()=>{vaultRevision++;clearVaultFields();$('vault-list').replaceChildren();await call('vaultLock');await refreshVault();});
$('vault-save').onsubmit=vaultTask(async()=>{await call('vaultSave',{origin:$('vault-origin').value,label:$('vault-label').value,username:$('vault-username').value,password:$('vault-password').value});clearVaultFields();$('vault-add').open=false;await refreshVault();});
$('vault-generate').onclick=vaultTask(async()=>{const generated=await call('vaultGenerate');$('vault-password').value=typeof generated==='string'?generated:generated.password;});
$('vault-add').ontoggle=()=>{if($('vault-add').open&&lastState?.url&&!$('vault-origin').value){try{$('vault-origin').value=new URL(lastState.url).origin;}catch{}}else if(!$('vault-add').open)$('vault-password').value='';};
window.addEventListener('blur',()=>{vaultRevision++;clearVaultFields();$('vault-list').replaceChildren();if(vaultPanel)call('vaultLock').then(refreshVault).catch(()=>{});});
window.addEventListener('pagehide',()=>{clearVaultFields();if(vaultPanel)call('vaultLock').catch(()=>{});});
setInterval(()=>{if(vaultPanel&&!vaultBusy)refreshVault().catch(()=>{clearVaultFields();$('vault-list').replaceChildren();});},5000);
function wire(id,handler){if($(id))$(id).onclick=task(handler);}
wire('sidebar-toggle',async()=>{requireSection('library');sidebarOpen=!sidebarOpen;$('sidebar-toggle').setAttribute('aria-expanded',String(sidebarOpen));await updateChrome();});
wire('new-tab',newTab);wire('tab-overview',async()=>{overviewOpen=!overviewOpen;startPageOpen=!!currentTab().startPage;$('tab-overview').setAttribute('aria-expanded',String(overviewOpen));renderTabs();await updateChrome();});
wire('overview-close',async()=>{overviewOpen=false;startPageOpen=!!currentTab().startPage;await updateChrome();});
for(const action of ['minimize','maximize','close'])wire(`window-${action}`,()=>call('windowControls',action));
wire('share-url',()=>call('shareURL'));
wire('bookmark',async()=>{requireSection('library',true);if(!lastState?.bookmarks?.allowUserAdd)throw Error('Adding bookmarks is disabled by your administrator');await openBookmarkEditor();});
wire('reading-add',async()=>{requireSection('library',true);await call('authorizeSetting','library');requireSection('library',true);if(!lastState?.ready)throw Error('Open an approved page first');const url=safeURL(lastState.url);if(!url)throw Error('This page cannot be saved');if(!readingList.some(b=>b.url===url)){readingList.push({url,title:String(lastState.pageTitle||new URL(url).hostname).slice(0,160)});readingList=readingList.slice(-100);remember('reading-list',readingList);renderLibrary();}});
wire('group-add',async()=>{requireSection('library',true);await call('authorizeSetting','library');requireSection('library',true);const field=$('group-name'),label=field?.value.trim();if(!label)throw Error('Enter a tab group name');if(label.length>40)throw Error('Tab group names are limited to 40 characters');if(!groups.includes(label)){if(groups.length>=20)throw Error('At most 20 tab groups are supported');groups.push(label);remember('groups',groups);}show(await call('groupTab',{id:currentTab().id,group:label}));groupFilter=label;field.value='';renderLibrary();renderTabs();});
for(const [quick,original] of [['quick-ai','ai-toggle'],['quick-passwords','passwords'],['quick-inspect','inspect'],['quick-agents','agents']])wire(quick,()=>$(original).click());
wire('tools-toggle',async()=>{if(!lastState?.settings?.available||!lastState.settings.visible)throw Error('Settings are managed by your administrator.');const menu=$('tools-menu');if(menu){menu.hidden=!menu.hidden;$('tools-toggle').setAttribute('aria-expanded',String(!menu.hidden));await updateChrome();}});
for(const id of ['theme-mode','accent-mode','tab-mode'])$(id).onchange=task(async()=>{
requireSection('appearance',true);
try{const settings=await call('updateSettings',{mode:$('theme-mode').value==='auto'?'system':$('theme-mode').value,accent:$('accent-mode').value,tabMode:$('tab-mode').value});lastState={...lastState,settings};}
finally{applyManagedSettings();}
});
window.addEventListener('resize',()=>updateChrome().catch(()=>{}));
document.addEventListener('click',event=>{const menu=$('tools-menu');if(!menu||menu.hidden)return;if(event.target.closest('.menu-item')||(!event.target.closest('#tools-menu')&&!event.target.closest('#tools-toggle'))){menu.hidden=true;$('tools-toggle')?.setAttribute('aria-expanded','false');updateChrome().catch(()=>{});}});
window.addEventListener('keydown',event=>{if(event.ctrlKey&&event.key.toLowerCase()==='t'&&lastState?.ready){event.preventDefault();newTab().catch(()=>{});}if(event.key==='Escape'&&overviewOpen){overviewOpen=false;startPageOpen=!!currentTab().startPage;updateChrome().catch(()=>{});}});
applyAppearance();renderTabs();renderLibrary();
window.safetynet.onState(show);call('state').then(show).catch(e=>{$('error').textContent=e.message;});