Skip to content

Conversation

@Freika
Copy link
Owner

@Freika Freika commented Dec 26, 2025

Summary by CodeRabbit

  • New Features

    • Year‑end "Year in Review" digests: generation, sharing, public view, email delivery and new Settings toggle to opt in/out.
    • Import deletion now runs in background with a "Deleting…" UI state.
  • Improvements

    • Client- and server-side trip date validation; fog‑of‑war radius is adjustable; heatmap visuals and map settings refined.
    • Stats recalculation now triggers cache invalidation to keep displays current.
  • Chores

    • Bumped app version to 0.36.5.

✏️ Tip: You can customize this high-level summary in your review settings.

rtuszik and others added 30 commits November 25, 2025 20:30
#1980)

* Update exporting code to stream points data to file in batches to reduce memory usage

* Update changelog
* Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet

* Implement phase 1

* Phases 1-3 + part of 4

* Fix e2e tests

* Phase 6

* Implement fog of war

* Phase 7

* Next step: fix specs, phase 7 done

* Use our own map tiles

* Extract v2 map logic to separate manager classes

* Update settings panel on v2 map

* Update v2 e2e tests structure

* Reimplement location search in maps v2

* Update speed routes

* Implement visits and places creation in v2

* Fix last failing test

* Implement visits merging

* Fix a routes e2e test and simplify the routes layer styling.

* Extract js to modules from maps_v2_controller.js

* Implement area creation

* Fix spec problem

* Fix some e2e tests

* Implement live mode in v2 map

* Update icons and panel

* Extract some styles

* Remove unused file

* Start adding dark theme to popups on MapLibre maps

* Make popups respect dark theme

* Move v2 maps to maplibre namespace

* Update v2 references to maplibre

* Put place, area and visit info into side panel

* Update API to use safe settings config method

* Fix specs

* Fix method name to config in SafeSettings and update usages accordingly

* Add missing public files

* Add handling for real time points

* Fix remembering enabled/disabled layers of the v2 map

* Fix lots of e2e tests

* Add settings to select map version

* Use maps/v2 as main path for MapLibre maps

* Update routing

