2026-10-13 10:33:31 +02:00
const $ = id => document . getElementById ( id ); let panel = false , vaultPanel = false , lastState , vaultState , vaultBusy = false , vaultRevision = 0 ;
2026-10-13 21:00:41 +02:00
window . safetynet . onVaultHandoff (()=>{ vaultRevision ++ ; vaultPanel = false ; $ ( 'vault' ). hidden = true ; $ ( 'passwords' ). setAttribute ( 'aria-expanded' , 'false' ); clearVaultFields (); $ ( 'vault-list' ). replaceChildren ();});
2026-10-13 10:33:31 +02:00
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 )})) : [];
2026-10-13 16:58:15 +02:00
// Company artwork is signed policy data, never a remote image or executable SVG.
function renderCompany ( s ){
const name = s . brand ? . name === 'SafetyNet' ? 'S Browser' : s . brand ? . name || 'S Browser' , logo = typeof s . brand ? . logoDataUrl === 'string' && /^data:image\/png;base64,[A-Za-z0-9+/=]+$/ . test ( s . brand . logoDataUrl ) && s . brand . logoDataUrl . length <= 180000 ? s . brand . logoDataUrl : null ;
document . documentElement . style . setProperty ( '--company-accent' , /^#[0-9a-f]{6}$/i . test ( s . brand ? . color || '' ) ? s . brand . color : 'var(--accent)' );
const setLogo = ( image )=>{ image . hidden =! logo ; if ( logo ) image . src = logo ; else image . removeAttribute ( 'src' ); image . alt = logo ? ` ${ name } logo` : '' ;};
setLogo ( $ ( 'start-company-logo' )); $ ( 'start-brand-name' ). textContent = name ;
const monogram = document . querySelector ( '.brand-monogram' ); monogram . replaceChildren (); if ( logo ){ const image = document . createElement ( 'img' ); image . className = 'company-logo' ; setLogo ( image ); monogram . append ( image );} else monogram . textContent = name . charAt ( 0 ). toUpperCase ();
for ( const id of [ 'welcome' , 'settings' , 'ai' , 'vault' ]){ const host = $ ( id ); if ( ! host ) continue ; let mark = host . querySelector ( '.company-surface-brand' ); if ( ! mark ){ mark = document . createElement ( 'div' ); mark . className = 'company-surface-brand' ; mark . append ( document . createElement ( 'img' ), document . createElement ( 'span' )); host . prepend ( mark );} setLogo ( mark . firstElementChild ); mark . firstElementChild . className = 'company-logo' ; mark . lastElementChild . textContent = name ;}
const start = s . startPage || {}; $ ( 'start-title' ). textContent = start . title || 'Your space to explore.' ; $ ( 'start-subtitle' ). textContent = start . subtitle || '' ; $ ( 'start-subtitle' ). hidden =! start . subtitle ;
$ ( 'company-news' ). hidden =! start . newsEnabled ; const list = $ ( 'company-news-items' ); list . replaceChildren ();
if ( start . newsEnabled ) for ( const item of ( Array . isArray ( start . news ) ? start . news : []). slice ( 0 , 12 )){ let url ; try { url = new URL ( item . url ); if ( url . protocol !== 'https:' || url . username || url . password ) continue ;} catch { continue ;} if ( typeof item . title !== 'string' ) continue ; const card = button ( '' ,()=> navigateTab ( url . href ), 'company-news-card' ), title = document . createElement ( 'strong' ); title . textContent = item . title . slice ( 0 , 240 ); card . append ( title ); if ( item . publishedAt ){ const date = new Date ( item . publishedAt ); if ( Number . isFinite ( date . getTime ())){ const time = document . createElement ( 'time' ); time . dateTime = date . toISOString (); time . textContent = date . toLocaleDateString (); card . append ( time );}} list . append ( card );}
$ ( 'company-news-empty' ). hidden =!! list . children . length ;
}
let bookmarks = [], readingList = metadata ( preference ( 'reading-list' ,[])), groups = [];
2026-10-13 10:33:31 +02:00
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' ));
2026-10-13 16:58:15 +02:00
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 ||!! $ ( 'bookmark-manager' ) ? . open ||!! $ ( 'agent-pairing' ) ? . 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 ;}}
2026-10-13 10:33:31 +02:00
}
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 );}}
}
2026-10-13 16:58:15 +02:00
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 ; b . title = item . url ; b . append ( initial , label ); const badge = document . createElement ( 'small' ); badge . className = 'favorite-badge' ; badge . textContent = item . managed ? 'Company' : '' ; b . append ( badge ); $ ( id ). append ( b );}}
2026-10-13 10:33:31 +02:00
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 (){
2026-10-13 16:58:15 +02:00
const key = JSON . stringify ({ bookmarks , bookmarkPermissions : lastState ? . bookmarks , libraryAI : lastState ? . libraryAI , 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 (); if ( $ ( 'bookmark-manager' ) ? . open ){ libraryProposal = null ; $ ( 'manager-proposal' ). hidden = true ; renderBookmarkManager ();} 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 ){ const row = document . createElement ( 'div' ); row . className = 'group-manager-row' ; row . append ( button ( group ,()=>{ groupFilter = group ; renderTabs ();}, 'group-item' )); if ( sectionEditable ( 'library' )){ const rename = button ( 'Rename' , async ()=>{ const label = $ ( 'group-name' ). value . trim (); if ( ! label || label . length > 40 ) throw Error ( 'Enter a new group name, up to 40 characters' ); for ( const tab of [... tabs ]. filter ( t => t . group === group )) show ( await call ( 'groupTab' ,{ id : tab . id , group : label })); groupFilter = label ; renderTabs ();}); rename . setAttribute ( 'aria-label' , `Rename group ${ group } ` ); const remove = button ( 'Ungroup' , async ()=>{ for ( const tab of [... tabs ]. filter ( t => t . group === group )) show ( await call ( 'groupTab' ,{ id : tab . id , group : '' })); groupFilter = '' ; renderTabs ();}); remove . setAttribute ( 'aria-label' , `Ungroup tabs in ${ group } ` ); row . append ( rename , remove );} $ ( 'library-groups' ). append ( row );}}
2026-10-13 10:33:31 +02:00
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' );}
2026-10-13 16:58:15 +02:00
const sections = { appearance : [ 'appearance-section' ], library : [ 'bookmark' , 'reading-add' , 'sidebar-toggle' , 'bookmark-manager-open' , 'favorites-manage' ], ai : [ 'ai-toggle' , 'sidebar' ], passwords : [ 'passwords' ], documentScan : [ 'inspect' ], agents : [ 'agents' ]};
2026-10-13 10:33:31 +02:00
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' );
2026-10-13 16:58:15 +02:00
if ( ! sectionVisible ( 'library' )){ if ( $ ( 'bookmark-manager' ) ? . open ) $ ( 'bookmark-manager' ). close (); sidebarOpen = false ; $ ( 'sidebar-toggle' ). setAttribute ( 'aria-expanded' , 'false' );}
2026-10-13 10:33:31 +02:00
if ( ! sectionEditable ( 'ai' ) && panel ) closeAI (). catch (()=>{});
if ( ! sectionEditable ( 'passwords' ) && vaultPanel ) closeVault (). catch (()=>{});
2026-10-13 16:58:15 +02:00
if ( $ ( 'agent-pairing' ) ? . open && ( ! lastState ? . ready ||! lastState ? . agentsEnabled ||! sectionEditable ( 'agents' ))) $ ( 'agent-pairing' ). close ();
2026-10-13 10:33:31 +02:00
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 ;
2026-10-13 16:58:15 +02:00
if ( Array . isArray ( s . tabs )){ nativeTabsRevision = s . tabsRevision ?? nativeTabsRevision ; tabs = s . tabs ; groups = [... new Set ( tabs . map ( t => t . group ). filter ( Boolean ))]; activeTab = s . activeTabId ; startPageOpen =!! currentTab (). startPage ; if ( groupFilter && currentTab (). group !== groupFilter ) groupFilter = currentTab (). group ;}
2026-10-13 10:33:31 +02:00
document . body . classList . toggle ( 'window-maximized' , !! s . windowMaximized );
2026-10-13 16:58:15 +02:00
lastState = s ; renderCompany ( 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 );
2026-10-13 10:33:31 +02:00
$ ( '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 ;
2026-10-13 16:58:15 +02:00
reconcileAgentPairing (); applyManagedSettings (); updateChrome (). catch (()=>{}); renderTabs (); renderLibrary ();
2026-10-13 10:33:31 +02:00
}
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 ();}
2026-10-13 16:58:15 +02:00
// Pairing selects a signed central identity. It never creates a local grant or exposes tokens.
let agentPairingRevision = 0 , agentPairingFingerprint = '' ;
function agentGrantFingerprint (){ return JSON . stringify ({ mode : lastState ? . agentIdentityMode , grants : approvedAgentGrants ()});}
function reconcileAgentPairing (){ if ( $ ( 'agent-pairing' ) ? . open && agentPairingFingerprint !== agentGrantFingerprint ()) $ ( 'agent-pairing' ). close ();}
setInterval ( reconcileAgentPairing , 1000 );
function approvedAgentGrants (){ return ( Array . isArray ( lastState ? . agentGrants ) ? lastState . agentGrants : []). filter ( g => g && typeof g . id === 'string' && typeof g . name === 'string' && Number . isSafeInteger ( g . generation ) && g . generation >= 1 && Date . parse ( g . expiresAt ) > Date . now ());}
function agentPairingDialog (){
let dialog = $ ( 'agent-pairing' ); if ( dialog ) return dialog ;
dialog = document . createElement ( 'dialog' ); dialog . id = 'agent-pairing' ; dialog . setAttribute ( 'aria-labelledby' , 'agent-pairing-title' );
const title = document . createElement ( 'h2' ); title . id = 'agent-pairing-title' ; title . textContent = 'Connect a local agent' ;
const description = document . createElement ( 'p' ); description . id = 'agent-pairing-description' ; dialog . setAttribute ( 'aria-describedby' , description . id );
const list = document . createElement ( 'div' ); list . id = 'agent-grant-list' ; list . className = 'agent-grant-list' ;
const result = document . createElement ( 'pre' ); result . id = 'agent-pairing-result' ; result . setAttribute ( 'role' , 'status' ); result . setAttribute ( 'aria-live' , 'polite' );
const close = button ( 'Close' ,()=> dialog . close (), 'secondary' ); close . setAttribute ( 'aria-label' , 'Close agent connection' );
dialog . append ( title , description , list , result , close ); document . body . append ( dialog );
dialog . addEventListener ( 'close' ,()=>{ agentPairingRevision ++ ; agentPairingFingerprint = '' ; list . replaceChildren (); result . textContent = '' ; updateChrome (). catch (()=>{});});
return dialog ;
}
async function openAgentPairing (){
requireSection ( 'agents' , true ); if ( ! lastState ? . ready ||! lastState ? . agentsEnabled ) throw Error ( 'Local agent control is disabled by your administrator.' );
const dialog = agentPairingDialog (), revision =++ agentPairingRevision , mode = lastState . agentIdentityMode === 'individual' ? 'individual' : 'legacy' , list = $ ( 'agent-grant-list' ), result = $ ( 'agent-pairing-result' ); list . replaceChildren (); result . textContent = '' ;
$ ( 'agent-pairing-description' ). textContent = mode === 'individual' ? 'Choose an identity approved by your administrator. Each identity has its own permissions and encrypted pairing file.' : 'Shared legacy credential: all connected agents share the same permissions. Your administrator can require individual identities.' ;
const pair = async grant =>{
requireSection ( 'agents' , true ); if ( revision !== agentPairingRevision ) return ;
if ( mode === 'individual' &&! approvedAgentGrants (). some ( g => g . id === grant . id && g . generation === grant . generation )) throw Error ( 'This agent identity is no longer approved.' );
const connection = await call ( 'agentConnection' , grant ? . id );
requireSection ( 'agents' , true ); if ( revision !== agentPairingRevision ||! dialog . open ) return ;
if ( ! lastState ? . ready ||! lastState ? . agentsEnabled || ( lastState . agentIdentityMode === 'individual' ? 'individual' : 'legacy' ) !== mode || ( grant &&! approvedAgentGrants (). some ( g => g . id === grant . id && g . generation === grant . generation ))) throw Error ( 'Agent permissions changed. Open the connection dialog again.' );
result . textContent = ` ${ grant ? `Identity: ${ grant . name } \n` : 'Shared legacy credential\n' } Endpoint: ${ connection . address } \nEncrypted pairing file: ${ connection . credentialPath } \nUse the trusted local helper. Every action follows company policy.` ;
};
if ( mode === 'individual' ){
const grants = approvedAgentGrants (); if ( ! grants . length ){ const empty = document . createElement ( 'p' ); empty . textContent = 'No active agent identities are approved. Ask your administrator to publish an agent grant.' ; list . append ( empty );}
for ( const grant of grants ){ const choice = button ( '' ,()=> pair ( grant ), 'agent-grant-choice' ), name = document . createElement ( 'strong' ), detail = document . createElement ( 'small' ); name . textContent = grant . name ; detail . textContent = `Expires ${ new Date ( grant . expiresAt ). toLocaleString () } · generation ${ grant . generation } ` ; choice . append ( name , detail ); list . append ( choice );}
} else list . append ( button ( 'Get shared legacy connection' ,()=> pair ( null ), 'agent-grant-choice' ));
agentPairingFingerprint = agentGrantFingerprint (); dialog . showModal (); await updateChrome ();
}
$ ( 'agents' ). onclick = task ( openAgentPairing );
2026-10-13 10:33:31 +02:00
$ ( '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 );}
2026-10-13 21:00:41 +02:00
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. Submission remains subject to company DLP policy.' ;}); 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 ();}); const prepare = document . createElement ( 'button' ); prepare . textContent = 'Prepare approved login' ; prepare . title = 'Fill and permit one approved login POST for up to 15 seconds. Submit on the website; other fields still pass company DLP.' ; prepare . hidden =! status . allowControlledSubmission ; prepare . disabled =! status . allowAutofill ; prepare . onclick = vaultTask ( async ()=>{ await call ( 'vaultPrepareSubmit' , item . id );}); buttons . append ( fill , prepare , remove ); card . append ( label , origin , username , buttons ); $ ( 'vault-list' ). append ( card );}
2026-10-13 10:33:31 +02:00
}
$ ( '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 ();}});
2026-10-13 16:58:15 +02:00
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 );} show ( await call ( 'groupTab' ,{ id : currentTab (). id , group : label })); groupFilter = label ; field . value = '' ; renderLibrary (); renderTabs ();});
2026-10-13 10:33:31 +02:00
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 ;});
2026-10-13 16:58:15 +02:00
let libraryProposal = null ;
function renderBookmarkManager (){
const query = $ ( 'bookmark-search' ). value . trim (). toLowerCase (), list = $ ( 'manager-list' ); list . replaceChildren ();
const matches = bookmarks . filter ( item =>[ item . title , item . url , item . folder || '' ]. some ( value => value . toLowerCase (). includes ( query )));
let section = '' ; for ( const item of matches ){ const key = ` ${ item . managed ? 'Company' : 'Personal' } - ${ item . folder || 'Unfiled' } ` ; if ( key !== section ){ const label = document . createElement ( 'h3' ); label . className = 'manager-folder' ; label . textContent = key ; list . append ( label ); section = key ;}
const row = document . createElement ( 'div' ); row . className = 'manager-row' ; row . dataset . bookmarkId = item . id ; const link = button ( '' ,()=> navigateTab ( item . url ), 'manager-link' ), title = document . createElement ( 'strong' ), detail = document . createElement ( 'small' ); title . textContent = item . title ; detail . textContent = item . url ; link . append ( title , detail ); row . append ( link );
if ( item . managed ){ const badge = document . createElement ( 'span' ); badge . className = 'manager-managed' ; badge . textContent = 'Managed' ; row . append ( badge );} else if ( sectionEditable ( 'library' )){ const actions = document . createElement ( 'div' ); actions . className = 'manager-row-actions' ; if ( lastState . bookmarks . allowUserEdit ) actions . append ( button ( 'Edit / move' ,()=> openBookmarkEditor ( item ))); if ( lastState . bookmarks . allowUserDelete ) actions . append ( button ( 'Remove' , async ()=>{ await mutateBookmark ( 'delete' , null , item . id ); renderBookmarkManager ();})); row . append ( actions );} list . append ( row );}
$ ( 'manager-empty' ). hidden = matches . length > 0 ; $ ( 'manager-add' ). disabled =! sectionEditable ( 'library' ) ||! lastState ? . bookmarks ? . allowUserAdd ; $ ( 'manager-ai' ). disabled =! sectionEditable ( 'library' ) ||! lastState ? . libraryAI ? . suggestionsEnabled ||!! lastState ? . libraryAI ? . busy ;
$ ( 'manager-policy' ). textContent = sectionEditable ( 'library' ) ? 'Company bookmarks are read-only. Personal bookmarks follow company policy. AI organization shares titles and website domains with your approved model.' : 'Your administrator manages this library. Changes are disabled.' ;
}
async function openBookmarkManager (){ requireSection ( 'library' ); $ ( 'tools-menu' ). hidden = true ; renderBookmarkManager (); $ ( 'bookmark-manager' ). showModal (); await updateChrome (); $ ( 'bookmark-search' ). focus ();}
for ( const id of [ 'bookmark-manager-open' , 'favorites-manage' ]) wire ( id , openBookmarkManager );
$ ( 'bookmark-search' ). addEventListener ( 'input' , renderBookmarkManager ); $ ( 'bookmark-manager-close' ). onclick = ()=> $ ( 'bookmark-manager' ). close (); $ ( 'bookmark-manager' ). addEventListener ( 'close' ,()=> updateChrome (). catch (()=>{})); wire ( 'manager-add' ,()=> openBookmarkEditor ());
wire ( 'manager-ai' , async ()=>{ requireSection ( 'library' , true ); libraryProposal = await call ( 'organizeLibrary' ); const diff = $ ( 'manager-diff' ); diff . replaceChildren (); for ( const [ index , item ] of ( libraryProposal . plan . bookmarks || []). entries ()){ const existing = bookmarks . find ( b => b . id === item . id ); if ( ! existing || existing . managed ) throw Error ( 'Organization proposal no longer matches your library' ); const row = document . createElement ( 'p' ); row . textContent = ` ${ index + 1 } . ${ existing . title } : ${ existing . folder || 'Unfiled' } -> ${ item . folder || 'Unfiled' } ` ; diff . append ( row );} for ( const item of libraryProposal . plan . tabs || []){ const existing = tabs . find ( t => t . id === item . id ); if ( ! existing ) throw Error ( 'Organization proposal no longer matches your tabs' ); const row = document . createElement ( 'p' ); row . textContent = `Tab ${ existing . title } : ${ existing . group || 'Ungrouped' } -> ${ item . group || 'Ungrouped' } ` ; diff . append ( row );} $ ( 'manager-proposal' ). hidden = false ;});
wire ( 'manager-apply' , async ()=>{ if ( ! libraryProposal ) throw Error ( 'Request a fresh organization proposal' ); await call ( 'applyLibraryOrganization' ,{ proposalId : libraryProposal . proposalId , confirm : true }); libraryProposal = null ; $ ( 'manager-proposal' ). hidden = true ; show ( await call ( 'state' )); renderBookmarkManager ();}); $ ( 'manager-discard' ). onclick = ()=>{ libraryProposal = null ; $ ( 'manager-proposal' ). hidden = true ;};