Skip to content

1.10.2 - #3199

Merged
Freika merged 51 commits into
masterfrom
dev
Jul 27, 2026
Merged

1.10.2#3199
Freika merged 51 commits into
masterfrom
dev

Conversation

@Freika

@Freika Freika commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Bug Fixes
    • Improved reverse-geocoding resilience by treating transient provider/TLS issues as non-fatal while still reporting real failures.
    • Broadened and stabilized write/upsert contention retries (deadlocks, lock timeouts, query cancellations), including visit/point update cases.
    • Prevented “Null Island” issues by rejecting (0,0) points and cleaning legacy anomalies; also reduced retry churn in background jobs (including email legacy trial handling) and fixed DNS caching to surface the correct SMTP/config error.
  • New Features
    • Added user-visible name locking across API responses, place UI, and map layers.
  • Tests
    • Expanded coverage for retry/error handling, (0,0) cleanup, email skipping/discarding, name-lock behavior, and visit suggestion/debouncing.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Reverse geocoding now classifies provider failures, retries broader database contention errors, and protects user-set place names. Visit scheduling, merging, and failure notifications are bounded more precisely. Legacy emails, migrations, null-island cleanup, and DNS resolution receive additional reliability handling.

Changes

Reverse-geocoding resilience

Layer / File(s) Summary
Provider error classification
app/services/reverse_geocoding/..., app/services/places/name_fetcher.rb, app/services/visits/names/fetcher.rb, spec/services/**/*
Transient provider and TLS failures are warned and suppressed, while non-transient errors remain reportable.
Write contention retries
app/models/concerns/archivable.rb, app/services/reverse_geocoding/..., spec/models/concerns/..., spec/services/reverse_geocoding/*
Upserts and point updates retry deadlocks, lock-wait timeouts, and query cancellations; place update payloads are deduplicated and sorted.
Timestamp-safe null-island cleanup
app/jobs/data_migrations/cleanup_null_island_job.rb, spec/jobs/data_migrations/*
Affected-month calculation skips timestamp-less points while preserving anomaly and track recalculation behavior.

User-locked place names

Layer / File(s) Summary
Name-lock persistence and creation
db/migrate/*, db/schema.rb, app/models/place.rb, app/controllers/*places_controller.rb, app/services/visits/{create,select_place}.rb, spec/models/place_spec.rb, spec/services/visits/select_place_spec.rb
Places track name locks, user naming, and machine naming across creation and updates.
Geocoding protection
app/services/places/name_fetcher.rb, app/services/reverse_geocoding/places/fetch_data.rb, spec/services/{places,reverse_geocoding}/*
Reverse geocoding refreshes location fields without overwriting locked names and propagates the saved name to default-named visits.
API and interface exposure
app/serializers/api/place_serializer.rb, app/controllers/api/v1/places_controller.rb, app/javascript/controllers/maps/maplibre/*, app/views/places/_drawer.html.erb, spec/requests/*
Lock state is included in API and map payloads and displayed in place interfaces.

Visit processing reliability

Layer / File(s) Summary
Suggestion scheduling and time bounds
app/jobs/visit_suggesting_job.rb, app/services/visits/{realtime_debouncer,time_chunks}.rb, spec/jobs/visit_suggesting_job_spec.rb, spec/services/visits/time_chunks_spec.rb
Debounce keys are released safely, realtime lookback is six hours, and suggestion chunks end at the requested boundary.
Visit merging and failure notifications
app/services/visits/{merger,suggest}.rb, spec/services/visits/{merger,suggest}_spec.rb
Merged visits recalculate duration, center, radius, and suggested name; repeated suggestion failures use Redis notification deduplication without exposing stack traces.

Stale email handling

Layer / File(s) Summary
Legacy trial suppression and missing-record discard
app/jobs/users/mailer_sending_job.rb, app/mailers/{application_mailer,users_mailer}.rb, spec/{jobs,mailers}/*
Legacy trial lifecycle emails are skipped, and deliveries for deleted records are discarded without retrying.

Migration stability

Layer / File(s) Summary
Retryable legacy-column removal
db/migrate/20260714090000_drop_legacy_lat_lon_from_points.rb, app/jobs/data_migrations/drop_legacy_lat_lon_job.rb, spec/migrations/*, spec/jobs/data_migrations/*
Legacy point columns use bounded lock retries and background-job fallback, with manual SQL logged when retries or enqueueing fail.

Runtime support and release notes

Layer / File(s) Summary
DNS resolver input guard
config/initializers/dns_cache.rb, spec/initializers/dns_cache_spec.rb
Non-string resolver inputs bypass caching and retain native resolver errors; IP literals and hostname caching remain covered.
Reliability changelog
CHANGELOG.md
The changelog records the reliability fixes across delivery, reverse geocoding, ingestion, visits, migrations, uploads, and DNS caching.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • Error suggesting visits #3091: Visit suggestion failures now use structured logging, exception reporting, and Redis-based notification deduplication.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is only a version number and does not describe the actual change set. Rename it to a descriptive title that summarizes the main change, such as "Prepare 1.11.0 release" or the primary fix area.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/services/reverse_geocoding/points/fetch_data.rb (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Transient-error warn logs lose useful detail for Geocoder::ResponseParseError.

All three sites log e.message for TRANSIENT errors, but Geocoder::ResponseParseError#initialize(response) doesn't call super, so #message returns the class name rather than the parsed response — the one case where diagnostic content would matter most for a warn-level log.

  • app/services/reverse_geocoding/points/fetch_data.rb#L53-54: include e.class (and/or e.response when present) alongside e.message in the warn log.
  • app/services/places/name_fetcher.rb#L44-45: same adjustment to the warn log format.
  • app/services/visits/names/fetcher.rb#L25-26: same adjustment to the warn log format.
♻️ Proposed fix (example for fetch_data.rb)
   rescue *ReverseGeocoding::ProviderErrors::TRANSIENT => e
-    Rails.logger.warn("Reverse geocoding provider error for point #{point.id}: #{e.message}")
+    Rails.logger.warn("Reverse geocoding provider error for point #{point.id}: #{e.class} - #{e.message}")

Per geocoder's exceptions.rb, ResponseParseError#initialize only sets @response without calling super.

🤖 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 `@app/services/reverse_geocoding/points/fetch_data.rb` at line 1, Update the
TRANSIENT warning logs in the fetch_data, name_fetcher, and visits names fetcher
error handlers to include e.class and, when available, e.response alongside
e.message. Preserve the existing warning context and handling while ensuring
Geocoder::ResponseParseError logs its parsed response details.
spec/services/reverse_geocoding/places/fetch_data_spec.rb (1)

439-448: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New spec tests a private method via send.

save_places is private; testing it directly via send violates the spec guideline to test through the public interface. Consider exercising this via service.call with two geocoded results resolving to existing places, then asserting on Place.upsert_all's argument ordering.

As per coding guidelines, "Never test private methods via send(). Test through the public interface instead."

🤖 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 `@spec/services/reverse_geocoding/places/fetch_data_spec.rb` around lines 439 -
448, Rewrite the ordering spec to exercise the public service.call interface
instead of invoking private save_places via send. Configure two geocoded results
that resolve to the existing first_place and second_place, then assert
Place.upsert_all receives their attributes ordered by primary key with
unique_by: :id.

Source: Coding guidelines

spec/jobs/data_migrations/cleanup_null_island_job_spec.rb (1)

45-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify test execution and assertions.

The not_to raise_error matcher is generally discouraged in RSpec, as unexpected errors will inherently fail the test and provide a useful stack trace, whereas not_to raise_error masks that output. Executing the job inline yields a cleaner structure.

♻️ Proposed refactor
   it 'flags legacy points without timestamps and recalculates their tracks' do
     zero_point.update_column(:timestamp, nil)
 
-    expect { described_class.perform_now(user.id) }.not_to raise_error
+    described_class.perform_now(user.id)
 
     expect(Tracks::RecalculateJob).to have_been_enqueued.with(track.id)
     expect(Stats::CalculatingJob).not_to have_been_enqueued
     expect(zero_point.reload.anomaly).to be(true)
   end
🤖 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 `@spec/jobs/data_migrations/cleanup_null_island_job_spec.rb` around lines 45 -
54, In the “flags legacy points without timestamps and recalculates their
tracks” example, remove the expect block wrapping described_class.perform_now
and invoke the job directly. Keep the existing enqueue and anomaly assertions
unchanged so unexpected errors fail naturally with their stack traces.
🤖 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.

Nitpick comments:
In `@app/services/reverse_geocoding/points/fetch_data.rb`:
- Line 1: Update the TRANSIENT warning logs in the fetch_data, name_fetcher, and
visits names fetcher error handlers to include e.class and, when available,
e.response alongside e.message. Preserve the existing warning context and
handling while ensuring Geocoder::ResponseParseError logs its parsed response
details.

In `@spec/jobs/data_migrations/cleanup_null_island_job_spec.rb`:
- Around line 45-54: In the “flags legacy points without timestamps and
recalculates their tracks” example, remove the expect block wrapping
described_class.perform_now and invoke the job directly. Keep the existing
enqueue and anomaly assertions unchanged so unexpected errors fail naturally
with their stack traces.

In `@spec/services/reverse_geocoding/places/fetch_data_spec.rb`:
- Around line 439-448: Rewrite the ordering spec to exercise the public
service.call interface instead of invoking private save_places via send.
Configure two geocoded results that resolve to the existing first_place and
second_place, then assert Place.upsert_all receives their attributes ordered by
primary key with unique_by: :id.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fb56b5b-3550-4735-b40e-4b445fa06378

📥 Commits

Reviewing files that changed from the base of the PR and between 5ea231d and 1ec0814.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • app/jobs/data_migrations/cleanup_null_island_job.rb
  • app/models/concerns/archivable.rb
  • app/services/places/name_fetcher.rb
  • app/services/reverse_geocoding/places/fetch_data.rb
  • app/services/reverse_geocoding/points/fetch_data.rb
  • app/services/reverse_geocoding/provider_errors.rb
  • app/services/visits/names/fetcher.rb
  • spec/jobs/data_migrations/cleanup_null_island_job_spec.rb
  • spec/models/concerns/archivable_spec.rb
  • spec/services/places/name_fetcher_spec.rb
  • spec/services/reverse_geocoding/places/fetch_data_spec.rb
  • spec/services/reverse_geocoding/points/fetch_data_spec.rb
  • spec/services/visits/names/fetcher_spec.rb

Freika and others added 8 commits July 20, 2026 23:56
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
Legacy Manager-owned trial lifecycle email types are skipped and logged
instead of raising UnknownEmailType, so stale jobs stop retrying forever.

Stale ActionMailer::MailDeliveryJob entries that bypass the wrapper are
absorbed by no-op mailer actions, and mail addressed to a record that has
since been hard-deleted is discarded rather than re-raising
ActiveJob::DeserializationError.
Adds a regression example for non-String hosts that are not string-like
(Integer), and covers the caching behaviour itself: a hostname resolves
once and later calls are served from Rails.cache. Also records the fix
in the changelog. (#3038)
Fix NoMethodError from the DNS cache when the resolver gets a nil name

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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 `@spec/initializers/dns_cache_spec.rb`:
- Around line 24-29: Update the hostname used in the Resolv cache example around
getaddress_without_cache to be unique per test run, or explicitly clear/isolate
its Rails.cache entry before exercising the two Resolv.getaddress calls.
Preserve the expectation that both calls return the stubbed address and
getaddress_without_cache is invoked exactly once.
- Around line 17-18: Update the “returns them without a DNS lookup” example to
verify that IP literals bypass the cache by asserting neither Rails.cache read
nor write operations are invoked, while retaining the existing Resolv.getaddress
result assertion.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 834de623-0f0a-4523-b464-9d3eaa7043a9

📥 Commits

Reviewing files that changed from the base of the PR and between dfe9d2d and bf04182.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • config/initializers/dns_cache.rb
  • spec/initializers/dns_cache_spec.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment on lines +17 to +18
it 'returns them without a DNS lookup' do
expect(Resolv.getaddress('127.0.0.1')).to eq('127.0.0.1')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that IP literals bypass the cache.

This example only verifies the returned value, so an implementation that still reads or writes Rails.cache would pass. Add expectations that both cache operations are not invoked.

As per coding guidelines, RSpec tests should test observable behavior rather than implementation details.

Suggested assertion
     it 'returns them without a DNS lookup' do
+      expect(Rails.cache).not_to receive(:read)
+      expect(Rails.cache).not_to receive(:write)
       expect(Resolv.getaddress('127.0.0.1')).to eq('127.0.0.1')
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it 'returns them without a DNS lookup' do
expect(Resolv.getaddress('127.0.0.1')).to eq('127.0.0.1')
it 'returns them without a DNS lookup' do
expect(Rails.cache).not_to receive(:read)
expect(Rails.cache).not_to receive(:write)
expect(Resolv.getaddress('127.0.0.1')).to eq('127.0.0.1')
🤖 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 `@spec/initializers/dns_cache_spec.rb` around lines 17 - 18, Update the
“returns them without a DNS lookup” example to verify that IP literals bypass
the cache by asserting neither Rails.cache read nor write operations are
invoked, while retaining the existing Resolv.getaddress result assertion.

Source: Coding guidelines

Comment on lines +24 to +29
allow(Resolv).to receive(:getaddress_without_cache).and_return('203.0.113.10')

expect(Resolv.getaddress('cache-me.invalid')).to eq('203.0.113.10')
expect(Resolv.getaddress('cache-me.invalid')).to eq('203.0.113.10')

expect(Resolv).to have_received(:getaddress_without_cache).once

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate cache state in the hostname example.

The fixed hostname can already exist in Rails.cache from another example or test process. In that case, the first call returns the stale entry, the resolver stub is never exercised, and the example fails or gives misleading coverage. Use a unique hostname or isolate the cache boundary.

Suggested fix
     it 'resolves once and serves later calls from the cache' do
+      hostname = "cache-me-#{SecureRandom.hex(8)}.invalid"
       allow(Resolv).to receive(:getaddress_without_cache).and_return('203.0.113.10')

-      expect(Resolv.getaddress('cache-me.invalid')).to eq('203.0.113.10')
-      expect(Resolv.getaddress('cache-me.invalid')).to eq('203.0.113.10')
+      expect(Resolv.getaddress(hostname)).to eq('203.0.113.10')
+      expect(Resolv.getaddress(hostname)).to eq('203.0.113.10')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
allow(Resolv).to receive(:getaddress_without_cache).and_return('203.0.113.10')
expect(Resolv.getaddress('cache-me.invalid')).to eq('203.0.113.10')
expect(Resolv.getaddress('cache-me.invalid')).to eq('203.0.113.10')
expect(Resolv).to have_received(:getaddress_without_cache).once
hostname = "cache-me-#{SecureRandom.hex(8)}.invalid"
allow(Resolv).to receive(:getaddress_without_cache).and_return('203.0.113.10')
expect(Resolv.getaddress(hostname)).to eq('203.0.113.10')
expect(Resolv.getaddress(hostname)).to eq('203.0.113.10')
expect(Resolv).to have_received(:getaddress_without_cache).once
🤖 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 `@spec/initializers/dns_cache_spec.rb` around lines 24 - 29, Update the
hostname used in the Resolv cache example around getaddress_without_cache to be
unique per test run, or explicitly clear/isolate its Rails.cache entry before
exercising the two Resolv.getaddress calls. Preserve the expectation that both
calls return the stubbed address and getaddress_without_cache is invoked exactly
once.

Fixes #3204 by aligning the server-side render distance limit with Poster Studio.
Freika and others added 14 commits July 25, 2026 22:37
- Support raster XYZ and style.json custom basemaps in Map v2
- Fall back to the default style when a custom basemap fails to load
- fix: recover from unavailable custom basemaps
PostgreSQL raises QueryCanceled, not StatementTimeout, when
statement_timeout fires, so the drop still aborted the migration on
instances that set one. Rescue QueryAborted, which covers both.

Wrap the job hand-off so an unreachable Redis logs and lets startup
finish instead of re-raising into the crash loop this change removes.

Flatten the job backoff to 5 minutes; polynomially_longer spanned days
at the tail rather than the intended few hours.
A pooled connection can carry a session-level statement_timeout set by
another job, which cancels the drop no matter how the lock race goes.
Clear it for the duration of the drop and reset it afterwards.

Capping retries re-raised into Sidekiq's own retry chain, turning a
quiet wait into weeks of reported failures ending in the dead set.

Narrow the enqueue rescue to connection failures so a NameError or a
serialization bug surfaces instead of silently stranding the columns.

Correct the comment claiming a waiting drop never queues ahead of
writers: it does, and the short lock timeout only bounds the stall.
A bare SET and the following ALTER can land on different backends under
PgBouncer transaction pooling, leaving the drop with no timeout and an
unbounded ACCESS EXCLUSIVE request queued ahead of every points read and
write. SET LOCAL inside an explicit transaction keeps both timeouts on
the backend that runs the ALTER, matching Visits::StayPointDetector.

Scoping the timeouts this way also removes both ensure blocks: a raise
in RESET lock_timeout used to mask the LockWaitTimeout that retry_on
needs to see, and a no-op run issued two RESETs for timeouts it never
set.

Cap the job's retries again and log once on exhaustion. Unlimited
attempts stalled points writes every five minutes with no retry set, no
dead set and no log line to find.

Widen the enqueue rescue back to StandardError. A malformed REDIS_URL
or an exhausted pool raises outside the connection-error families, and
no enqueue failure is worth restarting the container for.
Five bugs in the visit-suggestion subsystem, plus review follow-ups.

Visits::RealtimeDebouncer#clear was defined but never called, so the
`nx: true` guard never released for a continuously-tracking user and
realtime detection fired exactly once. VisitSuggestingJob now releases
the key, guarded so a Redis blip can't drop the run under retry: false.
The realtime lookback drops to 6h: clusters matching an existing visit
never claim their points, so a 25h window re-detected and re-geocoded
them on every run.

Visits::TimeChunks discarded end_at whenever start and end shared a
year, so the nightly job scanned from yesterday to 31 December. Three
specs asserted that in their own titles; they were characterization
tests, now rewritten.

Visits::Suggest interpolated a backtrace into a user-facing notification
and returned ExceptionReporter's value instead of an array.
ExceptionReporter no-ops when self-hosted, so the backtrace now goes to
Rails.logger unconditionally, and repeat notifications are gated by a
Redis SET NX claim that fails open.

Visits::Merger updated end_time and points but left duration, centre,
radius and suggested_name at their pre-merge values, so Creator wrote
the wrong duration and PlaceFinder resolved the first sub-cluster's
place. The centre is recomputed on every absorption because
can_merge_visits? compares against it; the rest is recomputed once when
a chain closes, keeping the name if the geocoder lookup fails.

Reverse geocoding overwrote user-chosen place names nightly, via
FetchData#update_place, FetchData#populate_place_attributes (upsert_all,
so callbacks cannot guard it) and Places::NameFetcher. A new
places.name_locked_at, set on rename and on user-driven creation,
protects them; renaming a place back to "Suggested place" hands it back
to automatic naming. The lock state is exposed through both place
serializers and surfaced in the Map v2 info panel and the drawer.
Poster Studio gains a 50-300% track width slider next to track opacity.
The value is persisted as route_width and converted server-side into a
0.5-3.0 multiplier on the trackWidth style parameter, which until now
was declared in style_builder.js but never set by any caller.

Also covers the #3204 regression: a continent-wide frame completes
instead of failing the area check.
Adds specs for a below-range width clamping to the 0.5 floor and a
negative width falling back to 1.0, both previously unexercised, and
records the new control in the changelog.
Every attempt queues an ACCESS EXCLUSIVE request that holds up each
points reader and writer behind it. The lock is either free almost
immediately or held by a long transaction a longer wait cannot outlast,
so wait 1s instead of 5s and let boot try three times rather than ten —
around 12s of startup instead of 185s, with the job as the real fallback.

Neither dead end promises a rescue that will not come. The migration is
recorded as applied whether or not the enqueue succeeds, and that enqueue
is the only one in the codebase, so a failed hand-off and an exhausted
job now both log the ALTER TABLE to run by hand.
Add a Poster Studio track width control
One second expires inside a batch. The same release backfills tracker
ids and clears raw_data in 5k and 10k row batches that each hold
RowExclusive on points for seconds, so the job would lose all 288
attempts against exactly the writers it has to wait out. At one try per
five minutes a five second stall costs a fraction of a percent of the
time, which the odds of ever finishing are worth. Boot keeps its short
wait and three tries — a deploy should not be held up.

Wrap both manual remedies in BEGIN and SET LOCAL so pasting one cannot
queue the unbounded ACCESS EXCLUSIVE request this migration exists to
avoid, and pin the tuned values so interpolating them into the SQL
assertions cannot hide a bad edit.
Make visit suggestion consistent and predictable

def populate_place_attributes(place, data)
place.name = place_name(data)
place.name = place_name(data) unless place.name_locked?
The area check built a flat-degree box around the poster centre while the
renderer frames in Mercator. At the old 20 km cap the two agreed to within
0.00 degrees, but at continental distances they diverge: at 60N over 5,000 km
the box sat 3.66 degrees north of the frame, so tracks inside the poster were
rejected and tracks outside it were accepted and rendered off-frame.

Latitude bounds now come from the same Mercator framing render.mjs uses, and
longitude is compared with wrapping so a frame straddling the antimeridian no
longer rejects a track three degrees from its centre.

Poster Studio shares the geometry through poster_studio/render/frame_geometry
and warns when a view is too wide for the largest poster area instead of
silently zooming the saved poster in.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
spec/services/reverse_geocoding/places/fetch_data_spec.rb (1)

483-493: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Avoid testing private save_places and its payload shape directly.

This test calls service.send(:save_places, ...) and asserts an internal upsert_all array, violating the spec guidelines to test observable behavior and avoid private-method testing. Exercise the behavior through service.call and verify persisted results or observable contention handling instead.

As per coding guidelines, specs must test observable behavior and must never test private methods via send().

🤖 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 `@spec/services/reverse_geocoding/places/fetch_data_spec.rb` around lines 483 -
493, Replace the private save_places invocation in the “orders bulk updates by
primary key” example with an observable service.call flow using the unsorted
places as input. Verify the resulting persisted places or other public behavior
that demonstrates deterministic primary-key ordering, without stubbing or
asserting the internal upsert_all payload.

Source: Coding guidelines

app/services/reverse_geocoding/places/fetch_data.rb (1)

148-160: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist machine_named in bulk writes.

update_place sets place.machine_named = true, but sibling name refreshes skip callbacks via Place.insert_all/Place.upsert_all, and the bulk payloads do not include machine_named. Add the marker to both creation and update payloads so reverse-geocoded place names keep their naming metadata consistent.

🤖 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 `@app/services/reverse_geocoding/places/fetch_data.rb` around lines 148 - 160,
Update the bulk payload construction in the reverse-geocoding persistence flow
to include machine_named in both creation and update attribute hashes used by
Place.insert_all/Place.upsert_all. Ensure the value reflects the marker set by
update_place so refreshed place names retain consistent naming metadata.
🤖 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 `@app/models/place.rb`:
- Around line 71-78: Update
ReverseGeocoding::Places::FetchData#populate_place_attributes and every other
machine-generated naming path to set machine_named before assigning or saving
the generated place name. Ensure lock_name_on_user_edit recognizes these changes
as machine-originated so they do not persist name_locked_at, while preserving
user-edit behavior for unmarked name changes.

In `@app/services/reverse_geocoding/places/fetch_data.rb`:
- Around line 39-50: Wrap the primary place update in update_place with the
existing bounded with_deadlock_retry helper, ensuring place.machine_named
assignment and place.update!(attributes) execute within the retry block.
Preserve the current attributes and update behavior while applying the same
contention policy used by Place#save_places and Place#upsert_all.

In `@app/services/visits/select_place.rb`:
- Line 18: Update the place-locking logic in the select-place flow to skip
name_locked_at updates when the place still has Place::DEFAULT_NAME. Preserve
locking for non-default names and rely on the model’s default-name handling so
Places::NameFetcher can replace the placeholder with a geocoded name.

In `@app/views/places/_drawer.html.erb`:
- Around line 17-19: Update the lock marker span in the place drawer to remove
the custom place-drawer__name-lock styling hook and use the project’s
Tailwind/DaisyUI utility classes for its styling, while preserving its title,
data-testid, lock icon, and naming behavior.

In `@CHANGELOG.md`:
- Line 16: Remove the duplicate Null Island changelog bullet near the top of
CHANGELOG.md, preserving the existing entry at line 22 with its timestamp-less
legacy-point detail.

In `@db/migrate/20260727120000_add_name_locked_at_to_places.rb`:
- Around line 4-7: Update the migration’s change method to remain reversible:
remove the column_exists? early return so Rails can automatically remove
name_locked_at on rollback, or replace change with explicit up and down methods
that add and remove the column respectively.

In `@spec/services/places/name_fetcher_spec.rb`:
- Around line 88-90: Update the example “still refreshes city and country” to
assert that service.call changes both place.city and place.country from their
initial values to the expected refreshed values, preserving the existing city
assertion while adding coverage for country updates.

In `@spec/services/reverse_geocoding/places/fetch_data_spec.rb`:
- Line 58: Resolve the Lint/AmbiguousBlockAssociation warnings in both
expectations around service.call by parenthesizing the matcher argument so the
block is unambiguously associated with expect. Apply the same adjustment to the
expectation at the second referenced location, then run bundle exec rubocop on
the modified Ruby file.

---

Outside diff comments:
In `@app/services/reverse_geocoding/places/fetch_data.rb`:
- Around line 148-160: Update the bulk payload construction in the
reverse-geocoding persistence flow to include machine_named in both creation and
update attribute hashes used by Place.insert_all/Place.upsert_all. Ensure the
value reflects the marker set by update_place so refreshed place names retain
consistent naming metadata.

In `@spec/services/reverse_geocoding/places/fetch_data_spec.rb`:
- Around line 483-493: Replace the private save_places invocation in the “orders
bulk updates by primary key” example with an observable service.call flow using
the unsorted places as input. Verify the resulting persisted places or other
public behavior that demonstrates deterministic primary-key ordering, without
stubbing or asserting the internal upsert_all payload.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 99ffed76-6031-4fd4-af04-1604003d32e4

📥 Commits

Reviewing files that changed from the base of the PR and between bf04182 and 5d35267.

📒 Files selected for processing (29)
  • CHANGELOG.md
  • app/controllers/api/v1/places_controller.rb
  • app/controllers/places_controller.rb
  • app/javascript/controllers/maps/maplibre/data_loader.js
  • app/javascript/controllers/maps/maplibre/event_handlers.js
  • app/jobs/visit_suggesting_job.rb
  • app/models/place.rb
  • app/serializers/api/place_serializer.rb
  • app/services/places/name_fetcher.rb
  • app/services/reverse_geocoding/places/fetch_data.rb
  • app/services/visits/create.rb
  • app/services/visits/merger.rb
  • app/services/visits/realtime_debouncer.rb
  • app/services/visits/select_place.rb
  • app/services/visits/suggest.rb
  • app/services/visits/time_chunks.rb
  • app/views/places/_drawer.html.erb
  • db/migrate/20260727120000_add_name_locked_at_to_places.rb
  • db/schema.rb
  • spec/jobs/visit_suggesting_job_spec.rb
  • spec/models/place_spec.rb
  • spec/requests/api/v1/places_spec.rb
  • spec/requests/places_spec.rb
  • spec/services/places/name_fetcher_spec.rb
  • spec/services/reverse_geocoding/places/fetch_data_spec.rb
  • spec/services/visits/merger_spec.rb
  • spec/services/visits/select_place_spec.rb
  • spec/services/visits/suggest_spec.rb
  • spec/services/visits/time_chunks_spec.rb

Comment thread app/models/place.rb
Comment on lines +71 to +78
def lock_name_on_user_edit
return if machine_named
return unless will_save_change_to_name?

return self.name_locked_at = nil if name == DEFAULT_NAME
return if new_record? && !user_named

self.name_locked_at = Time.current

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Mark machine-originated name changes explicitly.

This callback treats every unmarked name change as a user edit. However, ReverseGeocoding::Places::FetchData#populate_place_attributes assigns place.name without setting machine_named, so its machine-generated name will be persisted with name_locked_at. Later geocoding will incorrectly preserve that stale name. Mark that path, and every other machine naming path, before saving.

🤖 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 `@app/models/place.rb` around lines 71 - 78, Update
ReverseGeocoding::Places::FetchData#populate_place_attributes and every other
machine-generated naming path to set machine_named before assigning or saving
the generated place name. Ensure lock_name_on_user_edit recognizes these changes
as machine-originated so they do not persist name_locked_at, while preserving
user-edit behavior for unmarked name changes.

Comment on lines +39 to +50
attributes = {
lonlat: build_point_coordinates(data['geometry']['coordinates']),
city: data['properties']['city'],
country: data['properties']['country'],
geodata: data,
source: Place.sources[:photon],
reverse_geocoded_at: Time.current
)
}
attributes[:name] = place_name(data) unless place.name_locked?

place.machine_named = true
place.update!(attributes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline app/services/reverse_geocoding/places/fetch_data.rb || true

echo "== relevant source =="
sed -n '1,220p' app/services/reverse_geocoding/places/fetch_data.rb | cat -n

echo "== git diff stat =="
git diff --stat || true

echo "== searches for helper/usages =="
rg -n "with_deadlock_retry|Deadlocked|Deadlock|lock_wait|query_cancellation|upsert_all|machine_named" app/services/reverse_geocoding -S || true

Repository: Freika/dawarich

Length of output: 9138


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== reverse geocoding points helper =="
sed -n '1,120p' app/services/reverse_geocoding/points/fetch_data.rb | cat -n

echo "== model machine_named references =="
rg -n "machine_named|name_locked" app -S || true

echo "== ActiveRecord retry exceptions definitions usages =="
rg -n "ActiveRecord::ConnectionTimedOut|QueryCanceled|Deadlock|deadlock|query_cancellation|lock_wait_timeout" rails Gemfile . -S || true

Repository: Freika/dawarich

Length of output: 10493


🌐 Web query:

Rails 8.1.3 ActiveRecord::ConnectionTimedOut ActiveRecord::QueryCanceled ActiveRecord::Deadlocked retry_exceptions

💡 Result:

In Rails 8.1, ActiveRecord provides built-in mechanisms for handling retries, but they are focused on idempotent queries encountering specific connection errors rather than a general-purpose "retry_exceptions" configuration for all database exceptions [1][2][3]. Key distinctions for the exceptions mentioned: 1. ActiveRecord::ConnectionTimeoutError: This exception is raised when a connection cannot be obtained from the pool within the allowed timeout period [4]. It is not automatically retried by the adapter's connection retry mechanism because it indicates pool exhaustion rather than a transient connection failure [5][6]. 2. ActiveRecord::Deadlocked: This exception occurs when the database engine kills a transaction to resolve a deadlock [7]. Rails does not automatically retry deadlocked transactions at the adapter level because retrying them requires restarting the entire transaction block, which is not inherently safe to do automatically [7]. For deadlocks, the standard Rails pattern is to use ActiveJob's retry mechanism or manual rescue blocks [8]. For example: retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 [8] 3. ActiveRecord::QueryCanceled: This generally relates to query timeouts or external cancellations. It is not part of the automatic connection-level retry logic [5][6]. Automatic Retry Logic Rails 8.1 automatically retries certain idempotent queries (such as simple association loads and #exists? checks) only when it encounters specific connection-related errors (e.g., lost connections) [1][2]. This behavior is governed by the database configuration in database.yml [3]: - connection_retries: Defaults to 1. Set to 0 to disable [6][3]. - retry_deadline: A duration (in seconds) after which even retryable queries will stop being retried [6][3]. There is no "retry_exceptions" configuration key in ActiveRecord; rather, the adapter internally defines which specific connection errors are considered retryable [5][6]. If you need to retry higher-level application errors or transactional issues like Deadlocked, you must implement that logic at the application level (e.g., using a retry library or ActiveJob) [8].

Citations:


Apply the bounded contention-retry policy to the primary place update.

update_place calls place.update!(attributes) without with_deadlock_retry, so Place#save_places and Place#upsert_all have built-in contention handling while contention on the original place stays immediate.

Proposed fix
-    place.update!(attributes)
+    with_deadlock_retry { place.update!(attributes) }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
attributes = {
lonlat: build_point_coordinates(data['geometry']['coordinates']),
city: data['properties']['city'],
country: data['properties']['country'],
geodata: data,
source: Place.sources[:photon],
reverse_geocoded_at: Time.current
)
}
attributes[:name] = place_name(data) unless place.name_locked?
place.machine_named = true
place.update!(attributes)
attributes = {
lonlat: build_point_coordinates(data['geometry']['coordinates']),
city: data['properties']['city'],
country: data['properties']['country'],
geodata: data,
source: Place.sources[:photon],
reverse_geocoded_at: Time.current
}
attributes[:name] = place_name(data) unless place.name_locked?
place.machine_named = true
with_deadlock_retry { place.update!(attributes) }
🤖 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 `@app/services/reverse_geocoding/places/fetch_data.rb` around lines 39 - 50,
Wrap the primary place update in update_place with the existing bounded
with_deadlock_retry helper, ensuring place.machine_named assignment and
place.update!(attributes) execute within the retry block. Preserve the current
attributes and update behavior while applying the same contention policy used by
Place#save_places and Place#upsert_all.

def call
with_dedup_lock do
place = find_by_name_and_proximity || create_place
place.update!(name_locked_at: Time.current) unless place.name_locked?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not lock the default placeholder name.

This direct update also locks Place::DEFAULT_NAME. That bypasses the model callback’s default-name rule, so Places::NameFetcher will preserve "Suggested place" instead of replacing it with a geocoded name.

Proposed fix
-        place.update!(name_locked_at: Time.current) unless place.name_locked?
+        place.update!(name_locked_at: Time.current) unless place.name_locked? || place.name == Place::DEFAULT_NAME
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
place.update!(name_locked_at: Time.current) unless place.name_locked?
place.update!(name_locked_at: Time.current) unless place.name_locked? || place.name == Place::DEFAULT_NAME
🤖 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 `@app/services/visits/select_place.rb` at line 18, Update the place-locking
logic in the select-place flow to skip name_locked_at updates when the place
still has Place::DEFAULT_NAME. Preserve locking for non-default names and rely
on the model’s default-name handling so Places::NameFetcher can replace the
placeholder with a geocoded name.

Comment on lines +17 to +19
<span class="place-drawer__name-lock"
title="You named this place, so automatic naming will not change it. Rename it to &quot;<%= Place::DEFAULT_NAME %>&quot; to hand it back to automatic naming."
data-testid="place-name-lock"><%= icon 'lock', class: 'w-4 h-4 inline' %></span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use Tailwind/DaisyUI classes for the new lock marker.

Replace the new place-drawer__name-lock styling hook with project-standard utility or DaisyUI classes.

As per coding guidelines, app/**/*.{css,scss,html,erb} must “Use Tailwind + DaisyUI for styling.”

