feat(web): add Tools tab and row address type display setting#373
Conversation
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a ChangesTools Tab, System Actions, and Row Address Type
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 5 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web_interface/blueprints/api_v3.py`:
- Around line 1674-1684: The git subprocess commands (branch, status, log, and
remote) are not checking return codes before using their output. If any command
fails, the empty stdout is silently treated as a clean state (dirty: false,
empty strings). Check the returncode attribute of each subprocess.run() result
and if any command fails (returncode != 0), return an error response with a
clear message explaining which git command failed, rather than proceeding with
empty/blank values that misrepresent the repository state.
- Around line 1591-1599: The subprocess output from pip operations is unbounded
and can consume significant RAM on Raspberry Pi, especially with verbose
dependency resolution or build failures. Create a shared helper function that
captures only the last N bytes or lines of the combined stdout and stderr, and
includes the command, return code, and timeout context in the output. Apply this
helper to limit the output in the jsonify response's output field in both the
pip install operation at lines 1591-1599 (where subprocess.run is called and the
output field is populated in the jsonify response) and the similar pip operation
at lines 1607-1615 (which has an equivalent unbounded output field in its
jsonify response), replacing the direct concatenation of result.stdout and
result.stderr with calls to this new helper function.
- Around line 1600-1602: In the elif block for the 'install_plugin_requirements'
action, change the reference from the module-level plugin_manager to
api_v3.plugin_manager in the ternary expression on line 1601. This ensures
consistency with how plugin managers are accessed throughout the rest of the
blueprint and prevents inadvertently falling back to the PROJECT_ROOT /
'plugin-repos' default when only the blueprint attribute is initialized. Replace
plugin_manager with api_v3.plugin_manager in the condition that determines which
plugins directory to use.
- Around line 1677-1683: The remote_url field in the jsonify return statement is
including the raw output from git remote get-url, which may contain embedded
credentials in HTTPS URLs (e.g., https://user:password@github.com/repo.git).
Before including remote.stdout.strip() as the remote_url value in the response,
parse the URL using a library like urllib.parse to remove any user
authentication information, or extract only the safe components (protocol, host,
path) and reconstruct the URL without credentials. This prevents accidental
credential exposure in the API response.
- Around line 1640-1646: The `clear_pycache` action increments the `cleared`
counter unconditionally even though `shutil.rmtree()` is called with
`ignore_errors=True`, meaning failed deletions are still counted as successful.
Fix this by removing `ignore_errors=True` from the shutil.rmtree call in the
rglob loop and wrapping it in a try-except block. Only increment the `cleared`
counter when deletion succeeds, and track any failures in a separate counter.
Return both the cleared count and failed count (or a descriptive error message)
in the jsonify response so users get accurate feedback about what was actually
deleted versus what failed.
- Around line 1625-1633: The git commands in the subprocess.run calls rely on
PATH lookup without verifying git is available, which is inconsistent with the
existing pattern used for _SUDO and _JOURNALCTL. Add _GIT = shutil.which('git')
near the top of the module alongside the other tool resolution constants, then
validate that _GIT is not None at the start of both execute_system_action() and
get_git_info() functions by returning a jsonify response with status 503 if the
git command is unavailable, and finally replace all instances of 'git' strings
in the subprocess.run() calls within these endpoints with the resolved _GIT
variable path.
In `@web_interface/blueprints/pages_v3.py`:
- Around line 457-459: The exception handler for the Tools partial uses an
overly broad `except Exception` clause and lacks debugging context for remote
Raspberry Pi environments. Replace the broad `except Exception` with specific
template-related exceptions (such as TemplateNotFound, TemplateError, or other
Jinja2/Flask template exceptions) to catch only relevant errors. Update the
logger.error call to include the `[Pages V3][Tools]` context prefix in the log
message to provide structured logging context for troubleshooting. Additionally,
update the returned error message string to also include the `[Pages V3][Tools]`
prefix for consistency in error responses, ensuring both logs and client-facing
messages provide clear context about which component failed.
In `@web_interface/templates/v3/base.html`:
- Around line 1298-1308: The Tools tab in the template relies solely on HTMX
loading via hx-get without any fallback mechanism, whereas other tabs have
fallback paths in the loadTabContent function. Add a non-HTMX fallback branch in
the loadTabContent function to handle the Tools tab (when activeTab === 'tools')
so that if HTMX fails or is disabled, the tab content can still load gracefully
instead of remaining blank. Ensure the fallback mimics how other tabs in
loadTabContent degrade gracefully.
In `@web_interface/templates/v3/partials/tools.html`:
- Around line 222-246: The toolsAction function makes unauthenticated requests
to the /api/v3/system/action endpoint which performs dangerous state-changing
operations. Add authentication to protect this endpoint by modifying the fetch
call in toolsAction to include an authentication token or credential in the
request headers, and update the corresponding backend execute_system_action
endpoint to validate this authentication before allowing any system action to
proceed. This ensures only authorized clients can trigger destructive operations
like git reset, pip install, or service restarts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 68218690-be81-4288-811d-2645ea1c279f
📒 Files selected for processing (8)
first_time_install.shscripts/install/one-shot-install.shscripts/install_dependencies_apt.pyweb_interface/blueprints/api_v3.pyweb_interface/blueprints/pages_v3.pyweb_interface/templates/v3/base.htmlweb_interface/templates/v3/partials/display.htmlweb_interface/templates/v3/partials/tools.html
Adds a Tools/Utilities tab to the web interface with one-click maintenance buttons that previously required SSH: - Git status panel (branch, dirty state, recent commits) - Pull latest (rebase) and force reset to origin/main - Reinstall base requirements (pip, with output) - Reinstall per-plugin requirements (pass/fail per plugin) - Clear __pycache__ directories - Quick-access restart for display and web services Also exposes the hzeller row_address_type option (0–4) in the Display settings tab. The backend already read this value from config; the UI, API field list, and validation were missing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add _GIT = shutil.which('git') alongside _SUDO/_JOURNALCTL; return
503 in force_git_reset and get_git_info if git is unavailable
- Check git branch/status returncodes in get_git_info(); return a clear
500 error instead of silently treating a failed run as a clean repo
- Cap pip stdout+stderr at 50 KB via _truncate_output() helper to
avoid OOM on verbose dependency resolution or build failures
- Scrub embedded HTTPS credentials from remote_url via
_scrub_git_remote_url() using urllib.parse before returning to UI
- Fix clear_pycache to track and report failed deletions separately
instead of counting them as successes (removed ignore_errors=True,
wrapped in try/except OSError)
Skipped: plugin_manager-vs-api_v3.plugin_manager (api_v3 is the
Blueprint object; accessing .plugin_manager on it would fail — module-
level variable is the correct pattern used throughout this blueprint);
pages_v3 broad-except (identical to every other _load_*_partial in the
file); base.html HTMX fallback (loadTabContent handles all tabs
generically; named fallbacks only exist for tabs needing JS re-init);
tools.html auth (pre-existing architectural decision — reboot/shutdown
on the same endpoint are also unauthenticated).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
e5ae3cc to
344f64e
Compare
- api_v3: use getattr(api_v3, 'plugin_manager', None) instead of the module-level plugin_manager (always None); app.py sets the blueprint attribute, not the module global, so the fallback to plugin-repos was always taken - pages_v3: replace broad except Exception in _load_tools_partial with specific TemplateNotFound / OSError handlers and add [Pages V3][Tools] context prefix to log messages and error responses for easier Pi debugging - base.html: add Tools tab branch to the HTMX-unavailable fallback block in loadTabContent so the tab loads gracefully via direct fetch if HTMX never initialises Skipped: auth on execute_system_action — pre-existing app-wide design; reboot/shutdown and all other system actions share the same exposure. An app-level auth layer is the correct fix and is out of scope here. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web_interface/blueprints/api_v3.py`:
- Around line 1672-1690: The per-plugin install loop in the plugin requirements
handler is letting subprocess exceptions escape, which collapses the whole batch
into one generic failure. Update the logic around the subprocess.run call in the
plugins_dir loop to catch TimeoutExpired/OSError per plugin, append a failed
result entry with the plugin name and error text, and then continue iterating so
the existing results breakdown stays accurate. Keep the success/error summary
based on the accumulated results from this handler.
- Around line 825-838: The `double_sided_copies` validation in `api_v3.py` only
enforces the numeric range, but it also needs to match the selected hardware
dimension. Update the `ds_config` handling so that after parsing `copies`, it
checks divisibility against `chain_length` when `double_sided_axis` is
`horizontal` and against `parallel` when `double_sided_axis` is `vertical`; if
the value does not divide evenly, return a 400 error with a clear validation
message. Keep the existing `double_sided_axis` and range checks, and apply the
new logic in the same request path where `ds_config['copies']` is set.
- Around line 813-839: The double-sided form fields are being handled in the
display-specific block but still fall through to the generic config merge,
causing stray root-level keys to persist. Update the save flow in api_v3.py so
the double-sided handling in the current_config['display']['double_sided'] block
either removes double_sided_enabled, double_sided_copies, and double_sided_axis
from data after processing or explicitly excludes them from the later merge
loop. Use the existing double_sided_fields logic and the generic merge section
to ensure these keys are only stored under display.double_sided and never at the
config root.
In `@web_interface/templates/v3/partials/tools.html`:
- Around line 19-20: The “Pull latest (rebase)” tooltip in tools.html overstates
the behavior by promising stash restoration that the backend does not perform.
Update the copy for this action to match the actual git_pull flow: it stashes
local changes and runs git pull --rebase, but does not reapply the stash
afterward. Keep the wording aligned with the tool label and the git_pull
behavior so the UI no longer claims “then restores the stash.”
- Around line 266-299: The git info fetch flow in the panel rendering logic is
treating backend error responses as normal data, causing a false “clean” state
instead of showing the real failure. Update the fetch handling around r.json()
in the git-info block to check r.ok or an error status field before building the
badge and HTML. If the response indicates an error, render d.message in the
panel instead of deriving branch/dirty UI from the payload, and keep the
existing .catch path for transport failures.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 41c7d39d-3a41-404e-b813-ef0b53827eae
📒 Files selected for processing (5)
web_interface/blueprints/api_v3.pyweb_interface/blueprints/pages_v3.pyweb_interface/templates/v3/base.htmlweb_interface/templates/v3/partials/display.htmlweb_interface/templates/v3/partials/tools.html
🚧 Files skipped from review as they are similar to previous changes (1)
- web_interface/blueprints/pages_v3.py
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web_interface/blueprints/api_v3.py`:
- Around line 1672-1690: The per-plugin install loop in the plugin requirements
handler is letting subprocess exceptions escape, which collapses the whole batch
into one generic failure. Update the logic around the subprocess.run call in the
plugins_dir loop to catch TimeoutExpired/OSError per plugin, append a failed
result entry with the plugin name and error text, and then continue iterating so
the existing results breakdown stays accurate. Keep the success/error summary
based on the accumulated results from this handler.
- Around line 825-838: The `double_sided_copies` validation in `api_v3.py` only
enforces the numeric range, but it also needs to match the selected hardware
dimension. Update the `ds_config` handling so that after parsing `copies`, it
checks divisibility against `chain_length` when `double_sided_axis` is
`horizontal` and against `parallel` when `double_sided_axis` is `vertical`; if
the value does not divide evenly, return a 400 error with a clear validation
message. Keep the existing `double_sided_axis` and range checks, and apply the
new logic in the same request path where `ds_config['copies']` is set.
- Around line 813-839: The double-sided form fields are being handled in the
display-specific block but still fall through to the generic config merge,
causing stray root-level keys to persist. Update the save flow in api_v3.py so
the double-sided handling in the current_config['display']['double_sided'] block
either removes double_sided_enabled, double_sided_copies, and double_sided_axis
from data after processing or explicitly excludes them from the later merge
loop. Use the existing double_sided_fields logic and the generic merge section
to ensure these keys are only stored under display.double_sided and never at the
config root.
In `@web_interface/templates/v3/partials/tools.html`:
- Around line 19-20: The “Pull latest (rebase)” tooltip in tools.html overstates
the behavior by promising stash restoration that the backend does not perform.
Update the copy for this action to match the actual git_pull flow: it stashes
local changes and runs git pull --rebase, but does not reapply the stash
afterward. Keep the wording aligned with the tool label and the git_pull
behavior so the UI no longer claims “then restores the stash.”
- Around line 266-299: The git info fetch flow in the panel rendering logic is
treating backend error responses as normal data, causing a false “clean” state
instead of showing the real failure. Update the fetch handling around r.json()
in the git-info block to check r.ok or an error status field before building the
badge and HTML. If the response indicates an error, render d.message in the
panel instead of deriving branch/dirty UI from the payload, and keep the
existing .catch path for transport failures.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 41c7d39d-3a41-404e-b813-ef0b53827eae
📒 Files selected for processing (5)
web_interface/blueprints/api_v3.pyweb_interface/blueprints/pages_v3.pyweb_interface/templates/v3/base.htmlweb_interface/templates/v3/partials/display.htmlweb_interface/templates/v3/partials/tools.html
🚧 Files skipped from review as they are similar to previous changes (1)
- web_interface/blueprints/pages_v3.py
🛑 Comments failed to post (5)
web_interface/blueprints/api_v3.py (3)
813-839: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not persist
double_sided_*form fields at the config root.These keys are written into
current_config['display']['double_sided']here, but they are never removed fromdataand the generic merge loop at Lines 1070-1090 does not skip them. The next save will therefore also writedouble_sided_enabled,double_sided_copies, anddouble_sided_axisas stray top-level config keys.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/blueprints/api_v3.py` around lines 813 - 839, The double-sided form fields are being handled in the display-specific block but still fall through to the generic config merge, causing stray root-level keys to persist. Update the save flow in api_v3.py so the double-sided handling in the current_config['display']['double_sided'] block either removes double_sided_enabled, double_sided_copies, and double_sided_axis from data after processing or explicitly excludes them from the later merge loop. Use the existing double_sided_fields logic and the generic merge section to ensure these keys are only stored under display.double_sided and never at the config root.
825-838: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate
double_sided_copiesagainst the selected hardware dimension.The UI now says the copy count “must divide the panel evenly”, but the backend only checks
2..8. Values likecopies=2withparallel=1orcopies=3withchain_length=2will still save and produce an impossible layout. Please enforce divisibility againstchain_lengthforhorizontalandparallelforvertical.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/blueprints/api_v3.py` around lines 825 - 838, The `double_sided_copies` validation in `api_v3.py` only enforces the numeric range, but it also needs to match the selected hardware dimension. Update the `ds_config` handling so that after parsing `copies`, it checks divisibility against `chain_length` when `double_sided_axis` is `horizontal` and against `parallel` when `double_sided_axis` is `vertical`; if the value does not divide evenly, return a 400 error with a clear validation message. Keep the existing `double_sided_axis` and range checks, and apply the new logic in the same request path where `ds_config['copies']` is set.
1672-1690: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Keep per-plugin pip failures local to the failing plugin.
A single
TimeoutExpired/OSErrorhere escapes to the outer handler and turns the whole batch into one generic failure, so the Tools tab loses the per-plugin pass/fail breakdown it promises. Catch subprocess errors inside the loop, append a failed entry for that plugin, and continue with the rest. As per coding guidelines, “Implement graceful degradation to continue operation when non-critical features fail” and “Provide clear error messages for troubleshooting”.🧰 Tools
🪛 ast-grep (0.44.0)
[error] 1675-1678: Command coming from incoming request
Context: subprocess.run(
[sys.executable, '-m', 'pip', 'install', '--break-system-packages', '-r', str(req)],
capture_output=True, text=True, timeout=60
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').(subprocess-from-request)
🪛 Ruff (0.15.18)
[error] 1676-1676:
subprocesscall: check for execution of untrusted input(S603)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/blueprints/api_v3.py` around lines 1672 - 1690, The per-plugin install loop in the plugin requirements handler is letting subprocess exceptions escape, which collapses the whole batch into one generic failure. Update the logic around the subprocess.run call in the plugins_dir loop to catch TimeoutExpired/OSError per plugin, append a failed result entry with the plugin name and error text, and then continue iterating so the existing results breakdown stays accurate. Keep the success/error summary based on the accumulated results from this handler.Source: Coding guidelines
web_interface/templates/v3/partials/tools.html (2)
19-20: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not promise stash restoration unless the backend actually reapplies it.
The new copy says this flow “then restores the stash”, but
git_pullonly doesgit stash pushbefore the pull and never pops/applies it afterward. That makes the UI promise materially wrong for anyone updating with local changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/templates/v3/partials/tools.html` around lines 19 - 20, The “Pull latest (rebase)” tooltip in tools.html overstates the behavior by promising stash restoration that the backend does not perform. Update the copy for this action to match the actual git_pull flow: it stashes local changes and runs git pull --rebase, but does not reapply the stash afterward. Keep the wording aligned with the tool label and the git_pull behavior so the UI no longer claims “then restores the stash.”
266-299: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle
/system/git-infoerror payloads before rendering a clean state.This code calls
r.json()unconditionally and then derives the badge fromd.dirty, so a 503/500 JSON response from the backend currently renders asunknown+cleaninstead of showing the actual error. Checkr.okord.status === 'error'first and surfaced.messagein the panel. As per coding guidelines, “Provide user-friendly error messages that explain what went wrong and potential solutions”.🧰 Tools
🪛 ast-grep (0.44.0)
[warning] 295-295: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: panel.innerHTML = html
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').(inner-outer-html)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web_interface/templates/v3/partials/tools.html` around lines 266 - 299, The git info fetch flow in the panel rendering logic is treating backend error responses as normal data, causing a false “clean” state instead of showing the real failure. Update the fetch handling around r.json() in the git-info block to check r.ok or an error status field before building the badge and HTML. If the response indicates an error, render d.message in the panel instead of deriving branch/dirty UI from the payload, and keep the existing .catch path for transport failures.Source: Coding guidelines
- Wrap per-plugin subprocess.run in try/except TimeoutExpired/OSError so one plugin's failure appends a result entry and continues the loop rather than collapsing the whole batch into a 500 - Validate double_sided_copies divisibility against chain_length (horizontal axis) or parallel (vertical axis) after the range check; reads effective axis from the current request or stored config - Exclude double_sided_fields from the generic key-merge loop so double_sided_enabled/copies/axis are never written as root-level keys - Fix tools.html copy: "then restores the stash" removed — git_pull stashes changes but never pops them - Check r.ok and d.status in loadGitInfo before building the panel; backend error messages now surface instead of silently showing a false-clean state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CodeQL flagged str(exc) flowing into the JSON response for the
install_plugin_requirements action. Use exc.strerror instead, which
gives the OS error description ("No such file or directory",
"Permission denied") without the internal filesystem path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The `else if (++tries > 100)` block added by #373 was missing its closing `}`, leaving the setInterval arrow function syntactically unclosed. This caused a JS parse error that silenced the entire 1400-line script block — including the EventSource setup — so the connection-status indicator never left its default "Disconnected" state for all users after updating. Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
row_address_typeoption (0–4) in the Display settings tab; the backend already consumed this value from config but there was no UI or API handling for itTools tab features
Row address type
Added a dropdown to the Display → Hardware Configuration section with all five hzeller options:
`display_manager.py` already read `hardware_config.get('row_address_type', 0)` — this PR wires up the missing UI field, API field list, and 0–4 range validation.
Files changed
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes