Merge pull request #1905 from Freika/dev

0.34.1
This commit is contained in:
Evgenii Burmakin 2025-10-30 20:01:09 +01:00 committed by GitHub
commit 55e1f4a161
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 468 additions and 203 deletions

View file

@ -1 +1 @@
0.34.0 0.34.1

View file

@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
# [0.34.1] - 2025-10-30
## Fixed
- Broken Stats page for users with no reverse geocoding enabled. #1877
## Changed
- Date navigation on the map page is no longer shown as floating panel. It is now part of the top navigation bar to prevent overlapping with other map controls. #1894 #1881
## Added
- [Dawarich Cloud] Added support for UTM parameters during user registration. UTM parameters will be stored with the user record for marketing analytics purposes.
# [0.34.0] - 2025-10-10 # [0.34.0] - 2025-10-10
## The Family release ## The Family release

24
Gemfile
View file

@ -28,8 +28,8 @@ gem 'parallel'
gem 'pg' gem 'pg'
gem 'prometheus_exporter' gem 'prometheus_exporter'
gem 'puma' gem 'puma'
gem 'pundit' gem 'pundit', '>= 2.5.1'
gem 'rails', '~> 8.0' gem 'rails', '~> 8.0', '>= 8.0.3'
gem 'rails_icons' gem 'rails_icons'
gem 'redis' gem 'redis'
gem 'rexml' gem 'rexml'
@ -39,18 +39,18 @@ gem 'rgeo-geojson'
gem 'rqrcode', '~> 3.0' gem 'rqrcode', '~> 3.0'
gem 'rswag-api' gem 'rswag-api'
gem 'rswag-ui' gem 'rswag-ui'
gem 'rubyzip', '~> 3.1' gem 'rubyzip', '~> 3.2'
gem 'sentry-rails' gem 'sentry-rails', '>= 5.27.0'
gem 'sentry-ruby' gem 'sentry-ruby'
gem 'sidekiq' gem 'sidekiq', '>= 8.0.5'
gem 'sidekiq-cron' gem 'sidekiq-cron', '>= 2.3.1'
gem 'sidekiq-limit_fetch' gem 'sidekiq-limit_fetch'
gem 'sprockets-rails' gem 'sprockets-rails'
gem 'stackprof' gem 'stackprof'
gem 'stimulus-rails' gem 'stimulus-rails'
gem 'strong_migrations' gem 'strong_migrations', '>= 2.4.0'
gem 'tailwindcss-rails' gem 'tailwindcss-rails', '= 3.3.2'
gem 'turbo-rails' gem 'turbo-rails', '>= 2.0.17'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
group :development, :test, :staging do group :development, :test, :staging do
@ -62,7 +62,7 @@ group :development, :test, :staging do
gem 'ffaker' gem 'ffaker'
gem 'pry-byebug' gem 'pry-byebug'
gem 'pry-rails' gem 'pry-rails'
gem 'rspec-rails' gem 'rspec-rails', '>= 8.0.1'
gem 'rswag-specs' gem 'rswag-specs'
end end
@ -77,7 +77,7 @@ group :test do
end end
group :development do group :development do
gem 'database_consistency', require: false gem 'database_consistency', '>= 2.0.5', require: false
gem 'foreman' gem 'foreman'
gem 'rubocop-rails', require: false gem 'rubocop-rails', '>= 2.33.4', require: false
end end

View file

