251 lines
14 KiB
HTML
251 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin — Agent Command Center</title>
|
|
<style>
|
|
:root{--bg:#0f1117;--surface:#1a1d27;--surface2:#232733;--border:#2e3345;--text:#e4e6ed;--text-dim:#8b8fa3;--accent:#6c5ce7;--accent-hover:#7c6ef0;--green:#00b894;--red:#e17055;--yellow:#fdcb6e;--blue:#74b9ff}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
|
.header{background:var(--surface);border-bottom:1px solid var(--border);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}
|
|
.header h1{font-size:1.4rem;font-weight:600}
|
|
.header-right{display:flex;gap:.75rem;align-items:center}
|
|
.small-btn{background:none;border:1px solid var(--border);color:var(--text-dim);padding:.35rem .75rem;border-radius:6px;font-size:.8rem;cursor:pointer}
|
|
.small-btn:hover{border-color:var(--text-dim);color:var(--text)}
|
|
.container{max-width:1000px;margin:0 auto;padding:1.5rem 2rem}
|
|
.tabs{display:flex;gap:0;margin-bottom:1.5rem;border-bottom:1px solid var(--border)}
|
|
.tab{padding:.6rem 1.25rem;font-size:.9rem;cursor:pointer;color:var(--text-dim);border-bottom:2px solid transparent;transition:all .2s}
|
|
.tab:hover{color:var(--text)}.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
|
.panel{display:none}.panel.active{display:block}
|
|
|
|
table{width:100%;border-collapse:collapse;background:var(--surface);border-radius:10px;overflow:hidden;border:1px solid var(--border)}
|
|
th{text-align:left;padding:.75rem 1rem;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-dim);background:var(--surface2);border-bottom:1px solid var(--border)}
|
|
td{padding:.65rem 1rem;font-size:.85rem;border-bottom:1px solid var(--border)}
|
|
tr:last-child td{border-bottom:none}
|
|
tr:hover td{background:var(--surface2)}
|
|
|
|
.badge{display:inline-block;padding:.15rem .6rem;border-radius:12px;font-size:.75rem;font-weight:500;text-transform:uppercase}
|
|
.badge.admin{background:rgba(225,112,85,.15);color:var(--red)}
|
|
.badge.user{background:rgba(0,184,148,.15);color:var(--green)}
|
|
|
|
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:.75rem}
|
|
.form-row.full{grid-template-columns:1fr}
|
|
.field label{display:block;font-size:.75rem;font-weight:500;color:var(--text-dim);text-transform:uppercase;margin-bottom:.3rem}
|
|
.field input,.field select{width:100%;padding:.5rem .65rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.85rem;outline:none}
|
|
.field input:focus,.field select:focus{border-color:var(--accent)}
|
|
|
|
.form-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1.25rem;margin-bottom:1rem}
|
|
.form-card h3{margin-bottom:1rem;font-size:.95rem}
|
|
.btn{padding:.5rem 1.25rem;border:none;border-radius:6px;font-size:.85rem;cursor:pointer}
|
|
.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover{background:var(--accent-hover)}
|
|
.btn-danger{background:var(--red);color:#fff;opacity:.8}.btn-danger:hover{opacity:1}
|
|
.btn-sm{padding:.3rem .75rem;font-size:.8rem}
|
|
.action-btns{display:flex;gap:.5rem}
|
|
.msg{font-size:.8rem;margin-top:.5rem}
|
|
.msg.ok{color:var(--green)}.msg.err{color:var(--red)}
|
|
|
|
.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1rem;margin-bottom:1.5rem}
|
|
.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1rem;text-align:center}
|
|
.stat-card .val{font-size:1.8rem;font-weight:700;color:var(--accent)}
|
|
.stat-card .lbl{font-size:.8rem;color:var(--text-dim);margin-top:.25rem}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Admin Panel</h1>
|
|
<div class="header-right">
|
|
<button class="small-btn" onclick="location.href='/'">Dashboard</button>
|
|
<button class="small-btn" onclick="logout()">Logout</button>
|
|
</div>
|
|
</div>
|
|
<div class="container">
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="switchTab('users')">Users</div>
|
|
<div class="tab" onclick="switchTab('catalog')">Agent Catalog</div>
|
|
<div class="tab" onclick="switchTab('llm')">LLM Providers</div>
|
|
<div class="tab" onclick="switchTab('bridges')">Bridges</div>
|
|
<div class="tab" onclick="switchTab('system')">System</div>
|
|
</div>
|
|
|
|
<!-- Users -->
|
|
<div class="panel active" id="panel-users">
|
|
<div class="form-card">
|
|
<h3>Create User</h3>
|
|
<div class="form-row">
|
|
<div class="field"><label>Username</label><input id="nu-user" placeholder="username"></div>
|
|
<div class="field"><label>Email</label><input type="email" id="nu-email" placeholder="user@example.com"></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="field"><label>Password</label><input type="password" id="nu-pass"></div>
|
|
<div class="field"><label>Display Name</label><input id="nu-name" placeholder="Full Name"></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="field"><label>Role</label><select id="nu-role"><option value="user">User</option><option value="admin">Admin</option></select></div>
|
|
<div class="field"></div>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="createUser()">Create</button>
|
|
<span class="msg" id="nu-msg"></span>
|
|
</div>
|
|
<table id="users-table"><thead><tr><th>Username</th><th>Email</th><th>Display Name</th><th>Role</th><th>Agents</th><th>Actions</th></tr></thead><tbody></tbody></table>
|
|
</div>
|
|
|
|
<!-- Catalog -->
|
|
<div class="panel" id="panel-catalog">
|
|
<div class="form-card">
|
|
<h3>Add Catalog Entry</h3>
|
|
<div class="form-row">
|
|
<div class="field"><label>ID (slug)</label><input id="nc-id" placeholder="my-agent"></div>
|
|
<div class="field"><label>Name</label><input id="nc-name" placeholder="My Agent"></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="field"><label>Category</label><select id="nc-cat"><option value="data">Data</option><option value="briefing">Briefing</option><option value="utility">Utility</option></select></div>
|
|
<div class="field"><label>Sub-agent?</label><select id="nc-sub"><option value="false">No</option><option value="true">Yes</option></select></div>
|
|
</div>
|
|
<div class="form-row full"><div class="field"><label>Description</label><input id="nc-desc" placeholder="What this agent does"></div></div>
|
|
<button class="btn btn-primary" onclick="createCatalog()">Add</button>
|
|
<span class="msg" id="nc-msg"></span>
|
|
</div>
|
|
<table id="catalog-table"><thead><tr><th>ID</th><th>Name</th><th>Category</th><th>Type</th><th>Actions</th></tr></thead><tbody></tbody></table>
|
|
</div>
|
|
|
|
<!-- LLM Providers -->
|
|
<div class="panel" id="panel-llm">
|
|
<div class="form-card">
|
|
<h3>Add LLM Provider</h3>
|
|
<div class="form-row">
|
|
<div class="field"><label>Name</label><input id="nl-name" placeholder="Anthropic"></div>
|
|
<div class="field"><label>Type</label><select id="nl-type"><option value="anthropic">Anthropic</option><option value="openai">OpenAI</option><option value="litellm">LiteLLM</option><option value="ollama">Ollama</option></select></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="field"><label>API URL</label><input id="nl-url" placeholder="https://api.anthropic.com"></div>
|
|
<div class="field"><label>Default Model</label><input id="nl-model" placeholder="claude-sonnet-4-5-20250514"></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="field"><label>API Key</label><input type="password" id="nl-key"></div>
|
|
<div class="field"><label>Default?</label><select id="nl-default"><option value="false">No</option><option value="true">Yes</option></select></div>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="createProvider()">Add</button>
|
|
<span class="msg" id="nl-msg"></span>
|
|
</div>
|
|
<table id="llm-table"><thead><tr><th>Name</th><th>Type</th><th>URL</th><th>Model</th><th>Default</th><th>Actions</th></tr></thead><tbody></tbody></table>
|
|
</div>
|
|
|
|
<!-- Bridges -->
|
|
<div class="panel" id="panel-bridges">
|
|
<table id="bridges-table"><thead><tr><th>User</th><th>Hostname</th><th>URL</th><th>Platform</th><th>Status</th><th>Last Heartbeat</th><th>Capabilities</th></tr></thead><tbody></tbody></table>
|
|
</div>
|
|
|
|
<!-- System -->
|
|
<div class="panel" id="panel-system">
|
|
<div class="stat-grid" id="sys-stats"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API='';
|
|
function switchTab(name){
|
|
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
|
document.querySelector(`.tab[onclick*="${name}"]`).classList.add('active');
|
|
document.getElementById('panel-'+name).classList.add('active');
|
|
}
|
|
async function logout(){await fetch(API+'/api/logout',{method:'POST'});location.href='/login'}
|
|
function showMsg(id,text,ok){const el=document.getElementById(id);el.textContent=text;el.className='msg '+(ok?'ok':'err');setTimeout(()=>el.textContent='',3000)}
|
|
|
|
// --- Users ---
|
|
async function loadUsers(){
|
|
const res=await fetch(API+'/api/admin/users');if(res.status===403||res.status===401){location.href='/';return}
|
|
const users=await res.json();
|
|
document.querySelector('#users-table tbody').innerHTML=users.map(u=>`<tr>
|
|
<td>${u.username}</td><td style="font-size:.8rem">${u.email||'<span style="color:var(--yellow)">none</span>'}</td><td>${u.display_name}</td><td><span class="badge ${u.role}">${u.role}</span></td>
|
|
<td>${u.instance_count}</td>
|
|
<td><div class="action-btns"><button class="btn btn-danger btn-sm" onclick="deleteUser(${u.id},'${u.username}')">Delete</button></div></td>
|
|
</tr>`).join('');
|
|
}
|
|
async function createUser(){
|
|
const res=await fetch(API+'/api/admin/users',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
username:document.getElementById('nu-user').value,email:document.getElementById('nu-email').value,
|
|
password:document.getElementById('nu-pass').value,
|
|
display_name:document.getElementById('nu-name').value,role:document.getElementById('nu-role').value})});
|
|
if(res.ok){showMsg('nu-msg','Created',true);loadUsers();['nu-user','nu-email','nu-pass','nu-name'].forEach(id=>document.getElementById(id).value='')}
|
|
else{const e=await res.json();showMsg('nu-msg',e.detail||'Error',false)}
|
|
}
|
|
async function deleteUser(id,name){if(!confirm('Delete user '+name+'?'))return;await fetch(API+'/api/admin/users/'+id,{method:'DELETE'});loadUsers()}
|
|
|
|
// --- Catalog ---
|
|
async function loadCatalog(){
|
|
const res=await fetch(API+'/api/admin/users');// check auth
|
|
const cres=await fetch(API+'/api/catalog');
|
|
const catalog=await cres.json();
|
|
document.querySelector('#catalog-table tbody').innerHTML=catalog.map(c=>`<tr>
|
|
<td><code>${c.id}</code></td><td>${c.name}</td><td><span class="badge ${c.category}">${c.category}</span></td>
|
|
<td>${c.is_sub_agent?'Sub-agent':'Standalone'}</td>
|
|
<td><button class="btn btn-danger btn-sm" onclick="deleteCatalog('${c.id}')">Delete</button></td>
|
|
</tr>`).join('');
|
|
}
|
|
async function createCatalog(){
|
|
const res=await fetch(API+'/api/admin/catalog',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
id:document.getElementById('nc-id').value,name:document.getElementById('nc-name').value,
|
|
description:document.getElementById('nc-desc').value,category:document.getElementById('nc-cat').value,
|
|
is_sub_agent:document.getElementById('nc-sub').value==='true'})});
|
|
if(res.ok){showMsg('nc-msg','Added',true);loadCatalog()}
|
|
else{const e=await res.json();showMsg('nc-msg',e.detail||'Error',false)}
|
|
}
|
|
async function deleteCatalog(id){if(!confirm('Delete catalog entry '+id+'?'))return;await fetch(API+'/api/admin/catalog/'+id,{method:'DELETE'});loadCatalog()}
|
|
|
|
// --- LLM Providers ---
|
|
async function loadProviders(){
|
|
const res=await fetch(API+'/api/admin/llm-providers');
|
|
const providers=await res.json();
|
|
document.querySelector('#llm-table tbody').innerHTML=providers.map(p=>`<tr>
|
|
<td>${p.name}</td><td>${p.provider_type}</td><td style="font-size:.8rem">${p.api_url||'-'}</td>
|
|
<td>${p.default_model||'-'}</td><td>${p.is_default?'Yes':'-'}</td>
|
|
<td><button class="btn btn-danger btn-sm" onclick="deleteProvider(${p.id})">Delete</button></td>
|
|
</tr>`).join('');
|
|
}
|
|
async function createProvider(){
|
|
const res=await fetch(API+'/api/admin/llm-providers',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
name:document.getElementById('nl-name').value,provider_type:document.getElementById('nl-type').value,
|
|
api_url:document.getElementById('nl-url').value,api_key:document.getElementById('nl-key').value,
|
|
default_model:document.getElementById('nl-model').value,is_default:document.getElementById('nl-default').value==='true'})});
|
|
if(res.ok){showMsg('nl-msg','Added',true);loadProviders()}
|
|
else{const e=await res.json();showMsg('nl-msg',e.detail||'Error',false)}
|
|
}
|
|
async function deleteProvider(id){if(!confirm('Delete this provider?'))return;await fetch(API+'/api/admin/llm-providers/'+id,{method:'DELETE'});loadProviders()}
|
|
|
|
// --- Bridges ---
|
|
async function loadBridges(){
|
|
const res=await fetch(API+'/api/admin/bridges');
|
|
if(!res.ok)return;
|
|
const bridges=await res.json();
|
|
const tbody=document.querySelector('#bridges-table tbody');
|
|
if(!bridges.length){tbody.innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--text-dim)">No bridges connected</td></tr>';return}
|
|
tbody.innerHTML=bridges.map(b=>`<tr>
|
|
<td>${b.username}</td>
|
|
<td>${b.hostname||'-'}</td>
|
|
<td style="font-size:.8rem">${b.bridge_url||'-'}</td>
|
|
<td>${b.platform}</td>
|
|
<td><span class="badge ${b.status==='online'?'user':'admin'}">${b.status}</span></td>
|
|
<td style="font-size:.8rem">${b.last_heartbeat||'-'}</td>
|
|
<td style="font-size:.8rem">${(b.capabilities||[]).join(', ')}</td>
|
|
</tr>`).join('');
|
|
}
|
|
|
|
// --- System ---
|
|
async function loadSystem(){
|
|
const[usersRes,instRes]=await Promise.all([fetch(API+'/api/admin/users'),fetch(API+'/api/health')]);
|
|
const users=await usersRes.json();const health=await instRes.json();
|
|
document.getElementById('sys-stats').innerHTML=`
|
|
<div class="stat-card"><div class="val">${users.length}</div><div class="lbl">Users</div></div>
|
|
<div class="stat-card"><div class="val">${users.reduce((s,u)=>s+u.instance_count,0)}</div><div class="lbl">Agent Instances</div></div>
|
|
<div class="stat-card"><div class="val">${health.version||'?'}</div><div class="lbl">Version</div></div>
|
|
<div class="stat-card"><div class="val" style="color:var(--green)">OK</div><div class="lbl">Status</div></div>`;
|
|
}
|
|
|
|
// Init
|
|
loadUsers();loadCatalog();loadProviders();loadBridges();loadSystem();
|
|
</script>
|
|
</body>
|
|
</html>
|