From 90efb5b0bbafcc3163a11241559b374f8f78c7cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:30:16 +0000 Subject: [PATCH 01/16] Bump super_diff from 0.15.0 to 0.16.0 Bumps [super_diff](https://github.com/splitwise/super_diff) from 0.15.0 to 0.16.0. - [Release notes](https://github.com/splitwise/super_diff/releases) - [Changelog](https://github.com/splitwise/super_diff/blob/main/CHANGELOG.md) - [Commits](https://github.com/splitwise/super_diff/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: super_diff dependency-version: 0.16.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 640e815e..fda14e56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,7 +269,7 @@ GEM oj (3.16.11) bigdecimal (>= 3.0) ostruct (>= 0.2) - optimist (3.2.0) + optimist (3.2.1) orm_adapter (0.5.0) ostruct (0.6.1) parallel (1.27.0) @@ -462,7 +462,7 @@ GEM stringio (3.1.7) strong_migrations (2.3.0) activerecord (>= 7) - super_diff (0.15.0) + super_diff (0.16.0) attr_extras (>= 6.2.4) diff-lcs patience_diff From d0aaa3c6748850e819f0390834cbf6c7416be019 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:31:00 +0000 Subject: [PATCH 02/16] Bump debug from 1.10.0 to 1.11.0 Bumps [debug](https://github.com/ruby/debug) from 1.10.0 to 1.11.0. - [Release notes](https://github.com/ruby/debug/releases) - [Commits](https://github.com/ruby/debug/compare/v1.10.0...v1.11.0) --- updated-dependencies: - dependency-name: debug dependency-version: 1.11.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 640e815e..8ed965b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -144,7 +144,7 @@ GEM database_consistency (2.0.4) activerecord (>= 3.2) date (3.4.1) - debug (1.10.0) + debug (1.11.0) irb (~> 1.10) reline (>= 0.3.8) devise (4.9.4) @@ -342,7 +342,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.0) - rdoc (6.14.1) + rdoc (6.14.2) erb psych (>= 4.0.0) redis (5.4.0) From 4e35cdd305b359d86d962fd5d9778ffa7d528f86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:47:02 +0000 Subject: [PATCH 03/16] Bump chartkick from 5.1.5 to 5.2.0 Bumps [chartkick](https://github.com/ankane/chartkick) from 5.1.5 to 5.2.0. - [Changelog](https://github.com/ankane/chartkick/blob/master/CHANGELOG.md) - [Commits](https://github.com/ankane/chartkick/compare/v5.1.5...v5.2.0) --- updated-dependencies: - dependency-name: chartkick dependency-version: 5.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 640e815e..77b1ec20 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,7 +126,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - chartkick (5.1.5) + chartkick (5.2.0) coderay (1.1.3) concurrent-ruby (1.3.5) connection_pool (2.5.3) From 7afc399724fa0599f2970afba94d5c34e68b13fd Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 21 Jul 2025 22:27:20 +0200 Subject: [PATCH 04/16] Add cache to points limit exceeded check --- .app_version | 2 +- CHANGELOG.md | 8 +++++++- app/services/points_limit_exceeded.rb | 9 +++++++-- spec/services/points_limit_exceeded_spec.rb | 5 +++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.app_version b/.app_version index c25c8e5b..1a44cad7 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.30.0 +0.30.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e12393..04d1a143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +# [0.30.1] - 2025-07-21 + +## Fixed + +- Points limit exceeded check is now cached. + # [0.30.0] - 2025-07-21 -⚠️ If you were using RC, please run the following commands in the console, otherwise read on. ⚠️ +⚠️ If you were using 0.29.2 RC, please run the following commands in the console, otherwise read on. ⚠️ ```ruby # This will delete all tracks 👇 diff --git a/app/services/points_limit_exceeded.rb b/app/services/points_limit_exceeded.rb index f47543d1..2bf8de8a 100644 --- a/app/services/points_limit_exceeded.rb +++ b/app/services/points_limit_exceeded.rb @@ -7,13 +7,18 @@ class PointsLimitExceeded def call return false if DawarichSettings.self_hosted? - return true if @user.tracked_points.count >= points_limit - false + Rails.cache.fetch(cache_key, expires_in: 1.day) do + @user.tracked_points.count >= points_limit + end end private + def cache_key + "points_limit_exceeded/#{@user.id}" + end + def points_limit DawarichSettings::BASIC_PAID_PLAN_LIMIT end diff --git a/spec/services/points_limit_exceeded_spec.rb b/spec/services/points_limit_exceeded_spec.rb index 88cd6268..fed8a880 100644 --- a/spec/services/points_limit_exceeded_spec.rb +++ b/spec/services/points_limit_exceeded_spec.rb @@ -28,6 +28,11 @@ RSpec.describe PointsLimitExceeded do end it { is_expected.to be true } + + it 'caches the result' do + expect(user.tracked_points).to receive(:count).once + 2.times { described_class.new(user).call } + end end context 'when user points count exceeds the limit' do From c18b09181ea5d6a6a348332c04ae39ba19d6cd07 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 21 Jul 2025 22:45:57 +0200 Subject: [PATCH 05/16] Add index on places geodata osm id --- CHANGELOG.md | 1 + ...0250721204404_add_index_on_places_geodata_osm_id.rb | 10 ++++++++++ db/schema.rb | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d1a143..0c244831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Fixed - Points limit exceeded check is now cached. +- Reverse geocoding for places is now significantly faster. # [0.30.0] - 2025-07-21 diff --git a/db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb b/db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb new file mode 100644 index 00000000..83359ec4 --- /dev/null +++ b/db/migrate/20250721204404_add_index_on_places_geodata_osm_id.rb @@ -0,0 +1,10 @@ +class AddIndexOnPlacesGeodataOsmId < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + add_index :places, "(geodata->'properties'->>'osm_id')", + using: :btree, + name: 'index_places_on_geodata_osm_id', + algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 837c0927..402729b9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do +ActiveRecord::Schema[8.0].define(version: 2025_07_21_204404) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "postgis" @@ -77,6 +77,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do t.index ["name"], name: "index_countries_on_name" end + create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t| + end + create_table "exports", force: :cascade do |t| t.string "name", null: false t.string "url" @@ -143,6 +146,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.geography "lonlat", limit: {srid: 4326, type: "st_point", geographic: true} + t.index "(((geodata -> 'properties'::text) ->> 'osm_id'::text))", name: "index_places_on_geodata_osm_id" t.index ["lonlat"], name: "index_places_on_lonlat", using: :gist end From 97d6037448f5c8fc8564eccc0b0d6c97829b92b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 05:39:51 +0000 Subject: [PATCH 06/16] Bump the bundler group with 2 updates Bumps the bundler group with 2 updates: [nokogiri](https://github.com/sparklemotion/nokogiri) and [thor](https://github.com/rails/thor). Updates `nokogiri` from 1.18.8 to 1.18.9 - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.8...v1.18.9) Updates `thor` from 1.3.2 to 1.4.0 - [Release notes](https://github.com/rails/thor/releases) - [Commits](https://github.com/rails/thor/compare/v1.3.2...v1.4.0) --- updated-dependencies: - dependency-name: nokogiri dependency-version: 1.18.9 dependency-type: indirect dependency-group: bundler - dependency-name: thor dependency-version: 1.4.0 dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 640e815e..e566ce3e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -253,18 +253,18 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.8) + nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-gnu) + nokogiri (1.18.9-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-arm-linux-gnu) + nokogiri (1.18.9-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) + nokogiri (1.18.9-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) + nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) + nokogiri (1.18.9-x86_64-linux-gnu) racc (~> 1.4) oj (3.16.11) bigdecimal (>= 3.0) @@ -475,7 +475,7 @@ GEM tailwindcss-ruby (3.4.17-arm64-darwin) tailwindcss-ruby (3.4.17-x86_64-darwin) tailwindcss-ruby (3.4.17-x86_64-linux) - thor (1.3.2) + thor (1.4.0) timeout (0.4.3) turbo-rails (2.0.16) actionpack (>= 7.1.0) From 88909b3e9fdefbad417daf0d96ecf17f00995c2c Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 19:17:28 +0200 Subject: [PATCH 07/16] Optimize stats page performance --- Gemfile | 1 + Gemfile.lock | 5 ++++ app/controllers/stats_controller.rb | 28 +++++++++++++++++++---- app/jobs/trips/calculate_all_job.rb | 2 +- app/jobs/trips/calculate_countries_job.rb | 2 +- app/jobs/trips/calculate_distance_job.rb | 2 +- app/jobs/trips/calculate_path_job.rb | 2 +- app/models/country.rb | 4 +++- app/views/stats/_year.html.erb | 2 +- app/views/stats/index.html.erb | 2 +- config/environments/development.rb | 9 ++++++++ config/environments/production.rb | 2 +- config/environments/test.rb | 6 +++++ config/sidekiq.yml | 1 + 14 files changed, 56 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index 614a2e95..0e11a1fa 100644 --- a/Gemfile +++ b/Gemfile @@ -77,4 +77,5 @@ group :development do gem 'database_consistency', require: false gem 'foreman' gem 'rubocop-rails', require: false + gem 'bullet' end diff --git a/Gemfile.lock b/Gemfile.lock index 640e815e..7d65bab2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,6 +113,9 @@ GEM brakeman (7.0.2) racc builder (3.3.0) + bullet (8.0.8) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -486,6 +489,7 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + uniform_notifier (1.17.0) uri (1.0.3) useragent (0.16.11) warden (1.2.9) @@ -519,6 +523,7 @@ DEPENDENCIES aws-sdk-s3 (~> 1.177.0) bootsnap brakeman + bullet bundler-audit capybara chartkick diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index a456452f..08074462 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -5,10 +5,30 @@ class StatsController < ApplicationController before_action :authenticate_active_user!, only: %i[update update_all] def index - @stats = current_user.stats.group_by(&:year).transform_values { |stats| stats.sort_by(&:updated_at).reverse }.sort.reverse - @points_total = current_user.tracked_points.count - @points_reverse_geocoded = current_user.total_reverse_geocoded_points - @points_reverse_geocoded_without_data = current_user.total_reverse_geocoded_points_without_data + @stats = current_user.stats.group_by(&:year).transform_values do |stats| + stats.sort_by(&:updated_at).reverse + end.sort.reverse + + # Single aggregated query to replace 3 separate COUNT queries + result = current_user.tracked_points.connection.execute(<<~SQL.squish) + SELECT#{' '} + COUNT(*) as total, + COUNT(reverse_geocoded_at) as geocoded, + COUNT(CASE WHEN geodata = '{}' THEN 1 END) as without_data + FROM points#{' '} + WHERE user_id = #{current_user.id} + SQL + + row = result.first + @points_total = row['total'].to_i + @points_reverse_geocoded = row['geocoded'].to_i + @points_reverse_geocoded_without_data = row['without_data'].to_i + + # Precompute year distance data to avoid N+1 queries in view + @year_distances = {} + @stats.each do |year, _stats| + @year_distances[year] = Stat.year_distance(year, current_user) + end end def show diff --git a/app/jobs/trips/calculate_all_job.rb b/app/jobs/trips/calculate_all_job.rb index 0500881c..3710df3e 100644 --- a/app/jobs/trips/calculate_all_job.rb +++ b/app/jobs/trips/calculate_all_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculateAllJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id, distance_unit = 'km') Trips::CalculatePathJob.perform_later(trip_id) diff --git a/app/jobs/trips/calculate_countries_job.rb b/app/jobs/trips/calculate_countries_job.rb index e63365d3..ed5ee884 100644 --- a/app/jobs/trips/calculate_countries_job.rb +++ b/app/jobs/trips/calculate_countries_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculateCountriesJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id, distance_unit) trip = Trip.find(trip_id) diff --git a/app/jobs/trips/calculate_distance_job.rb b/app/jobs/trips/calculate_distance_job.rb index 8a28e06f..15ff83c4 100644 --- a/app/jobs/trips/calculate_distance_job.rb +++ b/app/jobs/trips/calculate_distance_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculateDistanceJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id, distance_unit) trip = Trip.find(trip_id) diff --git a/app/jobs/trips/calculate_path_job.rb b/app/jobs/trips/calculate_path_job.rb index 711cfef8..f1323c5f 100644 --- a/app/jobs/trips/calculate_path_job.rb +++ b/app/jobs/trips/calculate_path_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Trips::CalculatePathJob < ApplicationJob - queue_as :default + queue_as :trips def perform(trip_id) trip = Trip.find(trip_id) diff --git a/app/models/country.rb b/app/models/country.rb index 9ef64687..e40a0c6d 100644 --- a/app/models/country.rb +++ b/app/models/country.rb @@ -12,6 +12,8 @@ class Country < ApplicationRecord end def self.names_to_iso_a2 - pluck(:name, :iso_a2).to_h + Rails.cache.fetch('countries_names_to_iso_a2', expires_in: 1.day) do + pluck(:name, :iso_a2).to_h + end end end diff --git a/app/views/stats/_year.html.erb b/app/views/stats/_year.html.erb index 886e2c96..3d8989b8 100644 --- a/app/views/stats/_year.html.erb +++ b/app/views/stats/_year.html.erb @@ -4,7 +4,7 @@
<%= column_chart( - Stat.year_distance(year, current_user), + @year_distances[year], height: '200px', suffix: " #{current_user.safe_settings.distance_unit}", xtitle: 'Days', diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index 96050095..2f14fa36 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -82,7 +82,7 @@
<% end %> <%= column_chart( - Stat.year_distance(year, current_user).map { |month_name, distance_meters| + @year_distances[year].map { |month_name, distance_meters| [month_name, Stat.convert_distance(distance_meters, current_user.safe_settings.distance_unit).round(2)] }, height: '200px', diff --git a/config/environments/development.rb b/config/environments/development.rb index c940de0e..36cb4be1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -3,6 +3,15 @@ require 'active_support/core_ext/integer/time' Rails.application.configure do + config.after_initialize do + Bullet.enable = true + Bullet.alert = true + Bullet.bullet_logger = true + Bullet.console = true + Bullet.rails_logger = true + Bullet.add_footer = true + end + # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time diff --git a/config/environments/production.rb b/config/environments/production.rb index 1e4b392a..53eedb18 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -29,7 +29,7 @@ Rails.application.configure do # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = true + config.assets.compile = false config.assets.content_type = { geojson: 'application/geo+json' diff --git a/config/environments/test.rb b/config/environments/test.rb index e138d076..b4884952 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,6 +8,12 @@ require 'active_support/core_ext/integer/time' # and recreated between test runs. Don't rely on the data there! Rails.application.configure do + config.after_initialize do + Bullet.enable = true + Bullet.bullet_logger = true + Bullet.raise = true # raise an error if n+1 query occurs + end + # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 87109364..c1966a7f 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -6,6 +6,7 @@ - imports - exports - stats + - trips - tracks - reverse_geocoding - visit_suggesting From da38c128196e38d8d65f09118145b8fffb44ce96 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 19:43:27 +0200 Subject: [PATCH 08/16] Extract stats query --- Gemfile | 1 - Gemfile.lock | 26 +++--- app/controllers/stats_controller.rb | 53 +++++++----- app/queries/stats_query.rb | 30 +++++++ config/environments/development.rb | 9 -- config/environments/test.rb | 6 -- spec/queries/stats_query_spec.rb | 130 ++++++++++++++++++++++++++++ 7 files changed, 202 insertions(+), 53 deletions(-) create mode 100644 app/queries/stats_query.rb create mode 100644 spec/queries/stats_query_spec.rb diff --git a/Gemfile b/Gemfile index 0e11a1fa..614a2e95 100644 --- a/Gemfile +++ b/Gemfile @@ -77,5 +77,4 @@ group :development do gem 'database_consistency', require: false gem 'foreman' gem 'rubocop-rails', require: false - gem 'bullet' end diff --git a/Gemfile.lock b/Gemfile.lock index 7d65bab2..f647ff71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -163,7 +163,7 @@ GEM dotenv (= 3.1.8) railties (>= 6.1) drb (2.2.3) - erb (5.0.1) + erb (5.0.2) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -197,7 +197,7 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.8.0) + io-console (0.8.1) irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -246,7 +246,7 @@ GEM multi_json (1.15.0) multi_xml (0.7.1) bigdecimal (~> 3.1) - net-imap (0.5.8) + net-imap (0.5.9) date net-protocol net-pop (0.1.2) @@ -256,18 +256,18 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.8) + nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-gnu) + nokogiri (1.18.9-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-arm-linux-gnu) + nokogiri (1.18.9-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) + nokogiri (1.18.9-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) + nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) + nokogiri (1.18.9-x86_64-linux-gnu) racc (~> 1.4) oj (3.16.11) bigdecimal (>= 3.0) @@ -345,7 +345,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.0) - rdoc (6.14.1) + rdoc (6.14.2) erb psych (>= 4.0.0) redis (5.4.0) @@ -353,7 +353,7 @@ GEM redis-client (0.24.0) connection_pool regexp_parser (2.10.0) - reline (0.6.1) + reline (0.6.2) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) @@ -478,7 +478,7 @@ GEM tailwindcss-ruby (3.4.17-arm64-darwin) tailwindcss-ruby (3.4.17-x86_64-darwin) tailwindcss-ruby (3.4.17-x86_64-linux) - thor (1.3.2) + thor (1.4.0) timeout (0.4.3) turbo-rails (2.0.16) actionpack (>= 7.1.0) @@ -500,7 +500,7 @@ GEM hashdiff (>= 0.4.0, < 2.0.0) webrick (1.9.1) websocket (1.2.11) - websocket-driver (0.7.7) + websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 08074462..0300deae 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -5,30 +5,9 @@ class StatsController < ApplicationController before_action :authenticate_active_user!, only: %i[update update_all] def index - @stats = current_user.stats.group_by(&:year).transform_values do |stats| - stats.sort_by(&:updated_at).reverse - end.sort.reverse - - # Single aggregated query to replace 3 separate COUNT queries - result = current_user.tracked_points.connection.execute(<<~SQL.squish) - SELECT#{' '} - COUNT(*) as total, - COUNT(reverse_geocoded_at) as geocoded, - COUNT(CASE WHEN geodata = '{}' THEN 1 END) as without_data - FROM points#{' '} - WHERE user_id = #{current_user.id} - SQL - - row = result.first - @points_total = row['total'].to_i - @points_reverse_geocoded = row['geocoded'].to_i - @points_reverse_geocoded_without_data = row['without_data'].to_i - - # Precompute year distance data to avoid N+1 queries in view - @year_distances = {} - @stats.each do |year, _stats| - @year_distances[year] = Stat.year_distance(year, current_user) - end + @stats = build_stats + assign_points_statistics + @year_distances = precompute_year_distances end def show @@ -63,4 +42,30 @@ class StatsController < ApplicationController redirect_to stats_path, notice: 'Stats are being updated', status: :see_other end + + private + + def assign_points_statistics + points_stats = ::StatsQuery.new(current_user).points_stats + + @points_total = points_stats[:total] + @points_reverse_geocoded = points_stats[:geocoded] + @points_reverse_geocoded_without_data = points_stats[:without_data] + end + + def precompute_year_distances + year_distances = {} + + @stats.each do |year, _stats| + year_distances[year] = Stat.year_distance(year, current_user) + end + + year_distances + end + + def build_stats + current_user.stats.group_by(&:year).transform_values do |stats| + stats.sort_by(&:updated_at).reverse + end.sort.reverse + end end diff --git a/app/queries/stats_query.rb b/app/queries/stats_query.rb new file mode 100644 index 00000000..e3ef3503 --- /dev/null +++ b/app/queries/stats_query.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class StatsQuery + def initialize(user) + @user = user + end + + def points_stats + result = user.tracked_points.connection.execute(<<~SQL.squish) + SELECT#{' '} + COUNT(*) as total, + COUNT(reverse_geocoded_at) as geocoded, + COUNT(CASE WHEN geodata = '{}' THEN 1 END) as without_data + FROM points#{' '} + WHERE user_id = #{user.id} + SQL + + row = result.first + + { + total: row['total'].to_i, + geocoded: row['geocoded'].to_i, + without_data: row['without_data'].to_i + } + end + + private + + attr_reader :user +end diff --git a/config/environments/development.rb b/config/environments/development.rb index 36cb4be1..c940de0e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -3,15 +3,6 @@ require 'active_support/core_ext/integer/time' Rails.application.configure do - config.after_initialize do - Bullet.enable = true - Bullet.alert = true - Bullet.bullet_logger = true - Bullet.console = true - Bullet.rails_logger = true - Bullet.add_footer = true - end - # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time diff --git a/config/environments/test.rb b/config/environments/test.rb index b4884952..e138d076 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,12 +8,6 @@ require 'active_support/core_ext/integer/time' # and recreated between test runs. Don't rely on the data there! Rails.application.configure do - config.after_initialize do - Bullet.enable = true - Bullet.bullet_logger = true - Bullet.raise = true # raise an error if n+1 query occurs - end - # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. diff --git a/spec/queries/stats_query_spec.rb b/spec/queries/stats_query_spec.rb new file mode 100644 index 00000000..d4d8517f --- /dev/null +++ b/spec/queries/stats_query_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe StatsQuery do + describe '#points_stats' do + subject(:points_stats) { described_class.new(user).points_stats } + + let(:user) { create(:user) } + let!(:import) { create(:import, user: user) } + + context 'when user has no points' do + it 'returns zero counts for all statistics' do + expect(points_stats).to eq({ + total: 0, + geocoded: 0, + without_data: 0 + }) + end + end + + context 'when user has points' do + let!(:geocoded_point_with_data) do + create(:point, + user: user, + import: import, + reverse_geocoded_at: Time.current, + geodata: { 'address' => '123 Main St' }) + end + + let!(:geocoded_point_without_data) do + create(:point, + user: user, + import: import, + reverse_geocoded_at: Time.current, + geodata: {}) + end + + let!(:non_geocoded_point) do + create(:point, + user: user, + import: import, + reverse_geocoded_at: nil, + geodata: { 'some' => 'data' }) + end + + it 'returns correct counts for all statistics' do + expect(points_stats).to eq({ + total: 3, + geocoded: 2, + without_data: 1 + }) + end + + context 'when another user has points' do + let(:other_user) { create(:user) } + let!(:other_import) { create(:import, user: other_user) } + let!(:other_point) do + create(:point, + user: other_user, + import: other_import, + reverse_geocoded_at: Time.current, + geodata: { 'address' => 'Other Address' }) + end + + it 'only counts points for the specified user' do + expect(points_stats).to eq({ + total: 3, + geocoded: 2, + without_data: 1 + }) + end + end + end + + context 'when all points are geocoded with data' do + before do + create_list(:point, 5, + user: user, + import: import, + reverse_geocoded_at: Time.current, + geodata: { 'address' => 'Some Address' }) + end + + it 'returns correct statistics' do + expect(points_stats).to eq({ + total: 5, + geocoded: 5, + without_data: 0 + }) + end + end + + context 'when all points are without geodata' do + before do + create_list(:point, 3, + user: user, + import: import, + reverse_geocoded_at: Time.current, + geodata: {}) + end + + it 'returns correct statistics' do + expect(points_stats).to eq({ + total: 3, + geocoded: 3, + without_data: 3 + }) + end + end + + context 'when all points are not geocoded' do + before do + create_list(:point, 4, + user: user, + import: import, + reverse_geocoded_at: nil, + geodata: { 'some' => 'data' }) + end + + it 'returns correct statistics' do + expect(points_stats).to eq({ + total: 4, + geocoded: 0, + without_data: 0 + }) + end + end + end +end \ No newline at end of file From 58ffca74f61b7af8d0541ec1ed52dd6bc96ffbcd Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 19:44:50 +0200 Subject: [PATCH 09/16] Remove bullet --- Gemfile.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f647ff71..3beeac65 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,9 +113,6 @@ GEM brakeman (7.0.2) racc builder (3.3.0) - bullet (8.0.8) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) bundler-audit (0.9.2) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -489,7 +486,6 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) - uniform_notifier (1.17.0) uri (1.0.3) useragent (0.16.11) warden (1.2.9) @@ -523,7 +519,6 @@ DEPENDENCIES aws-sdk-s3 (~> 1.177.0) bootsnap brakeman - bullet bundler-audit capybara chartkick From 6e5dd4bed6040f9747655c4f2b57b78eb1b7ec42 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 19:52:24 +0200 Subject: [PATCH 10/16] Update stats query --- app/queries/stats_query.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/queries/stats_query.rb b/app/queries/stats_query.rb index e3ef3503..ad7ef90d 100644 --- a/app/queries/stats_query.rb +++ b/app/queries/stats_query.rb @@ -6,12 +6,12 @@ class StatsQuery end def points_stats - result = user.tracked_points.connection.execute(<<~SQL.squish) - SELECT#{' '} - COUNT(*) as total, + result = Point.connection.execute(<<~SQL.squish) + SELECT + COUNT(id) as total, COUNT(reverse_geocoded_at) as geocoded, - COUNT(CASE WHEN geodata = '{}' THEN 1 END) as without_data - FROM points#{' '} + COUNT(CASE WHEN geodata = '{}'::jsonb THEN 1 END) as without_data + FROM points WHERE user_id = #{user.id} SQL From cbdef5fa43286858d3cf3d55a18719cc81c962dc Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 19:56:12 +0200 Subject: [PATCH 11/16] Parameterize stats query --- app/queries/stats_query.rb | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/queries/stats_query.rb b/app/queries/stats_query.rb index ad7ef90d..0192a8c8 100644 --- a/app/queries/stats_query.rb +++ b/app/queries/stats_query.rb @@ -6,21 +6,24 @@ class StatsQuery end def points_stats - result = Point.connection.execute(<<~SQL.squish) - SELECT - COUNT(id) as total, - COUNT(reverse_geocoded_at) as geocoded, - COUNT(CASE WHEN geodata = '{}'::jsonb THEN 1 END) as without_data - FROM points - WHERE user_id = #{user.id} - SQL + sql = ActiveRecord::Base.sanitize_sql_array([ + <<~SQL.squish, + SELECT + COUNT(id) as total, + COUNT(reverse_geocoded_at) as geocoded, + COUNT(CASE WHEN geodata = '{}'::jsonb THEN 1 END) as without_data + FROM points + WHERE user_id = ? + SQL + user.id + ]) - row = result.first + result = Point.connection.select_one(sql) { - total: row['total'].to_i, - geocoded: row['geocoded'].to_i, - without_data: row['without_data'].to_i + total: result['total'].to_i, + geocoded: result['geocoded'].to_i, + without_data: result['without_data'].to_i } end From c14054fdc3fed4b09849b7bcc1e24a8259fb3c6a Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 20:15:52 +0200 Subject: [PATCH 12/16] Disable track generation failure notification for self-hosted users --- app/jobs/tracks/create_job.rb | 2 ++ spec/jobs/tracks/create_job_spec.rb | 46 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/app/jobs/tracks/create_job.rb b/app/jobs/tracks/create_job.rb index 919e5f82..a65805c4 100644 --- a/app/jobs/tracks/create_job.rb +++ b/app/jobs/tracks/create_job.rb @@ -27,6 +27,8 @@ class Tracks::CreateJob < ApplicationJob end def create_error_notification(user, error) + return unless DawarichSettings.self_hosted? + Notifications::Create.new( user: user, kind: :error, diff --git a/spec/jobs/tracks/create_job_spec.rb b/spec/jobs/tracks/create_job_spec.rb index bc2648d9..bddf430a 100644 --- a/spec/jobs/tracks/create_job_spec.rb +++ b/spec/jobs/tracks/create_job_spec.rb @@ -151,4 +151,50 @@ RSpec.describe Tracks::CreateJob, type: :job do expect(described_class.new.queue_name).to eq('tracks') end end + + context 'when self-hosted' do + let(:generator_instance) { instance_double(Tracks::Generator) } + let(:notification_service) { instance_double(Notifications::Create) } + let(:error_message) { 'Something went wrong' } + + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(true) + allow(Tracks::Generator).to receive(:new).and_return(generator_instance) + allow(generator_instance).to receive(:call).and_raise(StandardError, error_message) + allow(Notifications::Create).to receive(:new).and_return(notification_service) + allow(notification_service).to receive(:call) + end + + it 'creates a failure notification when self-hosted' do + described_class.new.perform(user.id) + + expect(Notifications::Create).to have_received(:new).with( + user: user, + kind: :error, + title: 'Track Generation Failed', + content: "Failed to generate tracks from your location data: #{error_message}" + ) + expect(notification_service).to have_received(:call) + end + end + + context 'when not self-hosted' do + let(:generator_instance) { instance_double(Tracks::Generator) } + let(:notification_service) { instance_double(Notifications::Create) } + let(:error_message) { 'Something went wrong' } + + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) + allow(Tracks::Generator).to receive(:new).and_return(generator_instance) + allow(generator_instance).to receive(:call).and_raise(StandardError, error_message) + allow(Notifications::Create).to receive(:new).and_return(notification_service) + allow(notification_service).to receive(:call) + end + + it 'does not create a failure notification' do + described_class.new.perform(user.id) + + expect(notification_service).not_to have_received(:call) + end + end end From 7c8a7e7f384004ea199f3ccfb645ddd1c81c3576 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 20:25:44 +0200 Subject: [PATCH 13/16] Temporary disable track creation --- CHANGELOG.md | 7 ++++++- app/models/point.rb | 2 +- config/schedule.yml | 8 ++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c244831..02ab8cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -# [0.30.1] - 2025-07-21 +# [0.30.1] - 2025-07-22 ## Fixed - Points limit exceeded check is now cached. - Reverse geocoding for places is now significantly faster. +## Changed + +- Stats page should load faster now. +- Track creation is temporarily disabled. + # [0.30.0] - 2025-07-21 diff --git a/app/models/point.rb b/app/models/point.rb index 75566be3..96e9e68a 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -34,7 +34,7 @@ class Point < ApplicationRecord after_create :set_country after_create_commit :broadcast_coordinates after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? } - after_commit :recalculate_track, on: :update, if: -> { track.present? } + # after_commit :recalculate_track, on: :update, if: -> { track.present? } def self.without_raw_data select(column_names - ['raw_data']) diff --git a/config/schedule.yml b/config/schedule.yml index dee572ce..0dc3c9e8 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -30,10 +30,10 @@ cache_preheating_job: class: "Cache::PreheatingJob" queue: default -tracks_cleanup_job: - cron: "0 2 * * 0" # every Sunday at 02:00 - class: "Tracks::CleanupJob" - queue: tracks +# tracks_cleanup_job: +# cron: "0 2 * * 0" # every Sunday at 02:00 +# class: "Tracks::CleanupJob" +# queue: tracks place_name_fetching_job: cron: "30 0 * * *" # every day at 00:30 From 27857ba078ceb77a7ed8ff8b0492879c58712a58 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 20:26:58 +0200 Subject: [PATCH 14/16] Disable tracks panel on the map --- app/javascript/controllers/maps_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index a675c0e9..12092891 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -219,7 +219,7 @@ export default class extends BaseController { this.setupTracksSubscription(); // Handle routes/tracks mode selection - this.addRoutesTracksSelector(); + // this.addRoutesTracksSelector(); # Temporarily disabled this.switchRouteMode('routes', true); // Initialize layers based on settings From 0bfddd932fbdee8a1b974a049fe1c88d4186cdb6 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 20:28:46 +0200 Subject: [PATCH 15/16] Disable specs for track generation --- app/models/point.rb | 2 +- spec/models/point_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/point.rb b/app/models/point.rb index 96e9e68a..7be8524a 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -33,7 +33,7 @@ class Point < ApplicationRecord after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? && !reverse_geocoded? } after_create :set_country after_create_commit :broadcast_coordinates - after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? } + # after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? } # after_commit :recalculate_track, on: :update, if: -> { track.present? } def self.without_raw_data diff --git a/spec/models/point_spec.rb b/spec/models/point_spec.rb index 644f8003..eaf3d4ba 100644 --- a/spec/models/point_spec.rb +++ b/spec/models/point_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Point, type: :model do end end - describe '#recalculate_track' do + xdescribe '#recalculate_track' do let(:point) { create(:point, track: track) } let(:track) { create(:track) } @@ -121,7 +121,7 @@ RSpec.describe Point, type: :model do end end - describe '#trigger_incremental_track_generation' do + xdescribe '#trigger_incremental_track_generation' do let(:point) do create(:point, track: track, import_id: nil, timestamp: 1.hour.ago.to_i, reverse_geocoded_at: 1.hour.ago) end From bd2558ed291d83c124b9a302dadf1eeb2dddacb1 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 22 Jul 2025 20:35:36 +0200 Subject: [PATCH 16/16] Enable assets compilation in production --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 53eedb18..1e4b392a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -29,7 +29,7 @@ Rails.application.configure do # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false + config.assets.compile = true config.assets.content_type = { geojson: 'application/geo+json'