🤖 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 `@app/views/places/_drawer.html.erb` around lines 17 - 19, Update the lock
marker span in the place drawer to remove the custom place-drawer__name-lock
styling hook and use the project’s Tailwind/DaisyUI utility classes for its
styling, while preserving its title, data-testid, lock icon, and naming
behavior.

Source: Coding guidelines

Comment thread CHANGELOG.md Outdated
Comment on lines +4 to +7
def change
return if column_exists?(:places, :name_locked_at)

add_column :places, :name_locked_at, :datetime

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Rails files and migration:\n'
git ls-files | rg '(^config/application\.rb$|^Gemfile$|^Gemfile\.lock$|db/migrate/20260727120000_add_name_locked_at_to_places\.rb)$' || true

printf '\nMigration contents:\n'
cat -n db/migrate/20260727120000_add_name_locked_at_to_places.rb

printf '\nGemfile relevant Rails version:\n'
if [ -f Gemfile ]; then
  rg -n "gem ['\"]rails['\"]|Rails \\(|VERSION|lock" Gemfile Gemfile.lock -A 3 -B 3 || true
fi

Repository: Freika/dawarich

Length of output: 1963


🌐 Web query:

Rails Active Record migration change method reversible returns early does ActiveRecord handle irrevserible

💡 Result:

Active Record handles irreversible migrations in the change method by raising an ActiveRecord::IrreversibleMigration exception when you attempt to roll them back [1][2][3]. The change method is designed for migrations where Active Record can automatically determine how to reverse actions [4][1]. When you include an operation that the CommandRecorder cannot automatically invert (such as certain raw SQL executions or unsupported methods), Active Record will fail if that migration is reverted [5][6]. Regarding your question about "returning early" in the change method: 1. How Active Record processes change: Active Record tracks the commands within the change method using a CommandRecorder [5]. It does not simply execute them linearly and stop if it encounters an irreversible command; it records them for potential reversal [5][6]. 2. Irreversible operations: If you include an irreversible operation (e.g., executing arbitrary SQL that doesn't define a reverse path), the migration will still run successfully during an "up" (forward) migration [4][1]. However, if you subsequently run a "down" (rollback) command, the migration will raise an ActiveRecord::IrreversibleMigration error at runtime [1][2]. 3. Best Practices: If your migration contains irreversible operations, you should avoid using the change method entirely and instead define explicit up and down methods [4][2]. Alternatively, you can use the reversible block within the change method to explicitly define both the forward and backward logic [4][2]. If you find yourself needing to stop the migration process based on a condition, you can manually raise ActiveRecord::IrreversibleMigration at any point in your code to intentionally prevent the migration from being rolled back or to halt execution [2][7][8].

Citations:


🌐 Web query:

ActiveRecord::CommandRecorder records method calls on return early method body does not record execution

💡 Result:

The behavior described regarding ActiveRecord::Migration::CommandRecorder is a fundamental design feature of how it handles migration commands. The CommandRecorder does not "execute" the method calls in the traditional sense when they are first encountered; rather, it intercepts and stores them in an internal array (@commands) so they can be replayed or inverted later [1][2][3]. If you are experiencing a situation where a method appears not to be recorded—specifically when using an early return—it is important to understand that the CommandRecorder only captures calls that explicitly pass through the methods defined in the recorder [3][4]. Key points regarding this behavior: 1. Recording Mechanism: When a migration uses the change method, Rails executes the migration block through the CommandRecorder [1][5]. The recorder wraps database-modifying methods (like create_table or add_column) [3][4]. These methods are defined to call the record method internally [3][4]. 2. Early Returns: If you have custom logic or an early return statement in your migration, the methods that would normally trigger the recording process will be skipped if the code path is not executed [3]. The CommandRecorder cannot record calls that the Ruby interpreter never reaches. 3. Not an Execution Engine: The CommandRecorder is not a proxy that watches your entire migration execution; it is a delegated object that expects specific known methods to be invoked upon it [1][3]. If your migration code returns early before reaching these specific calls, the recorder has no knowledge of them [3]. If you find that a command is missing, ensure that the execution path of your migration code actually reaches the migration method call [3]. If you are implementing custom reversible behavior, you may need to use the reversible method or define explicit up and down methods to ensure the migration behaves as expected during both migration and rollback [3][6].

Citations:


Keep this migration reversible.

On rollback, column_exists? is true and change returns before invoking remove_column, so the migration finishes successfully while leaving the already-existing column in place. Remove the guard, or use explicit up/down methods that mirror each other.

🤖 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 `@db/migrate/20260727120000_add_name_locked_at_to_places.rb` around lines 4 -
7, Update the migration’s change method to remain reversible: remove the
column_exists? early return so Rails can automatically remove name_locked_at on
rollback, or replace change with explicit up and down methods that add and
remove the column respectively.

Comment on lines +88 to +90
it 'still refreshes city and country' do
expect { service.call }.to change(place, :city).from(nil).to('New York')
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the country refresh promised by this example.

The example says it refreshes both city and country, but only checks city. A regression to country updates would pass unnoticed.

Proposed fix
-          expect { service.call }.to change(place, :city).from(nil).to('New York')
+          service.call
+          expect(place.reload).to have_attributes(city: 'New York', country: 'United States')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it 'still refreshes city and country' do
expect { service.call }.to change(place, :city).from(nil).to('New York')
end
it 'still refreshes city and country' do
service.call
expect(place.reload).to have_attributes(city: 'New York', country: 'United States')
end
🤖 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 `@spec/services/places/name_fetcher_spec.rb` around lines 88 - 90, Update the
example “still refreshes city and country” to assert that service.call changes
both place.city and place.country from their initial values to the expected
refreshed values, preserving the existing city assertion while adding coverage
for country updates.

let(:place) { create(:place, name: "Mum's house", name_locked_at: 1.day.ago) }

it 'keeps the user-supplied name' do
expect { service.call }.not_to change { place.reload.name }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the ambiguous RSpec block association.

RuboCop reports Lint/AmbiguousBlockAssociation for both expectations. Parenthesize the matcher argument:

Proposed fix
-          expect { service.call }.not_to change { place.reload.name }
+          expect { service.call }.not_to(change { place.reload.name })
...
-          expect { service.call }.not_to change { sibling.reload.name }
+          expect { service.call }.not_to(change { sibling.reload.name })

As per coding guidelines, always run RuboCop on modified Ruby files before committing: bundle exec rubocop <files>.

Also applies to: 90-90

🧰 Tools
🪛 GitHub Check: rubocop

[failure] 58-58:
Lint/AmbiguousBlockAssociation: Parenthesize the param change { place.reload.name } to make sure that the block will be associated with the change method call.

🤖 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 `@spec/services/reverse_geocoding/places/fetch_data_spec.rb` at line 58,
Resolve the Lint/AmbiguousBlockAssociation warnings in both expectations around
service.call by parenthesizing the matcher argument so the block is
unambiguously associated with expect. Apply the same adjustment to the
expectation at the second referenced location, then run bundle exec rubocop on
the modified Ruby file.

Sources: Coding guidelines, Linters/SAST tools

@Freika Freika changed the title 1.11.0 1.10.2 Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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 `@spec/migrations/drop_legacy_lat_lon_from_points_spec.rb`:
- Around line 7-28: Refactor the migration specs to exercise the public `up`
entry point instead of calling `migration.send(:drop_legacy_columns)`. Remove
direct stubs on the subject for `execute`, `column_exists?`, and `sleep`; stub
the underlying database connection or other external collaborators instead,
while preserving the existing retry assertions through `up` and the
`stub_drop_raising` helper.
- Around line 37-54: Replace the perform_later stubs and have_received
assertions in the “hands the drop to a background job once attempts are
exhausted” and “retries until the lock is acquired instead of failing on the
first loss” examples with
have_enqueued_job(DataMigrations::DropLegacyLatLonJob), matching the sibling
test’s enqueue assertion pattern. Preserve the existing expectations that the
job is enqueued only after retries are exhausted and not enqueued when the lock
succeeds.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e499580-aadd-44cd-a32d-60e14ff98cd2

📥 Commits

Reviewing files that changed from the base of the PR and between 5d35267 and abea28d.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • app/jobs/data_migrations/drop_legacy_lat_lon_job.rb
  • db/migrate/20260714090000_drop_legacy_lat_lon_from_points.rb
  • spec/jobs/data_migrations/drop_legacy_lat_lon_job_spec.rb
  • spec/migrations/drop_legacy_lat_lon_from_points_spec.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment thread spec/migrations/drop_legacy_lat_lon_from_points_spec.rb
Comment on lines +37 to +54
it 'hands the drop to a background job once attempts are exhausted' do
stub_drop_raising(ActiveRecord::LockWaitTimeout)
allow(DataMigrations::DropLegacyLatLonJob).to receive(:perform_later)

migration.send(:drop_legacy_columns)

expect(DataMigrations::DropLegacyLatLonJob).to have_received(:perform_later)
end

it 'retries until the lock is acquired instead of failing on the first loss' do
attempts = stub_drop_raising(ActiveRecord::LockWaitTimeout, times: 2)
allow(DataMigrations::DropLegacyLatLonJob).to receive(:perform_later)

migration.send(:drop_legacy_columns)

expect(attempts.call).to eq(3)
expect(DataMigrations::DropLegacyLatLonJob).not_to have_received(:perform_later)
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Prefer have_enqueued_job over mocking perform_later for asserting enqueue behavior.

These two tests stub DataMigrations::DropLegacyLatLonJob.perform_later and assert on have_received, while the sibling test at Lines 72-76 correctly uses have_enqueued_job(DataMigrations::DropLegacyLatLonJob). The same pattern should be used here for consistency and to avoid mocking an internal collaborator's class method.

As per coding guidelines, "Prefer have_enqueued_job over expect(Job).to receive(:perform_later) for testing background job enqueueing."

♻️ Proposed fix
   it 'hands the drop to a background job once attempts are exhausted' do
     stub_drop_raising(ActiveRecord::LockWaitTimeout)
-    allow(DataMigrations::DropLegacyLatLonJob).to receive(:perform_later)
-
-    migration.send(:drop_legacy_columns)
-
-    expect(DataMigrations::DropLegacyLatLonJob).to have_received(:perform_later)
+    expect { migration.send(:drop_legacy_columns) }.to have_enqueued_job(DataMigrations::DropLegacyLatLonJob)
   end

   it 'retries until the lock is acquired instead of failing on the first loss' do
     attempts = stub_drop_raising(ActiveRecord::LockWaitTimeout, times: 2)
-    allow(DataMigrations::DropLegacyLatLonJob).to receive(:perform_later)
-
-    migration.send(:drop_legacy_columns)
+    expect { migration.send(:drop_legacy_columns) }.not_to have_enqueued_job(DataMigrations::DropLegacyLatLonJob)

     expect(attempts.call).to eq(3)
-    expect(DataMigrations::DropLegacyLatLonJob).not_to have_received(:perform_later)
   end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it 'hands the drop to a background job once attempts are exhausted' do
stub_drop_raising(ActiveRecord::LockWaitTimeout)
allow(DataMigrations::DropLegacyLatLonJob).to receive(:perform_later)
migration.send(:drop_legacy_columns)
expect(DataMigrations::DropLegacyLatLonJob).to have_received(:perform_later)
end
it 'retries until the lock is acquired instead of failing on the first loss' do
attempts = stub_drop_raising(ActiveRecord::LockWaitTimeout, times: 2)
allow(DataMigrations::DropLegacyLatLonJob).to receive(:perform_later)
migration.send(:drop_legacy_columns)
expect(attempts.call).to eq(3)
expect(DataMigrations::DropLegacyLatLonJob).not_to have_received(:perform_later)
end
it 'hands the drop to a background job once attempts are exhausted' do
stub_drop_raising(ActiveRecord::LockWaitTimeout)
expect { migration.send(:drop_legacy_columns) }.to have_enqueued_job(DataMigrations::DropLegacyLatLonJob)
end
it 'retries until the lock is acquired instead of failing on the first loss' do
attempts = stub_drop_raising(ActiveRecord::LockWaitTimeout, times: 2)
expect { migration.send(:drop_legacy_columns) }.not_to have_enqueued_job(DataMigrations::DropLegacyLatLonJob)
expect(attempts.call).to eq(3)
end
🤖 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 `@spec/migrations/drop_legacy_lat_lon_from_points_spec.rb` around lines 37 -
54, Replace the perform_later stubs and have_received assertions in the “hands
the drop to a background job once attempts are exhausted” and “retries until the
lock is acquired instead of failing on the first loss” examples with
have_enqueued_job(DataMigrations::DropLegacyLatLonJob), matching the sibling
test’s enqueue assertion pattern. Preserve the existing expectations that the
job is enqueued only after retries are exhausted and not enqueued when the lock
succeeds.

Source: Coding guidelines

Freika and others added 4 commits July 27, 2026 22:04
MapLibre only fires style.load when it builds a Style from scratch; its
default setStyle path diffs the document into the live style silently.
Every style swap therefore stripped the app's data layers without ever
re-adding them, and the custom-style error listener stayed armed forever,
so the first failed tile request discarded a perfectly good basemap and
reverted to the default style.

- Pass diff: false on every setStyle call, via a shared swapStyle helper
- Only treat a failure of the style document itself as a style failure,
  not the tile, sprite and glyph requests it spawns
- Align classifyBasemapUrl with the API's style_json_url? so the browser
  no longer accepts URLs the API rejects
- Disable tile category and POI toggles under a raster or foreign-style
  basemap, where they have nothing to act on
Add custom raster and style basemap URLs
A one-time backfill sets name_locked_at for places whose name a user
set before 1.10.2, so the next reverse geocoding run no longer
overwrites pre-upgrade renames. Machine-generated names are recognised
by recomputing both historical formats from stored geodata and stay
unlocked; places without geodata are locked conservatively.

Also dedupes the (0,0) changelog bullet.
@Freika
Freika merged commit 5fa4789 into master Jul 27, 2026
25 of 32 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants