mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
fix(web): increase chain_length max from 8 to 32 (#300)
* fix(web): increase chain_length max from 8 to 32 The web UI form input capped chain_length at 8 panels, preventing users with larger displays (e.g. 16-panel setups) from configuring their hardware through the UI. The backend API had no such limit. Changed max="8" to max="32" to support large display configurations. Added panel count example to the help text. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): add server-side bounds validation for display hardware fields The API endpoint at /api/v3/config/main accepted any integer value for display hardware fields (chain_length, rows, cols, brightness, etc.) without bounds checking. Only the HTML form had min/max attributes, which are trivially bypassed by direct API calls. Added _int_field_limits dict with bounds for all integer hardware fields: chain_length: 1-32, parallel: 1-4, brightness: 1-100, rows: 8-128, cols: 16-128, scan_mode: 0-1, pwm_bits: 1-11, pwm_dither_bits: 0-2, pwm_lsb_nanoseconds: 50-500, limit_refresh_rate_hz: 0-1000, gpio_slowdown: 0-5 Out-of-bounds or non-integer values now return 400 with a clear error message (e.g. "Invalid chain_length value 99. Must be between 1 and 32.") before any config is persisted. Follows the same inline validation pattern already used for led_rgb_sequence, panel_type, and multiplexing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(api): strict int validation and add max_dynamic_duration_seconds bounds Reject bool/float types in _int_field_limits validation loop to prevent silent coercion, and add max_dynamic_duration_seconds to the validation map so it gets proper bounds checking instead of a raw int() call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -740,6 +740,39 @@ def save_main_config():
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return jsonify({'status': 'error', 'message': f"Invalid multiplexing value '{data['multiplexing']}'. Must be an integer from 0 to 22."}), 400
|
return jsonify({'status': 'error', 'message': f"Invalid multiplexing value '{data['multiplexing']}'. Must be an integer from 0 to 22."}), 400
|
||||||
|
|
||||||
|
# Validate integer display hardware fields (bounds check)
|
||||||
|
_int_field_limits = {
|
||||||
|
'rows': (8, 128),
|
||||||
|
'cols': (16, 128),
|
||||||
|
'chain_length': (1, 32),
|
||||||
|
'parallel': (1, 4),
|
||||||
|
'brightness': (1, 100),
|
||||||
|
'scan_mode': (0, 1),
|
||||||
|
'pwm_bits': (1, 11),
|
||||||
|
'pwm_dither_bits': (0, 2),
|
||||||
|
'pwm_lsb_nanoseconds': (50, 500),
|
||||||
|
'limit_refresh_rate_hz': (0, 1000),
|
||||||
|
'gpio_slowdown': (0, 5),
|
||||||
|
'max_dynamic_duration_seconds': (1, 3600),
|
||||||
|
}
|
||||||
|
for field, (lo, hi) in _int_field_limits.items():
|
||||||
|
if field in data:
|
||||||
|
raw = data[field]
|
||||||
|
if isinstance(raw, bool):
|
||||||
|
return jsonify({'status': 'error', 'message': f"Invalid {field} value '{raw}'. Must be an integer."}), 400
|
||||||
|
if isinstance(raw, float):
|
||||||
|
return jsonify({'status': 'error', 'message': f"Invalid {field} value '{raw}'. Must be an integer, not a float."}), 400
|
||||||
|
if isinstance(raw, int):
|
||||||
|
val = raw
|
||||||
|
elif isinstance(raw, str):
|
||||||
|
if not re.fullmatch(r'-?\d+', raw):
|
||||||
|
return jsonify({'status': 'error', 'message': f"Invalid {field} value '{raw}'. Must be an integer."}), 400
|
||||||
|
val = int(raw)
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Invalid {field} value '{raw}'. Must be an integer."}), 400
|
||||||
|
if val < lo or val > hi:
|
||||||
|
return jsonify({'status': 'error', 'message': f"Invalid {field} value {val}. Must be between {lo} and {hi}."}), 400
|
||||||
|
|
||||||
# Handle hardware settings
|
# Handle hardware settings
|
||||||
for field in ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping', 'scan_mode',
|
for field in ['rows', 'cols', 'chain_length', 'parallel', 'brightness', 'hardware_mapping', 'scan_mode',
|
||||||
'pwm_bits', 'pwm_dither_bits', 'pwm_lsb_nanoseconds', 'limit_refresh_rate_hz',
|
'pwm_bits', 'pwm_dither_bits', 'pwm_lsb_nanoseconds', 'limit_refresh_rate_hz',
|
||||||
@@ -767,7 +800,7 @@ def save_main_config():
|
|||||||
if 'max_dynamic_duration_seconds' in data:
|
if 'max_dynamic_duration_seconds' in data:
|
||||||
if 'dynamic_duration' not in current_config['display']:
|
if 'dynamic_duration' not in current_config['display']:
|
||||||
current_config['display']['dynamic_duration'] = {}
|
current_config['display']['dynamic_duration'] = {}
|
||||||
current_config['display']['dynamic_duration']['max_duration_seconds'] = int(data['max_dynamic_duration_seconds'])
|
current_config['display']['dynamic_duration']['max_duration_seconds'] = int(data['max_dynamic_duration_seconds']) # Already validated by _int_field_limits
|
||||||
|
|
||||||
# Handle Vegas scroll mode settings
|
# Handle Vegas scroll mode settings
|
||||||
vegas_fields = ['vegas_scroll_enabled', 'vegas_scroll_speed', 'vegas_separator_width',
|
vegas_fields = ['vegas_scroll_enabled', 'vegas_scroll_speed', 'vegas_separator_width',
|
||||||
|
|||||||
@@ -49,9 +49,9 @@
|
|||||||
name="chain_length"
|
name="chain_length"
|
||||||
value="{{ main_config.display.hardware.chain_length or 2 }}"
|
value="{{ main_config.display.hardware.chain_length or 2 }}"
|
||||||
min="1"
|
min="1"
|
||||||
max="8"
|
max="32"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
<p class="mt-1 text-sm text-gray-600">Number of LED panels chained together</p>
|
<p class="mt-1 text-sm text-gray-600">Number of LED panels chained together (e.g. 2 for 128×32, 5 for 320×32)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user