* Update live mode
* 0.36.2 (#2007)

* fix: move foreman to global gems to fix startup crash (#1971)

* Update exporting code to stream points data to file in batches to red… (#1980)

* Update exporting code to stream points data to file in batches to reduce memory usage

* Update changelog

* Update changelog

* Feature/maplibre frontend (#1953)

* Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet

* Implement phase 1

* Phases 1-3 + part of 4

* Fix e2e tests

* Phase 6

* Implement fog of war

* Phase 7

* Next step: fix specs, phase 7 done

* Use our own map tiles

* Extract v2 map logic to separate manager classes

* Update settings panel on v2 map

* Update v2 e2e tests structure

* Reimplement location search in maps v2

* Update speed routes

* Implement visits and places creation in v2

* Fix last failing test

* Implement visits merging

* Fix a routes e2e test and simplify the routes layer styling.

* Extract js to modules from maps_v2_controller.js

* Implement area creation

* Fix spec problem

* Fix some e2e tests

* Implement live mode in v2 map

* Update icons and panel

* Extract some styles

* Remove unused file

* Start adding dark theme to popups on MapLibre maps

* Make popups respect dark theme

* Move v2 maps to maplibre namespace

* Update v2 references to maplibre

* Put place, area and visit info into side panel

* Update API to use safe settings config method

* Fix specs

* Fix method name to config in SafeSettings and update usages accordingly

* Add missing public files

* Add handling for real time points

* Fix remembering enabled/disabled layers of the v2 map

* Fix lots of e2e tests

* Add settings to select map version

* Use maps/v2 as main path for MapLibre maps

* Update routing

* Update live mode

* Update maplibre controller

* Update changelog

* Remove some console.log statements

---------

Co-authored-by: Robin Tuszik <[email protected]>

* Remove esbuild scripts from package.json

* Remove sideEffects field from package.json

* Raw data archivation

* Add tests

* Fix tests

* Fix tests

* Update ExceptionReporter

* Add schedule to run raw data archival job monthly

* Change file structure for raw data archival feature

* Update changelog and version for raw data archival feature

---------

Co-authored-by: Robin Tuszik <[email protected]>
* Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation

* Remove raw data from visited cities api endpoint
* Use user timezone to show dates on maps

* Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates.

* Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates.

* Fix tests failing due to new index on stats table

* Fix failing specs
* Fix kml kmz import issues

* Refactor KML importer to improve readability and maintainability
#2027)

* Implement moving points in map v2 and fix route rendering logic to match map v1.

* Fix route spec
* Add verification step to raw data archival process

* Add actual verification of raw data archives after creation, and only clear raw_data for verified archives.

* Fix failing specs
Freika and others added 13 commits December 26, 2025 14:39
Bumps [sentry-rails](https://github.com/getsentry/sentry-ruby) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/getsentry/sentry-ruby/releases)
- [Changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md)
- [Commits](getsentry/sentry-ruby@6.0.0...6.1.0)

---
updated-dependencies:
- dependency-name: sentry-rails
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [turbo-rails](https://github.com/hotwired/turbo-rails) from 2.0.17 to 2.0.20.
- [Release notes](https://github.com/hotwired/turbo-rails/releases)
- [Commits](hotwired/turbo-rails@v2.0.17...v2.0.20)

---
updated-dependencies:
- dependency-name: turbo-rails
  dependency-version: 2.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evgenii Burmakin <[email protected]>
Bumps [webmock](https://github.com/bblimke/webmock) from 3.25.1 to 3.26.1.
- [Release notes](https://github.com/bblimke/webmock/releases)
- [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md)
- [Commits](bblimke/webmock@v3.25.1...v3.26.1)

---
updated-dependencies:
- dependency-name: webmock
  dependency-version: 3.26.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evgenii Burmakin <[email protected]>
Bumps [brakeman](https://github.com/presidentbeef/brakeman) from 7.1.0 to 7.1.1.
- [Release notes](https://github.com/presidentbeef/brakeman/releases)
- [Changelog](https://github.com/presidentbeef/brakeman/blob/main/CHANGES.md)
- [Commits](presidentbeef/brakeman@v7.1.0...v7.1.1)

---
updated-dependencies:
- dependency-name: brakeman
  dependency-version: 7.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [redis](https://github.com/redis/redis-rb) from 5.4.0 to 5.4.1.
- [Changelog](https://github.com/redis/redis-rb/blob/master/CHANGELOG.md)
- [Commits](redis/redis-rb@v5.4.0...v5.4.1)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 5.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Put import deletion into background job

* Update changelog
* fix: use constant weight for maplibre heatmap layer

* fix null type, update heatmap styling

* improve heatmap styling

* fix typo
#2065)

* Fix stats calculation to recursively reduce H3 resolution when too many hexagons are generated

* Update CHANGELOG.md
* Validate trip start and end dates

* Update changelog
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

📝 Walkthrough

Walkthrough

Patch release bump to 0.36.5. Adds Year‑End Digests (DB, model, services, jobs, mailer, UI, routes, tests), moves import deletion to a background job with status broadcasting and Turbo updates, adds trip start/end validation (server + client + tests), and applies several map, stats, cache, and index changes across the app.

Changes

Cohort / File(s) Summary
Version & Changelog
\.app_version, CHANGELOG.md
Bumped to 0.36.5; changelog records digest feature, index/migration updates, background import deletion, and assorted fixes.
Digests feature (DB, model, services, jobs, mailer, views, routes, tests)
db/migrate/20251227000001_create_digests.rb, db/migrate/20251227223614_change_digests_distance_to_bigint.rb, app/models/users/digest.rb, app/services/users/digests/..., app/jobs/users/digests/..., app/mailers/users/digests_mailer.rb, app/views/users/digests/*, app/views/users/digests_mailer/*, config/routes.rb, config/sidekiq.yml, spec/.../users/digests*, spec/factories/users/digests.rb
New digests table/model, calculation services, background jobs (calculating, email send, year‑end scheduler), mailer and HTML/text templates, user & shared digest controllers/views, routes, digests queue, factories and comprehensive tests. Review for DB migration order, indexes, and Sidekiq queue.
Async Import Deletion
app/controllers/imports_controller.rb, app/jobs/imports/destroy_job.rb, app/services/imports/destroy.rb, app/views/imports/destroy.turbo_stream.erb
Import destroy now marks deleting, enqueues Imports::DestroyJob. Job runs destroy via service (points.destroy_all → callbacks), logs count, broadcasts status/completion. New turbo stream view renders deleting state. Check broadcast channels and job error paths.
Imports UI & JS
app/javascript/controllers/imports_controller.js, app/views/imports/index.html.erb
Client-side handles missing rows, removes row on delete events, and updates points_count/status more directly; UI hides Delete while deleting? and shows spinner/text.
Trip Date Validation (model + client + tests)
app/models/trip.rb, app/javascript/controllers/datetime_controller.js, spec/models/trip_spec.rb, e2e/v2/trips.spec.js
Server validation started_at_before_ended_at; client validateDates(showPopup = false) wired to inputs and submission; model and E2E/unit tests added. Review client/server validation parity and localization of validation message.
Map Layers & Settings
app/javascript/controllers/maps/maplibre/settings_manager.js, app/javascript/controllers/maps/maplibre/layer_manager.js, app/javascript/maps_maplibre/layers/fog_layer.js, app/javascript/maps_maplibre/layers/heatmap_layer.js, e2e/v2/map/layers/advanced.spec.js
FogLayer now stores input data and clears with opaque fill; fog redraw guarded by presence of data; fog radius pulls from settings; heatmap style and parameters reworked (radius/intensity/color/opacity). E2E tests for fog behavior added.
Stats Calculation & Hexagons
app/services/stats/hexagon_calculator.rb, spec/services/stats/hexagon_calculator_spec.rb
When hexagon count exceeds MAX, instance calls calculate_hexagons(lower_resolution) recursively instead of instantiating a new object; unit test added to exercise fallback.
DB Indexes & Migrations (points/stats/performance cleanup)
db/migrate/20251208210410_add_composite_index_to_stats.rb, db/migrate/20251226170919_add_composite_index_to_points_user_id_timestamp.rb, db/migrate/20251228000000_remove_unused_indexes.rb, db/migrate/20251228100000_add_performance_indexes.rb, db/schema.rb
Adds/adjusts composite indexes (stats, points), pre-cleanup duplicate removal for stats, drops unused point indexes, and adds performance indexes (users.api_key, users.status, points composite). Review migration ordering, long‑running/concurrent index operations, and DDL transaction disabling.
Cache Invalidation & Related Changes
app/services/cache/invalidate_user_caches.rb, app/services/cache/clean.rb, app/services/stats/calculate_month.rb, app/jobs/points/nightly_reverse_geocoding_job.rb, spec/services/cache/*, spec/jobs/points/nightly_reverse_geocoding_job_spec.rb
New Cache::InvalidateUserCaches service; cache keys renamed to countries_visited/cities_visited; monthly stats persistence and nightly reverse geocoding now call invalidation per user (collecting user IDs). Tests added/updated.
Points creation / API query change
app/services/points/create.rb, app/controllers/api/v1/points_controller.rb
Deduplication key coerces timestamp to integer (to_i); API points index excludes raw data via without_raw_data scope.
Imports tests & job specs
spec/jobs/imports/destroy_job_spec.rb, spec/requests/imports_spec.rb
Tests updated/added to assert job enqueueing, deleting state, broadcasts, and job resilience.
Places & Reverse Geocoding
app/services/reverse_geocoding/places/fetch_data.rb
Batch upsert for places via upsert_all; guard clause style changes. Review data/unique_by behavior and timestamps on upserts.
Service tweaks & logging
app/services/imports/destroy.rb, app/services/countries_and_cities.rb, app/services/stats/calculate_month.rb
Points deletion now uses destroy_all to run callbacks; imports destroy logs points_count; stats month calc invalidates caches after save. Check performance and side effects.
RailsPulse integration & monitoring
Gemfile, config/initializers/rails_pulse.rb, db/rails_pulse_schema.rb, db/migrate/20251228163703_install_rails_pulse_tables.rb, config/schedule.yml, config/routes.rb
Adds rails_pulse gem, initializer, schema/migration, scheduled summary/cleanup jobs, and mounts engine. Review auth/config and DB schema installation migration.
Misc config & UI
config/application.rb, app/views/settings/index.html.erb, app/views/shared/_sharing_modal.html.erb, app/helpers/users/digests_helper.rb, app/models/user.rb, app/services/users/safe_settings.rb
Adds Rack::Deflater, settings UI toggle for digest emails, sharing expiration options extended (1w/1m), helper utilities for digests, user has_many :digests, safe_settings default added and digest_emails_enabled? method.

Sequence Diagram

sequenceDiagram
    autonumber
    participant User
    participant Browser
    participant Controller as ImportsController
    participant Job as Imports::DestroyJob
    participant Service as Imports::Destroy
    participant Channel as ImportsChannel
    participant DB as Database

    User->>Browser: Click "Delete" on import
    Browser->>Controller: DELETE /imports/:id
    Controller->>DB: import.deleting! (persist status)
    Controller->>Job: Imports::DestroyJob.perform_later(import.id)
    Controller-->>Browser: Redirect / turbo_stream → render "Deleting..." row

    alt Job executes
      Job->>DB: load import by id
      Job->>Channel: broadcast status update (deleting)
      Job->>Service: run delete workflow (Imports::Destroy)
      Service->>DB: `@import.points.destroy_all` (trigger callbacks)
      Service->>DB: `@import.destroy`
      Service->>Service: Stats::BulkCalculator recalculation
      Job->>Channel: broadcast deletion complete
      Channel-->>Browser: TurboStream updates (remove/replace row)
    else Import not found
      Job-->>Job: log warning and exit
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • 0.30.10 #1666 — Overlapping changes to imports lifecycle and controller/service/job code (background deletion flow).
  • 0.33.0 #1783 — Related modifications to Stats::HexagonCalculator behavior and tests.
  • 0.36.3 #2013 — Related migration/index work touching stats duplicate cleanup and points/stats index changes.

Poem

🐇 I queue a quiet job when an import must go,

Dates now line up, no backwards woe,
I sweep the fog with tiny paws so neat,
Hexagons shrink to fit the map's heartbeat,
A rabbit's patch: hop, broadcast, and show.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title '0.36.5' is a version number and does not clearly describe the actual changes made in this comprehensive release, which includes year-end digest features, performance improvements, sharing functionality, and various bug fixes. Use a descriptive title summarizing the primary features, e.g., 'Add year-end digest emails and sharing features for v0.36.5' or 'Release v0.36.5: year-end digests, performance indexes, and sharing'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • 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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
app/services/stats/hexagon_calculator.rb (1)

56-57: Good refactor: In-place recursion optimization.

Calling calculate_hexagons(lower_resolution) on the current instance instead of creating a new Stats::HexagonCalculator reduces object allocation overhead while maintaining the same behavior and state.

spec/services/stats/hexagon_calculator_spec.rb (1)

65-96: Good test coverage for recursive hexagon reduction.

The test correctly simulates the recursive behavior when hexagon count exceeds MAX_HEXAGONS by stubbing multiple calls and verifying the final result is within bounds.

Minor style improvements suggested by static analysis

The following are optional nitpicks from RuboCop that could improve code clarity:

  1. Line 72: Omit unused block arguments or prefix with _:
-          allow_any_instance_of(described_class).to receive(:calculate_h3_indexes) do |instance, points, resolution|
+          allow_any_instance_of(described_class).to receive(:calculate_h3_indexes) do |_instance, _points, _resolution|
  1. Line 76: Use literal {} instead of Hash.new:
-              Hash.new.tap do |hash|
+              {}.tap do |hash|
app/views/imports/destroy.turbo_stream.erb (1)

6-11: Verify: Intentional removal of import name link?

The index view (line 61) renders <%= link_to import.name, import, ... %>, but this template shows plain text. If this prevents users from navigating to a deleting import, it's good UX. Otherwise, it's an inconsistency.

app/jobs/imports/destroy_job.rb (1)

10-10: Optional: Consider removing redundant status update.

The controller already calls @import.deleting! (line 81 of imports_controller.rb) before enqueueing this job, so line 10 sets the same status again. While defensive programming in background jobs can be valuable, this redundancy may be unnecessary since the controller guarantees the status is already set.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f0aaa0 and 4be58d4.

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • .app_version
  • CHANGELOG.md
  • app/controllers/imports_controller.rb
  • app/javascript/controllers/datetime_controller.js
  • app/javascript/controllers/imports_controller.js
  • app/javascript/controllers/maps/maplibre/layer_manager.js
  • app/javascript/controllers/maps/maplibre/settings_manager.js
  • app/javascript/maps_maplibre/layers/fog_layer.js
  • app/javascript/maps_maplibre/layers/heatmap_layer.js
  • app/jobs/imports/destroy_job.rb
  • app/models/import.rb
  • app/models/trip.rb
  • app/services/imports/destroy.rb
  • app/services/stats/hexagon_calculator.rb
  • app/views/imports/destroy.turbo_stream.erb
  • app/views/imports/index.html.erb
  • db/migrate/20251208210410_add_composite_index_to_stats.rb
  • e2e/v2/map/layers/advanced.spec.js
  • e2e/v2/trips.spec.js
  • spec/models/trip_spec.rb
  • spec/services/stats/hexagon_calculator_spec.rb
🧰 Additional context used
📓 Path-based instructions (3)
spec/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

spec/**/*.rb: Use RSpec as the test framework
Use WebMock for external HTTP request mocking in tests

Files:

  • spec/models/trip_spec.rb
  • spec/services/stats/hexagon_calculator_spec.rb
db/migrate/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Use Strong Migrations practices for safe database schema changes

Files:

  • db/migrate/20251208210410_add_composite_index_to_stats.rb
app/jobs/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Implement long-running or asynchronous work as Sidekiq background jobs

Files:

  • app/jobs/imports/destroy_job.rb
🧠 Learnings (2)
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to db/migrate/**/*.rb : Use Strong Migrations practices for safe database schema changes

Applied to files:

  • db/migrate/20251208210410_add_composite_index_to_stats.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/jobs/**/*.rb : Implement long-running or asynchronous work as Sidekiq background jobs

Applied to files:

  • app/jobs/imports/destroy_job.rb
🧬 Code graph analysis (5)
spec/services/stats/hexagon_calculator_spec.rb (1)
app/services/stats/hexagon_calculator.rb (2)
  • points (77-87)
  • calculate_hexagons (40-67)
e2e/v2/trips.spec.js (2)
e2e/helpers/navigation.js (1)
  • closeOnboardingModal (9-16)
app/javascript/controllers/add_visit_controller.js (1)
  • submitButton (313-313)
app/jobs/imports/destroy_job.rb (1)
app/services/imports/destroy.rb (1)
  • call (11-22)
app/javascript/maps_maplibre/layers/fog_layer.js (1)
app/javascript/controllers/maps/maplibre_controller.js (1)
  • data (375-375)
app/services/imports/destroy.rb (2)
app/controllers/map/leaflet_controller.rb (1)
  • points_count (71-73)
app/controllers/notifications_controller.rb (1)
  • destroy_all (22-25)
🪛 RuboCop (1.82.0)
spec/services/stats/hexagon_calculator_spec.rb

[warning] 72-72: Unused block argument - instance. You can omit all the arguments if you don't care about them.

(Lint/UnusedBlockArgument)


[warning] 72-72: Unused block argument - points. You can omit all the arguments if you don't care about them.

(Lint/UnusedBlockArgument)


[warning] 72-72: Unused block argument - resolution. You can omit all the arguments if you don't care about them.

(Lint/UnusedBlockArgument)


[convention] 76-76: Use hash literal {} instead of Hash.new.

(Style/EmptyLiteral)

db/migrate/20251208210410_add_composite_index_to_stats.rb

[convention] 9-9: execute is not reversible.

(Rails/ReversibleMigration)


[convention] 29-29: execute is not reversible.

(Rails/ReversibleMigration)

🔇 Additional comments (22)
.app_version (1)

1-1: LGTM! Version bump to 0.36.5.

The version increment is appropriate for this patch release.

app/services/imports/destroy.rb (2)

12-12: Good practice: Capturing count before deletion.

Storing points_count before the transaction ensures the count is preserved for logging after the points are destroyed.


19-19: Helpful addition: Logging import deletion.

The info log provides useful tracking for import deletion operations.

app/models/trip.rb (2)

12-12: LGTM! Date validation added.

The custom validator ensures trip end dates are after start dates at the model level, complementing frontend validation.


52-57: Clear and correct validation logic.

The validation correctly:

  • Returns early when dates are blank (avoiding errors during initialization)
  • Enforces that started_at must be before ended_at (strict ordering)
  • Provides a clear error message on the ended_at field
spec/models/trip_spec.rb (1)

11-36: Comprehensive test coverage for date validation.

The test suite thoroughly covers all validation scenarios:

  • Valid case: started_at before ended_at
  • Invalid case: started_at after ended_at
  • Invalid case: started_at equals ended_at (boundary)
  • Valid case: blank dates during initialization

All assertions correctly verify the expected error messages from the model validator.

app/javascript/maps_maplibre/layers/fog_layer.js (3)

15-15: Good addition: Retaining original data for updates.

Storing the original data in this.data enables re-rendering the fog layer when settings change without requiring a full data reload.


19-19: Correct data storage in add() and update().

Both methods now store the incoming data, ensuring this.data is available for subsequent operations and settings updates.

Also applies to: 29-29


84-84: Improved fog clearing with fully opaque fill.

Using 'rgba(0, 0, 0, 1)' with destination-out composite operation ensures complete fog removal in the cleared circles.

app/javascript/controllers/maps/maplibre/settings_manager.js (1)

247-249: Good defensive check: Ensuring fog layer has data before update.

Adding the fogLayer.data check prevents calling update() on a fog layer that hasn't received data yet, avoiding potential errors when settings are applied before fog data loads.

app/javascript/controllers/maps/maplibre/layer_manager.js (1)

269-280: LGTM!

The fog layer now correctly derives clearRadius from this.settings.fogOfWarRadius with a sensible fallback to 1000. This aligns with the PR objective to fix fog of war radius settings being respected in map v2.

CHANGELOG.md (1)

7-20: LGTM!

The changelog accurately documents the key changes in this release: background processing for import deletion, fixes for negative points count, stats updates, trip date validation, and fog of war settings. The entries are clear and reference relevant issue numbers.

app/javascript/controllers/imports_controller.js (1)

26-46: LGTM!

The updated channel handling logic is well-structured:

  • Early return on missing row prevents errors
  • Delete action cleanly removes the row from DOM
  • Points count uses Intl.NumberFormat for proper localization
  • Guards ensure updates only occur when data is present

This aligns well with the background deletion workflow introduced via Imports::DestroyJob.

e2e/v2/trips.spec.js (1)

1-100: Good test coverage for trip date validation.

The tests appropriately cover:

  1. Blocking submission when start date > end date
  2. Allowing valid date ranges
  3. Validation when updating dates after initial valid entry

The tests align well with the model-level validation added in app/models/trip.rb and provide good end-to-end coverage.

app/javascript/maps_maplibre/layers/heatmap_layer.js (1)

23-80: LGTM! Simplified heatmap configuration with improved visibility.

The changes provide a more predictable heatmap rendering:

  • Fixed weight of 1 removes per-feature variability
  • Stepped intensity values (0.01 → 0.1 → 0.3) provide controlled visibility at different zoom levels
  • Transparent baseline in the color ramp prevents obscuring the map at low densities
  • Exponential radius growth provides better cluster visibility when zoomed in

The radius reaching 160px at zoom 20 may be intentional for high-zoom scenarios. If clusters appear too large, consider reducing the final value.

app/javascript/controllers/datetime_controller.js (2)

14-35: Good client-side validation implementation.

The validation setup properly prevents form submission with invalid date ranges and provides user feedback via reportValidity(). The integration with the existing updateCoordinates flow ensures invalid ranges don't trigger unnecessary API calls.

One minor observation: both change and blur listeners call validateDates(). For datetime-local inputs, change typically fires when the value changes and focus leaves, so blur may be partially redundant. However, this redundancy is harmless and ensures validation runs in edge cases.


38-62: Validation logic handles partial input gracefully.

Returning true when either date is invalid (lines 47-49) is a good design choice — it allows users to fill in dates incrementally without triggering premature validation errors. The actual date comparison validation only runs when both dates are parseable.

app/models/import.rb (1)

20-20: LGTM! Enum addition aligns with background deletion workflow.

The new deleting: 4 status enables proper UI state management during asynchronous import destruction.

app/views/imports/index.html.erb (1)

75-83: LGTM! UI correctly reflects async deletion state.

The conditional rendering prevents users from triggering duplicate deletions and provides clear feedback during background processing.

app/controllers/imports_controller.rb (1)

84-87: LGTM! Response handling supports both HTML and Turbo Stream requests.

The dual format support ensures proper user feedback whether JavaScript is enabled or not.

app/jobs/imports/destroy_job.rb (2)

6-18: Job structure follows guidelines, but ensure error handling for resilience.

The background job correctly implements asynchronous deletion following coding guidelines for Sidekiq jobs. However, as noted in the controller review, add error handling beyond RecordNotFound to prevent imports from being stuck in deleting status if the destruction fails.

Based on coding guidelines: "Implement long-running or asynchronous work as Sidekiq background jobs"


22-45: LGTM! Broadcast helpers enable multi-session/multi-tab updates.

The broadcast methods correctly notify all connected clients (e.g., multiple tabs) about import status changes, complementing the controller's Turbo Stream response.

Comment on lines 80 to +82
def destroy
Imports::Destroy.new(current_user, @import).call
@import.deleting!
Imports::DestroyJob.perform_later(@import.id)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error recovery to prevent stuck "deleting" status.

If Imports::DestroyJob fails (e.g., database errors, service exceptions), the import will remain in deleting status indefinitely with no way to retry or recover. Consider adding:

  1. A retry mechanism in the job (Sidekiq's built-in retry with exponential backoff)
  2. A failure callback to revert status to failed or log for manual intervention
  3. A timeout or staleness check for imports stuck in deleting state
🔎 Example: Add error recovery in the job

In app/jobs/imports/destroy_job.rb, wrap the destruction in error handling:

def perform(import_id)
  import = Import.find_by(id: import_id)
  return unless import

  import.deleting!
  broadcast_status_update(import)

  Imports::Destroy.new(import.user, import).call

  broadcast_deletion_complete(import)
rescue ActiveRecord::RecordNotFound
  Rails.logger.warn "Import #{import_id} not found, may have already been deleted"
rescue StandardError => e
  Rails.logger.error "Failed to delete import #{import_id}: #{e.message}"
  import&.failed! if import&.persisted?
  raise # Let Sidekiq retry
end
🤖 Prompt for AI Agents
In app/controllers/imports_controller.rb around lines 80 to 82, calling
Imports::DestroyJob.perform_later(@import.id) leaves the Import in "deleting" if
the job fails; update the job (app/jobs/imports/destroy_job.rb) to add error
recovery: wrap perform in begin/rescue (or use ActiveJob's retry_on for
transient errors), log failures with the exception, set import.failed! (or
another terminal status) when deletion cannot proceed, re-raise the exception to
allow retries for transient errors, and ensure RecordNotFound is handled
gracefully; additionally consider adding a periodic staleness check (rake task
or scheduled job) to mark imports stuck in "deleting" as failed after a timeout.


ActiveRecord::Base.transaction do
@import.points.delete_all
@import.points.destroy_all
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Point model has any destroy callbacks that justify using destroy_all
rg -n -A 3 -B 3 'after_destroy|before_destroy' --type=rb -g '**/point.rb'

Repository: Freika/dawarich

Length of output: 85


🏁 Script executed:

# First, let's locate and examine the imports/destroy.rb file to understand context
fd -n 'destroy.rb' | grep -E 'imports.*destroy'

Repository: Freika/dawarich

Length of output: 289


🏁 Script executed:

# Find the Point model file
fd -n 'point.rb' -type f

Repository: Freika/dawarich

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Check if Point model has any destroy callbacks - corrected rg syntax
rg -n -A 3 -B 3 'after_destroy|before_destroy' -t ruby

Repository: Freika/dawarich

Length of output: 1834


🏁 Script executed:

# Find the Point model and imports/destroy files
fd 'destroy.rb'

Repository: Freika/dawarich

Length of output: 135


🏁 Script executed:

# Find point model
fd 'point\.rb' -type f

Repository: Freika/dawarich

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Find Point model - try different patterns
find . -name '*point*.rb' -type f | grep -i model

Repository: Freika/dawarich

Length of output: 198


🏁 Script executed:

# Examine imports/destroy.rb to see context
cat -n app/services/imports/destroy.rb

Repository: Freika/dawarich

Length of output: 671


🏁 Script executed:

# Search for Point model more broadly
fd point

Repository: Freika/dawarich

Length of output: 5197


🏁 Script executed:

#!/bin/bash
# Examine the Point model file
cat -n app/models/point.rb

Repository: Freika/dawarich

Length of output: 4342


🏁 Script executed:

#!/bin/bash
# Search specifically in Point model for any callbacks
rg -n 'after_destroy|before_destroy' app/models/point.rb

Repository: Freika/dawarich

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check for included concerns in Point model that might have callbacks
rg -n 'include|extend' app/models/point.rb

Repository: Freika/dawarich

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Check the Archivable concern for destroy callbacks
rg -n 'after_destroy|before_destroy' app/models/concerns/archivable.rb

Repository: Freika/dawarich

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check all included concerns
cat app/models/concerns/archivable.rb

Repository: Freika/dawarich

Length of output: 2167


🏁 Script executed:

#!/bin/bash
# Check Nearable and Distanceable for callbacks
rg -n 'after_destroy|before_destroy' app/models/concerns/ -A 2 -B 2

Repository: Freika/dawarich

Length of output: 357


🏁 Script executed:

#!/bin/bash
# Double-check by searching for any destroy_all usage patterns and understanding the Import model
cat app/models/import.rb | head -50

Repository: Freika/dawarich

Length of output: 1530


Replace destroy_all with delete_all for better performance on large imports.

The Point model has no destroy callbacks, making the instantiation of each record during destroy_all unnecessary overhead. For imports with thousands of points, use delete_all instead. If point deletion must be cascaded through dependent relationships, rely on the existing dependent: :destroy in the Import model rather than explicitly calling destroy_all.

🤖 Prompt for AI Agents
In app/services/imports/destroy.rb at line 15, calling
@import.points.destroy_all iterates and instantiates each Point (slow for large
imports) even though Point has no destroy callbacks; replace this call with
@import.points.delete_all to perform a single SQL delete for better performance,
ensuring you only do this when no callbacks or dependent cascade logic are
required (rely on the Import model's dependent: :destroy for cascade behavior
where appropriate).

<td data-points-count>
<%= number_with_delimiter @import.processed %>
</td>
<td data-status-display>deleting</td>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use @import.status instead of hardcoded string for consistency.

Hardcoding "deleting" creates a maintenance burden and diverges from the index view (line 72: <%= import.status %>).

🔎 Proposed fix
-    <td data-status-display>deleting</td>
+    <td data-status-display><%= @import.status %></td>
📝 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
<td data-status-display>deleting</td>
<td data-status-display><%= @import.status %></td>
🤖 Prompt for AI Agents
In app/views/imports/destroy.turbo_stream.erb around line 17, the cell currently
hardcodes "deleting"; replace that literal with the model's status so it matches
the index view — use ERB to render the import's status (e.g. <%= @import.status
%>) inside the <td data-status-display> element and ensure proper
escaping/formatting to keep behavior consistent.

Comment on lines 8 to 56
def change
# Add composite index for the most common stats lookup pattern:
# Stat.find_or_initialize_by(year:, month:, user:)
# This query is called on EVERY stats calculation
#
# Using algorithm: :concurrently to avoid locking the table during index creation
# This is crucial for production deployments with existing data
total_duplicates = execute(<<-SQL.squish).first['count'].to_i
SELECT COUNT(*) as count
FROM stats s1
WHERE EXISTS (
SELECT 1 FROM stats s2
WHERE s2.user_id = s1.user_id
AND s2.year = s1.year
AND s2.month = s1.month
AND s2.id > s1.id
)
SQL

if total_duplicates.positive?
Rails.logger.info(
"Found #{total_duplicates} duplicate stats records. Starting cleanup in batches of #{BATCH_SIZE}..."
)
end

deleted_count = 0
loop do
batch_deleted = execute(<<-SQL.squish).cmd_tuples
DELETE FROM stats s1
WHERE EXISTS (
SELECT 1 FROM stats s2
WHERE s2.user_id = s1.user_id
AND s2.year = s1.year
AND s2.month = s1.month
AND s2.id > s1.id
)
LIMIT #{BATCH_SIZE}
SQL

break if batch_deleted.zero?

deleted_count += batch_deleted
Rails.logger.info("Cleaned up #{deleted_count}/#{total_duplicates} duplicate stats records")
end

Rails.logger.info("Completed cleanup: removed #{deleted_count} duplicate stats records") if deleted_count.positive?

add_index :stats, %i[user_id year month],
name: 'index_stats_on_user_id_year_month',
unique: true,
algorithm: :concurrently
algorithm: :concurrently,
if_not_exists: true

BulkStatsCalculatingJob.perform_later
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Migration is not reversible and has PostgreSQL compatibility issues.

Two issues:

  1. Non-reversible operations in change method: The execute statements and perform_later call are not reversible. If a rollback is needed, this migration will fail. Per Strong Migrations practices, consider using up/down methods instead.

  2. PostgreSQL doesn't support LIMIT in DELETE statements directly: The DELETE ... LIMIT syntax at line 38 is not valid PostgreSQL. Use a subquery with ctid or id instead.

🔎 Proposed fix for PostgreSQL-compatible batch deletion
     loop do
-      batch_deleted = execute(<<-SQL.squish).cmd_tuples
-        DELETE FROM stats s1
-        WHERE EXISTS (
-          SELECT 1 FROM stats s2
-          WHERE s2.user_id = s1.user_id
-            AND s2.year = s1.year
-            AND s2.month = s1.month
-            AND s2.id > s1.id
-        )
-        LIMIT #{BATCH_SIZE}
+      batch_deleted = execute(<<-SQL.squish).cmd_tuples
+        DELETE FROM stats
+        WHERE id IN (
+          SELECT s1.id FROM stats s1
+          WHERE EXISTS (
+            SELECT 1 FROM stats s2
+            WHERE s2.user_id = s1.user_id
+              AND s2.year = s1.year
+              AND s2.month = s1.month
+              AND s2.id > s1.id
+          )
+          LIMIT #{BATCH_SIZE}
+        )
       SQL

       break if batch_deleted.zero?

Based on coding guidelines, use Strong Migrations practices for safe database schema changes.

🧰 Tools
🪛 RuboCop (1.82.0)

[convention] 8-56: Assignment Branch Condition size for change is too high. [<4, 20, 3> 20.62/17]

(Metrics/AbcSize)


[convention] 8-56: Method has too many lines. [40/10]

(Metrics/MethodLength)


[convention] 9-9: execute is not reversible.

(Rails/ReversibleMigration)


[convention] 29-29: execute is not reversible.

(Rails/ReversibleMigration)

Comment on lines +78 to +84
// Verify no errors in console
const consoleErrors = []
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text())
})
await page.waitForTimeout(500)
expect(consoleErrors.filter(e => e.includes('fog_layer'))).toHaveLength(0)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Console error listener registered too late to capture errors.

The page.on('console', ...) listener is registered after clicking "Apply Settings" and waiting. Any console errors that occur during applyButton.click() or the first waitForTimeout(500) won't be captured because the listener doesn't exist yet.

🔎 Proposed fix: register the listener before the action
+    // Setup console error listener before clicking apply
+    const consoleErrors = []
+    page.on('console', msg => {
+      if (msg.type() === 'error') consoleErrors.push(msg.text())
+    })
+
     // Click Apply Settings button
     const applyButton = page.locator('button:has-text("Apply Settings")')
     await applyButton.click()
     await page.waitForTimeout(500)
 
-    // Verify no errors in console
-    const consoleErrors = []
-    page.on('console', msg => {
-      if (msg.type() === 'error') consoleErrors.push(msg.text())
-    })
-    await page.waitForTimeout(500)
     expect(consoleErrors.filter(e => e.includes('fog_layer'))).toHaveLength(0)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In e2e/v2/map/layers/advanced.spec.js around lines 78 to 84, the console error
listener is registered after the action so it misses errors triggered by
applyButton.click(); move the page.on('console', ...) registration to before any
UI interactions (before clicking "Apply Settings" and before the initial
waitForTimeout) so it captures errors during the click and subsequent
operations, then keep the rest of the assertions unchanged.

Comment on lines +106 to +112
// Verify no JavaScript errors occurred
const consoleErrors = []
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text())
})
await page.waitForTimeout(500)
expect(consoleErrors.filter(e => e.includes('undefined') || e.includes('fog'))).toHaveLength(0)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same timing issue with console listener.

Same issue as the previous test — the console error listener is registered after the action completes. Move the listener registration before applyButton.click() to capture any errors that occur during settings application.

🤖 Prompt for AI Agents
In e2e/v2/map/layers/advanced.spec.js around lines 106 to 112, the console error
listener is being added after the action that can produce console errors, so
some errors are missed; move the page.on('console', ...) registration to before
await applyButton.click() (i.e., register the listener before triggering the
settings application) so it captures errors emitted during the click, and
optionally clean up the listener after the assertion (page.off or a reference to
the handler) to avoid cross-test leakage.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4be58d4 and 573d527.

📒 Files selected for processing (1)
  • config/application.rb
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/jobs/**/*.rb : Implement long-running or asynchronous work as Sidekiq background jobs

Applied to files:

  • config/application.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to spec/{spec_helper,rails_helper}.rb : Enable code coverage reporting with SimpleCov in the test setup

Applied to files:

  • config/application.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push-docker
🔇 Additional comments (1)
config/application.rb (1)

40-40: LGTM: Explicit string conversion improves clarity.

The explicit .to_s conversion makes the type expectation clear and prevents potential issues with Pathname coercion in ActionMailer preview paths.

config.action_mailer.preview_paths << "#{Rails.root.join('spec/mailers/previews')}"
config.action_mailer.preview_paths << Rails.root.join('spec/mailers/previews').to_s

config.middleware.use Rack::Deflater
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check the repository structure
git ls-files | head -20

Repository: Freika/dawarich

Length of output: 450


🏁 Script executed:

# Locate config/application.rb
find . -name "application.rb" -type f

Repository: Freika/dawarich

Length of output: 82


🏁 Script executed:

# Read the application.rb file around line 42
cat -n config/application.rb | sed -n '1,60p'

Repository: Freika/dawarich

Length of output: 1822


🏁 Script executed:

# Check if there are other middleware configurations or compression-related gems
rg -i "middleware|deflater|compression" config/ --type ruby -B 2 -A 2

Repository: Freika/dawarich

Length of output: 2238


🏁 Script executed:

# Check Gemfile for rack and compression-related dependencies
cat Gemfile 2>/dev/null || echo "No Gemfile found"

Repository: Freika/dawarich

Length of output: 2254


🏁 Script executed:

# Check environment-specific middleware configurations
find config/environments -name "*.rb" -type f

Repository: Freika/dawarich

Length of output: 186


🏁 Script executed:

# Check if there are any compression-related settings in environment files
rg -i "deflater|compression|gzip" config/environments/ --type ruby

Repository: Freika/dawarich

Length of output: 41


Good performance optimization, but verify deployment compatibility.

Adding Rack::Deflater enables gzip compression of HTTP responses. The middleware order is correct—placed at the end of the stack to compress the final output. Rack::Deflater automatically detects existing Content-Encoding headers, so it prevents double compression when a reverse proxy already handles it.

Verify that your deployment environment (reverse proxy, load balancer, CDN) is compatible with this approach. If your infrastructure already compresses responses, ensure Rack::Deflater doesn't override that setup or add unnecessary overhead.

🤖 Prompt for AI Agents
In config/application.rb around line 42, the addition of config.middleware.use
Rack::Deflater may conflict with upstream compression; guard its registration
with an explicit deployment-controlled check (e.g. wrap the middleware
registration in a conditional that reads an ENV flag like ENABLE_RACK_DEFLATER
or a Rails config value), add a clear comment referencing that the flag should
be toggled based on whether the reverse proxy/CDN already compresses responses,
and ensure the default value reflects your most common deployment (disabled if
upstream handles compression).

Freika and others added 3 commits December 26, 2025 18:21
* Fix family layer toggle in Map v2 settings for non-selfhosted env

* Invalidate cache

* Remove comments

* Remove comment
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
app/javascript/controllers/maps/maplibre/settings_manager.js (1)

62-63: Consider extracting the capitalization logic for readability.

The dynamic property name construction is correct, but the string manipulation could be extracted into a helper function for improved readability and reusability.

🔎 Proposed refactor
+  // Helper to capitalize first letter
+  capitalizeFirst(str) {
+    return str.charAt(0).toUpperCase() + str.slice(1)
+  }
+
   syncToggleStates() {
     const controller = this.controller

     // Sync layer toggles
     const toggleMap = {
       pointsToggle: 'pointsVisible',
       routesToggle: 'routesVisible',
       heatmapToggle: 'heatmapEnabled',
       visitsToggle: 'visitsEnabled',
       photosToggle: 'photosEnabled',
       areasToggle: 'areasEnabled',
       placesToggle: 'placesEnabled',
       fogToggle: 'fogEnabled',
       scratchToggle: 'scratchEnabled',
       familyToggle: 'familyEnabled',
       speedColoredToggle: 'speedColoredRoutesEnabled'
     }

     Object.entries(toggleMap).forEach(([targetName, settingKey]) => {
       const target = `${targetName}Target`
-      const hasTarget = `has${targetName.charAt(0).toUpperCase()}${targetName.slice(1)}Target`
+      const hasTarget = `has${this.capitalizeFirst(targetName)}Target`
       if (controller[hasTarget]) {
         controller[target].checked = this.settings[settingKey]
       }
     })
app/services/stats/calculate_month.rb (1)

44-47: Consider moving cache invalidation outside the transaction block.

Placing Cache::InvalidateUserCaches.new(user.id).call inside the transaction means:

  1. If cache deletion fails (e.g., Redis connection error), the entire transaction rolls back, preventing the stat from being saved.
  2. If the transaction rolls back for other reasons after cache invalidation, you may have prematurely invalidated caches.

Cache invalidation is typically safer as a post-commit hook or outside the transaction:

🔎 Suggested refactor
     def update_month_stats(year, month)
       Stat.transaction do
         stat = Stat.find_or_initialize_by(year:, month:, user:)
         distance_by_day = stat.distance_by_day

         stat.assign_attributes(
           daily_distance: distance_by_day,
           distance: distance(distance_by_day),
           toponyms: toponyms,
           h3_hex_ids: calculate_h3_hex_ids
         )

         stat.save!
-
-        Cache::InvalidateUserCaches.new(user.id).call
       end
+
+      Cache::InvalidateUserCaches.new(user.id).call
     end
spec/services/stats/calculate_month_spec.rb (1)

205-224: LGTM!

Good test coverage for cache invalidation behavior:

  1. Verifies cache invalidation occurs after successful stat updates
  2. Confirms no invalidation when user has no points (early return path)

Minor note: The "does not invalidate caches when there are no points" test (lines 216-223) is nested inside the "when there are points" context but creates a new user without points. This works correctly but could be clearer if placed under the "when there are no points" context at lines 13-27.

app/services/cache/invalidate_user_caches.rb (1)

19-31: Consider making invalidation methods private.

The individual invalidation methods (invalidate_countries_visited, invalidate_cities_visited, invalidate_points_geocoded_stats) are currently public but appear to be implementation details. The private keyword on line 31 only applies to attr_reader :user_id.

🔎 Proposed fix to move methods below private
   def call
     invalidate_countries_visited
     invalidate_cities_visited
     invalidate_points_geocoded_stats
   end

+  private
+
+  attr_reader :user_id
+
   def invalidate_countries_visited
     Rails.cache.delete("dawarich/user_#{user_id}_countries_visited")
   end

   def invalidate_cities_visited
     Rails.cache.delete("dawarich/user_#{user_id}_cities_visited")
   end

   def invalidate_points_geocoded_stats
     Rails.cache.delete("dawarich/user_#{user_id}_points_geocoded_stats")
   end
-
-  private
-
-  attr_reader :user_id
 end
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a722e19 and 9e933af.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • app/javascript/controllers/maps/maplibre/settings_manager.js
  • app/jobs/points/nightly_reverse_geocoding_job.rb
  • app/services/cache/clean.rb
  • app/services/cache/invalidate_user_caches.rb
  • app/services/stats/calculate_month.rb
  • spec/jobs/imports/destroy_job_spec.rb
  • spec/jobs/points/nightly_reverse_geocoding_job_spec.rb
  • spec/requests/imports_spec.rb
  • spec/services/cache/clean_spec.rb
  • spec/services/cache/invalidate_user_caches_spec.rb
  • spec/services/stats/calculate_month_spec.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md
🧰 Additional context used
📓 Path-based instructions (2)
spec/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

spec/**/*.rb: Use RSpec as the test framework
Use WebMock for external HTTP request mocking in tests

Files:

  • spec/services/cache/clean_spec.rb
  • spec/jobs/imports/destroy_job_spec.rb
  • spec/services/stats/calculate_month_spec.rb
  • spec/jobs/points/nightly_reverse_geocoding_job_spec.rb
  • spec/requests/imports_spec.rb
  • spec/services/cache/invalidate_user_caches_spec.rb
app/jobs/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Implement long-running or asynchronous work as Sidekiq background jobs

Files:

  • app/jobs/points/nightly_reverse_geocoding_job.rb
🧠 Learnings (2)
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to spec/factories/**/*.rb : Define test data with FactoryBot factories

Applied to files:

  • spec/services/cache/clean_spec.rb
  • spec/jobs/points/nightly_reverse_geocoding_job_spec.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/controllers/shared/stats_controller.rb : When serving public stats, check `public_accessible?`, enforce expiration, and restrict data exposure

Applied to files:

  • spec/services/stats/calculate_month_spec.rb
  • app/services/stats/calculate_month.rb
🧬 Code graph analysis (6)
app/javascript/controllers/maps/maplibre/settings_manager.js (1)
app/javascript/controllers/maps_controller.js (2)
  • controller (1038-1038)
  • controller (1551-1551)
spec/services/stats/calculate_month_spec.rb (2)
app/services/cache/invalidate_user_caches.rb (1)
  • call (13-17)
app/services/stats/calculate_month.rb (1)
  • call (10-20)
app/jobs/points/nightly_reverse_geocoding_job.rb (2)
app/services/cache/clean.rb (2)
  • call (3-44)
  • call (5-13)
app/services/cache/invalidate_user_caches.rb (1)
  • call (13-17)
spec/services/cache/invalidate_user_caches_spec.rb (1)
app/services/cache/invalidate_user_caches.rb (4)
  • call (13-17)
  • invalidate_countries_visited (19-21)
  • invalidate_cities_visited (23-25)
  • invalidate_points_geocoded_stats (27-29)
app/services/stats/calculate_month.rb (1)
app/services/cache/invalidate_user_caches.rb (1)
  • call (13-17)
app/services/cache/invalidate_user_caches.rb (2)
app/services/stats/calculate_month.rb (3)
  • initialize (3-92)
  • initialize (4-8)
  • call (10-20)
app/services/cache/clean.rb (2)
  • call (3-44)
  • call (5-13)
🔇 Additional comments (10)
app/javascript/controllers/maps/maplibre/settings_manager.js (1)

248-250: Good defensive check to prevent updating without data.

The addition of the fogLayer.data check prevents attempting to redraw the fog layer when no data is available, which could lead to errors. This aligns well with the FogLayer's public data property mentioned in the context.

app/services/cache/clean.rb (1)

37-42: LGTM!

The cache key rename from countries/cities to countries_visited/cities_visited aligns with the new Cache::InvalidateUserCaches service (as seen in app/services/cache/invalidate_user_caches.rb lines 18-24), ensuring consistency across the cache invalidation layer.

app/jobs/points/nightly_reverse_geocoding_job.rb (1)

6-19: LGTM!

Good implementation using a Set to collect unique user IDs, ensuring each user's cache is invalidated exactly once regardless of how many points they have. This aligns with the coding guidelines for background jobs.

Note that cache invalidation occurs immediately after points are queued for reverse geocoding (via async_reverse_geocode), not after geocoding completes. This seems appropriate for signaling that data is about to change.

spec/services/cache/clean_spec.rb (1)

15-18: LGTM!

The test cache keys are correctly updated to match the production code changes in app/services/cache/clean.rb, ensuring consistent naming (_countries_visited, _cities_visited).

spec/requests/imports_spec.rb (1)

223-230: LGTM!

The test correctly validates the new asynchronous import deletion flow:

  1. Verifies Imports::DestroyJob is enqueued with the correct import ID
  2. Confirms the import transitions to deleting state
  3. Maintains the redirect expectation

This aligns with the background job pattern in the coding guidelines.

spec/services/cache/invalidate_user_caches_spec.rb (1)

1-57: LGTM!

Comprehensive test coverage for Cache::InvalidateUserCaches:

  • Tests the composite #call method validating all three caches are invalidated
  • Tests each individual invalidation method independently
  • Cache key patterns correctly match the service implementation in app/services/cache/invalidate_user_caches.rb
  • Uses FactoryBot as per coding guidelines
spec/jobs/points/nightly_reverse_geocoding_job_spec.rb (2)

64-73: LGTM!

Good addition of multi-user test scenario with user2 and a third point. This ensures the job correctly handles points belonging to different users.


101-141: LGTM!

Comprehensive test coverage for the per-user cache invalidation behavior:

  • Verifies cache service instantiation for each unique user (lines 102-110)
  • Confirms #call is invoked for each user's cache service (lines 112-125)
  • Validates that the same user's cache is only invalidated once despite having multiple points (lines 127-141) - this correctly tests the Set deduplication logic in the job
spec/jobs/imports/destroy_job_spec.rb (1)

1-14: Well-structured test suite for the background job.

The test file provides comprehensive coverage including:

  • Happy path with status transitions and broadcasts
  • Early return for non-existent imports
  • Race condition handling with graceful error recovery
  • Error propagation for broadcast and service failures
  • User isolation verification

Good use of instance_double, hash_including, and proper RSpec patterns.

Also applies to: 16-89, 91-117, 119-136, 138-146, 166-190

app/services/cache/invalidate_user_caches.rb (1)

1-17: Clean service design with consistent cache key patterns.

The cache keys align with those used in Cache::Clean (see app/services/cache/clean.rb), ensuring consistency across the codebase. The service provides a focused, single-responsibility approach to user-specific cache invalidation.


// Show/hide family members list based on initial toggle state
if (controller.hasFamilyToggleTarget && controller.hasFamilyMembersListTarget) {
if (controller.hasFamilyToggleTarget && controller.hasFamilyMembersListTarget && controller.familyToggleTarget) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n app/javascript/controllers/maps/maplibre/settings_manager.js | sed -n '70,90p'

Repository: Freika/dawarich

Length of output: 1211


🏁 Script executed:

rg -nP 'hasFamilyToggleTarget|familyToggleTarget' app/javascript/controllers/maps/maplibre/settings_manager.js -B2 -A2

Repository: Freika/dawarich

Length of output: 383


🏁 Script executed:

rg -nP 'has\w+Target.*&&.*\.\w+Target.*&&.*\.\w+Target' --type=js

Repository: Freika/dawarich

Length of output: 240


🏁 Script executed:

rg -nP 'has\w+Target.*&&.*\.\w+Target' app/javascript/controllers/ --type=js | head -20

Repository: Freika/dawarich

Length of output: 1315


Remove the redundant controller.familyToggleTarget check.

Line 79 checks hasFamilyToggleTarget && hasFamilyMembersListTarget && familyToggleTarget, but similar patterns elsewhere (lines 69, 74, 133) use only the has[Target]Target checks. In the Stimulus framework, when hasFamilyToggleTarget is true, familyToggleTarget is guaranteed to exist. The third condition is redundant and inconsistent with the codebase pattern. Simplify to: if (controller.hasFamilyToggleTarget && controller.hasFamilyMembersListTarget).

🤖 Prompt for AI Agents
In app/javascript/controllers/maps/maplibre/settings_manager.js around line 79,
the if condition redundantly checks controller.familyToggleTarget in addition to
controller.hasFamilyToggleTarget and controller.hasFamilyMembersListTarget;
remove the redundant controller.familyToggleTarget check so the condition reads
only controller.hasFamilyToggleTarget && controller.hasFamilyMembersListTarget
to match the codebase pattern and Stimulus guarantees.

Comment on lines +157 to +163
it 'has already set status to deleting before service is called' do
expect do
described_class.perform_now(import.id)
rescue StandardError
StandardError
end.to change { import.reload.status }.to('deleting')
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the no-op expression in rescue block.

Line 161 references StandardError as an expression after rescue, which does nothing. The intent appears to be suppressing the error to continue the expectation check.

🔎 Proposed fix
       it 'has already set status to deleting before service is called' do
         expect do
           described_class.perform_now(import.id)
         rescue StandardError
-          StandardError
+          nil
         end.to change { import.reload.status }.to('deleting')
       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 'has already set status to deleting before service is called' do
expect do
described_class.perform_now(import.id)
rescue StandardError
StandardError
end.to change { import.reload.status }.to('deleting')
end
it 'has already set status to deleting before service is called' do
expect do
described_class.perform_now(import.id)
rescue StandardError
nil
end.to change { import.reload.status }.to('deleting')
end
🤖 Prompt for AI Agents
In spec/jobs/imports/destroy_job_spec.rb around lines 157 to 163, the rescue
block currently contains a no-op expression `StandardError` which does nothing;
replace it with a proper rescue that suppresses the error (e.g., `rescue
StandardError => _` or `rescue StandardError; nil`) so the exception is
swallowed and the expectation can continue to assert that the import status
changes to 'deleting'.

Freika and others added 3 commits December 28, 2025 17:32
#2078)

* Add new indicies to improve performance and remove unused ones to optimize database.

* Remove comments

* Update map search suggestions panel styling
* Add yearly digest

* Rename YearlyDigests to Users::Digests

* Minor changes

* Update yearly digest layout and styles

* Add flags and chart to email

* Update colors

* Fix layout of stats in yearly digest view

* Remove cron job for yearly digest scheduling

* Update CHANGELOG.md

* Update digest email setting handling

* Allow sharing digest for 1 week or 1 month

* Change Digests Distance to Bigint

* Fix settings page
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
lib/tasks/points_raw_data.rake (1)

59-59: Inconsistent task argument syntax.

Line 59 still uses the old [:user_id] syntax, while other tasks (lines 6, 30, 145, 180) were updated to the modern %i[user_id] syntax. This inconsistency should be addressed for uniformity.

🔎 Proposed fix for consistency
-    task :restore_all, [:user_id] => :environment do |_t, args|
+    task :restore_all, %i[user_id] => :environment do |_t, args|
♻️ Duplicate comments (1)
app/controllers/imports_controller.rb (1)

81-88: Verify error recovery for background deletion.

The import transitions to deleting status before enqueueing the background job. If Imports::DestroyJob fails, the import may remain stuck in the deleting state indefinitely. This concern has been previously raised and should be addressed with proper error handling in the job.

🧹 Nitpick comments (21)
lib/tasks/points_raw_data.rake (1)

116-118: Consider breaking the long line for better readability.

Line 116 exceeds the 120-character limit (135 characters). While functional, breaking the SQL string across lines would improve readability and satisfy RuboCop conventions.

🔎 Suggested formatting
       total_blob_size = ActiveStorage::Blob
-                        .joins('INNER JOIN active_storage_attachments ON active_storage_attachments.blob_id = active_storage_blobs.id')
+                        .joins(
+                          'INNER JOIN active_storage_attachments ' \
+                          'ON active_storage_attachments.blob_id = active_storage_blobs.id'
+                        )
                         .where("active_storage_attachments.record_type = 'Points::RawDataArchive'")
                         .sum(:byte_size)
CHANGELOG.md (1)

13-19: Consider consolidating duplicate "Changed" sections.

There are two separate "## Changed" sections (lines 13 and 17). For clarity and consistency with the Keep a Changelog format, consider merging them into a single section.

🔎 Suggested consolidation
 ## Added
 
 - In the beginning of the year users will receive a year-end digest email with stats about their tracking activity during the past year. Users can opt out of receiving these emails in User Settings -> Notifications. Emails won't be sent if no email is configured in the SMTP settings or if user has no points tracked during the year.
 
 ## Changed
 
 - Added and removed some indexes to improve the app performance based on the production usage data.
-
-## Changed
-
 - Deleting an import will now be processed in the background to prevent request timeouts for large imports.
db/migrate/20251227000001_create_digests.rb (1)

33-36: Consider adding indexes concurrently for zero-downtime deployment.

While adding indexes on a new empty table is safe during initial creation, for production deployments where users are upgrading, it's safer to add indexes concurrently to avoid locking the table.

Based on coding guidelines for Strong Migrations practices.

🔎 Safer index creation approach
 class CreateDigests < ActiveRecord::Migration[8.0]
+  disable_ddl_transaction!
+
   def change
     create_table :digests do |t|
       t.references :user, null: false, foreign_key: true
       t.integer :year, null: false
       t.integer :period_type, null: false, default: 0 # enum: monthly: 0, yearly: 1

       # Aggregated data
       t.bigint :distance, null: false, default: 0 # Total distance in meters
       t.jsonb :toponyms, default: {}               # Countries/cities data
       t.jsonb :monthly_distances, default: {}      # {1: meters, 2: meters, ...}
       t.jsonb :time_spent_by_location, default: {} # Top locations by time

       # First-time visits (calculated from historical data)
       t.jsonb :first_time_visits, default: {} # {countries: [], cities: []}

       # Comparisons
       t.jsonb :year_over_year, default: {} # {distance_change_percent: 15, ...}
       t.jsonb :all_time_stats, default: {} # {total_countries: 50, ...}

       # Sharing (like Stat model)
       t.jsonb :sharing_settings, default: {}
       t.uuid :sharing_uuid

       # Email tracking
       t.datetime :sent_at

       t.timestamps
     end

-    add_index :digests, %i[user_id year period_type], unique: true
-    add_index :digests, :sharing_uuid, unique: true
-    add_index :digests, :year
-    add_index :digests, :period_type
+    add_index :digests, %i[user_id year period_type], unique: true, algorithm: :concurrently
+    add_index :digests, :sharing_uuid, unique: true, algorithm: :concurrently
+    add_index :digests, :year, algorithm: :concurrently
+    add_index :digests, :period_type, algorithm: :concurrently
   end
 end
spec/mailers/previews/users/digests_mailer_preview.rb (1)

4-9: Consider adding defensive guards for better developer experience.

The preview assumes both a user and digest exist in the development database. While this is typical for mailer previews, you could improve the developer experience by handling edge cases more gracefully.

🔎 Suggested improvement
 def year_end_digest
-  user = User.first
+  user = User.first || User.create!(
+    email: '[email protected]',
+    password: 'password123',
+    status: :active
+  )
   digest = user.digests.yearly.last || Users::Digest.last
+  
+  unless digest
+    # Create a sample digest if none exist
+    digest = Users::Digest.create!(
+      user: user,
+      year: Time.current.year,
+      digest_type: :yearly,
+      data: { total_distance: 1000, total_points: 5000 }
+    )
+  end

   Users::DigestsMailer.with(user: user, digest: digest).year_end_digest
 end

This ensures the preview always works, even with a fresh database.

app/views/users/digests_mailer/year_end_digest.text.erb (1)

4-4: Consider making the greeting configurable.

The hardcoded greeting "Hi, this is Evgenii from Dawarich!" may not scale if the application is multi-tenant, changes ownership, or if different senders are needed for different contexts. Consider extracting this to a configuration setting or environment variable.

spec/services/users/digests/first_time_visits_calculator_spec.rb (1)

15-17: Address RuboCop indentation warnings.

RuboCop has flagged several indentation inconsistencies in the toponyms array literals. Consider adjusting the indentation to align with the project's style guide.

As per coding guidelines, formatting issues flagged by static analysis tools should be addressed.

Also applies to: 18-20, 35-37, 42-44, 45-47, 62-64, 68-70, 74-77

spec/services/users/digests/year_over_year_calculator_spec.rb (1)

25-27: Address RuboCop indentation warnings.

RuboCop has flagged several indentation inconsistencies in the toponyms array literals. Consider adjusting the indentation to align with the project's style guide.

As per coding guidelines, formatting issues flagged by static analysis tools should be addressed.

Also applies to: 28-30, 36-38, 39-41, 95-98, 102-104

spec/jobs/users/digests/calculating_job_spec.rb (1)

29-29: Address line length warning.

RuboCop has flagged this line as exceeding the 120-character limit (121 characters). Consider breaking the line for better readability.

As per coding guidelines, formatting issues flagged by static analysis tools should be addressed.

🔎 Proposed fix
-        allow_any_instance_of(Users::Digests::CalculateYear).to receive(:call).and_raise(StandardError.new('Test error'))
+        allow_any_instance_of(Users::Digests::CalculateYear)
+          .to receive(:call)
+          .and_raise(StandardError.new('Test error'))
spec/requests/users/digests_spec.rb (1)

44-50: Remove unused variable assignment.

The digest variable is assigned but never referenced. The test only needs the side effect (creating the record in the database).

🔎 Proposed fix
      it 'displays existing digests' do
-       digest = create(:users_digest, user:, year: 2024)
+       create(:users_digest, user:, year: 2024)

        get users_digests_url

        expect(response.body).to include('2024')
      end
spec/services/users/safe_settings_spec.rb (1)

140-144: Consider adding test coverage for digest_emails_enabled? predicate.

The spec tests individual settings but doesn't explicitly verify the digest_emails_enabled? method behavior, including its nil-handling logic (defaulting to true when nil).

🔎 Suggested test additions
# Add to the 'with default values' context around line 144:
expect(safe_settings.digest_emails_enabled?).to be true

# Consider adding a separate context for nil handling:
context 'when digest_emails_enabled is nil' do
  let(:settings) { { 'digest_emails_enabled' => nil } }

  it 'defaults to true' do
    expect(safe_settings.digest_emails_enabled?).to be true
  end
end

context 'when digest_emails_enabled is false' do
  let(:settings) { { 'digest_emails_enabled' => false } }

  it 'returns false' do
    expect(safe_settings.digest_emails_enabled?).to be false
  end
end
spec/jobs/users/digests/year_end_scheduling_job_spec.rb (1)

20-26: Consider using the enum value instead of magic number.

Line 22 uses update_column(:status, 0) with a magic number. While the comment explains the intent, using the enum mapping would be more maintainable.

🔎 Proposed fix
      before do
        # Force inactive status after any after_commit callbacks
-       inactive_user.update_column(:status, 0) # inactive
+       inactive_user.update_column(:status, User.statuses[:inactive])

        create(:stat, user: active_user, year: previous_year, month: 1)
app/jobs/users/digests/calculating_job.rb (1)

17-22: Consider truncating stacktrace in user-facing notification.

Including the full stacktrace in the notification content may be overwhelming for end users. Consider logging the full stacktrace and showing only the error message to users.

🔎 Proposed fix
    Notifications::Create.new(
      user:,
      kind: :error,
      title: 'Year-End Digest calculation failed',
-     content: "#{error.message}, stacktrace: #{error.backtrace.join("\n")}"
+     content: "An error occurred while generating your digest: #{error.message}"
    ).call
+
+   Rails.logger.error("[CalculatingJob] Digest failed for user #{user_id}: #{error.message}\n#{error.backtrace.join("\n")}")
app/helpers/users/digests_helper.rb (1)

10-20: Code duplication with model method.

The distance_comparison_text logic duplicates the same method in Users::Digest model (lines 134-146 per relevant code snippets). The model already provides this functionality.

🔎 Proposed refactor to use model method
 def distance_comparison_text(distance_meters)
-  distance_km = distance_meters.to_f / 1000
-
-  if distance_km >= Users::Digest::MOON_DISTANCE_KM
-    percentage = ((distance_km / Users::Digest::MOON_DISTANCE_KM) * 100).round(1)
-    "That's #{percentage}% of the distance to the Moon!"
-  else
-    percentage = ((distance_km / Users::Digest::EARTH_CIRCUMFERENCE_KM) * 100).round(1)
-    "That's #{percentage}% of Earth's circumference!"
-  end
+  # Delegate to the digest model for consistency
+  digest = Users::Digest.new(distance: distance_meters)
+  digest.distance_comparison_text
 end

Alternatively, if you have a digest object in the view, call @digest.distance_comparison_text directly instead of using the helper.

app/views/users/digests/public_year.html.erb (1)

81-95: Move distance conversion logic to controller or helper.

The view contains business logic for converting monthly distances. This calculation should be performed in the controller or a dedicated helper method to keep views focused on presentation.

🔎 Proposed refactor

In the controller or helper, prepare the data:

# In controller or helper
def monthly_distance_chart_data(digest, unit)
  digest.monthly_distances.sort.map do |month, distance_meters|
    [
      Date::ABBR_MONTHNAMES[month.to_i],
      Users::Digest.convert_distance(distance_meters.to_i, unit).round
    ]
  end
end

Then in the view:

<%= column_chart(
  monthly_distance_chart_data(@digest, @distance_unit),
  height: '200px',
  suffix: " #{@distance_unit}",
  # ... rest of options
) %>
spec/factories/users/digests.rb (1)

127-127: Optional: Consider using %w for word array.

Line 127 could use %w[Berlin Paris Madrid Barcelona] instead of ['Berlin', 'Paris', 'Madrid', 'Barcelona'] for consistency with Ruby style conventions, though the current format is perfectly acceptable.

app/services/users/digests/calculate_year.rb (2)

42-73: Complex but necessary data aggregation; minor style suggestion.

The nested iteration over toponyms is inherently complex due to the data structure. The logic correctly handles:

  • Non-array toponyms (skip)
  • Non-hash toponym entries (skip)
  • Countries without cities (still tracked)
  • Deduplication via Set

One minor style improvement per RuboCop: use country.blank? instead of unless country.present?.

🔎 Minor style fix
-            next unless country.present?
+            next if country.blank?

90-120: Time-spent calculation is correct but has long lines.

The logic correctly aggregates time by country and city, returning the top 10 for each. Lines 117-118 exceed 120 characters.

🔎 Break long lines for readability
       {
-        'countries' => country_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } },
-        'cities' => city_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } }
+        'countries' => country_time.sort_by { |_, v| -v }
+                                   .first(10)
+                                   .map { |name, minutes| { 'name' => name, 'minutes' => minutes } },
+        'cities' => city_time.sort_by { |_, v| -v }
+                             .first(10)
+                             .map { |name, minutes| { 'name' => name, 'minutes' => minutes } }
       }
app/views/users/digests/show.html.erb (1)

10-13: Share buttons use inline onclick handlers.

The share buttons use onclick="sharing_modal.showModal()" which works but is slightly inconsistent with the Stimulus pattern used elsewhere. Consider using a Stimulus action for consistency, though this is optional since the native dialog API is being leveraged directly.

Also applies to: 214-216

app/controllers/users/digests_controller.rb (1)

35-39: Redundant exception handling with find_by!.

Using find_by! and then rescuing RecordNotFound is redundant. Either use find_by (returns nil) with a nil check, or use find_by! without the rescue block and let Rails handle the 404. The current pattern works but is unconventional.

🔎 Cleaner approach using find_by with nil check
   def set_digest
-    @digest = current_user.digests.yearly.find_by!(year: params[:year])
-  rescue ActiveRecord::RecordNotFound
-    redirect_to users_digests_path, alert: 'Digest not found'
+    @digest = current_user.digests.yearly.find_by(year: params[:year])
+    redirect_to users_digests_path, alert: 'Digest not found' unless @digest
   end
app/models/users/digest.rb (1)

91-97: First-time visit accessors could return nil from hash access.

The methods access first_time_visits['countries'] and first_time_visits['cities']. If first_time_visits itself is nil (not just an empty hash), this will raise NoMethodError. Given the schema default is {}, this should be safe, but defensive access would be more robust.

🔎 More defensive accessor pattern
   def first_time_countries
-    first_time_visits['countries'] || []
+    first_time_visits&.dig('countries') || []
   end

   def first_time_cities
-    first_time_visits['cities'] || []
+    first_time_visits&.dig('cities') || []
   end
app/services/countries_and_cities.rb (1)

31-38: Inconsistent access pattern: method vs. hash notation.

Line 32 uses pluck(:timestamp) (method-based), while line 34 uses city_points.first[:city] (hash-based). This inconsistency could cause issues depending on the data structure of the elements in city_points.

Consider updating line 32 for consistency:

🔎 Proposed change for consistent hash-based access
-    timestamps = city_points.pluck(:timestamp)
+    timestamps = city_points.map { |point| point[:timestamp] }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e933af and 14a0bb6.

⛔ Files ignored due to path filters (2)
  • app/assets/svg/icons/lucide/outline/calendar-plus-2.svg is excluded by !**/*.svg
  • app/assets/svg/icons/lucide/outline/mail.svg is excluded by !**/*.svg
📒 Files selected for processing (53)
  • CHANGELOG.md
  • app/assets/builds/tailwind.css
  • app/controllers/exports_controller.rb
  • app/controllers/imports_controller.rb
  • app/controllers/settings_controller.rb
  • app/controllers/shared/digests_controller.rb
  • app/controllers/users/digests_controller.rb
  • app/helpers/users/digests_helper.rb
  • app/jobs/users/digests/calculating_job.rb
  • app/jobs/users/digests/email_sending_job.rb
  • app/jobs/users/digests/year_end_scheduling_job.rb
  • app/mailers/users/digests_mailer.rb
  • app/models/stat.rb
  • app/models/user.rb
  • app/models/users/digest.rb
  • app/serializers/stats_serializer.rb
  • app/services/countries_and_cities.rb
  • app/services/reverse_geocoding/places/fetch_data.rb
  • app/services/users/digests/calculate_year.rb
  • app/services/users/digests/first_time_visits_calculator.rb
  • app/services/users/digests/year_over_year_calculator.rb
  • app/services/users/safe_settings.rb
  • app/views/map/maplibre/_settings_panel.html.erb
  • app/views/settings/index.html.erb
  • app/views/shared/_sharing_modal.html.erb
  • app/views/stats/index.html.erb
  • app/views/users/digests/index.html.erb
  • app/views/users/digests/public_year.html.erb
  • app/views/users/digests/show.html.erb
  • app/views/users/digests_mailer/year_end_digest.html.erb
  • app/views/users/digests_mailer/year_end_digest.text.erb
  • config/routes.rb
  • config/sidekiq.yml
  • db/migrate/20251227000001_create_digests.rb
  • db/migrate/20251227223614_change_digests_distance_to_bigint.rb
  • db/migrate/20251228000000_remove_unused_indexes.rb
  • db/migrate/20251228100000_add_performance_indexes.rb
  • db/schema.rb
  • db/seeds.rb
  • lib/tasks/points_raw_data.rake
  • lib/tasks/webmanifest.rake
  • spec/factories/users/digests.rb
  • spec/jobs/users/digests/calculating_job_spec.rb
  • spec/jobs/users/digests/email_sending_job_spec.rb
  • spec/jobs/users/digests/year_end_scheduling_job_spec.rb
  • spec/mailers/previews/users/digests_mailer_preview.rb
  • spec/models/users/digest_spec.rb
  • spec/requests/shared/digests_spec.rb
  • spec/requests/users/digests_spec.rb
  • spec/services/users/digests/calculate_year_spec.rb
  • spec/services/users/digests/first_time_visits_calculator_spec.rb
  • spec/services/users/digests/year_over_year_calculator_spec.rb
  • spec/services/users/safe_settings_spec.rb
✅ Files skipped from review due to trivial changes (1)
  • db/seeds.rb
🧰 Additional context used
📓 Path-based instructions (4)
spec/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

spec/**/*.rb: Use RSpec as the test framework
Use WebMock for external HTTP request mocking in tests

Files:

  • spec/jobs/users/digests/year_end_scheduling_job_spec.rb
  • spec/jobs/users/digests/calculating_job_spec.rb
  • spec/requests/users/digests_spec.rb
  • spec/services/users/digests/first_time_visits_calculator_spec.rb
  • spec/factories/users/digests.rb
  • spec/mailers/previews/users/digests_mailer_preview.rb
  • spec/requests/shared/digests_spec.rb
  • spec/services/users/digests/calculate_year_spec.rb
  • spec/services/users/digests/year_over_year_calculator_spec.rb
  • spec/models/users/digest_spec.rb
  • spec/services/users/safe_settings_spec.rb
  • spec/jobs/users/digests/email_sending_job_spec.rb
db/migrate/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Use Strong Migrations practices for safe database schema changes

Files:

  • db/migrate/20251227223614_change_digests_distance_to_bigint.rb
  • db/migrate/20251227000001_create_digests.rb
  • db/migrate/20251228000000_remove_unused_indexes.rb
  • db/migrate/20251228100000_add_performance_indexes.rb
app/jobs/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Implement long-running or asynchronous work as Sidekiq background jobs

Files:

  • app/jobs/users/digests/email_sending_job.rb
  • app/jobs/users/digests/year_end_scheduling_job.rb
  • app/jobs/users/digests/calculating_job.rb
spec/factories/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Define test data with FactoryBot factories

Files:

  • spec/factories/users/digests.rb
🧠 Learnings (6)
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/jobs/**/*.rb : Implement long-running or asynchronous work as Sidekiq background jobs

Applied to files:

  • spec/jobs/users/digests/year_end_scheduling_job_spec.rb
  • spec/jobs/users/digests/calculating_job_spec.rb
  • app/jobs/users/digests/email_sending_job.rb
  • app/controllers/imports_controller.rb
  • app/jobs/users/digests/year_end_scheduling_job.rb
  • app/jobs/users/digests/calculating_job.rb
  • spec/jobs/users/digests/email_sending_job_spec.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to db/migrate/**/*.rb : Use Strong Migrations practices for safe database schema changes

Applied to files:

  • db/migrate/20251227223614_change_digests_distance_to_bigint.rb
  • db/migrate/20251227000001_create_digests.rb
  • db/migrate/20251228000000_remove_unused_indexes.rb
  • db/migrate/20251228100000_add_performance_indexes.rb
  • spec/services/users/safe_settings_spec.rb
  • db/schema.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/controllers/shared/stats_controller.rb : When serving public stats, check `public_accessible?`, enforce expiration, and restrict data exposure

Applied to files:

  • app/serializers/stats_serializer.rb
  • config/routes.rb
  • app/views/stats/index.html.erb
  • app/models/stat.rb
  • app/views/shared/_sharing_modal.html.erb
  • app/models/users/digest.rb
  • app/controllers/shared/digests_controller.rb
  • app/controllers/users/digests_controller.rb
  • app/services/users/digests/first_time_visits_calculator.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to spec/factories/**/*.rb : Define test data with FactoryBot factories

Applied to files:

  • spec/services/users/digests/first_time_visits_calculator_spec.rb
  • spec/factories/users/digests.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/controllers/api/**/*.rb : Protect API endpoints with Bearer API keys, except explicitly public-share endpoints which must verify UUID access

Applied to files:

  • config/routes.rb
  • spec/requests/shared/digests_spec.rb
  • app/controllers/shared/digests_controller.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Add appropriate database indexes for geographic queries to ensure performance

Applied to files:

  • db/migrate/20251228100000_add_performance_indexes.rb
  • db/schema.rb
🧬 Code graph analysis (23)
spec/jobs/users/digests/year_end_scheduling_job_spec.rb (2)
app/controllers/users/digests_controller.rb (1)
  • create (20-31)
app/models/users/digest.rb (1)
  • previous_year (119-121)
spec/jobs/users/digests/calculating_job_spec.rb (2)
app/controllers/users/digests_controller.rb (1)
  • create (20-31)
db/migrate/20251227000001_create_digests.rb (2)
  • change (3-38)
  • change (4-37)
spec/requests/users/digests_spec.rb (2)
app/controllers/users/digests_controller.rb (1)
  • create (20-31)
app/models/user.rb (1)
  • include (3-203)
app/serializers/stats_serializer.rb (1)
app/queries/stats_query.rb (1)
  • points_stats (8-18)
app/jobs/users/digests/email_sending_job.rb (4)
app/jobs/users/digests/year_end_scheduling_job.rb (2)
  • queue_as (3-20)
  • perform (6-19)
app/mailers/users/digests_mailer.rb (1)
  • year_end_digest (7-16)
app/models/user.rb (1)
  • safe_settings (42-44)
app/services/users/safe_settings.rb (1)
  • digest_emails_enabled? (144-149)
spec/services/users/digests/first_time_visits_calculator_spec.rb (3)
app/services/users/digests/calculate_year.rb (1)
  • call (11-28)
app/services/users/digests/first_time_visits_calculator.rb (1)
  • call (11-16)
app/services/users/digests/year_over_year_calculator.rb (1)
  • call (11-20)
app/models/user.rb (1)
app/queries/stats_query.rb (1)
  • points_stats (8-18)
spec/factories/users/digests.rb (3)
app/services/stats/calculate_month.rb (1)
  • toponyms (65-74)
app/models/stat.rb (1)
  • enable_sharing! (69-89)
app/models/users/digest.rb (1)
  • enable_sharing! (48-67)
spec/mailers/previews/users/digests_mailer_preview.rb (2)
app/mailers/users/digests_mailer.rb (1)
  • year_end_digest (7-16)
app/services/tracks/track_builder.rb (1)
  • user (147-149)
spec/requests/shared/digests_spec.rb (1)
app/models/users/digest.rb (1)
  • sharing_enabled? (20-22)
spec/services/users/digests/calculate_year_spec.rb (4)
app/services/users/digests/first_time_visits_calculator.rb (1)
  • call (11-16)
app/services/users/digests/year_over_year_calculator.rb (1)
  • call (11-20)
db/migrate/20251227000001_create_digests.rb (2)
  • change (3-38)
  • change (4-37)
app/services/stats/calculate_month.rb (1)
  • toponyms (65-74)
app/helpers/users/digests_helper.rb (2)
app/models/concerns/distance_convertible.rb (1)
  • convert_distance (41-52)
app/models/users/digest.rb (2)
  • distance_comparison_text (139-147)
  • distance_km (135-137)
app/jobs/users/digests/year_end_scheduling_job.rb (2)
app/jobs/users/digests/calculating_job.rb (2)
  • queue_as (3-26)
  • perform (6-10)
app/jobs/users/digests/email_sending_job.rb (2)
  • queue_as (3-31)
  • perform (6-20)
app/services/users/safe_settings.rb (1)
app/serializers/api/user_serializer.rb (1)
  • settings (28-47)
spec/services/users/digests/year_over_year_calculator_spec.rb (3)
app/services/users/digests/calculate_year.rb (1)
  • call (11-28)
app/services/users/digests/first_time_visits_calculator.rb (1)
  • call (11-16)
app/services/users/digests/year_over_year_calculator.rb (1)
  • call (11-20)
app/controllers/shared/digests_controller.rb (3)
app/models/users/digest.rb (3)
  • public_accessible? (40-42)
  • enable_sharing! (48-67)
  • disable_sharing! (69-77)
app/models/user.rb (1)
  • safe_settings (42-44)
app/services/users/safe_settings.rb (1)
  • distance_unit (120-122)
db/migrate/20251228100000_add_performance_indexes.rb (1)
db/migrate/20251228000000_remove_unused_indexes.rb (2)
  • disable_ddl_transaction! (3-19)
  • change (6-18)
app/controllers/users/digests_controller.rb (4)
app/controllers/shared/digests_controller.rb (2)
  • helper (3-55)
  • show (10-24)
app/mailers/users/digests_mailer.rb (1)
  • helper (3-17)
app/models/user.rb (1)
  • safe_settings (42-44)
app/services/users/safe_settings.rb (1)
  • distance_unit (120-122)
app/services/users/digests/first_time_visits_calculator.rb (2)
app/services/users/digests/calculate_year.rb (3)
  • initialize (5-137)
  • initialize (6-9)
  • call (11-28)
app/models/users/digest.rb (2)
  • first_time_countries (91-93)
  • first_time_cities (95-97)
spec/services/users/safe_settings_spec.rb (3)
app/serializers/api/user_serializer.rb (1)
  • settings (28-47)
app/models/user.rb (1)
  • safe_settings (42-44)
app/services/users/safe_settings.rb (3)
  • maps (116-118)
  • visits_suggestions_enabled? (124-126)
  • enabled_map_layers (136-138)
app/jobs/users/digests/calculating_job.rb (3)
app/jobs/users/digests/email_sending_job.rb (2)
  • queue_as (3-31)
  • perform (6-20)
app/jobs/users/digests/year_end_scheduling_job.rb (2)
  • queue_as (3-20)
  • perform (6-19)
app/services/users/digests/calculate_year.rb (1)
  • call (11-28)
spec/jobs/users/digests/email_sending_job_spec.rb (1)
db/migrate/20251227000001_create_digests.rb (2)
  • change (3-38)
  • change (4-37)
app/services/countries_and_cities.rb (1)
app/serializers/api/photo_serializer.rb (1)
  • city (49-51)
🪛 markdownlint-cli2 (0.18.1)
CHANGELOG.md

17-17: Multiple headings with the same content

(MD024, no-duplicate-heading)

🪛 RuboCop (1.82.0)
spec/jobs/users/digests/calculating_job_spec.rb

[convention] 29-29: Line is too long. [121/120]

(Layout/LineLength)

spec/requests/users/digests_spec.rb

[warning] 45-45: Useless assignment to variable - digest.

(Lint/UselessAssignment)

spec/services/users/digests/first_time_visits_calculator_spec.rb

[convention] 16-16: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 17-17: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 19-19: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 20-20: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 36-36: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 37-37: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 43-43: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 44-44: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 46-46: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 47-47: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 63-63: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 64-64: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 69-69: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 70-70: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 75-75: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 77-77: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)

spec/factories/users/digests.rb

[convention] 127-127: Use %w or %W for an array of words.

(Style/WordArray)

db/migrate/20251227000001_create_digests.rb

[convention] 4-37: Assignment Branch Condition size for change is too high. [<1, 19, 0> 19.03/17]

(Metrics/AbcSize)


[convention] 4-37: Method has too many lines. [20/10]

(Metrics/MethodLength)

spec/services/users/digests/calculate_year_spec.rb

[convention] 25-28: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 29-29: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 34-36: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 37-37: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 111-111: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 112-112: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 117-117: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 119-119: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)

db/migrate/20251228000000_remove_unused_indexes.rb

[convention] 6-18: Method has too many lines. [11/10]

(Metrics/MethodLength)

lib/tasks/points_raw_data.rake

[convention] 116-116: Line is too long. [135/120]

(Layout/LineLength)

app/models/users/digest.rb

[convention] 3-154: Class has too many lines. [114/100]

(Metrics/ClassLength)


[convention] 48-67: Cyclomatic complexity for enable_sharing! is too high. [8/7]

(Metrics/CyclomaticComplexity)


[convention] 48-67: Method has too many lines. [16/10]

(Metrics/MethodLength)

spec/services/users/digests/year_over_year_calculator_spec.rb

[convention] 26-26: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 27-27: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 29-29: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 30-30: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 37-37: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 38-38: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 40-40: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 41-41: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 96-96: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 98-98: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 103-103: Use 2 spaces for indentation in an array, relative to the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)


[convention] 104-104: Indent the right bracket the same as the first position after the preceding left parenthesis.

(Layout/FirstArrayElementIndentation)

app/controllers/shared/digests_controller.rb

[convention] 26-54: Assignment Branch Condition size for update is too high. [<3, 19, 6> 20.15/17]

(Metrics/AbcSize)


[convention] 26-54: Method has too many lines. [23/10]

(Metrics/MethodLength)

spec/models/users/digest_spec.rb

[convention] 257-257: Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 259-259: Indent the right brace the same as the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 270-270: Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 273-273: Indent the right brace the same as the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 284-284: Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 287-287: Indent the right brace the same as the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 298-298: Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 301-301: Indent the right brace the same as the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 330-330: Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 333-333: Indent the right brace the same as the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 344-344: Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)


[convention] 347-347: Indent the right brace the same as the first position after the preceding left parenthesis.

(Layout/FirstHashElementIndentation)

app/services/users/digests/year_over_year_calculator.rb

[convention] 65-76: Assignment Branch Condition size for count_cities is too high. [<4, 15, 8> 17.46/17]

(Metrics/AbcSize)


[convention] 65-76: Cyclomatic complexity for count_cities is too high. [9/7]

(Metrics/CyclomaticComplexity)


[convention] 65-76: Perceived complexity for count_cities is too high. [9/8]

(Metrics/PerceivedComplexity)

db/migrate/20251228100000_add_performance_indexes.rb

[convention] 6-29: Method has too many lines. [15/10]

(Metrics/MethodLength)

app/services/users/digests/first_time_visits_calculator.rb

[convention] 63-74: Cyclomatic complexity for extract_cities is too high. [9/7]

(Metrics/CyclomaticComplexity)


[convention] 63-74: Perceived complexity for extract_cities is too high. [9/8]

(Metrics/PerceivedComplexity)

app/services/users/digests/calculate_year.rb

[convention] 11-28: Method has too many lines. [13/10]

(Metrics/MethodLength)


[convention] 42-73: Assignment Branch Condition size for aggregate_toponyms is too high. [<14, 26, 13> 32.26/17]

(Metrics/AbcSize)


[convention] 42-73: Cyclomatic complexity for aggregate_toponyms is too high. [13/7]

(Metrics/CyclomaticComplexity)


[convention] 42-73: Method has too many lines. [24/10]

(Metrics/MethodLength)


[convention] 42-73: Perceived complexity for aggregate_toponyms is too high. [14/8]

(Metrics/PerceivedComplexity)


[convention] 53-53: Use if country.blank? instead of unless country.present?.

(Rails/Blank)


[convention] 90-120: Assignment Branch Condition size for calculate_time_spent is too high. [<17, 29, 13> 36.04/17]

(Metrics/AbcSize)


[convention] 90-120: Cyclomatic complexity for calculate_time_spent is too high. [14/7]

(Metrics/CyclomaticComplexity)


[convention] 90-120: Method has too many lines. [22/10]

(Metrics/MethodLength)


[convention] 90-120: Perceived complexity for calculate_time_spent is too high. [14/8]

(Metrics/PerceivedComplexity)


[convention] 117-117: Line is too long. [135/120]

(Layout/LineLength)


[convention] 118-118: Line is too long. [128/120]

(Layout/LineLength)

db/schema.rb

[convention] 13-13: Use underscores(_) as thousands separator and separate every 3 digits with them.

(Style/NumericLiterals)


[convention] 83-83: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 84-84: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 85-85: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 86-86: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 87-87: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 88-88: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 89-89: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 90-90: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 91-91: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 92-92: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 93-93: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 94-94: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 95-95: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 96-96: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 97-97: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 98-98: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 99-99: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 99-99: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 100-100: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 100-100: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 102-102: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 102-102: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 103-103: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 103-103: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 261-261: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 261-261: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 261-261: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 261-261: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 262-262: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 262-262: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 262-262: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 262-262: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Line is too long. [147/120]

(Layout/LineLength)


[convention] 264-264: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 266-266: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 266-266: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 266-266: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 266-266: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 390-390: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 390-390: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 391-391: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 391-391: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 392-392: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 392-392: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 392-392: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 392-392: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 393-393: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 393-393: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 394-394: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 394-394: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 419-419: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 419-419: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)

Comment on lines +6 to +10
def perform(user_id, year)
Users::Digests::CalculateYear.new(user_id, year).call
rescue StandardError => e
create_digest_failed_notification(user_id, e)
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Swallowing errors prevents Sidekiq retries.

The rescue StandardError block catches the error and creates a notification, but doesn't re-raise. This prevents Sidekiq's automatic retry mechanism from attempting the job again for transient failures (e.g., network issues, temporary database unavailability).

Consider whether retries are desired. If so, re-raise after notification:

🔎 Proposed fix to allow retries
  def perform(user_id, year)
    Users::Digests::CalculateYear.new(user_id, year).call
  rescue StandardError => e
    create_digest_failed_notification(user_id, e)
+   raise # Allow Sidekiq to retry
  end

If retries are intentionally disabled (to avoid duplicate notifications), consider using discard_on or retry_on with specific exception types for more granular control.

🤖 Prompt for AI Agents
In app/jobs/users/digests/calculating_job.rb around lines 6 to 10, the rescue
block swallows exceptions which prevents Sidekiq retries; update the handler to
notify on failure and then re-raise the exception to allow Sidekiq to retry (or
alternatively, replace the blanket rescue with Sidekiq's discard_on/retry_on for
specific exception classes if you intentionally want to disable retries),
ensuring the notification still runs before the re-raise or handled by the
configured Sidekiq error handling strategy.

autocomplete="off" />
<!-- Search Results -->
<div class="absolute z-50 w-full mt-1 bg-base-100 rounded-lg shadow-lg border border-base-300 hidden max-h-full overflow-y-auto"
<div class="absolute z-50 w-full mt-1 bg-base-100 rounded-lg shadow-lg border border-base-300 hidden max-height:400px; overflow-y-auto"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix invalid CSS syntax in class attribute.

The max-height:400px; syntax is invalid inside the class attribute. CSS properties must be placed in a style attribute, not in class. This will prevent the search results container from having the intended height constraint.

🔎 Proposed fixes

Solution 1 (recommended): Use the style attribute

-            <div class="absolute z-50 w-full mt-1 bg-base-100 rounded-lg shadow-lg border border-base-300 hidden max-height:400px;  overflow-y-auto"
+            <div class="absolute z-50 w-full mt-1 bg-base-100 rounded-lg shadow-lg border border-base-300 hidden overflow-y-auto"
+                 style="max-height: 400px;"
                  data-maps--maplibre-target="searchResults">

Solution 2: Use Tailwind utility class (more consistent with the rest of the codebase)

-            <div class="absolute z-50 w-full mt-1 bg-base-100 rounded-lg shadow-lg border border-base-300 hidden max-height:400px;  overflow-y-auto"
+            <div class="absolute z-50 w-full mt-1 bg-base-100 rounded-lg shadow-lg border border-base-300 hidden max-h-[400px] overflow-y-auto"
                  data-maps--maplibre-target="searchResults">
🤖 Prompt for AI Agents
In app/views/map/maplibre/_settings_panel.html.erb around line 75, the class
attribute contains invalid CSS syntax `max-height:400px;`; remove that property
from the class list and either add it to a style attribute (e.g.,
style="max-height:400px;") or replace it with a Tailwind utility (e.g., add a
max-h-* or max-h-[400px] class), keeping the rest of the classes intact and
removing the stray semicolon.

<div class="w-full my-5">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">Statistics</h1>
<% if Date.today >= Date.new(2025, 12, 31) %>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider timezone for date comparison.

Date.today uses the server's timezone, which may differ from the user's timezone. This could cause the link to appear at different local times for users around the world. Consider using Time.current.to_date (respects Rails timezone config) or a user-specific timezone if available.

🔎 Proposed fix using Rails timezone
-    <% if Date.today >= Date.new(2025, 12, 31) %>
+    <% if Time.current.to_date >= Date.new(2025, 12, 31) %>
       <%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
         <%= icon 'earth' %> Year-End Digests
       <% end %>
     <% 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
<% if Date.today >= Date.new(2025, 12, 31) %>
<% if Time.current.to_date >= Date.new(2025, 12, 31) %>
<%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
<%= icon 'earth' %> Year-End Digests
<% end %>
<% end %>
🤖 Prompt for AI Agents
In app/views/stats/index.html.erb around line 6, the code uses Date.today which
relies on server timezone; replace it with Time.current.to_date to respect Rails
timezone config, or if you have a user-specific timezone available convert the
current time to that zone (e.g., user.time_zone) and call to_date, so the date
comparison reflects the configured/user timezone.

Comment on lines +6 to +10
<% if Date.today >= Date.new(2025, 12, 31) %>
<%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
<%= icon 'earth' %> Year-End Digests
<% end %>
<% end %>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use dynamic year for recurring Year-End Digests feature.

The hardcoded year 2025 means the link will appear on December 31, 2025, and remain visible indefinitely. For a recurring yearly digest feature, this should dynamically reference the current year. After 2025, users would expect to generate digests for 2026, 2027, etc., but this condition will always be true.

🔎 Proposed fix using dynamic year
-    <% if Date.today >= Date.new(2025, 12, 31) %>
+    <% if Date.today >= Date.new(Date.today.year, 12, 31) %>
       <%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
         <%= icon 'earth' %> Year-End Digests
       <% end %>
     <% end %>

Alternatively, if digests should only be accessible after the year ends:

-    <% if Date.today >= Date.new(2025, 12, 31) %>
+    <% if Date.today.month == 12 && Date.today.day >= 31 %>
       <%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
         <%= icon 'earth' %> Year-End Digests
       <% end %>
     <% end %>

Or if year-end digests should be available year-round after they're first generated:

-    <% if Date.today >= Date.new(2025, 12, 31) %>
+    <% if Date.today > Date.new(Date.today.year, 1, 1) %>
       <%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
         <%= icon 'earth' %> Year-End Digests
       <% end %>
     <% 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
<% if Date.today >= Date.new(2025, 12, 31) %>
<%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
<%= icon 'earth' %> Year-End Digests
<% end %>
<% end %>
<% if Date.today >= Date.new(Date.today.year, 12, 31) %>
<%= link_to users_digests_path, class: 'btn btn-outline btn-sm' do %>
<%= icon 'earth' %> Year-End Digests
<% end %>
<% end %>
🤖 Prompt for AI Agents
In app/views/stats/index.html.erb around lines 6 to 10, the condition uses a
hardcoded Date.new(2025, 12, 31) so the Year-End Digests link will behave
incorrectly after 2025; change the condition to use the current year dynamically
(e.g., build the year-end date from Date.current.year) so the link appears
relative to each calendar year's December 31st, and verify the logic matches
desired behavior (only on/after year-end or always after first generation).


<div class="content">
<p>
Hi, this is Evgenii from Dawarich! Pretty wild journey last yeah, huh? Let's take a look back at all the places you explored in <strong><%= @digest.year %></strong>.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in the email content.

Line 171 contains "last yeah" which should be "last year":

🔎 Proposed fix
-      Hi, this is Evgenii from Dawarich! Pretty wild journey last yeah, huh? Let's take a look back at all the places you explored in <strong><%= @digest.year %></strong>.
+      Hi, this is Evgenii from Dawarich! Pretty wild journey last year, huh? Let's take a look back at all the places you explored in <strong><%= @digest.year %></strong>.
📝 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
Hi, this is Evgenii from Dawarich! Pretty wild journey last yeah, huh? Let's take a look back at all the places you explored in <strong><%= @digest.year %></strong>.
Hi, this is Evgenii from Dawarich! Pretty wild journey last year, huh? Let's take a look back at all the places you explored in <strong><%= @digest.year %></strong>.
🤖 Prompt for AI Agents
In app/views/users/digests_mailer/year_end_digest.html.erb around line 171, the
email body contains a typo "last yeah" — replace it with "last year" so the
sentence reads correctly ("Pretty wild journey last year, huh?") and ensure
spacing/punctuation remains unchanged.

Comment on lines +30 to +32
<h2 class="text-xl font-semibold mb-2 flex items-center justify-center gap-2">
<%= icon 'earth' %>No Year-End Digests Yet
</h2>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: Add space between icon and heading text.

The icon and text run together without a gap.

🔎 Proposed fix
        <h2 class="text-xl font-semibold mb-2 flex items-center justify-center gap-2">
-         <%= icon 'earth' %>No Year-End Digests Yet
+         <%= icon 'earth' %> No Year-End Digests Yet
        </h2>
📝 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
<h2 class="text-xl font-semibold mb-2 flex items-center justify-center gap-2">
<%= icon 'earth' %>No Year-End Digests Yet
</h2>
<h2 class="text-xl font-semibold mb-2 flex items-center justify-center gap-2">
<%= icon 'earth' %> No Year-End Digests Yet
</h2>
🤖 Prompt for AI Agents
In app/views/users/digests/index.html.erb around lines 30–32 the icon and
heading text run together; add a small gap between them by applying a right
margin to the icon (e.g., render the icon inside a span or pass a class that
adds margin such as mr-2) or insert a single non-breaking space after the icon
so the heading reads "icon + space + No Year-End Digests Yet".

</span>
<span class="text-sm"><%= country['cities']&.length || 0 %> cities</span>
</div>
<progress class="progress progress-primary w-full" value="<%= 100 - (index * 15) %>" max="100"></progress>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Progress calculation uses magic numbers and could produce negative values.

The formula 100 - (index * 15) uses magic numbers (100, 15) and will produce negative values if there are more than ~6 countries. Consider using a more robust calculation or capping at 0.

🔎 Proposed fix
-<progress class="progress progress-primary w-full" value="<%= 100 - (index * 15) %>" max="100"></progress>
+<% progress_value = [100 - (index * 15), 0].max %>
+<progress class="progress progress-primary w-full" value="<%= progress_value %>" max="100"></progress>

Or better, calculate progress based on actual data (e.g., time spent or visit count):

<% max_cities = @digest.toponyms.map { |c| c['cities']&.length || 0 }.max %>
<% progress = max_cities > 0 ? ((country['cities']&.length || 0).to_f / max_cities * 100).round : 0 %>
<progress class="progress progress-primary w-full" value="<%= progress %>" max="100"></progress>
📝 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
<progress class="progress progress-primary w-full" value="<%= 100 - (index * 15) %>" max="100"></progress>
<% progress_value = [100 - (index * 15), 0].max %>
<progress class="progress progress-primary w-full" value="<%= progress_value %>" max="100"></progress>
🤖 Prompt for AI Agents
In app/views/users/digests/public_year.html.erb around line 139, the progress
value is computed with a magic-number formula `100 - (index * 15)` which can
produce negative results and is brittle; replace it with a data-driven, bounded
calculation: compute a meaningful denominator (e.g., maximum cities or max visit
count across countries), calculate the country's ratio as a float, convert to a
percentage and round, and then clamp the result between 0 and 100 while handling
nils/defaults so the progress value never goes below 0 or above 100.

* Add RailsPulse

* Add RailsPulse monitoring tool with basic HTTP authentication
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
Gemfile (1)

39-39: Consider pinning the rails_pulse gem version.

While the gem is correctly positioned, adding a version constraint (e.g., gem 'rails_pulse', '~> 1.0') would prevent unexpected breaking changes from automatic updates.

db/migrate/20251228163703_install_rails_pulse_tables.rb (1)

1-23: Migration approach looks safe for new table creation.

The pattern of loading an external schema file is appropriate for Rails Pulse integration. Since this creates new tables without touching existing data, there are no Strong Migrations concerns.

However, consider these optional style improvements:

🔎 Optional style improvements
+# frozen_string_literal: true
+
 # Generated from Rails Pulse schema - automatically loads current schema definition
 class InstallRailsPulseTables < ActiveRecord::Migration[8.0]
   def change
+    schema_file = Rails.root.join("db/rails_pulse_schema.rb")
-    # Load and execute the Rails Pulse schema directly
-    # This ensures the migration is always in sync with the schema file
-    schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_schema.rb")
-
-    if File.exist?(schema_file)
+    raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb" unless File.exist?(schema_file)
+
-      say "Loading Rails Pulse schema from db/rails_pulse_schema.rb"
+    say 'Loading Rails Pulse schema from db/rails_pulse_schema.rb'
-
-      # Load the schema file to define RailsPulse::Schema
-      load schema_file
+    load schema_file
+    RailsPulse::Schema.call(connection)
-
-      # Execute the schema in the context of this migration
-      RailsPulse::Schema.call(connection)
-
-      say "Rails Pulse tables created successfully"
-      say "The schema file db/rails_pulse_schema.rb remains as your single source of truth"
-    else
-      raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
-    end
+    say 'Rails Pulse tables created successfully'
+    say 'The schema file db/rails_pulse_schema.rb remains as your single source of truth'
   end
 end
config/schedule.yml (1)

53-61: RailsPulse scheduled jobs look correctly configured.

The cron schedules are appropriate (hourly summaries at :05, daily cleanup at 01:00).

Note: Both jobs use the default queue. If RailsPulse operations become resource-intensive, consider using a dedicated queue (the PR adds a digests queue in sidekiq.yml) to isolate performance monitoring workload.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14a0bb6 and 26062a1.

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • Gemfile
  • config/initializers/rails_pulse.rb
  • config/routes.rb
  • config/schedule.yml
  • db/migrate/20251228163703_install_rails_pulse_tables.rb
  • db/rails_pulse_migrate/.keep
  • db/rails_pulse_schema.rb
  • db/schema.rb
🧰 Additional context used
📓 Path-based instructions (1)
db/migrate/**/*.rb

📄 CodeRabbit inference engine (CLAUDE.md)

Use Strong Migrations practices for safe database schema changes

Files:

  • db/migrate/20251228163703_install_rails_pulse_tables.rb
🧠 Learnings (5)
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/jobs/**/*.rb : Implement long-running or asynchronous work as Sidekiq background jobs

Applied to files:

  • config/schedule.yml
  • config/routes.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to db/migrate/**/*.rb : Use Strong Migrations practices for safe database schema changes

Applied to files:

  • db/schema.rb
  • db/rails_pulse_schema.rb
  • db/migrate/20251228163703_install_rails_pulse_tables.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Add appropriate database indexes for geographic queries to ensure performance

Applied to files:

  • db/schema.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/controllers/shared/stats_controller.rb : When serving public stats, check `public_accessible?`, enforce expiration, and restrict data exposure

Applied to files:

  • config/routes.rb
📚 Learning: 2025-09-29T21:15:25.496Z
Learnt from: CR
Repo: Freika/dawarich PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-29T21:15:25.496Z
Learning: Applies to app/controllers/api/**/*.rb : Protect API endpoints with Bearer API keys, except explicitly public-share endpoints which must verify UUID access

Applied to files:

  • config/routes.rb
🪛 RuboCop (1.82.0)
db/schema.rb

[convention] 13-13: Use underscores(_) as thousands separator and separate every 3 digits with them.

(Style/NumericLiterals)


[convention] 83-83: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 84-84: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 85-85: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 86-86: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 87-87: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 88-88: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 89-89: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 90-90: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 91-91: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 92-92: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 93-93: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 94-94: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 95-95: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 96-96: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 97-97: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 98-98: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 99-99: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 99-99: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 100-100: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 100-100: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 102-102: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 102-102: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 103-103: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 103-103: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 261-261: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 261-261: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 261-261: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 261-261: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 262-262: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 262-262: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 262-262: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 262-262: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 263-263: Line is too long. [147/120]

(Layout/LineLength)


[convention] 264-264: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 264-264: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 265-265: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 266-266: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 266-266: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 266-266: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 266-266: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 288-288: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 289-289: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 289-289: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 290-290: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 290-290: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 291-291: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 291-291: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 292-292: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 292-292: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 293-293: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 293-293: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 294-294: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 294-294: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 295-295: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 295-295: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 296-296: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 296-296: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 297-297: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 298-298: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 299-299: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 299-299: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 299-299: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 299-299: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 300-300: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 300-300: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 301-301: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 301-301: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 301-301: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 301-301: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 301-301: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 302-302: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 302-302: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 303-303: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 303-303: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 304-304: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 304-304: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 304-304: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 304-304: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 304-304: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 305-305: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 305-305: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 305-305: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 305-305: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 306-306: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 306-306: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 307-307: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 307-307: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 310-310: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 311-311: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 311-311: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 311-311: Line is too long. [136/120]

(Layout/LineLength)


[convention] 312-312: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 312-312: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 313-313: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 313-313: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 314-314: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 314-314: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 315-315: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 315-315: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 316-316: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 316-316: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 317-317: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 317-317: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 318-318: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 318-318: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 319-319: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 319-319: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 320-320: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 320-320: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 321-321: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 321-321: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 322-322: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 323-323: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 324-324: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 324-324: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 327-327: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 328-328: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 328-328: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 329-329: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 329-329: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 330-330: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 330-330: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 331-331: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 331-331: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 332-332: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 332-332: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 333-333: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 333-333: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 334-334: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 334-334: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 335-335: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 335-335: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 336-336: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 337-337: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 338-338: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 338-338: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 338-338: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 338-338: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 339-339: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 339-339: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 340-340: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 340-340: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 341-341: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 341-341: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 342-342: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 342-342: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 342-342: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 342-342: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 343-343: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 343-343: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 346-346: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 347-347: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 347-347: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 348-348: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 348-348: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 349-349: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 349-349: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 350-350: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 351-351: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 352-352: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 352-352: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 352-352: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 352-352: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 355-355: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 356-356: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 356-356: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 357-357: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 357-357: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 358-358: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 358-358: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 359-359: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 360-360: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 360-360: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 361-361: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 361-361: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 362-362: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 362-362: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 363-363: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 363-363: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 364-364: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 364-364: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 365-365: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 365-365: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 366-366: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 366-366: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 367-367: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 367-367: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 368-368: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 368-368: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 369-369: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 369-369: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 370-370: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 370-370: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 371-371: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 371-371: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 372-372: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 372-372: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 373-373: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 373-373: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 374-374: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 374-374: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 375-375: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 375-375: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 376-376: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 377-377: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 378-378: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 378-378: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 379-379: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 379-379: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 379-379: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 379-379: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 380-380: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 380-380: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 380-380: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 380-380: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 380-380: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 380-380: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 380-380: Line is too long. [133/120]

(Layout/LineLength)


[convention] 381-381: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 381-381: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 381-381: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 381-381: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 486-486: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 486-486: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 487-487: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 487-487: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 488-488: Use %w or %W for an array of words.

(Style/WordArray)


[convention] 488-488: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 488-488: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 488-488: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 489-489: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 489-489: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 490-490: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 490-490: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 515-515: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 515-515: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 528-528: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 528-528: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 528-528: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 529-529: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 529-529: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 529-529: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 530-530: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 530-530: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 530-530: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)

