Files
PlatformIO_Esp32C3Boilerplate/data/admin.html

95 lines
12 KiB
HTML

<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32-C3 Admin</title>
<style>
:root{font-family:system-ui,-apple-system,Segoe UI,sans-serif;color:#202124;background:#f5f7f8}
body{margin:0}.top{background:#263238;color:white;padding:14px 18px;font-weight:650}
main{max-width:1040px;margin:0 auto;padding:18px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}
section,.box{background:white;border:1px solid #d8dee2;border-radius:8px;padding:14px}
label{display:block;font-size:13px;margin:10px 0 4px}input,select,textarea,button{font:inherit}
input,select,textarea{width:100%;box-sizing:border-box;padding:9px;border:1px solid #b9c2c8;border-radius:6px}
button{border:0;border-radius:6px;background:#1261a6;color:white;padding:9px 12px;cursor:pointer;margin-top:10px}
button.secondary{background:#607d8b}button.danger{background:#b3261e}
pre{white-space:pre-wrap;background:#111;color:#d7ffd7;padding:10px;border-radius:6px;max-height:300px;overflow:auto}
.row{display:flex;gap:8px;align-items:end}.row>*{flex:1}.hidden{display:none}.muted{color:#607d8b;font-size:13px}
.tabs{display:flex;gap:6px;border-bottom:1px solid #cfd8dc;margin:0 0 14px;overflow-x:auto}
.tab{background:transparent;color:#263238;border-radius:6px 6px 0 0;margin:0;padding:10px 14px;white-space:nowrap}
.tab.active{background:#1261a6;color:white}.panel{display:none}.panel.active{display:block}
.rangeRow{display:flex;gap:12px;align-items:center}.rangeRow input{flex:1}.rangeRow output{min-width:3ch;text-align:right;font-weight:650}
.list{display:flex;flex-direction:column;gap:6px;margin-top:10px}.item{background:#eef3f6;color:#263238;text-align:left;margin:0}.item.active{background:#1261a6;color:white}
.checkGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px;margin-top:8px}.checkGrid label{display:flex;gap:6px;align-items:center;margin:0}.checkGrid input{width:auto}
.pill{display:inline-flex;gap:6px;align-items:center;background:#eef3f6;border-radius:6px;padding:5px 8px;margin:4px 4px 0 0}.pill button{margin:0;padding:3px 7px;background:#b3261e}
.detailTitle{margin:0 0 8px}.inlineCheck{display:flex;gap:8px;align-items:center;margin-top:10px}.inlineCheck input{width:auto}
</style>
</head>
<body>
<div class="top">ESP32-C3 Admin</div>
<main>
<div id="loginPanel" class="box">
<h2>Login</h2>
<label>User</label><input id="user" value="admin">
<label>Password</label><input id="pass" type="password">
<button onclick="doLogin()">Login</button>
<pre id="loginOut"></pre>
</div>
<div id="app" class="hidden">
<div class="tabs">
<button class="tab active" data-tab="dashboard" onclick="showTab('dashboard')">Dashboard</button>
<button class="tab" data-tab="access" onclick="showTab('access')">Access</button>
<button class="tab" data-tab="ops" onclick="showTab('ops')">Operations</button>
</div>
<div id="dashboard" class="panel active"><div class="grid">
<section><h3>Status</h3><button onclick="call('GET','/api/ping')">Ping</button><button class="secondary" onclick="loadMe()">Me</button><pre id="statusOut"></pre></section>
<section><h3>LED</h3><label>Brightness</label><div class="rangeRow"><input id="brightness" type="range" min="0" max="100" value="0" oninput="ledVal.value=value"><output id="ledVal">0</output></div><button onclick="setLed()">Apply</button></section>
<section><h3>Add API</h3><div class="row"><div><label>A</label><input id="a" type="number" value="1"></div><div><label>B</label><input id="b" type="number" value="2"></div></div><button onclick="addNums()">Add</button><pre id="addOut"></pre></section>
</div></div>
<div id="access" class="panel"><div class="grid">
<section><h3>Roles</h3><div class="row"><div><label>Custom role</label><input id="roleName"></div><div><button onclick="addRole()">Add</button></div></div><div id="rolesOut"></div><pre id="roleMsg"></pre></section>
<section><h3>Users</h3><div class="row"><button onclick="loadAccess()">Refresh</button><button class="secondary" onclick="newUserRecord()">New</button></div><div id="userList" class="list"></div></section>
<section><h3>User Details</h3><h4 id="userTitle" class="detailTitle">Select a user</h4><label>Name</label><input id="userName"><label>Password</label><input id="userPass" type="password" placeholder="Leave blank to keep unchanged"><label class="inlineCheck"><input id="userActive" type="checkbox" checked> Active</label><label>Roles</label><div id="userRoles" class="checkGrid"></div><button onclick="saveSelectedUser()">Save user</button><pre id="usersOut"></pre></section>
<section><h3>API Security</h3><button onclick="loadApis()">Refresh</button><div id="apiList" class="list"></div></section>
<section><h3>API Details</h3><h4 id="apiTitle" class="detailTitle">Select an API</h4><label>Path</label><input id="apiPath" readonly><label>Method</label><input id="apiMethod" readonly><label>Required role</label><select id="apiRole"></select><button onclick="saveSelectedApi()">Save ACL</button><pre id="apisOut"></pre></section>
</div></div>
<div id="ops" class="panel"><div class="grid">
<section><h3>Logs</h3><div class="row"><button onclick="loadLogs()">Load</button><button class="danger" onclick="clearLogs()">Clear</button></div><pre id="logsOut"></pre></section>
<section><h3>Settings</h3><label>Log level</label><select id="logLevel"><option value="0">Error</option><option value="1">Warn</option><option value="2" selected>Info</option><option value="3">Debug</option></select><label>Max log bytes</label><input id="logMax" type="number" value="51200"><label>Update package URL</label><input id="upd"><button onclick="saveSettings()">Save</button><pre id="settingsOut"></pre></section>
<section><h3>Updates</h3><label>Update package</label><input id="pkg" type="file"><button onclick="uploadPkg()">Upload package</button><button class="secondary" onclick="checkOta()">Check URL</button><button class="danger" onclick="runOta()">Install from URL</button><pre id="fwOut"></pre></section>
</div></div>
</div>
</main>
<script>
let token='';const el=id=>document.getElementById(id);
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)o.body=JSON.stringify(b);let r=await fetch(u,o);return await r.text()}
function showTab(id){document.querySelectorAll('.tab').forEach(b=>b.classList.toggle('active',b.dataset.tab==id));document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active',p.id==id))}
let roles=[],users=[],apis=[],selectedUser='',selectedApi=-1;
async function doLogin(){let t=await req('POST','/api/login',{username:el('user').value,password:el('pass').value});el('loginOut').textContent=t;let j=JSON.parse(t);if(j.success){token=j.token;el('loginPanel').classList.add('hidden');el('app').classList.remove('hidden');loadMe();loadSettings();loadAccess()}}
async function call(m,u){el('statusOut').textContent=await req(m,u)}async function loadMe(){el('statusOut').textContent=await req('GET','/api/me')}
async function setLed(){el('statusOut').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 roleNames(){return roles.map(r=>r.name)}function roleChecks(id,selected){el(id).innerHTML=roleNames().map(r=>'<label><input type=checkbox value="'+r+'" '+(selected.includes(r)?'checked':'')+'> '+r+'</label>').join('')}
function selectedRoles(){return [...el('userRoles').querySelectorAll('input:checked')].map(x=>x.value)}
function renderRoles(){el('rolesOut').innerHTML=roles.map(r=>'<span class=pill>'+r.name+(r.system?'':' <button onclick="deleteRole(\''+r.name+'\')">Delete</button>')+'</span>').join('')}
async function loadRoles(){let j=JSON.parse(await req('GET','/api/roles'));if(j.success){roles=j.roles;renderRoles();roleOptions();if(selectedUser)selectUser(selectedUser)}}
async function addRole(){let t=await req('POST','/api/roles',{role:el('roleName').value});el('roleMsg').textContent=t;el('roleName').value='';await loadAccess()}
async function deleteRole(r){let t=await req('DELETE','/api/roles',{role:r});el('roleMsg').textContent=t;await loadAccess()}
function renderUsers(){el('userList').innerHTML=users.map(u=>'<button class="item '+(u.username==selectedUser?'active':'')+'" onclick="selectUser(\''+u.username+'\')">'+u.username+(u.active?'':' (inactive)')+'</button>').join('')}
async function loadUsers(){let j=JSON.parse(await req('GET','/api/users'));if(j.success){users=j.users;renderUsers();if(selectedUser)selectUser(selectedUser)}}
function newUserRecord(){selectedUser='';renderUsers();el('userTitle').textContent='New user';el('userName').value='';el('userName').readOnly=false;el('userPass').value='';el('userActive').checked=true;roleChecks('userRoles',[])}
function selectUser(name){selectedUser=name;let u=users.find(x=>x.username==name);if(!u)return;renderUsers();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('|'))}
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()}
function roleOptions(){let opts='<option>PUBLIC</option>'+roleNames().map(r=>'<option>'+r+'</option>').join('');el('apiRole').innerHTML=opts}
function renderApis(){el('apiList').innerHTML=apis.map((a,i)=>'<button class="item '+(i==selectedApi?'active':'')+'" onclick="selectApi('+i+')">'+a.method+' '+a.path+'</button>').join('')}
async function loadApis(){let j=JSON.parse(await req('GET','/api/apis'));if(j.success){apis=j.apis;renderApis();if(selectedApi>=0)selectApi(selectedApi)}}
function selectApi(i){selectedApi=i;let a=apis[i];if(!a)return;renderApis();el('apiTitle').textContent=a.method+' '+a.path;el('apiPath').value=a.path;el('apiMethod').value=a.method;roleOptions();el('apiRole').value=a.role}
async function saveSelectedApi(){if(selectedApi<0)return;let t=await req('POST','/api/apis',{path:el('apiPath').value,method:el('apiMethod').value,role:el('apiRole').value});el('apisOut').textContent=t;await loadApis()}
async function loadAccess(){await loadRoles();await loadUsers();await loadApis()}
async function loadLogs(){el('logsOut').textContent=await req('GET','/api/logs')}async function clearLogs(){el('logsOut').textContent=await req('POST','/api/logs/clear')}
async function loadSettings(){let t=await req('GET','/api/settings');el('settingsOut').textContent=t;let j=JSON.parse(t);if(j.success){el('logLevel').value=j.settings.logLevel;el('logMax').value=j.settings.maxLogBytes;el('upd').value=j.settings.updateUrl;el('brightness').value=j.settings.ledBrightness;el('ledVal').value=j.settings.ledBrightness}}
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 uploadPkg(){let fd=new FormData();fd.append('package',el('pkg').files[0]);let r=await fetch('/api/update',{method:'POST',headers:{Authorization:'Bearer '+token},body:fd});el('fwOut').textContent=await r.text()}
async function checkOta(){el('fwOut').textContent=await req('POST','/api/ota/check')}async function runOta(){el('fwOut').textContent=await req('POST','/api/ota/run')}
</script>
</body>
</html>