Compare commits

...

69 commits

Author SHA1 Message Date
Evgenii Burmakin
548e9765af
Merge branch 'dev' into dependabot/npm_and_yarn/npm_and_yarn-bb5408b087 2026-01-03 13:32:58 +01:00
Evgenii Burmakin
b037be3299
Update calculation of time spent in a country for year-end digest email (#2110)
* Update calculation of time spent in a country for year-end digest email

* Add a filter to exclude raw data points when calculating yearly digests.
2026-01-03 13:28:21 +01:00
Eugene Burmakin
b4c2def2be Merge remote-tracking branch 'origin' into dev 2025-12-30 19:05:30 +01:00
Eugene Burmakin
8a1e42a2e8 Update version 2025-12-30 19:04:15 +01:00
Eugene Burmakin
2f11003c29 Fix migrations and data verification to remove safety_assured blocks and handle missing points gracefully. 2025-12-30 19:02:38 +01:00
Eugene Burmakin
7f277612fc Update rubocop issues 2025-12-30 17:33:17 +01:00
Eugene Burmakin
2f5487cd35 Update migration and version 2025-12-30 16:57:17 +01:00
Eugene Burmakin
5455228b80 Bring points_count to integer 2025-12-30 16:57:17 +01:00
Evgenii Burmakin
26062a1278
Add RailsPulse (#2079)
* Add RailsPulse

* Add RailsPulse monitoring tool with basic HTTP authentication
2025-12-28 17:49:51 +01:00
Eugene Burmakin
14a0bb6478 Update changelog 2025-12-28 17:34:11 +01:00
Evgenii Burmakin
18b13fb915
Add yearly digest (#2073)
* 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
2025-12-28 17:33:35 +01:00
Evgenii Burmakin
e857f520cc
Add new indicies to improve performance and remove unused ones to opt… (#2078)
* Add new indicies to improve performance and remove unused ones to optimize database.

* Remove comments

* Update map search suggestions panel styling
2025-12-28 17:32:09 +01:00
Evgenii Burmakin
9e933aff9c
Fix/stats cache invalidation (#2072)
* Fix family layer toggle in Map v2 settings for non-selfhosted env

* Invalidate cache

* Remove comments

* Remove comment
2025-12-27 13:33:54 +01:00
Eugene Burmakin
a722e19a93 Deduplicte points based on timestamp brought to unix time 2025-12-26 19:10:02 +01:00
Eugene Burmakin
67d7123e47 Add composite index to points on user_id and timestamp 2025-12-26 18:21:06 +01:00
Eugene Burmakin
573d527455 Add Rack::Deflater middleware to config/application.rb to enable gzip compression for responses. 2025-12-26 17:49:19 +01:00
Eugene Burmakin
4be58d4b4c Update changelog 2025-12-26 17:07:48 +01:00
Evgenii Burmakin
3f436c1d3a
Fix fog of war radius setting being ignored and applying settings causing errors (#2068) 2025-12-26 17:06:56 +01:00
Eugene Burmakin
fe9d7d2f79 Merge remote-tracking branch 'origin' into dev 2025-12-26 17:05:26 +01:00
Eugene Burmakin
fab0121113 Update migration to clean up duplicate stats before adding unique index 2025-12-26 16:28:34 +01:00
Evgenii Burmakin
9805c5524c
Validate trip start and end dates (#2066)
* Validate trip start and end dates

* Update changelog
2025-12-26 16:16:49 +01:00
Evgenii Burmakin
f325fd7a4f
Fix stats calculation to recursively reduce H3 resolution when too ma… (#2065)
* Fix stats calculation to recursively reduce H3 resolution when too many hexagons are generated

* Update CHANGELOG.md
2025-12-26 15:42:32 +01:00
Robin Tuszik
3c1d17b806
fix null type error and update heatmap styling (#2037)
* fix: use constant weight for maplibre heatmap layer

* fix null type, update heatmap styling

* improve heatmap styling

* fix typo
2025-12-26 15:27:51 +01:00
Evgenii Burmakin
c9ba7914b6
Put import deletion into background job (#2045)
* Put import deletion into background job

* Update changelog
2025-12-26 15:27:09 +01:00
dependabot[bot]
03697ecef2
Bump redis from 5.4.0 to 5.4.1 (#1941)
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](https://github.com/redis/redis-rb/compare/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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-26 15:23:39 +01:00
dependabot[bot]
7347be9a87
Bump brakeman from 7.1.0 to 7.1.1 (#1942)
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](https://github.com/presidentbeef/brakeman/compare/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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-26 15:23:19 +01:00
dependabot[bot]
ce74b3d846
Bump webmock from 3.25.1 to 3.26.1 (#1943)
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](https://github.com/bblimke/webmock/compare/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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evgenii Burmakin <Freika@users.noreply.github.com>
2025-12-26 15:22:55 +01:00
dependabot[bot]
da9742bf4a
Bump turbo-rails from 2.0.17 to 2.0.20 (#1944)
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](https://github.com/hotwired/turbo-rails/compare/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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evgenii Burmakin <Freika@users.noreply.github.com>
2025-12-26 15:22:05 +01:00
dependabot[bot]
e12b45f93e
Bump sentry-rails from 6.0.0 to 6.1.0 (#1945)
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](https://github.com/getsentry/sentry-ruby/compare/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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-26 15:21:25 +01:00
Eugene Burmakin
32f5d2f89a Update tailwind file 2025-12-26 14:57:12 +01:00
Eugene Burmakin
ad385f4464 Update changelog 2025-12-26 14:41:55 +01:00
Eugene Burmakin
d4e87ce830 Return changelog 2025-12-26 14:39:36 +01:00
Eugene Burmakin
04fbe4d564 Merge remote-tracking branch 'origin' into dev 2025-12-26 14:39:16 +01:00
Eugene Burmakin
1471e4de40 Update changelog 2025-12-26 14:33:56 +01:00
Evgenii Burmakin
9ef0da27d6
Add family layer to MapLibre maps (#2055)
* Add family layer to MapLibre maps

* Update migration

* Don't show family toggle if feature is disabled
2025-12-26 14:27:16 +01:00
Eugene Burmakin
87baf8bb11 Update entrypoint to always sync static assets (not only new ones) 2025-12-16 18:30:07 +01:00
Eugene Burmakin
d40b2a1959 Update changelog 2025-12-14 22:22:16 +01:00
Eugene Burmakin
35995e7be8 Add composite index to stats table if not exists 2025-12-14 22:19:58 +01:00
Eugene Burmakin
20a4553921 Ensure file is being closed properly after reading in Archivable concern 2025-12-14 11:40:33 +01:00
Eugene Burmakin
c1bb7f3d87 Remove raw_data_archival_job 2025-12-14 11:37:34 +01:00
Eugene Burmakin
0b6149bfc0 Update changelog 2025-12-14 11:34:50 +01:00
Eugene Burmakin
f2d96e50f0 Add help section to navbar dropdown 2025-12-14 11:31:40 +01:00
Eugene Burmakin
1090bcd6e8 Use Toast instead of alert for notifications 2025-12-14 00:32:12 +01:00
Eugene Burmakin
b7f0b7ebc2 Return .keep files 2025-12-14 00:05:34 +01:00
Eugene Burmakin
b81d2580e3 Fix potential memory leak in js 2025-12-14 00:04:42 +01:00
Eugene Burmakin
acee848e72 Eliminate zip-bomb risk 2025-12-13 23:52:47 +01:00
Evgenii Burmakin
88f5e2a6ea
Add verification step to raw data archival process (#2028)
* 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
2025-12-13 21:25:06 +01:00
Robin Tuszik
353837e27f
fix(maplibre): update date format to ISO 8601 (#2029) 2025-12-12 00:21:21 +01:00
Evgenii Burmakin
2a4ed8bf82
Implement moving points in map v2 and fix route rendering logic to ma… (#2027)
* Implement moving points in map v2 and fix route rendering logic to match map v1.

* Fix route spec
2025-12-10 19:58:31 +01:00
Evgenii Burmakin
8af032a215
Fix kml kmz import issues (#2023)
* Fix kml kmz import issues

* Refactor KML importer to improve readability and maintainability
2025-12-09 19:37:27 +01:00
Eugene Burmakin
bb980f2210 Update changelog 2025-12-09 00:22:47 +01:00
Eugene Burmakin
c6d09c341d Update redis client configuration to support unix socket connection 2025-12-09 00:19:32 +01:00
Evgenii Burmakin
516cfabb06
Fix/pre epoch time (#2019)
* 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
2025-12-09 00:17:24 +01:00
Evgenii Burmakin
9ac4566b5a
Use user timezone to show dates on maps (#2020) 2025-12-08 22:12:17 +01:00
Evgenii Burmakin
1c9843dde7
Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation (#2018)
* Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation

* Remove raw data from visited cities api endpoint
2025-12-08 21:38:56 +01:00
Eugene Burmakin
6cc8ba0fbd Merge branch 'master' into dev 2025-12-08 19:52:05 +01:00
Eugene Burmakin
913d60812a Fix storage configuration and file extraction 2025-12-08 19:51:28 +01:00
Eugene Burmakin
a7f77b042e Set raw_data to an empty hash instead of nil when archiving 2025-12-07 23:58:05 +01:00
Eugene Burmakin
cdf1428e35 Merge remote-tracking branch 'origin' into dev 2025-12-07 14:39:30 +01:00
Evgenii Burmakin
9661e8e7f7
Feature/raw data archive (#2009)
* 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 <mail@robin.gg>

* 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 <mail@robin.gg>
2025-12-07 14:33:23 +01:00
Eugene Burmakin
2debcd88fa Pull only necessary data for map v2 points 2025-12-06 21:23:17 +01:00
Evgenii Burmakin
672c308f67
Merge branch 'master' into dev 2025-12-06 20:54:29 +01:00
Eugene Burmakin
c5ef4d3861 Remove some console.log statements 2025-12-06 20:42:52 +01:00
Eugene Burmakin
9fb4bc517b Update changelog 2025-12-06 20:39:47 +01:00
Eugene Burmakin
97d52f9edc Update maplibre controller 2025-12-06 20:36:52 +01:00
Evgenii Burmakin
4421a5bf3c
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
2025-12-06 20:34:49 +01:00
Eugene Burmakin
cebbc28912 Update changelog 2025-11-29 19:58:57 +01:00
Evgenii Burmakin
ac9b668c30
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
2025-11-27 21:29:59 +01:00
Robin Tuszik
6772f2f7b7
fix: move foreman to global gems to fix startup crash (#1971) 2025-11-25 20:30:34 +01:00
7 changed files with 179 additions and 41 deletions

View file

@ -1 +1 @@
0.37.1
0.37.2

View file

@ -4,6 +4,13 @@ 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.37.2] - 2026-01-03
## Fixed
- Time spent in a country and city is now calculated correctly for the year-end digest email. #2104
# [0.37.1] - 2025-12-30
## Fixed

View file

@ -88,18 +88,77 @@ module Users
end
def calculate_time_spent
country_time = Hash.new(0)
{
'countries' => calculate_country_time_spent,
'cities' => calculate_city_time_spent
}
end
def calculate_country_time_spent
country_days = build_country_days_map
# Convert days to minutes (days * 24 * 60) and return top 10
country_days
.transform_values { |days| days.size * 24 * 60 }
.sort_by { |_, minutes| -minutes }
.first(10)
.map { |name, minutes| { 'name' => name, 'minutes' => minutes } }
end
def build_country_days_map
year_points = fetch_year_points_with_country
country_days = Hash.new { |h, k| h[k] = Set.new }
year_points.each do |point|
date = Time.zone.at(point.timestamp).to_date
country_days[point.country_name].add(date)
end
country_days
end
def fetch_year_points_with_country
start_of_year = Time.zone.local(year, 1, 1, 0, 0, 0)
end_of_year = start_of_year.end_of_year
user.points
.without_raw_data
.where('timestamp >= ? AND timestamp <= ?', start_of_year.to_i, end_of_year.to_i)
.where.not(country_name: [nil, ''])
.select(:country_name, :timestamp)
end
def calculate_city_time_spent
city_time = aggregate_city_time_from_monthly_stats
city_time
.sort_by { |_, minutes| -minutes }
.first(10)
.map { |name, minutes| { 'name' => name, 'minutes' => minutes } }
end
def aggregate_city_time_from_monthly_stats
city_time = Hash.new(0)
monthly_stats.each do |stat|
process_stat_toponyms(stat, city_time)
end
city_time
end
def process_stat_toponyms(stat, city_time)
toponyms = stat.toponyms
next unless toponyms.is_a?(Array)
return unless toponyms.is_a?(Array)
toponyms.each do |toponym|
next unless toponym.is_a?(Hash)
process_toponym_cities(toponym, city_time)
end
end
country = toponym['country']
next unless toponym['cities'].is_a?(Array)
def process_toponym_cities(toponym, city_time)
return unless toponym.is_a?(Hash)
return unless toponym['cities'].is_a?(Array)
toponym['cities'].each do |city|
next unless city.is_a?(Hash)
@ -107,17 +166,9 @@ module Users
stayed_for = city['stayed_for'].to_i
city_name = city['city']
country_time[country] += stayed_for if country.present?
city_time[city_name] += stayed_for if city_name.present?
end
end
end
{
'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 } }
}
end
def calculate_first_time_visits
FirstTimeVisitsCalculator.new(user, year).call

View file

@ -3,10 +3,11 @@ class InstallRailsPulseTables < ActiveRecord::Migration[8.0]
def change
# 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")
schema_file = Rails.root.join('db/rails_pulse_schema.rb').to_s
if File.exist?(schema_file)
say "Loading Rails Pulse schema from db/rails_pulse_schema.rb"
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'
# Load the schema file to define RailsPulse::Schema
load schema_file
@ -14,10 +15,7 @@ class InstallRailsPulseTables < ActiveRecord::Migration[8.0]
# 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

View file

@ -0,0 +1,21 @@
class AddIndexesToPointsForStatsQuery < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def change
# Index for counting reverse geocoded points
# This speeds up: COUNT(reverse_geocoded_at)
add_index :points, [:user_id, :reverse_geocoded_at],
where: "reverse_geocoded_at IS NOT NULL",
algorithm: :concurrently,
if_not_exists: true,
name: 'index_points_on_user_id_and_reverse_geocoded_at'
# Index for finding points with empty geodata
# This speeds up: COUNT(CASE WHEN geodata = '{}'::jsonb THEN 1 END)
add_index :points, [:user_id, :geodata],
where: "geodata = '{}'::jsonb",
algorithm: :concurrently,
if_not_exists: true,
name: 'index_points_on_user_id_and_empty_geodata'
end
end

3
db/schema.rb generated
View file

@ -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_12_28_163703) do
ActiveRecord::Schema[8.0].define(version: 2026_01_03_114630) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "postgis"
@ -260,6 +260,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_28_163703) do
t.index ["track_id"], name: "index_points_on_track_id"
t.index ["user_id", "city"], name: "idx_points_user_city"
t.index ["user_id", "country_name"], name: "idx_points_user_country_name"
t.index ["user_id", "geodata"], name: "index_points_on_user_id_and_empty_geodata", where: "(geodata = '{}'::jsonb)"
t.index ["user_id", "reverse_geocoded_at"], name: "index_points_on_user_id_and_reverse_geocoded_at", where: "(reverse_geocoded_at IS NOT NULL)"
t.index ["user_id", "timestamp", "track_id"], name: "idx_points_track_generation"
t.index ["user_id", "timestamp"], name: "idx_points_user_visit_null_timestamp", where: "(visit_id IS NULL)"

View file

@ -77,18 +77,78 @@ RSpec.describe Users::Digests::CalculateYear do
end
it 'calculates time spent by location' do
# Create points to enable country time calculation based on unique days
jan_1 = Time.zone.local(2024, 1, 1, 10, 0, 0).to_i
jan_2 = Time.zone.local(2024, 1, 2, 10, 0, 0).to_i
feb_1 = Time.zone.local(2024, 2, 1, 10, 0, 0).to_i
create(:point, user: user, timestamp: jan_1, country_name: 'Germany', city: 'Berlin')
create(:point, user: user, timestamp: jan_2, country_name: 'Germany', city: 'Munich')
create(:point, user: user, timestamp: feb_1, country_name: 'France', city: 'Paris')
countries = calculate_digest.time_spent_by_location['countries']
cities = calculate_digest.time_spent_by_location['cities']
expect(countries.first['name']).to eq('Germany')
expect(countries.first['minutes']).to eq(720) # 480 + 240
# Countries: based on unique days (2 days in Germany, 1 day in France)
germany_country = countries.find { |c| c['name'] == 'Germany' }
expect(germany_country['minutes']).to eq(2 * 24 * 60) # 2 days = 2880 minutes
# Cities: based on stayed_for from monthly stats (sum across months)
expect(cities.first['name']).to eq('Berlin')
expect(cities.first['minutes']).to eq(480)
end
it 'calculates all time stats' do
expect(calculate_digest.all_time_stats['total_distance']).to eq('125000')
end
context 'when user visits same country across multiple months' do
it 'does not double-count days' do
# Create a user who was in Germany for 10 days in March and 10 days in July
# If we summed the stayed_for values from cities, we might get inflated numbers
# The fix counts unique days to prevent exceeding 365 days per year
mar_start = Time.zone.local(2024, 3, 1, 10, 0, 0).to_i
jul_start = Time.zone.local(2024, 7, 1, 10, 0, 0).to_i
# Create 10 days of points in March
10.times do |i|
timestamp = mar_start + (i * 24 * 60 * 60)
create(:point, user: user, timestamp: timestamp, country_name: 'Germany', city: 'Berlin')
end
# Create 10 days of points in July
10.times do |i|
timestamp = jul_start + (i * 24 * 60 * 60)
create(:point, user: user, timestamp: timestamp, country_name: 'Germany', city: 'Munich')
end
# Create the monthly stats (simulating what would be created by the stats calculation)
create(:stat, user: user, year: 2024, month: 3, distance: 10_000, toponyms: [
{ 'country' => 'Germany', 'cities' => [
{ 'city' => 'Berlin', 'stayed_for' => 14_400 } # 10 days in minutes
] }
])
create(:stat, user: user, year: 2024, month: 7, distance: 15_000, toponyms: [
{ 'country' => 'Germany', 'cities' => [
{ 'city' => 'Munich', 'stayed_for' => 14_400 } # 10 days in minutes
] }
])
digest = calculate_digest
countries = digest.time_spent_by_location['countries']
germany = countries.find { |c| c['name'] == 'Germany' }
# Should be 20 days total (10 unique days in Mar + 10 unique days in Jul)
expected_minutes = 20 * 24 * 60 # 28,800 minutes
expect(germany['minutes']).to eq(expected_minutes)
# Verify this is less than 365 days (the bug would cause inflated numbers)
total_days = germany['minutes'] / (24 * 60)
expect(total_days).to be <= 365
end
end
context 'when digest already exists' do
let!(:existing_digest) do
create(:users_digest, user: user, year: 2024, period_type: :yearly, distance: 10_000)