Install Hotwire, migrate inline code to Stimulus + CSS tokens #4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "hotwire"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
application.csswith design tokens from~/ror-css-guidelocation_controller.jsStimulus controllerChanges
app/assets/stylesheets/application.css-- design tokens, reset, component styles replacing empty manifestapp/views/properties/index.html.erb-- clean ERB with Stimulus data attributes, no inline codeapp/views/layouts/application.html.erb-- addedjavascript_importmap_tagsapp/controllers/properties_controller.rb-- resolve accepts client_name, special_notes, service_idsapp/javascript/controllers/location_controller.js-- GPS detection, address resolve, recent propertiesapp/javascript/application.js-- imports turbo and stimulus controllersconfig/importmap.rb-- pins for turbo, stimulus, controllersdocs/hotwire.md-- documents setup, migration, and CSS conventionsTest Plan
Review Checklist
Related Notes
landscaping-assistant-- projectPR #4 Review
DOMAIN REVIEW
Tech stack: Rails 8.1.3, Propshaft, importmap-rails, Hotwire (Turbo + Stimulus), PostgreSQL, plain CSS with design tokens.
Hotwire/Stimulus patterns -- well done:
data-action="click->location#detect"anddata-action="click->location#save"follow canonical Stimulus action syntax..is-visibleclass rather than inlinestyle.display-- correct separation of concerns.connect()lifecycle hook used properly for localStorage hydration.eagerLoadControllersFrom+pin_all_fromis the standard Rails 8 importmap setup. Correct.Hotwire/Stimulus patterns -- issues:
skip_forgery_protectionstill active onresolve(line 2 ofproperties_controller.rb).The Stimulus controller now correctly sends
X-CSRF-Token, which means CSRF protection should work. Butskip_forgery_protection only: [:resolve]disables it for that exact action. This was a necessary workaround when the inline JS did not send the token, but now that the Stimulus controller sends it, this line should be removed. Leaving it means CSRF protection is still bypassed on the only mutating endpoint in the app. This is a security concern but not a BLOCKER per the criteria below since there is no new unvalidated user input introduced -- the bypass existed before this PR.renderRecent()uses.innerHTMLwith unsanitized property data (line 121 oflocation_controller.js).address_lineandcitycome from user input via the Nominatim API response. If a malicious value gets stored (e.g., through direct API calls to/properties/resolve), this is an XSS vector. The template literal${p.address_line}, ${p.city}is injected directly into the DOM via.innerHTML. Should usetextContenton individual elements or DOM API methods instead.resultTarget.textContent(line 95) is safe --textContentdoes not parse HTML. Good.Missing
submitOnEnteraction. The docs (docs/hotwire.mdline 50) reference asubmitOnEnter()method, but the controller does not implement it and the ERB template has nokeydown->location#submitOnEnteraction. The old inline JS hadhouseNumber.addEventListener('keydown', ...)for Enter key submission. This is a behavioral regression -- users can no longer press Enter to save.Rails controller -- correctness bugs:
Redundant double-update in
resolveaction (lines 26-27). Thefind_or_create_by!block already setsclient_nameandspecial_noteson creation. But lines 26-27 then callproperty.update()separately. For new records, this means: create withclient_name = params[:client_name].presence || "TBD", then immediately update again with the same value. For existing records, the intent is to update fields -- butspecial_notesgets overwritten unconditionally wheneverparams[:special_notes].present?, even if the existing value was more complete. This logic is confusing and causes unnecessary DB writes. Consider consolidating into a single update-after-find-or-create pattern.service_idsnot validated.params[:service_ids].map(&:to_i)(line 30) will accept any array of values. Non-numeric strings become0, which may createPropertyServicerecords pointing to service ID 0 (nonexistent). The foreign key constraint in PostgreSQL would catch this, but the error would be an unhandled 500. Should validate that all service IDs exist before assignment.No error handling around
find_or_create_by!(line 16). The bang method raisesActiveRecord::RecordInvalidon validation failure. Withvalidates :client_name, presence: trueonProperty, if somehowclient_nameends up nil (edge case:params[:client_name]is blank ANDpresencereturns nil), the action will 500 with no JSON error response. Should rescue and return structured error JSON.CSS -- good:
:rootwith no hardcoded hex outside definitions. Clean.BLOCKERS
1. XSS via
.innerHTMLinrenderRecent()--location_controller.jsline 121.This is unvalidated user input rendered into the DOM. Property data originates from user-submitted values and is injected via
.innerHTMLwithout sanitization. This meets the BLOCKER criteria: "Unvalidated user input (SQL injection, XSS, path traversal risk)."Fix: Replace
.innerHTMLwith DOM construction usingtextContent:NITS
skip_forgery_protectiononresolveshould be removed now that the Stimulus controller sends CSRF tokens. Not a BLOCKER since the bypass existed before this PR, but it should not persist.Missing
submitOnEntermethod referenced indocs/hotwire.md. Either add the method + action binding, or remove the reference from the docs. Behavioral regression from the prior inline JS.Double DB writes in
resolve-- theproperty.update()calls on lines 26-27 are redundant for new records and could be consolidated. Consider:Service.order(:name).eachin the ERB template (line 32 ofindex.html.erb) queries the DB directly from the view. Should be loaded in the controller (@services = Service.order(:name)) and referenced as@services.eachin the view. This is a standard Rails convention to keep views free of query logic.service_idsvalidation -- consider validating that submitted IDs correspond to actualServicerecords before assignment, to avoid unhandled 500s from FK violations.formSectiontarget declared but never toggled. The old inline JS hid#number-sectionuntil GPS succeeded (display: none). The new ERB shows the form always. TheformSectiontarget is declared in the controller but never used -- dead code. Either remove it or restore the hide/show behavior.SOP COMPLIANCE
hotwire, not3-hotwireor3-install-hotwire. Does not follow{issue-number}-{kebab-case-purpose}convention.PROCESS OBSERVATIONS
docs/hotwire.mdis a good reference for the Hotwire setup and conventions. Minor inaccuracy: referencessubmitOnEnter()which does not exist in the controller.spec/directory exists andrspec-railsis in the Gemfile, but no request specs for theresolveaction (which now accepts new parameters) and no system/integration tests. The Test Plan section lists manual checks only. For an early-stage app this is understandable, but the newservice_ids,client_name, andspecial_notesparameters onresolveshould have request specs covering: happy path, missing fields, invalid service IDs, and the find-or-create vs. update-existing paths.renderRecent()is the primary risk. The CSRF bypass is inherited, not new, but should be cleaned up.VERDICT: NOT APPROVED
One BLOCKER: XSS via
.innerHTMLinrenderRecent(). Fix the DOM injection to usetextContentor DOM construction methods. Theskip_forgery_protectionremoval andsubmitOnEnterregression are strong recommendations but not formal blockers per criteria.