db/rails_pulse_schema.rb

[convention] 1-1: Missing frozen string literal comment.

(Style/FrozenStringLiteralComment)


[convention] 7-7: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 7-7: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 7-7: Line is too long. [137/120]

(Layout/LineLength)


[convention] 7-7: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 9-9: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 9-9: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 12-12: Do not write to stdout. Use Rails's logger if you want to log.

(Rails/Output)


[convention] 13-13: Do not write to stdout. Use Rails's logger if you want to log.

(Rails/Output)


[convention] 19-19: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 20-20: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 21-21: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 25-25: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 25-25: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 25-25: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 25-25: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 25-25: Line is too long. [129/120]

(Layout/LineLength)


[convention] 28-28: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 28-28: Line is too long. [135/120]

(Layout/LineLength)


[convention] 29-29: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 30-30: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 31-31: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 32-32: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 33-33: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 34-34: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 35-35: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 36-36: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 37-37: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 38-38: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 42-42: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 42-42: Line is too long. [140/120]

(Layout/LineLength)


[convention] 45-45: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 46-46: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 47-47: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 48-48: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 49-49: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 50-50: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 51-51: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 52-52: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 56-56: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 57-57: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 57-57: Line is too long. [125/120]

(Layout/LineLength)


[convention] 58-58: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 58-58: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 58-58: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 58-58: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 58-58: Line is too long. [137/120]

(Layout/LineLength)


[convention] 61-61: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 62-62: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 62-62: Line is too long. [130/120]

(Layout/LineLength)


[convention] 63-63: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 64-64: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 65-65: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 66-66: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 67-67: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 68-68: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 72-72: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 73-73: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 74-74: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 74-74: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 74-74: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 74-74: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 74-74: Line is too long. [131/120]

(Layout/LineLength)


[convention] 75-75: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 75-75: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 75-75: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 75-75: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 75-75: Line is too long. [142/120]

(Layout/LineLength)


[convention] 76-76: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 76-76: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 76-76: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 76-76: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 76-76: Line is too long. [152/120]

(Layout/LineLength)


[convention] 80-80: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 81-81: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 82-82: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 85-85: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 90-90: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 91-91: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 92-92: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 93-93: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 94-94: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 95-95: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 96-96: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 97-97: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 98-98: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 101-101: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 102-102: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 103-103: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 104-104: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 105-105: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 106-106: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 112-112: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 112-112: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 112-112: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 113-114: Align the arguments of a method call if they span more than one line.

(Layout/ArgumentAlignment)


[convention] 114-114: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 115-115: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 115-115: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 115-115: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 115-115: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 115-115: Line is too long. [125/120]

(Layout/LineLength)


[convention] 116-116: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 119-119: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 119-119: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 119-119: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 119-119: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 120-120: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 122-122: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 122-122: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 122-122: Do not use space inside array brackets.

(Layout/SpaceInsideArrayLiteralBrackets)


[convention] 122-122: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 123-123: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 125-125: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 125-125: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 127-127: Do not write to stdout. Use Rails's logger if you want to log.

(Rails/Output)


[convention] 131-131: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.

(Style/IfUnlessModifier)

db/migrate/20251228163703_install_rails_pulse_tables.rb

[convention] 1-1: Missing frozen string literal comment.

(Style/FrozenStringLiteralComment)


[convention] 6-6: Prefer Rails.root.join('path/to').to_s.

(Rails/FilePath)


[convention] 6-6: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 8-8: Use a guard clause (raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb" unless File.exist?(schema_file)) instead of wrapping the code inside a conditional expression.

(Style/GuardClause)


[convention] 9-9: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 17-17: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 18-18: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 20-20: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 23-23: Final newline missing.

(Layout/TrailingEmptyLines)

config/initializers/rails_pulse.rb

[convention] 1-1: Missing frozen string literal comment.

(Style/FrozenStringLiteralComment)

🔇 Additional comments (5)
config/routes.rb (2)

29-29: RailsPulse engine mounting looks correct.

The engine is mounted at a standard path, consistent with other engine mounts in this file.


102-111: Digest routes follow existing patterns correctly.

The route structure mirrors the existing stats sharing implementation (lines 94-100), maintaining consistency. Year constraints and UUID-based public sharing are appropriate.

All authentication requirements are properly implemented:

  • users/digests actions are protected with authenticate_user! filter
  • shared/digests#show validates UUID access via public_accessible? check without requiring authentication
  • shared/digests#update requires authentication with authenticate_active_user! filter

The implementation follows the same proven pattern as the stats sharing endpoints.

db/rails_pulse_schema.rb (2)

5-129: RailsPulse schema design is well-structured with appropriate indexes.

The schema provides comprehensive coverage for performance monitoring:

  • Proper unique constraints (request_uuid, routes, queries)
  • Foreign key relationships ensure referential integrity
  • Composite indexes support aggregation queries efficiently
  • Polymorphic association in summaries is appropriate for flexible summarization

The CI-specific logging (lines 9-14, 125-128) helps debug table creation issues and can remain as is.

Based on learnings: The index strategy follows best practices for geographic and time-series queries.


131-133: Conditional schema execution looks correct.

The guard clause ensures the schema is only applied when RailsPulse::ApplicationRecord is defined, preventing errors during migration runs.

db/schema.rb (1)

13-13: Schema changes reflect safe database migrations.

The auto-generated schema correctly captures:

  • New digests table with proper constraints and indexes
  • RailsPulse tables with foreign key relationships
  • Performance indexes on existing tables (users.api_key, users.status, points optimizations)

All changes create new tables or add indexes without modifying existing data, which is safe from a Strong Migrations perspective.

Based on learnings: The new indexes on points (lines 261, 265-266) will improve geographic query performance for city-based filtering and visit timestamp lookups.

Also applies to: 83-104, 288-382, 486-490, 515-530

Comment on lines +1 to +205
RailsPulse.configure do |config|
# ====================================================================================================
# GLOBAL CONFIGURATION
# ====================================================================================================

# Enable or disable Rails Pulse
config.enabled = true

# ====================================================================================================
# THRESHOLDS
# ====================================================================================================
# These thresholds are used to determine if a route, request, or query is slow, very slow, or critical.
# Values are in milliseconds (ms). Adjust these based on your application's performance requirements.

# Thresholds for an individual route
config.route_thresholds = {
slow: 500,
very_slow: 1500,
critical: 3000
}

# Thresholds for an individual request
config.request_thresholds = {
slow: 700,
very_slow: 2000,
critical: 4000
}

# Thresholds for an individual database query
config.query_thresholds = {
slow: 100,
very_slow: 500,
critical: 1000
}

# ====================================================================================================
# FILTERING
# ====================================================================================================

# Asset Tracking Configuration
# By default, Rails Pulse ignores asset requests (images, CSS, JS files) to focus on application performance.
# Set track_assets to true if you want to monitor asset delivery performance.
config.track_assets = false

# Custom asset patterns to ignore (in addition to the built-in defaults)
# Only applies when track_assets is false. Add patterns for app-specific asset paths.
config.custom_asset_patterns = [
# Example: ignore specific asset directories
# %r{^/uploads/},
# %r{^/media/},
# "/special-assets/"
]

# Rails Pulse Mount Path (optional)
# If Rails Pulse is mounted at a custom path, specify it here to prevent
# Rails Pulse from tracking its own requests. Leave as nil for default '/rails_pulse'.
# Examples:
# config.mount_path = "/admin/monitoring"
config.mount_path = nil

# Manual route filtering
# Specify additional routes, requests, or queries to ignore from performance tracking.
# Each array can include strings (exact matches) or regular expressions.
#
# Examples:
# config.ignored_routes = ["/health_check", %r{^/admin}]
# config.ignored_requests = ["GET /status", %r{POST /api/v1/.*}]
# config.ignored_queries = ["SELECT 1", %r{FROM \"schema_migrations\"}]

config.ignored_routes = []
config.ignored_requests = []
config.ignored_queries = []

# ====================================================================================================
# TAGGING
# ====================================================================================================
# Define custom tags for categorizing routes, requests, and queries.
# You can add any custom tags you want for filtering and organization.
#
# Tag names should be in present tense and describe the current state or category.
# Examples of good tag names:
# - "critical" (for high-priority endpoints)
# - "experimental" (for routes under development)
# - "deprecated" (for routes being phased out)
# - "external" (for third-party API calls)
# - "background" (for async job-related operations)
# - "admin" (for administrative routes)
# - "public" (for public-facing routes)
#
# Example configuration:
# config.tags = ["ignored", "critical", "experimental", "deprecated", "external", "admin"]

config.tags = %w[ignored critical experimental]

# ====================================================================================================
# DATABASE CONFIGURATION
# ====================================================================================================
# Configure Rails Pulse to use a separate database for performance monitoring data.
# This is optional but recommended for production applications to isolate performance
# data from your main application database.
#
# Uncomment and configure one of the following patterns:

# Option 1: Separate single database for Rails Pulse
# config.connects_to = {
# database: { writing: :rails_pulse, reading: :rails_pulse }
# }

# Option 2: Primary/replica configuration for Rails Pulse
# config.connects_to = {
# database: { writing: :rails_pulse_primary, reading: :rails_pulse_replica }
# }

# Don't forget to add the database configuration to config/database.yml:
#
# production:
# # ... your main database config ...
# rails_pulse:
# adapter: postgresql # or mysql2, sqlite3
# database: myapp_rails_pulse_production
# username: rails_pulse_user
# password: <%= Rails.application.credentials.dig(:rails_pulse, :database_password) %>
# host: localhost
# pool: 5

# ====================================================================================================
# AUTHENTICATION
# ====================================================================================================
# Configure authentication to secure access to the Rails Pulse dashboard.
# Authentication is ENABLED BY DEFAULT in production environments for security.
#
# If no authentication method is configured, Rails Pulse will use HTTP Basic Auth
# with credentials from RAILS_PULSE_USERNAME (default: 'admin') and RAILS_PULSE_PASSWORD
# environment variables. Set RAILS_PULSE_PASSWORD to enable this fallback.
#
# Uncomment and configure one of the following patterns based on your authentication system:

# Enable/disable authentication (enabled by default in production)
config.authentication_enabled = true

# Where to redirect unauthorized users
config.authentication_redirect_path = '/'

# Custom authentication method - choose one of the examples below:

# Example 1: Devise with admin role check
# config.authentication_method = proc {
# redirect_to main_app.root_path, alert: 'Access denied' unless user_signed_in? && current_user.admin?
# }

# Example 2: Custom session-based authentication
# config.authentication_method = proc {
# unless session[:user_id] && User.find_by(id: session[:user_id])&.admin?
# redirect_to main_app.login_path, alert: "Please log in as an admin"
# end
# }

# Example 3: Warden authentication
# config.authentication_method = proc {
# warden.authenticate!(:scope => :admin)
# }

# Example 4: Basic HTTP authentication
config.authentication_method = proc {
authenticate_or_request_with_http_basic do |username, password|
username == ENV['RAILS_PULSE_USERNAME'] && password == ENV['RAILS_PULSE_PASSWORD']
end
}

# Example 5: Custom authorization check
# config.authentication_method = proc {
# current_user = User.find_by(id: session[:user_id])
# unless current_user&.can_access_rails_pulse?
# render plain: "Forbidden", status: :forbidden
# end
# }

# ====================================================================================================
# DATA CLEANUP
# ====================================================================================================
# Configure automatic cleanup of old performance data to manage database size.
# Rails Pulse provides two cleanup mechanisms that work together:
#
# 1. Time-based cleanup: Delete records older than the retention period
# 2. Count-based cleanup: Keep only the specified number of records per table
#
# Cleanup order respects foreign key constraints:
# operations → requests → queries/routes

# Enable or disable automatic data cleanup
config.archiving_enabled = true

# Time-based retention - delete records older than this period
config.full_retention_period = 2.weeks

# Count-based retention - maximum records to keep per table
# After time-based cleanup, if tables still exceed these limits,
# the oldest remaining records will be deleted to stay under the limit
config.max_table_records = {
rails_pulse_requests: 10_000, # HTTP requests (moderate volume)
rails_pulse_operations: 50_000, # Operations within requests (high volume)
rails_pulse_routes: 1000, # Unique routes (low volume)
rails_pulse_queries: 500 # Normalized SQL queries (low volume)
}
end
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for documentation about RAILS_PULSE environment variables
echo "=== Searching for RailsPulse documentation files ==="
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "*.rdoc" \) | head -20

