Timezone-aware completion timestamps and location-based time #184
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/timezone-aware-completion"
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
completed_attimestamp and display "Finished at [time]" in the property's local timezone when checked offtime_zone(IANA string) set automatically during GPS detection, validated on writeDate.currentso "Today" and all date logic respect the crew's actual locationrake devnow starts docker compose on port 7143 with automatic takeover -- stops whatever currently owns the port so the dev URL always points to the current checkoutChanges
db/migrate/20260608100000_add_timezone_support.rb: addscompleted_at(datetime) to work_queue_items,time_zone(string) to propertiesapp/models/work_queue_item.rb:toggle_completion!sets/clears completed_at;finished_at_localconverts to property timezoneapp/models/property.rb:local_time_zonehelper, validates time_zone is recognized IANA string with 50-char length limitapp/controllers/application_controller.rb:set_time_zone_from_browserbefore_action readsbrowser_tzcookieapp/controllers/work_queue_items_controller.rb: usestoggle_completion!instead of manual boolean flipapp/controllers/properties_controller.rb: permits:time_zonein strong paramsapp/views/layouts/application.html.erb: inline script setsbrowser_tzcookie fromIntl.DateTimeFormatapp/views/work_queue_items/_queue_item.html.erb: shows "Finished at [time]" when completed, address when pendingapp/javascript/controllers/add_location_controller.js: sends latitude, longitude, and time_zone on GPS saveRakefile:devtask starts docker compose on :7143 with port takeover (stops existing container first)spec/models/work_queue_item_spec.rb: 5 new tests for toggle_completion! and finished_at_localspec/models/property_spec.rb: 3 new tests for timezone validationspec/requests/work_queue_items_spec.rb: updated toggle test + new uncomplete testTest Plan
bundle exec rspec spec/models/work_queue_item_spec.rb spec/models/property_spec.rb)Review Checklist
Related Notes
landscaping-assistant-- project this affectsPR #184 Review
DOMAIN REVIEW
Stack: Ruby on Rails 8.1, Hotwire/Turbo, Stimulus JS, PostgreSQL, Docker-based dev.
Model layer (
WorkQueueItem,Property)toggle_completion!is clean -- usesupdate!so failures raise, and atomically pairscompletedwithcompleted_at. Good pattern.finished_at_localcorrectly falls back toTime.zone(set from the browser cookie) when the property has no stored timezone. The chain: property timezone -> browser timezone -> app default is sound.ActiveSupport::TimeZone[]returnsnilfor unrecognized strings, solocal_time_zoneis safe against garbage input. Good.Controller layer
set_time_zone_from_browserinApplicationControllerreadscookies[:browser_tz]and only setsTime.zoneifActiveSupport::TimeZone[]returns a valid zone. This is safe --ActiveSupport::TimeZone[]acts as implicit validation; arbitrary cookie values are rejected silently. No injection risk.properties_controller.rb::time_zonecorrectly added to strong params. Thetime_zonevalue comes fromIntl.DateTimeFormat().resolvedOptions().timeZoneon the client (IANA string likeAmerica/Denver), which is appropriate.View layer
<script>tag inapplication.html.erbfor setting thebrowser_tzcookie is minimal and correct.SameSite=Laxandpath=/are appropriate. The 1-yearmax-ageis reasonable for a timezone preference._queue_item.html.erbguards withitem.completed? && item.completed_at.present?before callingfinished_at_local.strftime(...). This correctly handles the edge case where legacy items might havecompleted: truebutcompleted_at: nil(from before this migration). Good defensive coding.JavaScript (
add_location_controller.js)this.lat/this.lonfrom the GPS callback and sending them alongsidetime_zonein the PATCH payload is clean. This fixes a gap where lat/lng were detected but never persisted.Migration
add_columncalls in a singlechangemigration -- simple and reversible. No data migration needed since these are new nullable columns.bin/devdocker compose up. This is a dev tooling fix, not a feature change. Appropriate for the repo's Docker-based workflow.BLOCKERS
None.
toggle_completion!andfinished_at_local, plus 2 updated/new request specs for the toggle endpoint. Both happy path and edge cases (nil completed_at, uncomplete flow) are covered.browser_tzcookie is validated throughActiveSupport::TimeZone[]before use, andtime_zonein strong params is a plain string column with no execution risk.NITS
finished_at_localfallback test could be more specific (spec/models/work_queue_item_spec.rb:49-51): The "falls back to app timezone when property has no timezone" test only assertsbe_present. It would be stronger to assert the actual timezone name matchesTime.zone.nameto confirm the fallback logic works correctly, not just that a non-nil value is returned.Cookie value encoding: The inline script does not URI-encode the timezone string before writing it to the cookie. IANA timezone names like
America/New_Yorkcontain/which is technically safe in cookie values per RFC 6265, butAmerica/Argentina/Buenos_Aires(three-part names) also work fine. This is a non-issue in practice -- just noting for completeness.time_zonecolumn has no length constraint: Thestringcolumn fortime_zonedefaults to varchar(255) in PostgreSQL, which is more than sufficient for any IANA timezone name (max ~30 chars). No action needed, but a model-levelvalidates :time_zone, length: { maximum: 50 }, allow_blank: truecould add defense-in-depth if desired.No validation that
time_zoneis a recognized IANA string on write: Currently any string can be stored inproperties.time_zone. A model validation likevalidate { errors.add(:time_zone, "is not recognized") if time_zone.present? && ActiveSupport::TimeZone[time_zone].nil? }would catch bad data at write time rather than silently falling back at read time. Non-blocking sincelocal_time_zonealready handles invalid values gracefully.SOP COMPLIANCE
feature/timezone-aware-completion-- does not follow the{issue-number}-{kebab-case}convention (would be183-timezone-aware-completion), but this is a nit given the branch clearly relates to the feature.bin/devchange is a dev tooling fix bundled with the feature -- minor scope expansion but reasonable since it is a one-liner fixing the dev workflow.PROCESS OBSERVATIONS
completeditems will show the address (not "Finished at") until they are toggled again, which is acceptable behavior. Thecompleted? && completed_at.present?guard handles this correctly.VERDICT: APPROVED
Nit fixes (post-review)
All 4 nits from QA addressed:
eq("Mountain Time (US & Canada)")instead ofbe_presentPropertynow validatestime_zoneis a recognized IANA string viaActiveSupport::TimeZone[]lookupvalidates :time_zone, length: { maximum: 50 }Also added:
devtask -- now runsdocker compose upon port 7143 with automatic takeover (stops whatever currently owns the port). This makesrake devthe single entry point for local dev, pointing the dev URL (landscaping-dev.tail5b443a.ts.net) at whichever checkout you run it from.All 19 model specs pass.
PR #184 Review (Re-review)
Previous review: APPROVED with 4 nits. This re-review covers the nit fixes and additional changes since then.
DOMAIN REVIEW
Stack: Ruby on Rails 8.1, Stimulus JS, ERB templates, RSpec, Docker Compose, PostgreSQL.
Model layer (Property)
validates :time_zone, length: { maximum: 50 }, allow_blank: truecombined with the customtime_zone_must_be_validvalidator that checksActiveSupport::TimeZone[time_zone]. Theallow_blank: trueon length plusif: -> { time_zone.present? }on the custom validator means blank strings pass cleanly and only non-blank values are checked against the IANA registry. Correct.local_time_zonehelper returns theActiveSupport::TimeZoneobject or nil. Clean interface.Model layer (WorkQueueItem)
toggle_completion!usesupdate!which will raise on validation failure -- appropriate for a bang method.finished_at_localfallback chain: property timezone ->Time.zone(set by browser cookie or app default). Sound design.Controller layer
set_time_zone_from_browser: readsbrowser_tzcookie, looks it up inActiveSupport::TimeZone, setsTime.zoneonly if valid. TheActiveSupport::TimeZone[]lookup acts as implicit validation -- arbitrary cookie strings that are not valid IANA names return nil and are silently ignored. No injection risk.:time_zoneproperly added to strong params inproperties_controller.rb.View layer
<script>tag for the browser_tz cookie is minimal and correct.SameSite=Laxis appropriate. Max-age of 31536000 (1 year) is reasonable.item.completed?ANDitem.completed_at.present?before callingfinished_at_local.strftime, which avoids nil errors for any legacy data that hascompleted: truebut nocompleted_at.JavaScript (add_location_controller.js)
reverseGeocode, then sends all three (latitude, longitude, time_zone) in the PATCH body.Intl.DateTimeFormat().resolvedOptions().timeZoneis the standard browser API for IANA timezone detection. Good.Migration
completed_at(datetime) andtime_zone(string). Both nullable, no default needed. Clean and reversible viachange.Test coverage
toggle_completion!specs (complete and uncomplete paths).finished_at_localspecs (property timezone, fallback to app timezone, nil when not completed).eq("Mountain Time (US & Canada)")matchingconfig/application.rbline 37. Correct fix from previous nit.Rakefile
devtaskdocker ps --filter "publish=7143"to find the existing container, then inspects the compose project label and runsdocker compose -p #{project} down. One note: the#{existing}and#{project}variables are interpolated into backtick/system calls, but both values come fromdocker psanddocker inspectoutput (container IDs and compose project names), not from user input. The risk is minimal in a dev-only Rake task.bin/setup
.ruby-versionis a good guard. Thedocker compose versioncheck prevents cryptic errors later. Both abort with actionable messages.docs/local-dev-setup.md
BLOCKERS
None.
NITS
Rakefile shell interpolation (
Rakefileline 13):#{existing}is interpolated into a backtick expression. While the value comes fromdocker psoutput and this is a dev-only task, usingOpen3.capture2or passing the variable as a separate argument would be more defensive. Non-blocking since the input source is trusted.Cookie encoding (
application.html.erbline 24): If the timezone string ever contains characters that need URL encoding (unlikely for IANA names but technically possible for some edge locales), the cookie value is set raw. ConsiderencodeURIComponent()for defense-in-depth:document.cookie="browser_tz="+encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone)+.... Non-blocking.SOP COMPLIANCE
feature/timezone-aware-completion-- descriptive, though convention is{issue-number}-{purpose}(would be183-timezone-aware-completion). Non-blocking.PROCESS OBSERVATIONS
toggle_completion!pattern is a good refactor from the previous inline boolean flip in the controller, centralizing the completed/completed_at coupling in the model where it belongs.VERDICT: APPROVED