@ -10,29 +10,29 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.2.1) actioncable (8.0.3)
actionpack (= 8.0.2.1) actionpack (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.2.1) actionmailbox (8.0.3)
actionpack (= 8.0.2.1) actionpack (= 8.0.3)
activejob (= 8.0.2.1) activejob (= 8.0.3)
activerecord (= 8.0.2.1) activerecord (= 8.0.3)
activestorage (= 8.0.2.1) activestorage (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.2.1) actionmailer (8.0.3)
actionpack (= 8.0.2.1) actionpack (= 8.0.3)
actionview (= 8.0.2.1) actionview (= 8.0.3)
activejob (= 8.0.2.1) activejob (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.2.1) actionpack (8.0.3)
actionview (= 8.0.2.1) actionview (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@ -40,38 +40,38 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.2.1) actiontext (8.0.3)
actionpack (= 8.0.2.1) actionpack (= 8.0.3)
activerecord (= 8.0.2.1) activerecord (= 8.0.3)
activestorage (= 8.0.2.1) activestorage (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.2.1) actionview (8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.0.2.1) activejob (8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.2.1) activemodel (8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
activerecord (8.0.2.1) activerecord (8.0.3)
activemodel (= 8.0.2.1) activemodel (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activerecord-postgis-adapter (11.0.0) activerecord-postgis-adapter (11.0.0)
activerecord (~> 8.0.0) activerecord (~> 8.0.0)
rgeo-activerecord (~> 8.0.0) rgeo-activerecord (~> 8.0.0)
activestorage (8.0.2.1) activestorage (8.0.3)
actionpack (= 8.0.2.1) actionpack (= 8.0.3)
activejob (= 8.0.2.1) activejob (= 8.0.3)
activerecord (= 8.0.2.1) activerecord (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.2.1) activesupport (8.0.3)
base64 base64
benchmark (>= 0.3) benchmark (>= 0.3)
bigdecimal bigdecimal
@ -142,7 +142,7 @@ GEM
data_migrate (11.3.0) data_migrate (11.3.0)
activerecord (>= 6.1) activerecord (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
database_consistency (2.0.4) database_consistency (2.0.6)
activerecord (>= 3.2) activerecord (>= 3.2)
date (3.4.1) date (3.4.1)
debug (1.11.0) debug (1.11.0)
@ -163,7 +163,7 @@ GEM
drb (2.2.3) drb (2.2.3)
erb (5.0.2) erb (5.0.2)
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.2.11) et-orbi (1.4.0)
tzinfo tzinfo
factory_bot (6.5.5) factory_bot (6.5.5)
activesupport (>= 6.1.0) activesupport (>= 6.1.0)
@ -180,10 +180,10 @@ GEM
ffi (1.17.2-x86_64-linux-gnu) ffi (1.17.2-x86_64-linux-gnu)
foreman (0.90.0) foreman (0.90.0)
thor (~> 1.4) thor (~> 1.4)
fugit (1.11.1) fugit (1.12.1)
et-orbi (~> 1, >= 1.2.11) et-orbi (~> 1.4)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.2.1) globalid (1.3.0)
activesupport (>= 6.1) activesupport (>= 6.1)
gpx (1.2.1) gpx (1.2.1)
csv csv
@ -212,7 +212,7 @@ GEM
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.13.2) json (2.15.0)
json-schema (5.0.1) json-schema (5.0.1)
addressable (~> 2.8) addressable (~> 2.8)
jwt (2.10.1) jwt (2.10.1)
@ -240,12 +240,13 @@ GEM
loofah (2.24.1) loofah (2.24.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.9.0)
logger
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.0.4) marcel (1.1.0)
matrix (0.4.2) matrix (0.4.2)
method_source (1.1.0) method_source (1.1.0)
mini_mime (1.1.5) mini_mime (1.1.5)
@ -255,7 +256,7 @@ GEM
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.7.1) multi_xml (0.7.1)
bigdecimal (~> 3.1) bigdecimal (~> 3.1)
net-imap (0.5.9) net-imap (0.5.12)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@ -265,18 +266,18 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.18.9) nokogiri (1.18.10)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-aarch64-linux-gnu) nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-arm-linux-gnu) nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-arm64-darwin) nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-x86_64-darwin) nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu) nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.11) oj (3.16.11)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
@ -313,13 +314,13 @@ GEM
date date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.6.1) puma (7.1.0)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.5.0) pundit (2.5.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.1) rack (3.2.2)
rack-session (2.1.1) rack-session (2.1.1)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (>= 3.0.0) rack (>= 3.0.0)
@ -327,20 +328,20 @@ GEM
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.2.1)
rack (>= 3) rack (>= 3)
rails (8.0.2.1) rails (8.0.3)
actioncable (= 8.0.2.1) actioncable (= 8.0.3)
actionmailbox (= 8.0.2.1) actionmailbox (= 8.0.3)
actionmailer (= 8.0.2.1) actionmailer (= 8.0.3)
actionpack (= 8.0.2.1) actionpack (= 8.0.3)
actiontext (= 8.0.2.1) actiontext (= 8.0.3)
actionview (= 8.0.2.1) actionview (= 8.0.3)
activejob (= 8.0.2.1) activejob (= 8.0.3)
activemodel (= 8.0.2.1) activemodel (= 8.0.3)
activerecord (= 8.0.2.1) activerecord (= 8.0.3)
activestorage (= 8.0.2.1) activestorage (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.2.1) railties (= 8.0.3)
rails-dom-testing (2.3.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@ -351,13 +352,14 @@ GEM
rails_icons (1.4.0) rails_icons (1.4.0)
nokogiri (~> 1.16, >= 1.16.4) nokogiri (~> 1.16, >= 1.16.4)
rails (> 6.1) rails (> 6.1)
railties (8.0.2.1) railties (8.0.3)
actionpack (= 8.0.2.1) actionpack (= 8.0.3)
activesupport (= 8.0.2.1) activesupport (= 8.0.3)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.3.0) rake (13.3.0)
@ -368,7 +370,7 @@ GEM
redis-client (>= 0.22.0) redis-client (>= 0.22.0)
redis-client (0.24.0) redis-client (0.24.0)
connection_pool connection_pool
regexp_parser (2.11.2) regexp_parser (2.11.3)
reline (0.6.2) reline (0.6.2)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.7.0) request_store (1.7.0)
@ -390,13 +392,13 @@ GEM
rqrcode_core (2.0.0) rqrcode_core (2.0.0)
rspec-core (3.13.3) rspec-core (3.13.3)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.4) rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-mocks (3.13.4) rspec-mocks (3.13.6)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (8.0.0) rspec-rails (8.0.2)
actionpack (>= 7.2) actionpack (>= 7.2)
activesupport (>= 7.2) activesupport (>= 7.2)
railties (>= 7.2) railties (>= 7.2)
@ -416,7 +418,7 @@ GEM
rswag-ui (2.16.0) rswag-ui (2.16.0)
actionpack (>= 5.2, < 8.1) actionpack (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1) railties (>= 5.2, < 8.1)
rubocop (1.80.2) rubocop (1.81.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -424,20 +426,20 @@ GEM
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.46.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.46.0) rubocop-ast (1.47.1)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
prism (~> 1.4) prism (~> 1.4)
rubocop-rails (2.33.3) rubocop-rails (2.33.4)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
rubyzip (3.1.0) rubyzip (3.2.0)
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.35.0) selenium-webdriver (4.35.0)
base64 (~> 0.2) base64 (~> 0.2)
@ -445,21 +447,21 @@ GEM
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0) rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0) websocket (~> 1.0)
sentry-rails (5.26.0) sentry-rails (5.28.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.26.0) sentry-ruby (~> 5.28.0)
sentry-ruby (5.26.0) sentry-ruby (5.28.0)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
shoulda-matchers (6.5.0) shoulda-matchers (6.5.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (8.0.4) sidekiq (8.0.8)
connection_pool (>= 2.5.0) connection_pool (>= 2.5.0)
json (>= 2.9.0) json (>= 2.9.0)
logger (>= 1.6.2) logger (>= 1.6.2)
rack (>= 3.1.0) rack (>= 3.1.0)
redis-client (>= 0.23.2) redis-client (>= 0.23.2)
sidekiq-cron (2.3.0) sidekiq-cron (2.3.1)
cronex (>= 0.13.0) cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1) fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1) globalid (>= 1.0.1)
@ -483,13 +485,13 @@ GEM
stimulus-rails (1.3.4) stimulus-rails (1.3.4)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.1.7) stringio (3.1.7)
strong_migrations (2.3.0) strong_migrations (2.5.1)
activerecord (>= 7) activerecord (>= 7.1)
super_diff (0.16.0) super_diff (0.16.0)
attr_extras (>= 6.2.4) attr_extras (>= 6.2.4)
diff-lcs diff-lcs
patience_diff patience_diff
tailwindcss-rails (3.3.1) tailwindcss-rails (3.3.2)
railties (>= 7.0.0) railties (>= 7.0.0)
tailwindcss-ruby (~> 3.0) tailwindcss-ruby (~> 3.0)
tailwindcss-ruby (3.4.17) tailwindcss-ruby (3.4.17)
@ -500,7 +502,8 @@ GEM
tailwindcss-ruby (3.4.17-x86_64-linux) tailwindcss-ruby (3.4.17-x86_64-linux)
thor (1.4.0) thor (1.4.0)
timeout (0.4.3) timeout (0.4.3)
turbo-rails (2.0.16) tsort (0.2.0)
turbo-rails (2.0.17)
actionpack (>= 7.1.0) actionpack (>= 7.1.0)
railties (>= 7.1.0) railties (>= 7.1.0)
tzinfo (2.0.6) tzinfo (2.0.6)
@ -546,7 +549,7 @@ DEPENDENCIES
capybara capybara
chartkick chartkick
data_migrate data_migrate
database_consistency database_consistency (>= 2.0.5)
debug debug
devise devise
dotenv-rails dotenv-rails
@ -570,8 +573,8 @@ DEPENDENCIES
pry-byebug pry-byebug
pry-rails pry-rails
puma puma
pundit pundit (>= 2.5.1)
rails (~> 8.0) rails (~> 8.0, >= 8.0.3)
rails_icons rails_icons
redis redis
rexml rexml
@ -579,27 +582,27 @@ DEPENDENCIES
rgeo-activerecord rgeo-activerecord
rgeo-geojson rgeo-geojson
rqrcode (~> 3.0) rqrcode (~> 3.0)
rspec-rails rspec-rails (>= 8.0.1)
rswag-api rswag-api
rswag-specs rswag-specs
rswag-ui rswag-ui
rubocop-rails rubocop-rails (>= 2.33.4)
rubyzip (~> 3.1) rubyzip (~> 3.2)
selenium-webdriver selenium-webdriver
sentry-rails sentry-rails (>= 5.27.0)
sentry-ruby sentry-ruby
shoulda-matchers shoulda-matchers
sidekiq sidekiq (>= 8.0.5)
sidekiq-cron sidekiq-cron (>= 2.3.1)
sidekiq-limit_fetch sidekiq-limit_fetch
simplecov simplecov
sprockets-rails sprockets-rails
stackprof stackprof
stimulus-rails stimulus-rails
strong_migrations strong_migrations (>= 2.4.0)
super_diff super_diff
tailwindcss-rails tailwindcss-rails (= 3.3.2)
turbo-rails turbo-rails (>= 2.0.17)
tzinfo-data tzinfo-data
webmock webmock

View file

@ -8,14 +8,17 @@
## 📸 Screenshots ## 📸 Screenshots
![Map](screenshots/map.jpeg) ![Map](screenshots/map.png)
*Map View* *Map View*
![Stats](screenshots/stats.jpeg) ![Family](screenshots/family.png)
*Family Page*
![Stats](screenshots/stats.png)
*Statistics Overview* *Statistics Overview*
![Import](screenshots/imports.jpeg) ![Trips](screenshots/trips.png)
*Imports page* *Trips page*
--- ---
@ -28,6 +31,9 @@ It enables you to:
- Track your location history. - Track your location history.
- Visualize your data on an interactive map. - Visualize your data on an interactive map.
- Create trips and analyze your travel history.
- Share your location with family members.
- Integrate with photo management apps like Immich and Photoprism to visualize geotagged photos.
- Import your location history from Google Maps Timeline, OwnTracks, GPX, GeoJSON and some other sources - Import your location history from Google Maps Timeline, OwnTracks, GPX, GeoJSON and some other sources
- Explore statistics like the number of countries and cities visited, total distance traveled, and more! - Explore statistics like the number of countries and cities visited, total distance traveled, and more!
@ -99,6 +105,11 @@ Feel free to change them in the account settings.
- Lines between points - Lines between points
- Fog of War - Fog of War
### 👪 Family Sharing
- Share your location with family members.
- View locations of family members on the map (with their consent).
- Each family member can enable or disable location sharing individually.
### 🔵 Areas ### 🔵 Areas
- Draw areas on the map so Dawarich could suggest your visits there. - Draw areas on the map so Dawarich could suggest your visits there.
@ -109,7 +120,6 @@ Feel free to change them in the account settings.
- Analyze your travel history: number of countries/cities visited, distance traveled, and time spent, broken down by year and month. - Analyze your travel history: number of countries/cities visited, distance traveled, and time spent, broken down by year and month.
### ✈️ Trips ### ✈️ Trips
- Create a trip to visualize your travels between two points in time. You'll be able to see the route, distance, and time spent, and also add notes to your trip. If you have Immich or Photoprism integration, you'll also be able to see photos from your trips! - Create a trip to visualize your travels between two points in time. You'll be able to see the route, distance, and time spent, and also add notes to your trip. If you have Immich or Photoprism integration, you'll also be able to see photos from your trips!
### 📸 Integrations ### 📸 Integrations

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
extend ActiveSupport::Concern
UTM_PARAMS = %w[utm_source utm_medium utm_campaign utm_term utm_content].freeze
def store_utm_params
UTM_PARAMS.each do |param|
session[param] = params[param] if params[param].present?
end
end
def assign_utm_params(record)
utm_data = extract_utm_data_from_session
return unless utm_data.any?
record.update_columns(utm_data)
clear_utm_session
end
private
def extract_utm_data_from_session
UTM_PARAMS.each_with_object({}) do |param, hash|
hash[param] = session[param] if session[param].present?
end
end
def clear_utm_session
UTM_PARAMS.each { |param| session.delete(param) }
end
end

