feature/app_update (#18)

implement slim update functinallity
This commit was merged in pull request #18.
This commit is contained in:
2026-01-05 16:10:25 +01:00
parent 51885c534f
commit dd02e0ddc3
2 changed files with 233 additions and 0 deletions

View File

@@ -916,6 +916,69 @@ with st.expander("System control", expanded=False):
except Exception as e:
st.error(f"Failed to update password: {e}")
st.subheader("Software Version")
# Show current version
try:
ver_resp = requests.get(f"{BACKEND_URL}/version", timeout=2)
if ver_resp.ok:
ver_data = ver_resp.json()
current_version = ver_data.get('version', 'unknown')
ver_type = ver_data.get('type', '')
ver_label = current_version if ver_type == 'tag' else f"{current_version} (dev)"
st.write(f"**Current version:** {ver_label}")
else:
st.write("**Current version:** unknown")
current_version = "unknown"
except Exception:
st.write("**Current version:** unknown")
current_version = "unknown"
# Initialize session state for update check
if 'available_update' not in st.session_state:
st.session_state['available_update'] = None
col_check, col_status = st.columns([1, 2])
with col_check:
if st.button("Check for updates"):
try:
check_resp = requests.get(f"{BACKEND_URL}/check_update", timeout=30)
if check_resp.ok:
check_data = check_resp.json()
if check_data.get('error'):
st.session_state['available_update'] = {'error': check_data['error']}
else:
st.session_state['available_update'] = check_data
else:
st.session_state['available_update'] = {'error': f"Failed: {check_resp.status_code}"}
except Exception as e:
st.session_state['available_update'] = {'error': str(e)}
st.rerun()
with col_status:
if st.session_state['available_update']:
upd = st.session_state['available_update']
if upd.get('error'):
st.warning(f"Check failed: {upd['error']}")
elif upd.get('update_available'):
st.info(f"Update available: **{upd['available']}**")
else:
st.success("You are on the latest version.")
# Update button (only show if update is available)
if st.session_state['available_update'] and st.session_state['available_update'].get('update_available'):
if st.button("Update now"):
try:
r = requests.post(f"{BACKEND_URL}/system_update", timeout=120)
if r.ok:
result = r.json()
tag = result.get('tag', 'unknown')
st.success(f"Update to {tag} initiated. The UI will restart shortly.")
st.session_state['available_update'] = None
else:
st.error(f"Failed to update: {r.status_code} {r.text}")
except Exception as e:
st.error(f"Error calling update: {e}")
st.subheader("Reboot")
if st.button("Reboot now", type="primary"):
try:

View File

@@ -899,6 +899,176 @@ async def system_reboot():
raise HTTPException(status_code=500, detail=str(e))
@app.get("/version")
async def get_version():
"""Get the current software version (git tag or commit)."""
try:
# server -> auracast -> src -> bumble-auracast (3 levels up)
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
# Try to get the current tag
proc = await asyncio.create_subprocess_exec(
"git", "describe", "--tags", "--exact-match",
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
return {"version": stdout.decode().strip(), "type": "tag"}
# Fallback: get the current commit short hash
proc = await asyncio.create_subprocess_exec(
"git", "rev-parse", "--short", "HEAD",
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
return {"version": stdout.decode().strip(), "type": "commit"}
return {"version": "unknown", "type": "unknown"}
except Exception as e:
log.error("Exception in /version: %s", e, exc_info=True)
return {"version": "unknown", "type": "error"}
@app.get("/check_update")
async def check_update():
"""Check for available updates by comparing current version with latest release tag."""
try:
# server -> auracast -> src -> bumble-auracast (3 levels up)
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
# Fetch all tags and refs from origin
proc = await asyncio.create_subprocess_exec(
"git", "fetch", "--tags", "--force", "origin",
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.warning("git fetch failed: %s", stderr.decode())
# Get the latest tag sorted by version (semver-like sorting)
# This gets all tags, sorts them by version, and takes the last one
proc = await asyncio.create_subprocess_exec(
"git", "tag", "--sort=version:refname",
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
return {"available": None, "error": f"Could not list tags: {stderr.decode()}"}
tags = stdout.decode().strip().split('\n')
tags = [t for t in tags if t] # Filter empty strings
if not tags:
return {"available": None, "error": "No tags found in repository"}
latest_tag = tags[-1] # Last tag after version sorting
# Get current version
current_version = (await get_version()).get("version", "unknown")
update_available = latest_tag != current_version
return {
"current": current_version,
"available": latest_tag,
"update_available": update_available
}
except Exception as e:
log.error("Exception in /check_update: %s", e, exc_info=True)
return {"available": None, "error": str(e)}
@app.post("/system_update")
async def system_update():
"""Update application: git pull release branch (latest tag), poetry install, restart services."""
try:
# Best-effort: stop any active streaming cleanly
try:
await _stop_all()
except Exception:
pass
# server -> auracast -> src -> bumble-auracast (3 levels up)
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
# 1. Fetch and checkout the latest tag from the release branch
# First fetch all tags and the release branch
proc = await asyncio.create_subprocess_exec(
"git", "fetch", "--tags", "origin", "release",
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.error("git fetch failed: %s", stderr.decode())
raise HTTPException(status_code=500, detail=f"git fetch failed: {stderr.decode()}")
# Get the latest tag on the release branch
proc = await asyncio.create_subprocess_exec(
"git", "describe", "--tags", "--abbrev=0", "origin/release",
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.error("git describe failed: %s", stderr.decode())
raise HTTPException(status_code=500, detail=f"Failed to get latest tag: {stderr.decode()}")
latest_tag = stdout.decode().strip()
log.info("Updating to tag: %s", latest_tag)
# Checkout the latest tag
proc = await asyncio.create_subprocess_exec(
"git", "checkout", latest_tag,
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.error("git checkout failed: %s", stderr.decode())
raise HTTPException(status_code=500, detail=f"git checkout failed: {stderr.decode()}")
# 2. Run poetry install (use full path as poetry is in user's ~/.local/bin)
poetry_path = os.path.expanduser("~/.local/bin/poetry")
proc = await asyncio.create_subprocess_exec(
poetry_path, "install",
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
log.error("poetry install failed: %s", stderr.decode())
raise HTTPException(status_code=500, detail=f"poetry install failed: {stderr.decode()}")
# 3. Restart services via the update script
update_script = os.path.join(project_root, 'src', 'service', 'update_and_run_server_and_frontend.sh')
proc = await asyncio.create_subprocess_exec(
"bash", update_script,
cwd=project_root,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
# Don't wait for completion as we'll be restarted
await asyncio.sleep(0.5)
return {"status": "updating", "tag": latest_tag}
except HTTPException:
raise
except Exception as e:
log.error("Exception in /system_update: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
if __name__ == '__main__':
import os
os.chdir(os.path.dirname(__file__))