echo -e "\n=== Checking for mentions of RAILS_PULSE environment variables ==="
rg "RAILS_PULSE_(USERNAME|PASSWORD)" --type md --type txt 2>/dev/null | head -20

echo -e "\n=== Searching for .env example files ==="
fd -e example -e sample | grep -E "(env|config)" | head -20

echo -e "\n=== Checking if .env.example exists ==="
fd "\.env" . | head -20

echo -e "\n=== Searching for authentication documentation in code comments ==="
rg -B2 -A2 "RAILS_PULSE_PASSWORD" . 2>/dev/null | head -40

Repository: Freika/dawarich

Length of output: 1747


🏁 Script executed:

#!/bin/bash
# Check the actual behavior when ENV vars are missing
echo "=== Examining the initializer to understand ENV var handling ==="
sed -n '160,175p' config/initializers/rails_pulse.rb

echo -e "\n=== Checking if there are any defaults set elsewhere ==="
rg "RAILS_PULSE_USERNAME|RAILS_PULSE_PASSWORD" . -t rb --type-list 2>/dev/null | grep -v "config/initializers"

echo -e "\n=== Looking for setup or installation documentation ==="
find . -type f -name "*.md" | xargs grep -l "setup\|install\|authentication" 2>/dev/null | head -10

Repository: Freika/dawarich

