mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-02 17:28:05 +00:00
Compare commits
3
Commits
344f64ebba
...
f40e6f3127
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f40e6f3127 | ||
|
|
453d46563f | ||
|
|
06ea4aa496 |
@@ -829,6 +829,19 @@ def save_main_config():
|
||||
return jsonify({'status': 'error', 'message': "Double-sided copies must be an integer"}), 400
|
||||
if not (2 <= copies <= 8):
|
||||
return jsonify({'status': 'error', 'message': "Double-sided copies must be between 2 and 8"}), 400
|
||||
# Validate divisibility against the relevant hardware dimension.
|
||||
# Use axis from this request if provided, else from stored config.
|
||||
hw = current_config.get('display', {}).get('hardware', {})
|
||||
effective_axis = (data.get('double_sided_axis')
|
||||
or current_config.get('display', {}).get('double_sided', {}).get('axis', 'horizontal'))
|
||||
if effective_axis == 'horizontal':
|
||||
chain_length = int(hw.get('chain_length', 2) or 2)
|
||||
if chain_length % copies != 0:
|
||||
return jsonify({'status': 'error', 'message': f"Double-sided copies ({copies}) must divide chain length ({chain_length}) evenly"}), 400
|
||||
elif effective_axis == 'vertical':
|
||||
parallel = int(hw.get('parallel', 1) or 1)
|
||||
if parallel % copies != 0:
|
||||
return jsonify({'status': 'error', 'message': f"Double-sided copies ({copies}) must divide parallel ({parallel}) evenly"}), 400
|
||||
ds_config['copies'] = copies
|
||||
|
||||
if 'double_sided_axis' in data:
|
||||
@@ -1082,6 +1095,8 @@ def save_main_config():
|
||||
continue
|
||||
if key in vegas_fields:
|
||||
continue
|
||||
if key in double_sided_fields:
|
||||
continue
|
||||
# For any remaining keys (including plugin keys), use deep merge to preserve existing settings
|
||||
if key in current_config and isinstance(current_config[key], dict) and isinstance(data[key], dict):
|
||||
# Deep merge to preserve existing settings
|
||||
@@ -1666,21 +1681,27 @@ def execute_system_action():
|
||||
'output': _truncate_output(result.stdout, result.stderr)
|
||||
})
|
||||
elif action == 'install_plugin_requirements':
|
||||
plugins_dir = Path(plugin_manager.plugins_dir) if plugin_manager else PROJECT_ROOT / 'plugin-repos'
|
||||
active_pm = getattr(api_v3, 'plugin_manager', None)
|
||||
plugins_dir = Path(active_pm.plugins_dir) if active_pm else PROJECT_ROOT / 'plugin-repos'
|
||||
results = []
|
||||
if plugins_dir.exists():
|
||||
for p in sorted(plugins_dir.iterdir()):
|
||||
req = p / 'requirements.txt'
|
||||
if p.is_dir() and req.exists():
|
||||
r = subprocess.run(
|
||||
[sys.executable, '-m', 'pip', 'install', '--break-system-packages', '-r', str(req)],
|
||||
capture_output=True, text=True, timeout=60
|
||||
)
|
||||
results.append({
|
||||
'plugin': p.name,
|
||||
'ok': r.returncode == 0,
|
||||
'output': _truncate_output(r.stdout, r.stderr)
|
||||
})
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[sys.executable, '-m', 'pip', 'install', '--break-system-packages', '-r', str(req)],
|
||||
capture_output=True, text=True, timeout=60
|
||||
)
|
||||
results.append({
|
||||
'plugin': p.name,
|
||||
'ok': r.returncode == 0,
|
||||
'output': _truncate_output(r.stdout, r.stderr)
|
||||
})
|
||||
except subprocess.TimeoutExpired:
|
||||
results.append({'plugin': p.name, 'ok': False, 'output': 'pip install timed out'})
|
||||
except OSError as exc:
|
||||
results.append({'plugin': p.name, 'ok': False, 'output': exc.strerror or 'OS error'})
|
||||
ok_count = sum(1 for r in results if r['ok'])
|
||||
all_ok = all(r['ok'] for r in results) if results else True
|
||||
return jsonify({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, render_template, flash
|
||||
from jinja2 import TemplateNotFound
|
||||
from markupsafe import escape
|
||||
import json
|
||||
import logging
|
||||
@@ -454,9 +455,12 @@ def _load_tools_partial():
|
||||
"""Load tools/utilities partial."""
|
||||
try:
|
||||
return render_template('v3/partials/tools.html')
|
||||
except Exception:
|
||||
logger.error("Error loading partial", exc_info=True)
|
||||
return "Error loading partial", 500
|
||||
except TemplateNotFound:
|
||||
logger.error("[Pages V3][Tools] Template not found: v3/partials/tools.html", exc_info=True)
|
||||
return "[Pages V3][Tools] Template is missing.", 500
|
||||
except OSError as exc:
|
||||
logger.error("[Pages V3][Tools] I/O error loading tools partial: %s", exc, exc_info=True)
|
||||
return "[Pages V3][Tools] Failed to load due to a file system error. Check logs.", 500
|
||||
|
||||
|
||||
def _load_plugin_config_partial(plugin_id):
|
||||
|
||||
@@ -1922,7 +1922,22 @@
|
||||
if (tab === 'overview' && typeof loadOverviewDirect === 'function') loadOverviewDirect();
|
||||
else if (tab === 'wifi' && typeof loadWifiDirect === 'function') loadWifiDirect();
|
||||
else if (tab === 'plugins' && typeof loadPluginsDirect === 'function') loadPluginsDirect();
|
||||
}
|
||||
else if (tab === 'tools') {
|
||||
fetch('/v3/partials/tools')
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
|
||||
return r.text();
|
||||
})
|
||||
.then(html => {
|
||||
contentEl.innerHTML = html;
|
||||
contentEl.setAttribute('data-loaded', 'true');
|
||||
if (window.Alpine) window.Alpine.initTree(contentEl);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load tools content:', err);
|
||||
contentEl.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4"><p class="text-red-800">Failed to load Tools. Please refresh the page.</p></div>';
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Pull latest (rebase)</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Stashes local changes, runs <code class="bg-gray-100 px-1 rounded">git pull --rebase</code>, then restores the stash.</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Stashes any local changes, then runs <code class="bg-gray-100 px-1 rounded">git pull --rebase</code>. The stash is preserved but not re-applied.</p>
|
||||
</div>
|
||||
<button id="btn-git-pull" onclick="toolsAction('git_pull', 'btn-git-pull', 'result-git-pull')"
|
||||
class="shrink-0 inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
@@ -264,8 +264,16 @@
|
||||
if (!panel) return;
|
||||
|
||||
fetch('/api/v3/system/git-info')
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
if (!r.ok) return r.json().then(d => Promise.reject(d.message || `HTTP ${r.status}`));
|
||||
return r.json();
|
||||
})
|
||||
.then(d => {
|
||||
if (d.status === 'error') {
|
||||
panel.innerHTML = `<span class="text-sm text-red-600">${escHtml(d.message || 'Git info unavailable.')}</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const dirtyBadge = d.dirty
|
||||
? '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">dirty</span>'
|
||||
: '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">clean</span>';
|
||||
@@ -295,8 +303,8 @@
|
||||
html += `</div>`;
|
||||
panel.innerHTML = html;
|
||||
})
|
||||
.catch(() => {
|
||||
panel.innerHTML = '<span class="text-sm text-red-600">Could not load git info.</span>';
|
||||
.catch(err => {
|
||||
panel.innerHTML = `<span class="text-sm text-red-600">Could not load git info: ${escHtml(String(err))}</span>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user