Bridge distribution: auth, heartbeat, installer script, per-user ports
This commit is contained in:
+225
-1
@@ -11,7 +11,7 @@ import os
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from database import get_db, init_db
|
from database import get_db, init_db
|
||||||
from models import User, AgentCatalog, AgentInstance, Run, LLMProvider
|
from models import User, AgentCatalog, AgentInstance, Run, LLMProvider, Bridge
|
||||||
|
|
||||||
app = FastAPI(title="Agent Command Center", version="2026.04.12.01")
|
app = FastAPI(title="Agent Command Center", version="2026.04.12.01")
|
||||||
|
|
||||||
@@ -479,6 +479,230 @@ def admin_delete_catalog(catalog_id: str, admin: dict = Depends(require_admin),
|
|||||||
return {"status": "deleted"}
|
return {"status": "deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Bridge Management ---
|
||||||
|
|
||||||
|
class BridgeRegister(BaseModel):
|
||||||
|
user_id: int
|
||||||
|
api_key: str
|
||||||
|
bridge_url: str
|
||||||
|
hostname: str = ""
|
||||||
|
platform: str = "macos"
|
||||||
|
capabilities: list = []
|
||||||
|
|
||||||
|
|
||||||
|
class BridgeHeartbeat(BaseModel):
|
||||||
|
bridge_url: str = ""
|
||||||
|
capabilities: list = []
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/bridge/register")
|
||||||
|
def register_bridge(data: BridgeRegister, db: Session = Depends(get_db)):
|
||||||
|
"""Called by bridge installer to register with the dashboard."""
|
||||||
|
user = db.query(User).filter(User.id == data.user_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
# Remove existing bridge for this user (one bridge per user)
|
||||||
|
db.query(Bridge).filter(Bridge.user_id == data.user_id).delete()
|
||||||
|
bridge = Bridge(
|
||||||
|
user_id=data.user_id,
|
||||||
|
api_key=data.api_key,
|
||||||
|
bridge_url=data.bridge_url,
|
||||||
|
hostname=data.hostname,
|
||||||
|
platform=data.platform,
|
||||||
|
capabilities=data.capabilities,
|
||||||
|
status="online",
|
||||||
|
last_heartbeat=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
db.add(bridge)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "registered", "bridge_id": bridge.id}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/bridge/heartbeat")
|
||||||
|
def bridge_heartbeat(data: BridgeHeartbeat, request_api_key: str = None,
|
||||||
|
db: Session = Depends(get_db)):
|
||||||
|
"""Called periodically by the bridge to update its status and IP."""
|
||||||
|
# Auth via X-Bridge-Token header
|
||||||
|
from fastapi import Request
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/bridge/{api_key}/heartbeat")
|
||||||
|
def bridge_heartbeat_keyed(api_key: str, data: BridgeHeartbeat, db: Session = Depends(get_db)):
|
||||||
|
"""Heartbeat endpoint keyed by bridge API key."""
|
||||||
|
bridge = db.query(Bridge).filter(Bridge.api_key == api_key).first()
|
||||||
|
if not bridge:
|
||||||
|
raise HTTPException(status_code=404, detail="Bridge not found")
|
||||||
|
if data.bridge_url:
|
||||||
|
bridge.bridge_url = data.bridge_url
|
||||||
|
if data.capabilities:
|
||||||
|
bridge.capabilities = data.capabilities
|
||||||
|
bridge.status = "online"
|
||||||
|
bridge.last_heartbeat = datetime.now(timezone.utc)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/bridge/me")
|
||||||
|
def get_my_bridge(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||||
|
"""Get the current user's bridge info."""
|
||||||
|
bridge = db.query(Bridge).filter(Bridge.user_id == user["user_id"]).first()
|
||||||
|
if not bridge:
|
||||||
|
return {"connected": False}
|
||||||
|
# Mark offline if no heartbeat in 10 minutes
|
||||||
|
if bridge.last_heartbeat:
|
||||||
|
age = (datetime.now(timezone.utc) - bridge.last_heartbeat).total_seconds()
|
||||||
|
if age > 600:
|
||||||
|
bridge.status = "offline"
|
||||||
|
db.commit()
|
||||||
|
return {
|
||||||
|
"connected": True,
|
||||||
|
"bridge_url": bridge.bridge_url,
|
||||||
|
"hostname": bridge.hostname,
|
||||||
|
"platform": bridge.platform,
|
||||||
|
"capabilities": bridge.capabilities or [],
|
||||||
|
"status": bridge.status,
|
||||||
|
"last_heartbeat": bridge.last_heartbeat.isoformat() if bridge.last_heartbeat else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/bridge/me")
|
||||||
|
def disconnect_bridge(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||||
|
"""Disconnect the current user's bridge."""
|
||||||
|
db.query(Bridge).filter(Bridge.user_id == user["user_id"]).delete()
|
||||||
|
db.commit()
|
||||||
|
return {"status": "disconnected"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/users/{user_id}/bridge")
|
||||||
|
def get_user_bridge(user_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Internal endpoint for agents to get a user's bridge URL + auth."""
|
||||||
|
bridge = db.query(Bridge).filter(Bridge.user_id == user_id).first()
|
||||||
|
if not bridge or bridge.status == "offline":
|
||||||
|
return {"available": False}
|
||||||
|
return {
|
||||||
|
"available": True,
|
||||||
|
"bridge_url": bridge.bridge_url,
|
||||||
|
"api_key": bridge.api_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/bridge/install-script")
|
||||||
|
def get_install_script(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||||
|
"""Generate a personalized bridge installer script for the current user."""
|
||||||
|
user_id = user["user_id"]
|
||||||
|
username = user["username"]
|
||||||
|
api_key = secrets.token_urlsafe(32)
|
||||||
|
dashboard_url = "https://agents.jfamily.io"
|
||||||
|
|
||||||
|
script = f'''#!/bin/bash
|
||||||
|
# Agent Command Center — Mac Bridge Installer
|
||||||
|
# Generated for: {username} (user {user_id})
|
||||||
|
# Run this on your Mac to install the Apple Ecosystem Bridge.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
echo "Installing Agent Command Center Mac Bridge for {username}..."
|
||||||
|
|
||||||
|
BRIDGE_DIR="$HOME/.local/apple-bridge"
|
||||||
|
mkdir -p "$BRIDGE_DIR"
|
||||||
|
|
||||||
|
# Download bridge.py
|
||||||
|
cat > "$BRIDGE_DIR/bridge.py" << 'BRIDGE_EOF'
|
||||||
|
BRIDGE_PLACEHOLDER
|
||||||
|
BRIDGE_EOF
|
||||||
|
|
||||||
|
# Create config
|
||||||
|
cat > "$BRIDGE_DIR/config.json" << 'CONFIG_EOF'
|
||||||
|
{{
|
||||||
|
"user_id": {user_id},
|
||||||
|
"username": "{username}",
|
||||||
|
"api_key": "{api_key}",
|
||||||
|
"dashboard_url": "{dashboard_url}",
|
||||||
|
"port": {8551 + user_id}
|
||||||
|
}}
|
||||||
|
CONFIG_EOF
|
||||||
|
|
||||||
|
# Create launcher
|
||||||
|
cat > "$BRIDGE_DIR/start-bridge.sh" << 'LAUNCHER_EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
exec /opt/homebrew/bin/python3 "$HOME/.local/apple-bridge/bridge.py"
|
||||||
|
LAUNCHER_EOF
|
||||||
|
chmod +x "$BRIDGE_DIR/start-bridge.sh"
|
||||||
|
|
||||||
|
# Create launchd plist
|
||||||
|
PLIST="$HOME/Library/LaunchAgents/com.jfamily.apple-bridge.plist"
|
||||||
|
cat > "$PLIST" << PLIST_EOF
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.jfamily.apple-bridge</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>$BRIDGE_DIR/start-bridge.sh</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/tmp/apple-bridge.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/tmp/apple-bridge.err</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
PLIST_EOF
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
echo "Installing Python dependencies..."
|
||||||
|
/opt/homebrew/bin/pip3 install --break-system-packages -q fastapi uvicorn 2>/dev/null || pip3 install --break-system-packages -q fastapi uvicorn
|
||||||
|
|
||||||
|
# Start the bridge
|
||||||
|
echo "Starting bridge..."
|
||||||
|
launchctl unload "$PLIST" 2>/dev/null
|
||||||
|
launchctl load "$PLIST"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Register with dashboard
|
||||||
|
BRIDGE_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "unknown")
|
||||||
|
BRIDGE_PORT={8551 + user_id}
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
|
||||||
|
echo "Registering with dashboard..."
|
||||||
|
curl -s -X POST "{dashboard_url}/api/bridge/register" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{{"user_id": {user_id}, "api_key": "{api_key}", "bridge_url": "http://'$BRIDGE_IP':'$BRIDGE_PORT'", "hostname": "'$HOSTNAME'", "platform": "macos", "capabilities": ["notes", "reading-list"]}}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===================================="
|
||||||
|
echo " Mac Bridge installed for {username}!"
|
||||||
|
echo " Bridge URL: http://$BRIDGE_IP:$BRIDGE_PORT"
|
||||||
|
echo " Dashboard: {dashboard_url}"
|
||||||
|
echo "===================================="
|
||||||
|
'''
|
||||||
|
from fastapi.responses import PlainTextResponse
|
||||||
|
return PlainTextResponse(script, media_type="text/x-shellscript",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename=install-bridge-{username}.sh"})
|
||||||
|
|
||||||
|
|
||||||
|
# Admin: Bridge overview
|
||||||
|
@app.get("/api/admin/bridges")
|
||||||
|
def admin_list_bridges(admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||||
|
bridges = db.query(Bridge).all()
|
||||||
|
return [{
|
||||||
|
"id": b.id,
|
||||||
|
"user_id": b.user_id,
|
||||||
|
"username": db.query(User).filter(User.id == b.user_id).first().username if db.query(User).filter(User.id == b.user_id).first() else "?",
|
||||||
|
"bridge_url": b.bridge_url,
|
||||||
|
"hostname": b.hostname,
|
||||||
|
"platform": b.platform,
|
||||||
|
"capabilities": b.capabilities or [],
|
||||||
|
"status": b.status,
|
||||||
|
"last_heartbeat": b.last_heartbeat.isoformat() if b.last_heartbeat else None,
|
||||||
|
} for b in bridges]
|
||||||
|
|
||||||
|
|
||||||
# --- Static files ---
|
# --- Static files ---
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|||||||
@@ -65,6 +65,23 @@ class Run(Base):
|
|||||||
instance = relationship("AgentInstance", back_populates="runs")
|
instance = relationship("AgentInstance", back_populates="runs")
|
||||||
|
|
||||||
|
|
||||||
|
class Bridge(Base):
|
||||||
|
__tablename__ = "bridges"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
api_key = Column(String, nullable=False) # Auth token for bridge requests
|
||||||
|
bridge_url = Column(String, default="") # http://ip:port
|
||||||
|
hostname = Column(String, default="") # e.g. "Jungbauers-MBP"
|
||||||
|
platform = Column(String, default="macos") # macos, ios (future)
|
||||||
|
capabilities = Column(JSON, default=list) # ["notes", "reading-list"]
|
||||||
|
status = Column(String, default="offline") # online, offline
|
||||||
|
last_heartbeat = Column(DateTime, nullable=True)
|
||||||
|
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
user = relationship("User")
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(Base):
|
class LLMProvider(Base):
|
||||||
__tablename__ = "llm_providers"
|
__tablename__ = "llm_providers"
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ tr:hover td{background:var(--surface2)}
|
|||||||
<div class="tab active" onclick="switchTab('users')">Users</div>
|
<div class="tab active" onclick="switchTab('users')">Users</div>
|
||||||
<div class="tab" onclick="switchTab('catalog')">Agent Catalog</div>
|
<div class="tab" onclick="switchTab('catalog')">Agent Catalog</div>
|
||||||
<div class="tab" onclick="switchTab('llm')">LLM Providers</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 class="tab" onclick="switchTab('system')">System</div>
|
||||||
</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>
|
<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>
|
</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 -->
|
<!-- System -->
|
||||||
<div class="panel" id="panel-system">
|
<div class="panel" id="panel-system">
|
||||||
<div class="stat-grid" id="sys-stats"></div>
|
<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()}
|
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 ---
|
// --- System ---
|
||||||
async function loadSystem(){
|
async function loadSystem(){
|
||||||
const[usersRes,instRes]=await Promise.all([fetch(API+'/api/admin/users'),fetch(API+'/api/health')]);
|
const[usersRes,instRes]=await Promise.all([fetch(API+'/api/admin/users'),fetch(API+'/api/health')]);
|
||||||
@@ -215,7 +239,7 @@ async function loadSystem(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
loadUsers();loadCatalog();loadProviders();loadSystem();
|
loadUsers();loadCatalog();loadProviders();loadBridges();loadSystem();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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-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}
|
.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{text-align:center;padding:3rem;color:var(--text-dim)}
|
||||||
.empty-state h3{margin-bottom:.5rem;color:var(--text)}
|
.empty-state h3{margin-bottom:.5rem;color:var(--text)}
|
||||||
.time-ago{color:var(--text-dim)}
|
.time-ago{color:var(--text-dim)}
|
||||||
@@ -105,6 +112,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div id="bridge-bar"></div>
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>My Agents</h2>
|
<h2>My Agents</h2>
|
||||||
<button class="btn-secondary" onclick="showCatalog()">+ Add Agent</button>
|
<button class="btn-secondary" onclick="showCatalog()">+ Add Agent</button>
|
||||||
@@ -332,11 +340,51 @@ async function enableAgent(catalogId,name){
|
|||||||
if(res.ok){closeModal();refresh()}
|
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(){
|
async function refresh(){
|
||||||
try{
|
try{
|
||||||
const[instRes,runsRes,meRes]=await Promise.all([fetch(API+'/api/instances'),fetch(API+'/api/runs?limit=25'),fetch(API+'/api/me')]);
|
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}
|
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){
|
if(meRes.ok){
|
||||||
currentUser=await meRes.json();
|
currentUser=await meRes.json();
|
||||||
document.getElementById('user-display').textContent=currentUser.display_name||currentUser.username;
|
document.getElementById('user-display').textContent=currentUser.display_name||currentUser.username;
|
||||||
|
|||||||
Reference in New Issue
Block a user