View file

@ -1,8 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController class Users::RegistrationsController < Devise::RegistrationsController
include UtmTrackable
before_action :set_invitation, only: %i[new create] before_action :set_invitation, only: %i[new create]
before_action :check_registration_allowed, only: %i[new create] before_action :check_registration_allowed, only: %i[new create]
before_action :store_utm_params, only: %i[new], unless: -> { DawarichSettings.self_hosted? }
def new def new
build_resource({}) build_resource({})
@ -16,8 +19,9 @@ class Users::RegistrationsController < Devise::RegistrationsController
def create def create
super do |resource| super do |resource|
if resource.persisted? && @invitation if resource.persisted?
accept_invitation_for_user(resource) assign_utm_params(resource)
accept_invitation_for_user(resource) if @invitation
end end
end end
end end
@ -65,8 +69,8 @@ class Users::RegistrationsController < Devise::RegistrationsController
def invitation_token def invitation_token
@invitation_token ||= params[:invitation_token] || @invitation_token ||= params[:invitation_token] ||
params.dig(:user, :invitation_token) || params.dig(:user, :invitation_token) ||
session[:invitation_token] session[:invitation_token]
end end
def accept_invitation_for_user(user) def accept_invitation_for_user(user)
@ -80,11 +84,13 @@ class Users::RegistrationsController < Devise::RegistrationsController
if service.call if service.call
flash[:notice] = "Welcome to #{@invitation.family.name}! You're now part of the family." flash[:notice] = "Welcome to #{@invitation.family.name}! You're now part of the family."
else else
flash[:alert] = "Account created successfully, but there was an issue accepting the invitation: #{service.error_message}" flash[:alert] =
"Account created successfully, but there was an issue accepting the invitation: #{service.error_message}"
end end
rescue StandardError => e rescue StandardError => e
Rails.logger.error "Error accepting invitation during registration: #{e.message}" Rails.logger.error "Error accepting invitation during registration: #{e.message}"
flash[:alert] = "Account created successfully, but there was an issue accepting the invitation. Please try accepting it again." flash[:alert] =
'Account created successfully, but there was an issue accepting the invitation. Please try accepting it again.'
end end
def sign_up_params def sign_up_params

