let busyCount=0;function setGlobalBusy(on){busyCount+=on?1:-1;if(busyCount<0)busyCount=0;el('busyChip').classList.toggle('hidden',busyCount==0)}
async function tracked(p){setGlobalBusy(true);try{return await p()}finally{setGlobalBusy(false)}}
async function apiFetch(u,o){return await tracked(()=>fetch(u,o))}
async function req(m,u,b){let o={method:m,headers:{'Content-Type':'application/json'}};if(token)o.headers.Authorization='Bearer '+token;if(b!==undefined&&m!='GET')o.body=JSON.stringify(b);let r=await apiFetch(u,o);return await r.text()}
function showTab(id){document.querySelectorAll('.tab[data-tab]').forEach(b=>b.classList.toggle('active',b.dataset.tab==id));document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active',p.id==id));if(id=='network')loadSettings()}
function showAccessTab(id){document.querySelectorAll('.subtab').forEach(b=>b.classList.toggle('active',b.dataset.subtab==id));document.querySelectorAll('.subpanel').forEach(p=>p.classList.toggle('active',p.id==id));if(id=='users')showUserOverview();if(id=='roles')showRoleOverview();if(id=='apis')showApiOverview()}
let roles=[],users=[],apis=[],selectedUser='',selectedRole='',selectedApi=-1;
async function setLed(){el('ledOut').textContent=await req('POST','/api/led',{brightness:+el('brightness').value})}async function addNums(){el('addOut').textContent=await req('POST','/api/add',{a:+el('a').value,b:+el('b').value})}
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
function roleNames(){return roles.map(r=>r.name)}function roleChecks(id,selected){el(id).innerHTML=roleNames().map(r=>'<label><inputtype=checkboxvalue="'+esc(r)+'"'+(selected.includes(r)?'checked':'')+'> '+esc(r)+'</label>').join('')}
function showUserOverview(){el('usersOverview').classList.remove('hidden');el('userDetail').classList.add('hidden')}
function showRoleOverview(){el('rolesOverview').classList.remove('hidden');el('roleDetail').classList.add('hidden')}
function showApiOverview(){el('apisOverview').classList.remove('hidden');el('apiDetail').classList.add('hidden')}
function renderRoles(){el('rolesOut').innerHTML=roles.map((r,i)=>'<buttonclass=itemonclick="selectRole('+i+')"><strong>'+esc(r.name)+'</strong><spanclass=meta><spanclass=badge>'+(r.system?'System':'Custom')+'</span></span></button>').join('')}
async function loadRoles(){let j=JSON.parse(await req('GET','/api/roles'));if(j.success){roles=j.roles;renderRoles();roleOptions();if(selectedUser&&el('userDetail').classList.contains('hidden')==false)selectUser(selectedUser)}}
function newRoleRecord(){selectedRole='';el('rolesOverview').classList.add('hidden');el('roleDetail').classList.remove('hidden');el('roleTitle').textContent='New role';el('roleName').value='';el('roleName').readOnly=false;el('roleMeta').innerHTML='';el('roleSave').classList.remove('hidden');el('roleDelete').classList.add('hidden');el('roleMsg').textContent=''}
function selectRole(i){let r=roles[i];if(!r)return;selectedRole=r.name;el('rolesOverview').classList.add('hidden');el('roleDetail').classList.remove('hidden');el('roleTitle').textContent=r.name;el('roleName').value=r.name;el('roleName').readOnly=true;el('roleMeta').innerHTML='<spanclass=badge>'+(r.system?'System role':'Custom role')+'</span>';el('roleSave').classList.add('hidden');el('roleDelete').classList.toggle('hidden',r.system);el('roleMsg').textContent=''}
async function addRole(){let t=await req('POST','/api/roles',{role:el('roleName').value});el('roleMsg').textContent=t;await loadAccess();showRoleOverview()}
async function deleteSelectedRole(){if(!selectedRole)return;let t=await req('DELETE','/api/roles',{role:selectedRole});el('roleMsg').textContent=t;selectedRole='';await loadAccess();showRoleOverview()}
function jsArg(s){return JSON.stringify(String(s)).replace(/"/g,'"')}
function renderUsers(){el('userList').innerHTML=users.map(u=>'<buttonclass=itemonclick="selectUser('+jsArg(u.username)+')"><strong>'+esc(u.username)+'</strong><spanclass=meta><spanclass=badge>'+(u.active?'Active':'Inactive')+'</span><spanclass=badge>'+esc(u.roles||'No roles')+'</span></span></button>').join('')}
async function loadUsers(){let j=JSON.parse(await req('GET','/api/users'));if(j.success){users=j.users;renderUsers();if(selectedUser&&el('userDetail').classList.contains('hidden')==false)selectUser(selectedUser)}}
function newUserRecord(){selectedUser='';el('usersOverview').classList.add('hidden');el('userDetail').classList.remove('hidden');el('userTitle').textContent='New user';el('userName').value='';el('userName').readOnly=false;el('userPass').value='';el('userActive').checked=true;roleChecks('userRoles',[]);el('usersOut').textContent=''}
function selectUser(name){selectedUser=name;let u=users.find(x=>x.username==name);if(!u)return;el('usersOverview').classList.add('hidden');el('userDetail').classList.remove('hidden');el('userTitle').textContent=name;el('userName').value=name;el('userName').readOnly=true;el('userPass').value='';el('userActive').checked=u.active;roleChecks('userRoles',(u.roles||'').split('|'));el('usersOut').textContent=''}
async function saveSelectedUser(){let t=await req('POST','/api/users',{username:el('userName').value,password:el('userPass').value,active:el('userActive').checked,roles:selectedRoles()});el('usersOut').textContent=t;selectedUser=el('userName').value;await loadUsers();showUserOverview()}
function roleOptions(){let opts='<option>PUBLIC</option>'+roleNames().map(r=>'<option>'+esc(r)+'</option>').join('');el('apiRole').innerHTML=opts}
function renderApis(){el('apiList').innerHTML=apis.map((a,i)=>'<buttonclass=itemonclick="selectApi('+i+')"><strong>'+esc(a.method+' '+a.path)+'</strong><spanclass=meta><spanclass=badge>'+esc(a.role)+'</span></span></button>').join('')}
async function loadApis(){let j=JSON.parse(await req('GET','/api/apis'));if(j.success){apis=j.apis;renderApis();if(selectedApi>=0&&el('apiDetail').classList.contains('hidden')==false)selectApi(selectedApi)}}
function selectApi(i){selectedApi=i;let a=apis[i];if(!a)return;el('apisOverview').classList.add('hidden');el('apiDetail').classList.remove('hidden');el('apiTitle').textContent=a.method+' '+a.path;el('apiPath').value=a.path;el('apiMethod').value=a.method;roleOptions();el('apiRole').value=a.role;el('apisOut').textContent='';el('apiTestOut').textContent=''}
async function saveSelectedApi(){if(selectedApi<0)return;lett=awaitreq('POST','/api/apis',{path:el('apiPath').value,method:el('apiMethod').value,role:el('apiRole').value});el('apisOut').textContent=t;awaitloadApis()}
function clearApiTestBody(){el('apiTestBody').value=''}
async function testSelectedApi(){if(selectedApi<0)return;leta=apis[selectedApi],body=el('apiTestBody').value.trim(),payload=undefined;if(body){try{payload=JSON.parse(body)}catch(e){el('apiTestOut').textContent='InvalidJSONbody';return}}el('apiTestOut').textContent=awaitreq(a.method,a.path,payload)}
function fileName(p){let i=String(p).lastIndexOf('/');return i>=0?p.substring(i+1):p}
function renderFiles(files){el('fileList').innerHTML=files.map(f=>'<divclass=item><divclass=detailActions><buttononclick="downloadFile('+jsArg(f.path)+')">Download</button><div><strong>'+esc(f.path)+'</strong><spanclass=meta><spanclass=badge>'+f.size+' bytes</span></span></div></div></div>').join('')}
async function loadFiles(){let t=await req('GET','/api/files');el('filesOut').textContent='';try{let j=JSON.parse(t);if(j.success){renderFiles(j.files||[]);if(!(j.files||[]).length)el('filesOut').textContent='No files found'}else{el('filesOut').textContent=t}}catch(e){el('filesOut').textContent=t}}
async function downloadFile(path){let r=await apiFetch('/api/files/download?path='+encodeURIComponent(path),{headers:{Authorization:'Bearer '+token}});if(!r.ok){el('filesOut').textContent=await r.text();return}let b=await r.blob(),a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=fileName(path)||'download';document.body.appendChild(a);a.click();URL.revokeObjectURL(a.href);a.remove()}
async function restoreConfig(){if(!el('configFile').files.length){el('maintOut').textContent='Select a configuration JSON file first';return}if(!confirm('Restore configuration from this file? WiFi settings will not change.'))return;setBusy('restoreConfigBtn',true,'Restoring');try{await tracked(async()=>{let body=await el('configFile').files[0].text();let r=await fetch('/api/config/import',{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+token},body});el('maintOut').textContent=await r.text();if(r.ok){loadSettings();loadAccess()}})}catch(e){el('maintOut').textContent='Restore failed: '+e.message}finally{setBusy('restoreConfigBtn',false,'Restore config')}}
async function loadCert(){let t=await req('GET','/api/cert');try{let j=JSON.parse(t);el('certOut').textContent=j.success?(j.certificate||'No certificate stored'):t}catch(e){el('certOut').textContent=t}}
async function saveCertFile(){if(!el('certFile').files.length){el('certOut').textContent='Select a PEM/CER/CRT file first';return}setBusy('certSaveBtn',true,'Saving');try{await tracked(async()=>{let certificate=await el('certFile').files[0].text();el('certOut').textContent=await req('POST','/api/cert',{certificate})})}catch(e){el('certOut').textContent='Certificate save failed: '+e.message}finally{setBusy('certSaveBtn',false,'Save certificate')}}
async function factoryResetDevice(){if(!confirm('Factory reset this device and restart?'))return;el('maintOut').textContent=await req('POST','/api/factory-reset')}
async function loadLogs(){let t=await req('GET','/api/logs');try{let j=JSON.parse(t);el('logsOut').textContent=j.success?(j.logs||''):t}catch(e){el('logsOut').textContent=t}}async function clearLogs(){let t=await req('POST','/api/logs/clear');el('logsOut').textContent=t;try{let j=JSON.parse(t);if(j.success)el('logsOut').textContent='Logs cleared'}catch(e){}}
function syncNetworkMode(){let s=el('netDhcp').value=='false';['netIp','netGateway','netSubnet','netDns1','netDns2'].forEach(id=>el(id).disabled=!s)}
async function saveSettings(){el('settingsOut').textContent=await req('POST','/api/settings',{logLevel:+el('logLevel').value,maxLogBytes:+el('logMax').value,updateUrl:el('upd').value,ledBrightness:+el('brightness').value})}
async function saveNetwork(){setBusy('networkSaveBtn',true,'Saving');try{let body={hostname:el('netHost').value,dhcp:el('netDhcp').value=='true',ip:el('netIp').value,gateway:el('netGateway').value,subnet:el('netSubnet').value,dns1:el('netDns1').value,dns2:el('netDns2').value};let t=await req('POST','/api/settings',body);el('networkOut').textContent=t+'\\nNetwork changes apply on the next WiFi reconnect or reboot.';if(JSON.parse(t).success)await loadSettings()}catch(e){el('networkOut').textContent='Network save failed: '+e.message}finally{setBusy('networkSaveBtn',false,'Save network')}}
function setBusy(id,busy,label){let b=el(id);if(!b)return;b.disabled=busy;b.innerHTML=busy?'<spanclass=spinner></span>'+label:label}
async function uploadPkg(){if(!el('pkg').files.length){el('fwOut').textContent='Select an update package first';return}setBusy('uploadPkgBtn',true,'Uploading');el('fwOut').textContent='Uploading update package...';try{let fd=new FormData();fd.append('package',el('pkg').files[0]);let r=await apiFetch('/api/update',{method:'POST',headers:{Authorization:'Bearer '+token},body:fd});el('fwOut').textContent=await r.text()}catch(e){el('fwOut').textContent='Upload failed: '+e.message}finally{setBusy('uploadPkgBtn',false,'Upload package')}}
async function checkOta(){el('fwOut').textContent=await req('POST','/api/ota/check')}async function runOta(){el('fwOut').textContent=await req('POST','/api/ota/run')}