Length of output: 1068


Add deployment documentation for RailsPulse authentication environment variables.

The configuration lacks documentation in project setup guides. No mention of RAILS_PULSE_USERNAME and RAILS_PULSE_PASSWORD exists in README.md, DEVELOPMENT.md, or deployment docs, yet these variables are required for dashboard access. Additionally, the initializer comment at line 146 is misleading—it mentions "default: 'admin'" but the code implements no actual default; if RAILS_PULSE_PASSWORD is not set, authentication will fail with a 401 prompt.

Add setup guidance (e.g., in DEVELOPMENT.md or a .env.example) documenting:

  • Both environment variables are required when authentication_enabled = true
  • Example values for development environments
  • Production deployment instructions for securely setting credentials
🧰 Tools
🪛 RuboCop (1.82.0)

[convention] 1-1: Missing frozen string literal comment.

(Style/FrozenStringLiteralComment)

🤖 Prompt for AI Agents
In config/initializers/rails_pulse.rb around line 146 and in project docs, the
initializer mentions a "default: 'admin'" for RAILS_PULSE_USERNAME but no
default is implemented and there is no project documentation for
RAILS_PULSE_USERNAME/RAILS_PULSE_PASSWORD; add deployment/setup docs and fix the
comment: update README.md or DEVELOPMENT.md to state that when
config.authentication_enabled = true both RAILS_PULSE_USERNAME and
RAILS_PULSE_PASSWORD are required, show example .env entries (e.g.
RAILS_PULSE_USERNAME=admin, RAILS_PULSE_PASSWORD=changeme) for local/dev, add a
.env.example entry, provide production guidance to set credentials securely
(Rails encrypted credentials / secrets manager / environment variables via the
deploy system), and either remove or correct the misleading "default: 'admin'"
comment in the initializer (or implement a safe default fallback) so the code
and docs match.

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