View file

@ -122,9 +122,8 @@ export default class extends BaseController {
}); });
}); });
// Add markers and route // Add route (no markers on trip forms)
if (this.coordinates?.length > 0) { if (this.coordinates?.length > 0) {
this.addMarkers()
this.addPolyline() this.addPolyline()
this.fitMapToBounds() this.fitMapToBounds()
} }
@ -246,9 +245,8 @@ export default class extends BaseController {
this.polylinesLayer.clearLayers() this.polylinesLayer.clearLayers()
this.photoMarkers.clearLayers() this.photoMarkers.clearLayers()
// Add new markers and route if coordinates exist // Add only polyline (no markers) when coordinates exist
if (this.coordinates?.length > 0) { if (this.coordinates?.length > 0) {
this.addMarkers()
this.addPolyline() this.addPolyline()
this.fitMapToBounds() this.fitMapToBounds()
} }

View file

@ -1,30 +1,30 @@
export const mapsConfig = { export const mapsConfig = {
"Light": { "Light": {
url: "https://tyles.dwri.xyz/20250420/{z}/{x}/{y}.mvt", url: "https://tyles.dwri.xyz/planet/{z}/{x}/{y}.mvt",
flavor: "light", flavor: "light",
maxZoom: 16, maxZoom: 16,
attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>" attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>"
}, },
"Dark": { "Dark": {
url: "https://tyles.dwri.xyz/20250420/{z}/{x}/{y}.mvt", url: "https://tyles.dwri.xyz/planet/{z}/{x}/{y}.mvt",
flavor: "dark", flavor: "dark",
maxZoom: 16, maxZoom: 16,
attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>" attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>"
}, },
"White": { "White": {
url: "https://tyles.dwri.xyz/20250420/{z}/{x}/{y}.mvt", url: "https://tyles.dwri.xyz/planet/{z}/{x}/{y}.mvt",
flavor: "white", flavor: "white",
maxZoom: 16, maxZoom: 16,
attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>" attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>"
}, },
"Grayscale": { "Grayscale": {
url: "https://tyles.dwri.xyz/20250420/{z}/{x}/{y}.mvt", url: "https://tyles.dwri.xyz/planet/{z}/{x}/{y}.mvt",
flavor: "grayscale", flavor: "grayscale",
maxZoom: 16, maxZoom: 16,
attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>" attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>"
}, },
"Black": { "Black": {
url: "https://tyles.dwri.xyz/20250420/{z}/{x}/{y}.mvt", url: "https://tyles.dwri.xyz/planet/{z}/{x}/{y}.mvt",
flavor: "black", flavor: "black",
maxZoom: 16, maxZoom: 16,
attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>" attribution: "<a href='https://github.com/protomaps/basemaps'>Protomaps</a>, &copy; <a href='https://openstreetmap.org'>OpenStreetMap</a>"

View file

@ -1,3 +1,5 @@
<% content_for :title, "Editing Family" %>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="bg-base-200 rounded-lg p-6"> <div class="bg-base-200 rounded-lg p-6">

View file

@ -1,3 +1,5 @@
<% content_for :title, "Family Management" %>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="text-center mb-8"> <div class="text-center mb-8">
@ -44,4 +46,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,3 +1,5 @@
<% content_for :title, "New Family" %>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="text-center mb-8"> <div class="text-center mb-8">

View file

@ -1,3 +1,5 @@
<% content_for :title, "Family Details" %>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<!-- Family Header --> <!-- Family Header -->

View file

@ -38,7 +38,7 @@
</div> </div>
<!-- Full Screen Map Container --> <!-- Full Screen Map Container -->
<div class='absolute top-16 left-0 right-0 w-full z-20' style='height: calc(100vh - 4rem);'> <div class='absolute top-16 left-0 right-0 bottom-0 w-full z-20 flex flex-col'>
<%= yield %> <%= yield %>
</div> </div>

View file

@ -1,10 +1,9 @@
<% content_for :title, 'Map' %> <% content_for :title, 'Map' %>
<!-- Floating Date Navigation Controls --> <!-- Date Navigation Controls - Native Page Element -->
<div class="fixed top-20 left-0 right-0 flex justify-center" style="z-index: 9999; margin-left: 80px; margin-right: 80px;"> <div class="w-full px-4 py-3 bg-base-100" data-controller="map-controls">
<div style="width: 1500px; max-width: 100%;" data-controller="map-controls">
<!-- Mobile: Compact Toggle Button --> <!-- Mobile: Compact Toggle Button -->
<div class="lg:hidden justify-center flex"> <div class="lg:hidden flex justify-center">
<button <button
type="button" type="button"
data-action="click->map-controls#toggle" data-action="click->map-controls#toggle"
@ -19,7 +18,7 @@
<!-- Expandable Panel (hidden on mobile by default, always visible on desktop) --> <!-- Expandable Panel (hidden on mobile by default, always visible on desktop) -->
<div <div
data-map-controls-target="panel" data-map-controls-target="panel"
class="hidden lg:!block bg-base-100 bg-opacity-95 rounded-lg shadow-lg p-4 mt-2 lg:mt-0 scale-80"> class="hidden lg:!block bg-base-100 rounded-lg shadow-lg p-4 mt-2 lg:mt-0">
<%= form_with url: map_path(import_id: params[:import_id]), method: :get do |f| %> <%= form_with url: map_path(import_id: params[:import_id]), method: :get do |f| %>
<div class="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4 lg:items-end"> <div class="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4 lg:items-end">
<div class="w-full lg:w-1/12"> <div class="w-full lg:w-1/12">
@ -71,29 +70,30 @@
</div> </div>
<% end %> <% end %>
</div> </div>
</div>
</div> </div>
<!-- Full Screen Map --> <!-- Map Container - Fills remaining space -->
<div <div class="w-full h-full">
id='map' <div
class="absolute inset-0 w-full h-full z-0" id='map'
data-controller="maps points add-visit family-members" class="w-full h-full"
data-points-target="map" data-controller="maps points add-visit family-members"
data-api_key="<%= current_user.api_key %>" data-points-target="map"
data-self_hosted="<%= @self_hosted %>" data-api_key="<%= current_user.api_key %>"
data-user_settings='<%= current_user.safe_settings.settings.to_json %>' data-self_hosted="<%= @self_hosted %>"
data-user_theme="<%= current_user&.theme || 'dark' %>" data-user_settings='<%= current_user.safe_settings.settings.to_json %>'
data-coordinates='<%= @coordinates.to_json.html_safe %>' data-user_theme="<%= current_user&.theme || 'dark' %>"
data-tracks='<%= @tracks.to_json.html_safe %>' data-coordinates='<%= @coordinates.to_json.html_safe %>'
data-distance="<%= @distance %>" data-tracks='<%= @tracks.to_json.html_safe %>'
data-points_number="<%= @points_number %>" data-distance="<%= @distance %>"
data-timezone="<%= Rails.configuration.time_zone %>" data-points_number="<%= @points_number %>"
data-features='<%= @features.to_json.html_safe %>' data-timezone="<%= Rails.configuration.time_zone %>"
data-family-members-features-value='<%= @features.to_json.html_safe %>' data-features='<%= @features.to_json.html_safe %>'
data-family-members-user-theme-value="<%= current_user&.theme || 'dark' %>"> data-family-members-features-value='<%= @features.to_json.html_safe %>'
<div data-maps-target="container" class="w-full h-full"> data-family-members-user-theme-value="<%= current_user&.theme || 'dark' %>">
<div id="fog" class="fog"></div> <div data-maps-target="container" class="w-full h-full">
<div id="fog" class="fog"></div>
</div>
</div> </div>
</div> </div>

View file

@ -18,6 +18,8 @@
<% if DawarichSettings.reverse_geocoding_enabled? %> <% if DawarichSettings.reverse_geocoding_enabled? %>
<%= render 'stats/reverse_geocoding_stats' %> <%= render 'stats/reverse_geocoding_stats' %>
<% else %>
</div>
<% end %> <% end %>
<div class='text-xs text-gray-500 text-center mt-5'> <div class='text-xs text-gray-500 text-center mt-5'>

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddUtmParametersToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :utm_source, :string
add_column :users, :utm_medium, :string
add_column :users, :utm_campaign, :string
add_column :users, :utm_term, :string
add_column :users, :utm_content, :string
end
end

33
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_09_30_150256) do ActiveRecord::Schema[8.0].define(version: 2025_10_30_190924) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
enable_extension "postgis" enable_extension "postgis"
@ -113,10 +113,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_30_150256) do
t.integer "status", default: 0, null: false t.integer "status", default: 0, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["email"], name: "index_family_invitations_on_email" t.index ["family_id", "email"], name: "index_family_invitations_on_family_id_and_email"
t.index ["expires_at"], name: "index_family_invitations_on_expires_at" t.index ["family_id", "status", "expires_at"], name: "index_family_invitations_on_family_status_expires"
t.index ["family_id"], name: "index_family_invitations_on_family_id" t.index ["status", "expires_at"], name: "index_family_invitations_on_status_and_expires_at"
t.index ["status"], name: "index_family_invitations_on_status" t.index ["status", "updated_at"], name: "index_family_invitations_on_status_and_updated_at"
t.index ["token"], name: "index_family_invitations_on_token", unique: true t.index ["token"], name: "index_family_invitations_on_token", unique: true
end end
@ -126,8 +126,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_30_150256) do
t.integer "role", default: 1, null: false t.integer "role", default: 1, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["family_id", "role"], name: "index_family_memberships_on_family_id_and_role" t.index ["family_id", "role"], name: "index_family_memberships_on_family_and_role"
t.index ["family_id"], name: "index_family_memberships_on_family_id"
t.index ["user_id"], name: "index_family_memberships_on_user_id", unique: true t.index ["user_id"], name: "index_family_memberships_on_user_id", unique: true
end end
@ -316,6 +315,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_30_150256) do
t.integer "status", default: 0 t.integer "status", default: 0
t.datetime "active_until" t.datetime "active_until"
t.integer "points_count", default: 0, null: false t.integer "points_count", default: 0, null: false
t.string "provider"
t.string "uid"
t.text "patreon_access_token"
t.text "patreon_refresh_token"
t.datetime "patreon_token_expires_at"
t.string "utm_source"
t.string "utm_medium"
t.string "utm_campaign"
t.string "utm_term"
t.string "utm_content"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end end
@ -342,11 +351,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_30_150256) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "areas", "users" add_foreign_key "areas", "users"
add_foreign_key "families", "users", column: "creator_id", validate: false add_foreign_key "families", "users", column: "creator_id"
add_foreign_key "family_invitations", "families", validate: false add_foreign_key "family_invitations", "families"
add_foreign_key "family_invitations", "users", column: "invited_by_id", validate: false add_foreign_key "family_invitations", "users", column: "invited_by_id"
add_foreign_key "family_memberships", "families", validate: false add_foreign_key "family_memberships", "families"
add_foreign_key "family_memberships", "users", validate: false add_foreign_key "family_memberships", "users"
add_foreign_key "notifications", "users" add_foreign_key "notifications", "users"
add_foreign_key "place_visits", "places" add_foreign_key "place_visits", "places"
add_foreign_key "place_visits", "visits" add_foreign_key "place_visits", "visits"

