feat(update): stable/beta update channel; stable follows release tags (#684)

Adds auto_update.channel: stable follows the newest vX.Y.Z release tag
(detached HEAD; pre-releases and other tags ignored), beta follows main as
before. Nothing ever moves a device backwards: a checkout newer than the
newest release keeps following main (or stays put when detached) until a
release contains its commit. Legacy configs migrate to stable when they
reach a release. Update Code, the weekly updater's preflight, and the
verifier's rollback (back to old_ref: branch or detached release) all
honour the channel. General tab Update Channel select, GET/POST
/api/v3/system/update-channel, release-aware Overview banner and Tools git
panel. New installs default to stable.

Rig fix (ledpi): /system/check-update reports update_available: false when
the channel's action is none (a detached HEAD newer than the newest
release), matching Update Code; the Tools panel no longer calls every
detached HEAD "a release".

Merged with main through #687 (heartbeat verifier, #683 login, #688
plugin_catalog, #685 Tailwind build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-30 15:35:26 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 64c7289593
commit 7804ea8f69
21 changed files with 1499 additions and 50 deletions
+15 -1
View File
@@ -31,7 +31,7 @@ FORM_SECTION_FIELD = '__form_section'
#: Fields of the General tab. Any one of them in a /config/main post means the
#: General form was submitted, so its unchecked checkboxes read as False.
GENERAL_FIELDS = ('timezone', 'city', 'state', 'country', 'web_display_autostart',
'plugins_directory', 'auto_update_enabled')
'plugins_directory', 'auto_update_enabled', 'auto_update_channel')
#: Top-level fields save_main_config stores somewhere of its own (location,
#: plugin_system, ...), never as a config key of the same name.
@@ -522,6 +522,20 @@ def save_main_config():
if not isinstance(current_config.get('auto_update'), dict):
current_config['auto_update'] = {}
_set_checkbox(current_config['auto_update'], 'enabled', 'auto_update_enabled')
if 'auto_update_channel' in data:
# stable/beta (web_interface/update_channel.py). Only stored
# here; the next update applies it, never moving backwards.
from web_interface import update_channel
channel = update_channel.normalize_channel(data['auto_update_channel'])
if channel is None:
return jsonify({'status': 'error',
'message': "auto_update_channel must be 'stable' or 'beta'"}), 400
if not isinstance(current_config.get('auto_update'), dict):
current_config['auto_update'] = {}
if current_config['auto_update'].get('channel') != channel:
current_config['auto_update']['channel'] = channel
# The Overview banner compares against the channel's target.
_pkg._update_check_cache['result'] = None
if 'timezone' in data:
current_config['timezone'] = data['timezone']