0.32.0: StackPilot self-update (check on page load + one-click update)
- backend/version.py is now the single version source (main.py, agent). - GET /api/system/update: reads the version tags of the backend's own image repo (anonymous v2 token flow, https→http fallback for insecure registries), compares the highest semver tag against APP_VERSION; reports update_supported from the container's compose labels. 10 min cache. - POST /api/system/update (admin, audited): spawns a detached helper container from the current backend image that runs docker compose pull && up -d on StackPilot's own compose project (project name, working dir and config files resolved from its own container labels) — the helper outlives the backend being recreated. Non-compose installs get a 400. - /api/health now returns the version so the UI can detect the switchover. - TopNav version badge: queries the update status on page load; when a newer release exists an amber pill shows the version — one click (admin) confirms, triggers the update and overlays a wait screen that polls /api/health and reloads once the new version answers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
11effdc2ca
commit
a0dda120f5
@@ -4,13 +4,15 @@ from __future__ import annotations
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlmodel import Session
|
||||
|
||||
from auth import get_current_user
|
||||
from auth import get_current_user, require_admin
|
||||
from config import settings
|
||||
from database import get_session
|
||||
from docker_client import DockerError, get_client, safe_call
|
||||
from models.user import User
|
||||
from services import device_service, gpu_service
|
||||
from services import audit_service, device_service, gpu_service, self_update_service
|
||||
|
||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||
|
||||
@@ -103,3 +105,31 @@ def gpus(_user: User = Depends(get_current_user)) -> list[dict]:
|
||||
def devices(_user: User = Depends(get_current_user)) -> dict:
|
||||
"""List host USB / serial / DRI devices for passthrough."""
|
||||
return device_service.detect_devices()
|
||||
|
||||
|
||||
@router.get("/update")
|
||||
async def self_update_status(
|
||||
refresh: bool = False,
|
||||
_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Is a newer StackPilot release available? (registry check, cached)"""
|
||||
return await self_update_service.get_status(refresh=refresh)
|
||||
|
||||
|
||||
@router.post("/update")
|
||||
def self_update_apply(
|
||||
request: Request,
|
||||
session: Session = Depends(get_session),
|
||||
user: User = Depends(require_admin),
|
||||
) -> dict:
|
||||
"""Update this StackPilot in place via a detached compose helper."""
|
||||
try:
|
||||
result = self_update_service.apply_update()
|
||||
except self_update_service.SelfUpdateError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
audit_service.record(
|
||||
session, user=user.username, action="system.update",
|
||||
target=result.get("helper", ""), detail=result.get("command"),
|
||||
ip=request.client.host if request.client else "",
|
||||
)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user