50
package-lock.json generated
View file

@ -13,7 +13,7 @@
"trix": "^2.1.15" "trix": "^2.1.15"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.54.1", "@playwright/test": "^1.56.1",
"@types/node": "^24.0.13" "@types/node": "^24.0.13"
}, },
"engines": { "engines": {
@ -39,13 +39,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.54.1", "version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.54.1" "playwright": "1.56.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -169,7 +168,6 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@ -206,13 +204,12 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.54.1", "version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.54.1" "playwright-core": "1.56.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -225,11 +222,10 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.54.1", "version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
@ -328,12 +324,12 @@
} }
}, },
"@playwright/test": { "@playwright/test": {
"version": "1.54.1", "version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true, "dev": true,
"requires": { "requires": {
"playwright": "1.54.1" "playwright": "1.56.1"
} }
}, },
"@rails/actioncable": { "@rails/actioncable": {
@ -443,19 +439,19 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
}, },
"playwright": { "playwright": {
"version": "1.54.1", "version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true, "dev": true,
"requires": { "requires": {
"fsevents": "2.3.2", "fsevents": "2.3.2",
"playwright-core": "1.54.1" "playwright-core": "1.56.1"
} }
}, },
"playwright-core": { "playwright-core": {
"version": "1.54.1", "version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true "dev": true
}, },
"postcss": { "postcss": {

View file

@ -12,7 +12,7 @@
"npm": "9.6.7" "npm": "9.6.7"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.54.1", "@playwright/test": "^1.56.1",
"@types/node": "^24.0.13" "@types/node": "^24.0.13"
}, },
"scripts": {} "scripts": {}

