mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
fix(web): don't validate double-sided settings when the feature is disabled (#422)
* fix(web): don't validate double-sided settings when the feature is disabled
Saving anything on the Display tab failed with a 400 when double-sided
mode was off:
Double-sided copies (2) must divide chain length (3) evenly
The Display form posts every field in one request, including
double_sided_copies (default 2) and double_sided_axis, whether or not
the Enabled checkbox is ticked. The server block was gated only on "is
any double-sided field present in the payload" — it wrote
ds_config['enabled'] but never read it. A user with chain_length: 3 and
the untouched default copies: 2 was locked out of saving any display
setting at all: brightness, GPIO slowdown, Vegas, sync.
Gate the checks on the enabled flag:
- Divisibility against chain_length/parallel is hardware-relational and
only runs when the feature is on.
- Structural checks (copies parses as an int in 2..8, axis in the
whitelist) still 400 when enabled; when disabled they drop the value
and leave the stored one untouched rather than rejecting the save.
The runtime already gated correctly (_resolve_double_sided returns None
when disabled), so nothing there changes.
Also in the Display tab:
- Hide Copies / Split Axis until Enabled is ticked, mirroring the Vegas
Scroll pattern. Hidden rather than disabled, so the fields keep
submitting and the server still sees an 'off' state to persist.
- Fix the save toast: the form's handler read xhr.responseJSON, a jQuery
property that doesn't exist on a native XMLHttpRequest, so it was
always undefined and every save reported a green "Display settings
saved" — even the 400s. Parse responseText and use the real status.
Tests: two existing double-sided tests asserted 200 on payloads that the
divisibility check (added later, in #373) turns into 400s; the first now
supplies matching hardware values and the second passes as written now
that a disabled save skips the check. Added coverage for the reported
regression, for bad values while disabled, and for the check still
firing when enabled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aUvzTXpEYhNEWYQvFqQU9
* fix(web): treat only 2xx as a successful display save
Review feedback on #422.
- showDisplaySaveResult tested `xhr.status >= 400` for failure, so a
network error — which reports status 0 — was waved through as
"Display settings saved". That's the same class of false-success bug
this branch set out to fix. Test the 2xx range instead, and let a
response body refine a successful verdict without overturning a
failed one.
- Annotate the _copies_fits_hardware helper, matching the annotated
helpers already in api_v3.py.
- Cover the vertical divisibility branch: chain_length 2 would divide
evenly, so only parallel 3 can produce the rejection, which pins the
branch to the right hardware dimension.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aUvzTXpEYhNEWYQvFqQU9
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import uuid
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -864,16 +864,15 @@ def save_main_config():
|
||||
ds_config = current_config['display']['double_sided']
|
||||
|
||||
# Enabled checkbox: omitted from the form when unchecked.
|
||||
ds_config['enabled'] = _coerce_to_bool(data.get('double_sided_enabled'))
|
||||
# The Display form posts copies/axis on every save regardless of this
|
||||
# checkbox, so when the feature is off we accept the values without
|
||||
# rejecting the whole save — otherwise a stale copies/chain_length
|
||||
# mismatch locks the user out of every other display setting.
|
||||
enabled = _coerce_to_bool(data.get('double_sided_enabled'))
|
||||
ds_config['enabled'] = enabled
|
||||
|
||||
if 'double_sided_copies' in data and data['double_sided_copies'] not in ('', None):
|
||||
try:
|
||||
copies = int(data['double_sided_copies'])
|
||||
except (ValueError, TypeError):
|
||||
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.
|
||||
def _copies_fits_hardware(copies: int) -> Optional[str]:
|
||||
"""Error message if copies doesn't divide the panel evenly, else None."""
|
||||
# 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')
|
||||
@@ -881,18 +880,41 @@ def save_main_config():
|
||||
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
|
||||
return f"Double-sided copies ({copies}) must divide chain length ({chain_length}) evenly"
|
||||
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
|
||||
return f"Double-sided copies ({copies}) must divide parallel ({parallel}) evenly"
|
||||
return None
|
||||
|
||||
if 'double_sided_copies' in data and data['double_sided_copies'] not in ('', None):
|
||||
copies = None
|
||||
try:
|
||||
copies = int(data['double_sided_copies'])
|
||||
except (ValueError, TypeError):
|
||||
if enabled:
|
||||
return jsonify({'status': 'error', 'message': "Double-sided copies must be an integer"}), 400
|
||||
if copies is not None and not (2 <= copies <= 8):
|
||||
if enabled:
|
||||
return jsonify({'status': 'error', 'message': "Double-sided copies must be between 2 and 8"}), 400
|
||||
# Disabled: leave the stored value alone rather than writing junk.
|
||||
copies = None
|
||||
if copies is not None:
|
||||
# Divisibility is a hardware-relational check — only meaningful
|
||||
# when the feature is actually on.
|
||||
if enabled:
|
||||
fit_error = _copies_fits_hardware(copies)
|
||||
if fit_error:
|
||||
return jsonify({'status': 'error', 'message': fit_error}), 400
|
||||
ds_config['copies'] = copies
|
||||
|
||||
if 'double_sided_axis' in data:
|
||||
axis = data['double_sided_axis']
|
||||
if axis not in ('horizontal', 'vertical'):
|
||||
return jsonify({'status': 'error', 'message': "Double-sided axis must be 'horizontal' or 'vertical'"}), 400
|
||||
ds_config['axis'] = axis
|
||||
if enabled:
|
||||
return jsonify({'status': 'error', 'message': "Double-sided axis must be 'horizontal' or 'vertical'"}), 400
|
||||
else:
|
||||
ds_config['axis'] = axis
|
||||
|
||||
# Handle Vegas scroll mode settings
|
||||
vegas_fields = ['vegas_scroll_enabled', 'vegas_scroll_speed', 'vegas_separator_width',
|
||||
|
||||
Reference in New Issue
Block a user