Bridge distribution: auth, heartbeat, installer script, per-user ports
This commit is contained in:
@@ -64,6 +64,7 @@ tr:hover td{background:var(--surface2)}
|
||||
<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>
|
||||
|
||||
@@ -126,6 +127,11 @@ tr:hover td{background:var(--surface2)}
|
||||
<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>
|
||||
@@ -203,6 +209,24 @@ async function createProvider(){
|
||||
}
|
||||
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')]);
|
||||
@@ -215,7 +239,7 @@ async function loadSystem(){
|
||||
}
|
||||
|
||||
// Init
|
||||
loadUsers();loadCatalog();loadProviders();loadSystem();
|
||||
loadUsers();loadCatalog();loadProviders();loadBridges();loadSystem();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -87,6 +87,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
||||
.cal-item .cal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
|
||||
.cal-item .cal-remove{background:none;border:none;color:var(--red);cursor:pointer;font-size:.85rem}
|
||||
|
||||
.bridge-bar{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1rem 1.25rem;margin-bottom:1.5rem;display:flex;justify-content:space-between;align-items:center}
|
||||
.bridge-bar .bridge-info{display:flex;align-items:center;gap:.75rem;font-size:.85rem}
|
||||
.bridge-bar .bridge-dot{width:8px;height:8px;border-radius:50%}
|
||||
.bridge-bar .bridge-dot.online{background:var(--green)}
|
||||
.bridge-bar .bridge-dot.offline{background:var(--red)}
|
||||
.bridge-bar .bridge-actions{display:flex;gap:.5rem}
|
||||
|
||||
.empty-state{text-align:center;padding:3rem;color:var(--text-dim)}
|
||||
.empty-state h3{margin-bottom:.5rem;color:var(--text)}
|
||||
.time-ago{color:var(--text-dim)}
|
||||
@@ -105,6 +112,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="bridge-bar"></div>
|
||||
<div class="section-header">
|
||||
<h2>My Agents</h2>
|
||||
<button class="btn-secondary" onclick="showCatalog()">+ Add Agent</button>
|
||||
@@ -332,11 +340,51 @@ async function enableAgent(catalogId,name){
|
||||
if(res.ok){closeModal();refresh()}
|
||||
}
|
||||
|
||||
// --- Bridge ---
|
||||
async function loadBridge(){
|
||||
try{
|
||||
const res=await fetch(API+'/api/bridge/me');
|
||||
if(!res.ok)return;
|
||||
const b=await res.json();
|
||||
const el=document.getElementById('bridge-bar');
|
||||
if(!b.connected){
|
||||
el.innerHTML=`<div class="bridge-bar">
|
||||
<div class="bridge-info"><div class="bridge-dot offline"></div><span>Mac Bridge: Not installed</span></div>
|
||||
<div class="bridge-actions"><button class="btn-save" onclick="installBridge()">Install Mac Bridge</button></div>
|
||||
</div>`;
|
||||
} else {
|
||||
const ago=timeAgo(b.last_heartbeat);
|
||||
el.innerHTML=`<div class="bridge-bar">
|
||||
<div class="bridge-info">
|
||||
<div class="bridge-dot ${b.status}"></div>
|
||||
<span>Mac Bridge: <strong>${b.status}</strong> on ${b.hostname||'unknown'}</span>
|
||||
<span style="color:var(--text-dim)">${b.bridge_url} · heartbeat ${ago}</span>
|
||||
<span style="color:var(--text-dim)">${(b.capabilities||[]).join(', ')}</span>
|
||||
</div>
|
||||
<div class="bridge-actions">
|
||||
<button class="btn-secondary" onclick="installBridge()">Reinstall</button>
|
||||
<button class="btn-secondary" style="border-color:var(--red);color:var(--red)" onclick="disconnectBridge()">Disconnect</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}catch(e){console.error('Bridge check failed:',e)}
|
||||
}
|
||||
|
||||
async function installBridge(){
|
||||
window.open(API+'/api/bridge/install-script','_blank');
|
||||
}
|
||||
|
||||
async function disconnectBridge(){
|
||||
if(!confirm('Disconnect your Mac Bridge? You can reinstall later.'))return;
|
||||
await fetch(API+'/api/bridge/me',{method:'DELETE'});
|
||||
loadBridge();
|
||||
}
|
||||
|
||||
async function refresh(){
|
||||
try{
|
||||
const[instRes,runsRes,meRes]=await Promise.all([fetch(API+'/api/instances'),fetch(API+'/api/runs?limit=25'),fetch(API+'/api/me')]);
|
||||
if(instRes.status===401||runsRes.status===401){location.href='/login';return}
|
||||
renderInstances(await instRes.json());renderRuns(await runsRes.json());
|
||||
renderInstances(await instRes.json());renderRuns(await runsRes.json());loadBridge();
|
||||
if(meRes.ok){
|
||||
currentUser=await meRes.json();
|
||||
document.getElementById('user-display').textContent=currentUser.display_name||currentUser.username;
|
||||
|
||||
Reference in New Issue
Block a user