mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
feat(web): update flow installs changed Python dependencies automatically
The in-app updater (Update Now banner -> git_pull action) stashed, pulled, and purged plugins - but never touched Python dependencies. Any release adding a package (e.g. this branch's flask-compress, which lives in web_interface/requirements.txt) silently required an SSH session and a manual pip install that most users will never do. - git_pull now records HEAD before pulling; after a successful pull it diffs old..new and, if requirements.txt or web_interface/requirements.txt changed, installs exactly those via _pip_install_requirements - the same vetted root-visible sudo path the Tools-tab buttons use (with its existing graceful fallback when the sudo wrapper isn't configured). Results are appended to the update toast; a failure points the user at the Tools-tab button instead of failing the whole update. - install_base_requirements (Tools tab) now also installs web_interface/requirements.txt - previously it only covered the root file, so web-only dependencies were unreachable from the UI entirely. No install happens when the pull was already-up-to-date or when no requirements file changed, so routine updates stay fast. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e00d10ce60
commit
aec1368d63
@@ -1674,6 +1674,16 @@ def execute_system_action():
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
logger.warning("git stash timed out, proceeding with pull")
|
logger.warning("git stash timed out, proceeding with pull")
|
||||||
|
|
||||||
|
# Record HEAD before the pull so dependency changes can be detected
|
||||||
|
old_head = None
|
||||||
|
try:
|
||||||
|
_pre = subprocess.run(['git', 'rev-parse', 'HEAD'],
|
||||||
|
capture_output=True, text=True, timeout=10, cwd=project_dir)
|
||||||
|
if _pre.returncode == 0:
|
||||||
|
old_head = _pre.stdout.strip()
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("git rev-parse timed out before pull")
|
||||||
|
|
||||||
# Perform the git pull
|
# Perform the git pull
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['git', 'pull', '--rebase'],
|
['git', 'pull', '--rebase'],
|
||||||
@@ -1690,6 +1700,38 @@ def execute_system_action():
|
|||||||
pull_message = f"Code updated successfully. Local changes were automatically stashed.{stash_info}"
|
pull_message = f"Code updated successfully. Local changes were automatically stashed.{stash_info}"
|
||||||
if result.stdout and "Already up to date" not in result.stdout:
|
if result.stdout and "Already up to date" not in result.stdout:
|
||||||
pull_message = f"Code updated successfully.{stash_info}"
|
pull_message = f"Code updated successfully.{stash_info}"
|
||||||
|
|
||||||
|
# Keep Python dependencies in sync automatically: if the pull
|
||||||
|
# changed a requirements file, install it now — users updating
|
||||||
|
# from the web UI (most of them) never SSH in to pip install.
|
||||||
|
# Installs go through the same root-visible path as the
|
||||||
|
# Tools-tab buttons (_pip_install_requirements).
|
||||||
|
dep_notes = []
|
||||||
|
try:
|
||||||
|
_post = subprocess.run(['git', 'rev-parse', 'HEAD'],
|
||||||
|
capture_output=True, text=True, timeout=10, cwd=project_dir)
|
||||||
|
new_head = _post.stdout.strip() if _post.returncode == 0 else None
|
||||||
|
if old_head and new_head and old_head != new_head:
|
||||||
|
diff = subprocess.run(
|
||||||
|
['git', 'diff', '--name-only', f'{old_head}..{new_head}'],
|
||||||
|
capture_output=True, text=True, timeout=15, cwd=project_dir)
|
||||||
|
changed = set(diff.stdout.split()) if diff.returncode == 0 else set()
|
||||||
|
for rel in ('requirements.txt', 'web_interface/requirements.txt'):
|
||||||
|
req_path = PROJECT_ROOT / rel
|
||||||
|
if rel in changed and req_path.exists():
|
||||||
|
r = _pip_install_requirements(req_path, timeout=180)
|
||||||
|
if r.returncode == 0:
|
||||||
|
dep_notes.append(f"Dependencies from {rel} updated.")
|
||||||
|
else:
|
||||||
|
dep_notes.append(
|
||||||
|
f"Dependency install from {rel} failed — "
|
||||||
|
"run Install Base Requirements from the Tools tab.")
|
||||||
|
logger.warning("post-update pip install failed for %s: %s",
|
||||||
|
rel, _truncate_output(r.stdout, r.stderr))
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("post-update dependency sync timed out")
|
||||||
|
if dep_notes:
|
||||||
|
pull_message += " " + " ".join(dep_notes)
|
||||||
# A `git pull` restores built-in plugins (committed under
|
# A `git pull` restores built-in plugins (committed under
|
||||||
# plugin-repos/) even if the user uninstalled them. Re-remove
|
# plugin-repos/) even if the user uninstalled them. Re-remove
|
||||||
# any the user previously uninstalled so the update doesn't
|
# any the user previously uninstalled so the update doesn't
|
||||||
@@ -1720,14 +1762,24 @@ def execute_system_action():
|
|||||||
result = subprocess.run(['sudo', 'systemctl', 'restart', 'ledmatrix-web.service'],
|
result = subprocess.run(['sudo', 'systemctl', 'restart', 'ledmatrix-web.service'],
|
||||||
capture_output=True, text=True, timeout=10)
|
capture_output=True, text=True, timeout=10)
|
||||||
elif action == 'install_base_requirements':
|
elif action == 'install_base_requirements':
|
||||||
req_file = PROJECT_ROOT / 'requirements.txt'
|
# Base + web interface requirements: flask-compress and friends
|
||||||
if not req_file.exists():
|
# live in web_interface/requirements.txt, not the root file.
|
||||||
|
req_files = [f for f in (PROJECT_ROOT / 'requirements.txt',
|
||||||
|
PROJECT_ROOT / 'web_interface' / 'requirements.txt')
|
||||||
|
if f.exists()]
|
||||||
|
if not req_files:
|
||||||
return jsonify({'status': 'error', 'message': 'No requirements.txt found at project root'})
|
return jsonify({'status': 'error', 'message': 'No requirements.txt found at project root'})
|
||||||
result = _pip_install_requirements(req_file, timeout=120)
|
outputs = []
|
||||||
|
all_ok = True
|
||||||
|
for req_file in req_files:
|
||||||
|
result = _pip_install_requirements(req_file, timeout=120)
|
||||||
|
all_ok = all_ok and result.returncode == 0
|
||||||
|
outputs.append(f"== {req_file.relative_to(PROJECT_ROOT)} ==\n"
|
||||||
|
+ _truncate_output(result.stdout, result.stderr))
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success' if result.returncode == 0 else 'error',
|
'status': 'success' if all_ok else 'error',
|
||||||
'message': 'Base requirements installed successfully' if result.returncode == 0 else 'pip install failed',
|
'message': 'Base requirements installed successfully' if all_ok else 'pip install failed',
|
||||||
'output': _truncate_output(result.stdout, result.stderr)
|
'output': "\n".join(outputs)
|
||||||
})
|
})
|
||||||
elif action == 'install_plugin_requirements':
|
elif action == 'install_plugin_requirements':
|
||||||
active_pm = getattr(api_v3, 'plugin_manager', None)
|
active_pm = getattr(api_v3, 'plugin_manager', None)
|
||||||
|
|||||||
Reference in New Issue
Block a user