feat: add auto-update detection via version check API

Adds GET /api/version endpoint that compares local git commit
against origin/HEAD. Dashboard shows a clickable update banner
when new commits are available. Also adds POST /api/git/pull
for one-click updates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 16:15:03 +08:00
co-authored by Claude Opus 4.7
parent 988b8cc27b
commit cb172ab88a
2 changed files with 79 additions and 0 deletions
+28
View File
@@ -157,6 +157,9 @@
<div class="header-left">
<div class="logo"><span>OpsPilot</span></div>
<span><span class="dot live" id="live-dot"></span> <span id="conn-text" style="font-size:11px;color:var(--text-muted);">Connecting...</span></span>
<span id="update-banner" style="display:none;font-size:10px;background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:4px;cursor:pointer;" onclick="runGitPull()" title="Click to git pull">
⬆ Update available · <span id="update-count"></span> commits behind
</span>
</div>
<div class="header-right">
<span>🖥 <strong id="stat-agents">0</strong> Agents</span>
@@ -905,8 +908,33 @@ initSplitter('splitter-1', '.col-left', 160);
// splitter-2: drag to resize col-right (min 300px)
initSplitter('splitter-2', '.col-right', 300);
// ── Version check ──
async function checkForUpdates() {
try {
const r = await fetch('/api/version');
const v = await r.json();
if (v.update_available) {
document.getElementById('update-banner').style.display = 'inline';
document.getElementById('update-count').textContent = v.commits_behind;
}
} catch(e) {}
}
async function runGitPull() {
toast('Pulling latest code...');
try {
const r = await api('POST', '/api/git/pull');
toast(r.message || 'Updated! Please restart the server.');
document.getElementById('update-banner').style.display = 'none';
} catch(e) {
toast('Pull failed. Run manually: cd OpsPilot && git pull');
}
}
// Init
connect();
checkForUpdates();
setInterval(checkForUpdates, 300000); // Check every 5 min
setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) ws.send('ping'); }, 8000);
setInterval(() => {
document.getElementById('stat-time').textContent = new Date().toLocaleTimeString('zh-CN');
+51
View File
@@ -140,6 +140,57 @@ async def get_state():
return await _build_state()
@app.post("/api/git/pull")
async def git_pull():
"""Pull latest code from GitHub."""
import subprocess as _sp
try:
r = _sp.run(["git", "pull", "origin", "main"], capture_output=True, text=True, cwd=str(BASE_DIR), timeout=30)
add_log("info", "update", f"git pull: {r.stdout.strip() or r.stderr.strip()}")
if "Already up to date" in r.stdout or "Already up-to-date" in r.stdout:
return {"status": "ok", "message": "Already up to date"}
return {"status": "ok", "message": r.stdout.strip() or "Updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/version")
async def check_version():
"""Check current version and compare with latest GitHub release."""
import subprocess as _sp
info = {
"current_commit": "",
"current_short": "",
"latest_commit": "",
"latest_short": "",
"update_available": False,
"commits_behind": 0,
}
try:
# Get current commit
r = _sp.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True, cwd=str(BASE_DIR), timeout=5)
if r.returncode == 0:
info["current_commit"] = r.stdout.strip()
info["current_short"] = info["current_commit"][:7]
# Get latest from remote (without pulling)
r2 = _sp.run(["git", "ls-remote", "origin", "HEAD"], capture_output=True, text=True, timeout=10)
if r2.returncode == 0 and r2.stdout.strip():
info["latest_commit"] = r2.stdout.split()[0]
info["latest_short"] = info["latest_commit"][:7]
if info["current_commit"] and info["latest_commit"] and info["current_commit"] != info["latest_commit"]:
# Count commits behind
r3 = _sp.run(
["git", "rev-list", "--count", f"{info['current_commit']}..{info['latest_commit']}"],
capture_output=True, text=True, timeout=5
)
if r3.returncode == 0:
info["commits_behind"] = int(r3.stdout.strip())
info["update_available"] = info["commits_behind"] > 0
except Exception:
pass
return info
@app.post("/api/agents")
async def register_agent(data: dict):
"""Register or update an agent."""