Enable true one-click panel update from Settings (v2.5.1).

Download release ZIP from git.evilfox.cc when git is unavailable, add upgrade_panel API, and show a primary Update panel button.
This commit is contained in:
orohi
2026-07-28 09:41:51 +03:00
parent 77c6f0dab7
commit 599e0a487c
9 changed files with 350 additions and 83 deletions
+57 -14
View File
@@ -102,7 +102,7 @@ else:
application_path = os.path.dirname(__file__)
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
CURRENT_VERSION = "v2.5.0"
CURRENT_VERSION = "v2.5.1"
RELEASES_REPO_URL = repo_url()
RELEASES_API_LATEST = api_latest_url()
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
@@ -5682,7 +5682,7 @@ async def api_check_updates(request: Request):
'releases_url': f'{RELEASES_REPO_URL}/releases',
'body': body,
'can_auto_update': bool(mode.get('can_auto_update')),
'update_mode': mode.get('mode'),
'update_mode': mode.get('update_method') or mode.get('mode'),
'mode': mode,
}
except Exception as e:
@@ -5690,26 +5690,28 @@ async def api_check_updates(request: Request):
return JSONResponse({'error': str(e)}, status_code=502)
async def _fetch_latest_release_tag() -> str:
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
resp = await client.get(RELEASES_API_LATEST, headers={'Accept': 'application/json'})
if resp.status_code >= 400:
raise RuntimeError(f'Release API returned HTTP {resp.status_code}')
data = resp.json() if resp.content else {}
return (data.get('tag_name') or data.get('name') or '').strip()
@app.post('/api/settings/apply_update', tags=["Settings"])
async def api_apply_update(request: Request, req: ApplyUpdateRequest):
"""Pull the selected release tag from git.evilfox.cc and restart the panel."""
"""Install a release tag (git checkout or archive download) and restart the panel."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
updater = UpdateManager(application_path, CURRENT_VERSION)
target = (req.target_version or '').strip()
if not target:
# Resolve latest if UI did not pass a tag
try:
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
resp = await client.get(
RELEASES_API_LATEST,
headers={'Accept': 'application/json'},
)
if resp.status_code < 400 and resp.content:
payload = resp.json()
target = (payload.get('tag_name') or payload.get('name') or '').strip()
except Exception:
target = await _fetch_latest_release_tag()
except Exception as e:
logger.exception('Failed to resolve latest tag for apply_update')
return JSONResponse({'error': str(e)}, status_code=502)
if not target:
return JSONResponse({'error': 'No target version specified'}, status_code=400)
@@ -5719,13 +5721,54 @@ async def api_apply_update(request: Request, req: ApplyUpdateRequest):
bool(req.allow_dirty),
)
if result.get('status') != 'success':
return JSONResponse({'error': result.get('message') or 'Update failed', **result}, status_code=500)
status_code = 409 if result.get('needs_confirm_dirty') else 500
return JSONResponse({'error': result.get('message') or 'Update failed', **result}, status_code=status_code)
if result.get('restart'):
UpdateManager.schedule_restart(1.5)
return result
@app.post('/api/settings/upgrade_panel', tags=["Settings"])
async def api_upgrade_panel(request: Request):
"""One-click: fetch latest release, install if newer, restart."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
updater = UpdateManager(application_path, CURRENT_VERSION)
mode = updater.detect_mode()
if not mode.get('can_auto_update'):
return JSONResponse({
'error': (
'In-panel update is not available for this install. '
'Use git clone / writable app directory, or rebuild Docker on the host.'
),
'mode': mode,
}, status_code=400)
try:
latest = await _fetch_latest_release_tag()
except Exception as e:
return JSONResponse({'error': str(e)}, status_code=502)
if not latest:
return JSONResponse({'error': 'Could not resolve latest release tag'}, status_code=502)
if not version_gt(latest, CURRENT_VERSION):
return {
'status': 'success',
'up_to_date': True,
'current_version': CURRENT_VERSION,
'latest_version': latest,
'message': 'Already on the latest version',
}
result = await asyncio.to_thread(updater.apply_update, latest, True)
if result.get('status') != 'success':
return JSONResponse({'error': result.get('message') or 'Update failed', **result}, status_code=500)
if result.get('restart'):
UpdateManager.schedule_restart(1.5)
result['up_to_date'] = False
result['previous_version'] = CURRENT_VERSION
return result
@app.get('/api/settings/tunnels/status', tags=["Settings"])
async def api_tunnels_status(request: Request):
if not _check_admin(request):