BIN
screenshots/family.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 KiB

BIN
screenshots/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

BIN
screenshots/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
screenshots/trips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View file

@ -325,4 +325,179 @@ RSpec.describe 'Users::Registrations', type: :request do
end end
end end
end end
describe 'UTM Parameter Tracking' do
let(:utm_params) do
{
utm_source: 'google',
utm_medium: 'cpc',
utm_campaign: 'winter_2025',
utm_term: 'location_tracking',
utm_content: 'banner_ad'
}
end
context 'when self-hosted mode is disabled' do
before do
allow(DawarichSettings).to receive(:self_hosted?).and_return(false)
end
it 'captures UTM parameters from registration page URL' do
get new_user_registration_path, params: utm_params
expect(response).to have_http_status(:ok)
expect(session[:utm_source]).to eq('google')
expect(session[:utm_medium]).to eq('cpc')
expect(session[:utm_campaign]).to eq('winter_2025')
expect(session[:utm_term]).to eq('location_tracking')
expect(session[:utm_content]).to eq('banner_ad')
end
it 'stores UTM parameters in user record after registration' do
# Visit registration page with UTM params
get new_user_registration_path, params: utm_params
# Create account
unique_email = "utm-user-#{Time.current.to_i}@example.com"
post user_registration_path, params: {
user: {
email: unique_email,
password: 'password123',
password_confirmation: 'password123'
}
}
# Verify UTM params were saved to user
user = User.find_by(email: unique_email)
expect(user.utm_source).to eq('google')
expect(user.utm_medium).to eq('cpc')
expect(user.utm_campaign).to eq('winter_2025')
expect(user.utm_term).to eq('location_tracking')
expect(user.utm_content).to eq('banner_ad')
end
it 'clears UTM parameters from session after registration' do
# Visit registration page with UTM params
get new_user_registration_path, params: utm_params
# Create account
unique_email = "utm-cleanup-#{Time.current.to_i}@example.com"
post user_registration_path, params: {
user: {
email: unique_email,
password: 'password123',
password_confirmation: 'password123'
}
}
# Verify session was cleaned up
expect(session[:utm_source]).to be_nil
expect(session[:utm_medium]).to be_nil
expect(session[:utm_campaign]).to be_nil
expect(session[:utm_term]).to be_nil
expect(session[:utm_content]).to be_nil
end
it 'handles partial UTM parameters' do
partial_utm = { utm_source: 'twitter', utm_campaign: 'spring_promo' }
get new_user_registration_path, params: partial_utm
unique_email = "partial-utm-#{Time.current.to_i}@example.com"
post user_registration_path, params: {
user: {
email: unique_email,
password: 'password123',
password_confirmation: 'password123'
}
}
user = User.find_by(email: unique_email)
expect(user.utm_source).to eq('twitter')
expect(user.utm_campaign).to eq('spring_promo')
expect(user.utm_medium).to be_nil
expect(user.utm_term).to be_nil
expect(user.utm_content).to be_nil
end
it 'does not store empty UTM parameters' do
empty_utm = {
utm_source: '',
utm_medium: '',
utm_campaign: 'campaign_only'
}
get new_user_registration_path, params: empty_utm
unique_email = "empty-utm-#{Time.current.to_i}@example.com"
post user_registration_path, params: {
user: {
email: unique_email,
password: 'password123',
password_confirmation: 'password123'
}
}
user = User.find_by(email: unique_email)
expect(user.utm_source).to be_nil
expect(user.utm_medium).to be_nil
expect(user.utm_campaign).to eq('campaign_only')
end
it 'works with family invitations' do
get new_user_registration_path, params: utm_params.merge(invitation_token: invitation.token)
post user_registration_path, params: {
user: {
email: invitation.email,
password: 'password123',
password_confirmation: 'password123'
},
invitation_token: invitation.token
}
user = User.find_by(email: invitation.email)
expect(user.utm_source).to eq('google')
expect(user.utm_campaign).to eq('winter_2025')
expect(user.family).to eq(family)
end
end
context 'when self-hosted mode is enabled' do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with('SELF_HOSTED').and_return('true')
end
it 'does not capture UTM parameters' do
# With valid invitation to allow registration in self-hosted mode
get new_user_registration_path, params: utm_params.merge(invitation_token: invitation.token)
expect(session[:utm_source]).to be_nil
expect(session[:utm_medium]).to be_nil
expect(session[:utm_campaign]).to be_nil
end
it 'does not store UTM parameters in user record' do
# With valid invitation to allow registration in self-hosted mode
get new_user_registration_path, params: utm_params.merge(invitation_token: invitation.token)
post user_registration_path, params: {
user: {
email: invitation.email,
password: 'password123',
password_confirmation: 'password123'
},
invitation_token: invitation.token
}
user = User.find_by(email: invitation.email)
expect(user.utm_source).to be_nil
expect(user.utm_medium).to be_nil
expect(user.utm_campaign).to be_nil
expect(user.utm_term).to be_nil
expect(user.utm_content).to be_nil
end
end
end
end end