Compare commits

...

8 commits

Author SHA1 Message Date
Diogo Correia
598ebc1861
Merge 35f4c0f1f6 into 8b03b0c7f5 2025-07-20 19:56:28 +02:00
Eugene Burmakin
8b03b0c7f5 Recalculate stats after changing distance units 2025-07-20 19:14:20 +02:00
Eugene Burmakin
f969d5d3e6 Clean up some mess 2025-07-20 18:57:53 +02:00
Eugene Burmakin
708bca26eb Fix owntracks point creation 2025-07-20 17:43:55 +02:00
Eugene Burmakin
45713f46dc Fix domain in development and production 2025-07-20 17:31:31 +02:00
Evgenii Burmakin
3149767675
Merge pull request #1531 from Freika/fix/map-tracks-popup
Fix/map tracks popup
2025-07-20 17:31:26 +02:00
Eugene Burmakin
002b3bd635 Fix settings controller spec and tracks popup 2025-07-20 17:06:45 +02:00
Diogo Correia
35f4c0f1f6
fix: use db parameter when constructing redis client
Fixes #1507
2025-07-09 19:39:08 +01:00
39 changed files with 450 additions and 355 deletions

View file

@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [0.29.2] - 2025-07-12 # [0.29.2] - 2025-07-12
⚠️ If you were using RC, please run the following commands in the console, otherwise read on. ⚠️
```ruby
# This will delete all tracks 👇
Track.delete_all
# This will remove all tracks relations from points 👇
Point.update_all(track_id: nil)
# This will create tracks for all users 👇
User.find_each do |user|
Tracks::CreateJob.perform_later(user.id, start_at: nil, end_at: nil, mode: :bulk)
end
```
## Added ## Added
- In the User Settings -> Background Jobs, you can now disable visits suggestions, which is enabled by default. It's a background task that runs every day around midnight. Disabling it might be useful if you don't want to receive visits suggestions or if you're using the Dawarich iOS app, which has its own visits suggestions. - In the User Settings -> Background Jobs, you can now disable visits suggestions, which is enabled by default. It's a background task that runs every day around midnight. Disabling it might be useful if you don't want to receive visits suggestions or if you're using the Dawarich iOS app, which has its own visits suggestions.
@ -56,6 +71,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Fixed ## Fixed
- Swagger documentation is now valid again. - Swagger documentation is now valid again.
- Invalid owntracks points are now ignored.
# [0.29.1] - 2025-07-02 # [0.29.1] - 2025-07-02

View file

@ -5,7 +5,7 @@ class Api::V1::SettingsController < ApiController
def index def index
render json: { render json: {
settings: current_api_user.settings, settings: current_api_user.safe_settings,
status: 'success' status: 'success'
}, status: :ok }, status: :ok
end end

View file

@ -31,7 +31,8 @@ class MapController < ApplicationController
def build_tracks def build_tracks
track_ids = extract_track_ids track_ids = extract_track_ids
TrackSerializer.new(current_user, track_ids).call
TracksSerializer.new(current_user, track_ids).call
end end
def calculate_distance def calculate_distance

View file

@ -30,7 +30,7 @@ export function createTrackPopupContent(track, distanceUnit) {
<strong>🕐 Start:</strong> ${startTime}<br> <strong>🕐 Start:</strong> ${startTime}<br>
<strong>🏁 End:</strong> ${endTime}<br> <strong>🏁 End:</strong> ${endTime}<br>
<strong> Duration:</strong> ${durationFormatted}<br> <strong> Duration:</strong> ${durationFormatted}<br>
<strong>📏 Distance:</strong> ${formatDistance(track.distance, distanceUnit)}<br> <strong>📏 Distance:</strong> ${formatDistance(track.distance / 1000, distanceUnit)}<br>
<strong> Avg Speed:</strong> ${formatSpeed(track.avg_speed, distanceUnit)}<br> <strong> Avg Speed:</strong> ${formatSpeed(track.avg_speed, distanceUnit)}<br>
<strong> Elevation:</strong> +${track.elevation_gain || 0}m / -${track.elevation_loss || 0}m<br> <strong> Elevation:</strong> +${track.elevation_gain || 0}m / -${track.elevation_loss || 0}m<br>
<strong>📊 Max Alt:</strong> ${track.elevation_max || 0}m<br> <strong>📊 Max Alt:</strong> ${track.elevation_max || 0}m<br>

View file

@ -4,7 +4,7 @@ class BulkStatsCalculatingJob < ApplicationJob
queue_as :stats queue_as :stats
def perform def perform
user_ids = User.pluck(:id) user_ids = User.active.pluck(:id)
user_ids.each do |user_id| user_ids.each do |user_id|
Stats::BulkCalculator.new(user_id).call Stats::BulkCalculator.new(user_id).call

View file

@ -8,7 +8,7 @@ class Owntracks::PointCreatingJob < ApplicationJob
def perform(point_params, user_id) def perform(point_params, user_id)
parsed_params = OwnTracks::Params.new(point_params).call parsed_params = OwnTracks::Params.new(point_params).call
return if parsed_params[:timestamp].nil? || parsed_params[:lonlat].nil? return if parsed_params.try(:[], :timestamp).nil? || parsed_params.try(:[], :lonlat).nil?
return if point_exists?(parsed_params, user_id) return if point_exists?(parsed_params, user_id)
Point.create!(parsed_params.merge(user_id:)) Point.create!(parsed_params.merge(user_id:))

View file

@ -6,20 +6,7 @@ class Tracks::CreateJob < ApplicationJob
def perform(user_id, start_at: nil, end_at: nil, mode: :daily) def perform(user_id, start_at: nil, end_at: nil, mode: :daily)
user = User.find(user_id) user = User.find(user_id)
# Translate mode parameter to Generator mode tracks_created = Tracks::Generator.new(user, start_at:, end_at:, mode:).call
generator_mode = case mode
when :daily then :daily
when :none then :incremental
else :bulk
end
# Generate tracks and get the count of tracks created
tracks_created = Tracks::Generator.new(
user,
start_at: start_at,
end_at: end_at,
mode: generator_mode
).call
create_success_notification(user, tracks_created) create_success_notification(user, tracks_created)
rescue StandardError => e rescue StandardError => e

View file

@ -19,7 +19,6 @@
# track.distance # => 5000 (meters stored in DB) # track.distance # => 5000 (meters stored in DB)
# track.distance_in_unit('km') # => 5.0 (converted to km) # track.distance_in_unit('km') # => 5.0 (converted to km)
# track.distance_in_unit('mi') # => 3.11 (converted to miles) # track.distance_in_unit('mi') # => 3.11 (converted to miles)
# track.formatted_distance('km') # => "5.0 km"
# #
module DistanceConvertible module DistanceConvertible
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -38,21 +37,11 @@ module DistanceConvertible
distance.to_f / conversion_factor distance.to_f / conversion_factor
end end
def formatted_distance(unit, precision: 2)
converted_distance = distance_in_unit(unit)
"#{converted_distance.round(precision)} #{unit}"
end
def distance_for_user(user) def distance_for_user(user)
user_unit = user.safe_settings.distance_unit user_unit = user.safe_settings.distance_unit
distance_in_unit(user_unit) distance_in_unit(user_unit)
end end
def formatted_distance_for_user(user, precision: 2)
user_unit = user.safe_settings.distance_unit
formatted_distance(user_unit, precision: precision)
end
module ClassMethods module ClassMethods
def convert_distance(distance_meters, unit) def convert_distance(distance_meters, unit)
return 0.0 unless distance_meters.present? return 0.0 unless distance_meters.present?
@ -66,10 +55,5 @@ module DistanceConvertible
distance_meters.to_f / conversion_factor distance_meters.to_f / conversion_factor
end end
def format_distance(distance_meters, unit, precision: 2)
converted = convert_distance(distance_meters, unit)
"#{converted.round(precision)} #{unit}"
end
end end
end end

View file

@ -34,7 +34,7 @@ class Point < ApplicationRecord
after_create :set_country after_create :set_country
after_create_commit :broadcast_coordinates 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 after_commit :recalculate_track, on: :update, if: -> { track.present? }
def self.without_raw_data def self.without_raw_data
select(column_names - ['raw_data']) select(column_names - ['raw_data'])
@ -99,8 +99,6 @@ class Point < ApplicationRecord
end end
def recalculate_track def recalculate_track
return unless track.present?
track.recalculate_path_and_distance! track.recalculate_path_and_distance!
end end

View file

@ -1,38 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
class TrackSerializer class TrackSerializer
def initialize(user, track_ids) def initialize(track)
@user = user @track = track
@track_ids = track_ids
end end
def call def call
return [] if track_ids.empty?
tracks = user.tracks
.where(id: track_ids)
.order(start_at: :asc)
tracks.map { |track| serialize_track_data(track) }
end
private
attr_reader :user, :track_ids
def serialize_track_data(track)
{ {
id: track.id, id: @track.id,
start_at: track.start_at.iso8601, start_at: @track.start_at.iso8601,
end_at: track.end_at.iso8601, end_at: @track.end_at.iso8601,
distance: track.distance.to_i, distance: @track.distance.to_i,
avg_speed: track.avg_speed.to_f, avg_speed: @track.avg_speed.to_f,
duration: track.duration, duration: @track.duration,
elevation_gain: track.elevation_gain, elevation_gain: @track.elevation_gain,
elevation_loss: track.elevation_loss, elevation_loss: @track.elevation_loss,
elevation_max: track.elevation_max, elevation_max: @track.elevation_max,
elevation_min: track.elevation_min, elevation_min: @track.elevation_min,
original_path: track.original_path.to_s original_path: @track.original_path.to_s
} }
end end
end end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class TracksSerializer
def initialize(user, track_ids)
@user = user
@track_ids = track_ids
end
def call
return [] if track_ids.empty?
tracks = user.tracks
.where(id: track_ids)
.order(start_at: :asc)
tracks.map { |track| TrackSerializer.new(track).call }
end
private
attr_reader :user, :track_ids
end

View file

@ -10,6 +10,8 @@ class OwnTracks::Params
# rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/AbcSize
def call def call
return unless valid_point?
{ {
lonlat: "POINT(#{params[:lon]} #{params[:lat]})", lonlat: "POINT(#{params[:lon]} #{params[:lat]})",
battery: params[:batt], battery: params[:batt],
@ -84,4 +86,8 @@ class OwnTracks::Params
def owntracks_point? def owntracks_point?
params[:topic].present? params[:topic].present?
end end
def valid_point?
params[:lon].present? && params[:lat].present? && params[:tst].present?
end
end end

View file

@ -7,7 +7,7 @@ module Places
end end
def call def call
geodata = Geocoder.search([@place.lat, @place.lon], units: :km, limit: 1, distance_sort: true).first geodata = Geocoder.search([place.lat, place.lon], units: :km, limit: 1, distance_sort: true).first
return if geodata.blank? return if geodata.blank?
@ -15,21 +15,29 @@ module Places
return if properties.blank? return if properties.blank?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@place.name = properties['name'] if properties['name'].present? update_place_name(properties, geodata)
@place.city = properties['city'] if properties['city'].present?
@place.country = properties['country'] if properties['country'].present?
@place.geodata = geodata.data if DawarichSettings.store_geodata?
@place.save!
if properties['name'].present? update_visits_name(properties) if properties['name'].present?
@place
.visits
.where(name: Place::DEFAULT_NAME)
.update_all(name: properties['name'])
end
@place place
end end
end end
private
attr_reader :place
def update_place_name(properties, geodata)
place.name = properties['name'] if properties['name'].present?
place.city = properties['city'] if properties['city'].present?
place.country = properties['country'] if properties['country'].present?
place.geodata = geodata.data if DawarichSettings.store_geodata?
place.save!
end
def update_visits_name(properties)
place.visits.where(name: Place::DEFAULT_NAME).update_all(name: properties['name'])
end
end end
end end

View file

@ -48,6 +48,7 @@ class Tracks::Generator
Rails.logger.debug "Generator: created #{segments.size} segments" Rails.logger.debug "Generator: created #{segments.size} segments"
tracks_created = 0 tracks_created = 0
segments.each do |segment| segments.each do |segment|
track = create_track_from_segment(segment) track = create_track_from_segment(segment)
tracks_created += 1 if track tracks_created += 1 if track
@ -146,10 +147,6 @@ class Tracks::Generator
day.beginning_of_day.to_i..day.end_of_day.to_i day.beginning_of_day.to_i..day.end_of_day.to_i
end end
def incremental_mode?
mode == :incremental
end
def clean_existing_tracks def clean_existing_tracks
case mode case mode
when :bulk then clean_bulk_tracks when :bulk then clean_bulk_tracks

View file

@ -36,12 +36,7 @@ class Tracks::IncrementalProcessor
start_at = find_start_time start_at = find_start_time
end_at = find_end_time end_at = find_end_time
Tracks::CreateJob.perform_later( Tracks::CreateJob.perform_later(user.id, start_at:, end_at:, mode: :incremental)
user.id,
start_at: start_at,
end_at: end_at,
mode: :none
)
end end
private private

View file

@ -77,7 +77,7 @@ module Tracks::Segmentation
return true if time_diff_seconds > time_threshold_seconds return true if time_diff_seconds > time_threshold_seconds
# Check distance threshold - convert km to meters to match frontend logic # Check distance threshold - convert km to meters to match frontend logic
distance_km = calculate_distance_kilometers_between_points(previous_point, current_point) distance_km = calculate_km_distance_between_points(previous_point, current_point)
distance_meters = distance_km * 1000 # Convert km to meters distance_meters = distance_km * 1000 # Convert km to meters
return true if distance_meters > distance_threshold_meters return true if distance_meters > distance_threshold_meters
@ -85,7 +85,7 @@ module Tracks::Segmentation
false false
end end
def calculate_distance_kilometers_between_points(point1, point2) def calculate_km_distance_between_points(point1, point2)
lat1, lon1 = point_coordinates(point1) lat1, lon1 = point_coordinates(point1)
lat2, lon2 = point_coordinates(point2) lat2, lon2 = point_coordinates(point2)

View file

@ -113,7 +113,6 @@ class Users::SafeSettings
end end
def distance_unit def distance_unit
# km or mi
settings.dig('maps', 'distance_unit') settings.dig('maps', 'distance_unit')
end end

View file

@ -50,7 +50,7 @@
</div> </div>
</div> </div>
<div class="card bg-base-300 w-96 shadow-xl m-5"> <div class="card bg-base-300 w-96 shadow-xl m-5">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">Visits suggestions</h2> <h2 class="card-title">Visits suggestions</h2>
<p>Enable or disable visits suggestions. It's a background task that runs every day at midnight. Disabling it might be useful if you don't want to receive visits suggestions or if you're using the Dawarich iOS app, which has its own visits suggestions.</p> <p>Enable or disable visits suggestions. It's a background task that runs every day at midnight. Disabling it might be useful if you don't want to receive visits suggestions or if you're using the Dawarich iOS app, which has its own visits suggestions.</p>

View file

@ -1,11 +1,13 @@
development: development:
adapter: redis adapter: redis
url: <%= "#{ENV.fetch("REDIS_URL")}/#{ENV.fetch('RAILS_WS_DB', 2)}" %> url: <%= "#{ENV.fetch("REDIS_URL")}" %>
db: <%= "#{ENV.fetch('RAILS_WS_DB', 2)}" %>
test: test:
adapter: test adapter: test
production: production:
adapter: redis adapter: redis
url: <%= "#{ENV.fetch("REDIS_URL")}/#{ENV.fetch('RAILS_WS_DB', 2)}" %> url: <%= "#{ENV.fetch("REDIS_URL")}" %>
db: <%= "#{ENV.fetch('RAILS_WS_DB', 2)}" %>
channel_prefix: dawarich_production channel_prefix: dawarich_production

View file

@ -26,7 +26,7 @@ Rails.application.configure do
# Enable/disable caching. By default caching is disabled. # Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching. # Run rails dev:cache to toggle caching.
config.cache_store = :redis_cache_store, { url: "#{ENV['REDIS_URL']}/#{ENV.fetch('RAILS_CACHE_DB', 0)}" } config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], db: ENV.fetch('RAILS_CACHE_DB', 0) }
if Rails.root.join('tmp/caching-dev.txt').exist? if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
@ -88,7 +88,7 @@ Rails.application.configure do
hosts = ENV.fetch('APPLICATION_HOSTS', 'localhost').split(',') hosts = ENV.fetch('APPLICATION_HOSTS', 'localhost').split(',')
config.action_mailer.default_url_options = { host: ENV['SMTP_DOMAIN'] || hosts.first, port: ENV.fetch('PORT', 3000) } config.action_mailer.default_url_options = { host: ENV['DOMAIN'] || hosts.first, port: ENV.fetch('PORT', 3000) }
config.hosts.concat(hosts) if hosts.present? config.hosts.concat(hosts) if hosts.present?

View file

@ -73,7 +73,7 @@ Rails.application.configure do
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info') config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info')
# Use a different cache store in production. # Use a different cache store in production.
config.cache_store = :redis_cache_store, { url: "#{ENV['REDIS_URL']}/#{ENV.fetch('RAILS_CACHE_DB', 0)}" } config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], db: ENV.fetch('RAILS_CACHE_DB', 0) }
# Use a real queuing backend for Active Job (and separate queues per environment). # Use a real queuing backend for Active Job (and separate queues per environment).
config.active_job.queue_adapter = :sidekiq config.active_job.queue_adapter = :sidekiq
@ -103,7 +103,7 @@ Rails.application.configure do
config.host_authorization = { exclude: ->(request) { request.path == "/api/v1/health" } } config.host_authorization = { exclude: ->(request) { request.path == "/api/v1/health" } }
hosts = ENV.fetch('APPLICATION_HOSTS', 'localhost').split(',') hosts = ENV.fetch('APPLICATION_HOSTS', 'localhost').split(',')
config.action_mailer.default_url_options = { host: ENV['SMTP_DOMAIN'] } config.action_mailer.default_url_options = { host: ENV['DOMAIN'] }
config.hosts.concat(hosts) if hosts.present? config.hosts.concat(hosts) if hosts.present?
config.action_mailer.delivery_method = :smtp config.action_mailer.delivery_method = :smtp

View file

@ -5,11 +5,11 @@ SELF_HOSTED = ENV.fetch('SELF_HOSTED', 'true') == 'true'
MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i
DISTANCE_UNITS = { DISTANCE_UNITS = {
km: 1000, # to meters km: 1000, # to meters
mi: 1609.34, # to meters mi: 1609.34, # to meters
m: 1, # already in meters m: 1, # already in meters
ft: 0.3048, # to meters ft: 0.3048, # to meters
yd: 0.9144 # to meters yd: 0.9144 # to meters
}.freeze }.freeze
APP_VERSION = File.read('.app_version').strip APP_VERSION = File.read('.app_version').strip

View file

@ -4,7 +4,7 @@ settings = {
debug_mode: true, debug_mode: true,
timeout: 5, timeout: 5,
units: :km, units: :km,
cache: Redis.new(url: "#{ENV['REDIS_URL']}/#{ENV.fetch('RAILS_CACHE_DB', 0)}"), cache: Redis.new(url: ENV['REDIS_URL'], db: ENV.fetch('RAILS_CACHE_DB', 0)),
always_raise: :all, always_raise: :all,
http_headers: { http_headers: {
'User-Agent' => "Dawarich #{APP_VERSION} (https://dawarich.app)" 'User-Agent' => "Dawarich #{APP_VERSION} (https://dawarich.app)"

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = { url: "#{ENV['REDIS_URL']}/#{ENV.fetch('RAILS_JOB_QUEUE_DB', 1)}" } config.redis = { url: ENV['REDIS_URL'], db: ENV.fetch('RAILS_JOB_QUEUE_DB', 1) }
config.logger = Sidekiq::Logger.new($stdout) config.logger = Sidekiq::Logger.new($stdout)
if ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true' if ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true'
@ -24,7 +24,7 @@ Sidekiq.configure_server do |config|
end end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.redis = { url: "#{ENV['REDIS_URL']}/#{ENV.fetch('RAILS_JOB_QUEUE_DB', 1)}" } config.redis = { url: ENV['REDIS_URL'], db: ENV.fetch('RAILS_JOB_QUEUE_DB', 1) }
end end
Sidekiq::Queue['reverse_geocoding'].limit = 1 if Sidekiq.server? && DawarichSettings.photon_uses_komoot_io? Sidekiq::Queue['reverse_geocoding'].limit = 1 if Sidekiq.server? && DawarichSettings.photon_uses_komoot_io?

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class RecalculateStatsAfterChangingDistanceUnits < ActiveRecord::Migration[8.0]
def up
BulkStatsCalculatingJob.perform_later
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View file

@ -1 +1 @@
DataMigrate::Data.define(version: 20250709195003) DataMigrate::Data.define(version: 20250720171241)

View file

@ -4,13 +4,13 @@ require 'rails_helper'
RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do
describe '#perform' do describe '#perform' do
let!(:user) { create(:user) } let(:user) { create(:user) }
let!(:area) { create(:area, user: user) } let(:area) { create(:area, user: user) }
it 'calls the AreaVisitsCalculationService' do it 'calls the AreaVisitsCalculationService' do
expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id) expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id).and_call_original
described_class.new.perform_now described_class.new.perform
end end
end end
end end

View file

@ -28,5 +28,13 @@ RSpec.describe Owntracks::PointCreatingJob, type: :job do
expect { perform }.not_to(change { Point.count }) expect { perform }.not_to(change { Point.count })
end end
end end
context 'when point is invalid' do
let(:point_params) { { lat: 1.0, lon: 1.0, tid: 'test', tst: nil, topic: 'iPhone 12 pro' } }
it 'does not create a point' do
expect { perform }.not_to(change { Point.count })
end
end
end end
end end

View file

@ -14,12 +14,10 @@ RSpec.describe Tracks::CreateJob, type: :job do
allow(generator_instance).to receive(:call) allow(generator_instance).to receive(:call)
allow(Notifications::Create).to receive(:new).and_return(notification_service) allow(Notifications::Create).to receive(:new).and_return(notification_service)
allow(notification_service).to receive(:call) allow(notification_service).to receive(:call)
allow(generator_instance).to receive(:call).and_return(2)
end end
it 'calls the generator and creates a notification' do it 'calls the generator and creates a notification' do
# Mock the generator to return the count of tracks created
allow(generator_instance).to receive(:call).and_return(2)
described_class.new.perform(user.id) described_class.new.perform(user.id)
expect(Tracks::Generator).to have_received(:new).with( expect(Tracks::Generator).to have_received(:new).with(
@ -48,12 +46,10 @@ RSpec.describe Tracks::CreateJob, type: :job do
allow(generator_instance).to receive(:call) allow(generator_instance).to receive(:call)
allow(Notifications::Create).to receive(:new).and_return(notification_service) allow(Notifications::Create).to receive(:new).and_return(notification_service)
allow(notification_service).to receive(:call) allow(notification_service).to receive(:call)
allow(generator_instance).to receive(:call).and_return(1)
end end
it 'passes custom parameters to the generator' do it 'passes custom parameters to the generator' do
# Mock generator to return the count of tracks created
allow(generator_instance).to receive(:call).and_return(1)
described_class.new.perform(user.id, start_at: start_at, end_at: end_at, mode: mode) described_class.new.perform(user.id, start_at: start_at, end_at: end_at, mode: mode)
expect(Tracks::Generator).to have_received(:new).with( expect(Tracks::Generator).to have_received(:new).with(
@ -73,72 +69,6 @@ RSpec.describe Tracks::CreateJob, type: :job do
end end
end end
context 'with mode translation' do
before do
allow(Tracks::Generator).to receive(:new).and_return(generator_instance)
allow(generator_instance).to receive(:call) # No tracks created for mode tests
allow(Notifications::Create).to receive(:new).and_return(notification_service)
allow(notification_service).to receive(:call)
end
it 'translates :none to :incremental' do
allow(generator_instance).to receive(:call).and_return(0)
described_class.new.perform(user.id, mode: :none)
expect(Tracks::Generator).to have_received(:new).with(
user,
start_at: nil,
end_at: nil,
mode: :incremental
)
expect(Notifications::Create).to have_received(:new).with(
user: user,
kind: :info,
title: 'Tracks Generated',
content: 'Created 0 tracks from your location data. Check your tracks section to view them.'
)
end
it 'translates :daily to :daily' do
allow(generator_instance).to receive(:call).and_return(0)
described_class.new.perform(user.id, mode: :daily)
expect(Tracks::Generator).to have_received(:new).with(
user,
start_at: nil,
end_at: nil,
mode: :daily
)
expect(Notifications::Create).to have_received(:new).with(
user: user,
kind: :info,
title: 'Tracks Generated',
content: 'Created 0 tracks from your location data. Check your tracks section to view them.'
)
end
it 'translates other modes to :bulk' do
allow(generator_instance).to receive(:call).and_return(0)
described_class.new.perform(user.id, mode: :replace)
expect(Tracks::Generator).to have_received(:new).with(
user,
start_at: nil,
end_at: nil,
mode: :bulk
)
expect(Notifications::Create).to have_received(:new).with(
user: user,
kind: :info,
title: 'Tracks Generated',
content: 'Created 0 tracks from your location data. Check your tracks section to view them.'
)
end
end
context 'when generator raises an error' do context 'when generator raises an error' do
let(:error_message) { 'Something went wrong' } let(:error_message) { 'Something went wrong' }
let(:notification_service) { instance_double(Notifications::Create) } let(:notification_service) { instance_double(Notifications::Create) }
@ -175,12 +105,13 @@ RSpec.describe Tracks::CreateJob, type: :job do
end end
context 'when user does not exist' do context 'when user does not exist' do
it 'handles the error gracefully and creates error notification' do before do
allow(User).to receive(:find).with(999).and_raise(ActiveRecord::RecordNotFound) allow(User).to receive(:find).with(999).and_raise(ActiveRecord::RecordNotFound)
allow(ExceptionReporter).to receive(:call) allow(ExceptionReporter).to receive(:call)
allow(Notifications::Create).to receive(:new).and_return(instance_double(Notifications::Create, call: nil)) allow(Notifications::Create).to receive(:new).and_return(instance_double(Notifications::Create, call: nil))
end
# Should not raise an error because it's caught by the rescue block it 'handles the error gracefully and creates error notification' do
expect { described_class.new.perform(999) }.not_to raise_error expect { described_class.new.perform(999) }.not_to raise_error
expect(ExceptionReporter).to have_received(:call) expect(ExceptionReporter).to have_received(:call)
@ -188,15 +119,14 @@ RSpec.describe Tracks::CreateJob, type: :job do
end end
context 'when tracks are deleted and recreated' do context 'when tracks are deleted and recreated' do
it 'returns the correct count of newly created tracks' do let(:existing_tracks) { create_list(:track, 3, user: user) }
# Create some existing tracks first
create_list(:track, 3, user: user)
# Mock the generator to simulate deleting existing tracks and creating new ones before do
# This should return the count of newly created tracks, not the difference
allow(generator_instance).to receive(:call).and_return(2) allow(generator_instance).to receive(:call).and_return(2)
end
described_class.new.perform(user.id, mode: :bulk) it 'returns the correct count of newly created tracks' do
described_class.new.perform(user.id, mode: :incremental)
expect(Tracks::Generator).to have_received(:new).with( expect(Tracks::Generator).to have_received(:new).with(
user, user,

View file

@ -160,7 +160,7 @@ RSpec.describe Trip, type: :model do
end end
end end
describe '#recalculate_distance!' do describe '#recalculate_distance!' do
it 'recalculates and saves the distance' do it 'recalculates and saves the distance' do
original_distance = trip.distance original_distance = trip.distance

View file

@ -41,9 +41,6 @@ RSpec.configure do |config|
config.before(:suite) do config.before(:suite) do
Rails.application.reload_routes! Rails.application.reload_routes!
# DatabaseCleaner.strategy = :transaction
# DatabaseCleaner.clean_with(:truncation)
end end
config.before do config.before do
@ -92,12 +89,6 @@ RSpec.configure do |config|
config.after(:suite) do config.after(:suite) do
Rake::Task['rswag:generate'].invoke Rake::Task['rswag:generate'].invoke
end end
# config.around(:each) do |example|
# DatabaseCleaner.cleaning do
# example.run
# end
# end
end end
Shoulda::Matchers.configure do |config| Shoulda::Matchers.configure do |config|

View file

@ -5,95 +5,166 @@ require 'rails_helper'
RSpec.describe TrackSerializer do RSpec.describe TrackSerializer do
describe '#call' do describe '#call' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:track) { create(:track, user: user) }
let(:serializer) { described_class.new(track) }
context 'when serializing user tracks with track IDs' do subject(:serialized_track) { serializer.call }
subject(:serializer) { described_class.new(user, track_ids).call }
let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) } it 'returns a hash with all required attributes' do
let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) } expect(serialized_track).to be_a(Hash)
let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) } expect(serialized_track.keys).to contain_exactly(
let(:track_ids) { [track1.id, track2.id] } :id, :start_at, :end_at, :distance, :avg_speed, :duration,
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path
)
end
it 'returns an array of serialized tracks' do it 'serializes the track ID correctly' do
expect(serializer).to be_an(Array) expect(serialized_track[:id]).to eq(track.id)
expect(serializer.length).to eq(2) end
end
it 'serializes each track correctly' do it 'formats start_at as ISO8601 timestamp' do
serialized_ids = serializer.map { |track| track[:id] } expect(serialized_track[:start_at]).to eq(track.start_at.iso8601)
expect(serialized_ids).to contain_exactly(track1.id, track2.id) expect(serialized_track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
expect(serialized_ids).not_to include(track3.id) end
end
it 'formats timestamps as ISO8601 for all tracks' do it 'formats end_at as ISO8601 timestamp' do
serializer.each do |track| expect(serialized_track[:end_at]).to eq(track.end_at.iso8601)
expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) expect(serialized_track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) end
end
end
it 'includes all required fields for each track' do it 'converts distance to integer' do
serializer.each do |track| expect(serialized_track[:distance]).to eq(track.distance.to_i)
expect(track.keys).to contain_exactly( expect(serialized_track[:distance]).to be_a(Integer)
:id, :start_at, :end_at, :distance, :avg_speed, :duration, end
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path
)
end
end
it 'handles numeric values correctly' do it 'converts avg_speed to float' do
serializer.each do |track| expect(serialized_track[:avg_speed]).to eq(track.avg_speed.to_f)
expect(track[:distance]).to be_a(Numeric) expect(serialized_track[:avg_speed]).to be_a(Float)
expect(track[:avg_speed]).to be_a(Numeric) end
expect(track[:duration]).to be_a(Numeric)
expect(track[:elevation_gain]).to be_a(Numeric)
expect(track[:elevation_loss]).to be_a(Numeric)
expect(track[:elevation_max]).to be_a(Numeric)
expect(track[:elevation_min]).to be_a(Numeric)
end
end
it 'orders tracks by start_at in ascending order' do it 'serializes duration as numeric value' do
serialized_tracks = serializer expect(serialized_track[:duration]).to eq(track.duration)
expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago expect(serialized_track[:duration]).to be_a(Numeric)
expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago end
it 'serializes elevation_gain as numeric value' do
expect(serialized_track[:elevation_gain]).to eq(track.elevation_gain)
expect(serialized_track[:elevation_gain]).to be_a(Numeric)
end
it 'serializes elevation_loss as numeric value' do
expect(serialized_track[:elevation_loss]).to eq(track.elevation_loss)
expect(serialized_track[:elevation_loss]).to be_a(Numeric)
end
it 'serializes elevation_max as numeric value' do
expect(serialized_track[:elevation_max]).to eq(track.elevation_max)
expect(serialized_track[:elevation_max]).to be_a(Numeric)
end
it 'serializes elevation_min as numeric value' do
expect(serialized_track[:elevation_min]).to eq(track.elevation_min)
expect(serialized_track[:elevation_min]).to be_a(Numeric)
end
it 'converts original_path to string' do
expect(serialized_track[:original_path]).to eq(track.original_path.to_s)
expect(serialized_track[:original_path]).to be_a(String)
end
context 'with decimal distance values' do
let(:track) { create(:track, user: user, distance: 1234.56) }
it 'truncates distance to integer' do
expect(serialized_track[:distance]).to eq(1234)
end end
end end
context 'when track IDs belong to different users' do context 'with decimal avg_speed values' do
subject(:serializer) { described_class.new(user, track_ids).call } let(:track) { create(:track, user: user, avg_speed: 25.75) }
let(:other_user) { create(:user) } it 'converts avg_speed to float' do
let!(:user_track) { create(:track, user: user) } expect(serialized_track[:avg_speed]).to eq(25.75)
let!(:other_user_track) { create(:track, user: other_user) }
let(:track_ids) { [user_track.id, other_user_track.id] }
it 'only returns tracks belonging to the specified user' do
serialized_ids = serializer.map { |track| track[:id] }
expect(serialized_ids).to contain_exactly(user_track.id)
expect(serialized_ids).not_to include(other_user_track.id)
end end
end end
context 'when track IDs array is empty' do context 'with different original_path formats' do
subject(:serializer) { described_class.new(user, []).call } let(:track) { create(:track, user: user, original_path: 'LINESTRING(0 0, 1 1, 2 2)') }
it 'returns an empty array' do it 'converts geometry to WKT string format' do
expect(serializer).to eq([]) expect(serialized_track[:original_path]).to eq('LINESTRING (0 0, 1 1, 2 2)')
expect(serialized_track[:original_path]).to be_a(String)
end end
end end
context 'when track IDs contain non-existent IDs' do context 'with zero values' do
subject(:serializer) { described_class.new(user, track_ids).call } let(:track) do
create(:track, user: user,
distance: 0,
avg_speed: 0.0,
duration: 0,
elevation_gain: 0,
elevation_loss: 0,
elevation_max: 0,
elevation_min: 0)
end
let!(:existing_track) { create(:track, user: user) } it 'handles zero values correctly' do
let(:track_ids) { [existing_track.id, 999999] } expect(serialized_track[:distance]).to eq(0)
expect(serialized_track[:avg_speed]).to eq(0.0)
expect(serialized_track[:duration]).to eq(0)
expect(serialized_track[:elevation_gain]).to eq(0)
expect(serialized_track[:elevation_loss]).to eq(0)
expect(serialized_track[:elevation_max]).to eq(0)
expect(serialized_track[:elevation_min]).to eq(0)
end
end
it 'only returns existing tracks' do context 'with very large values' do
serialized_ids = serializer.map { |track| track[:id] } let(:track) do
expect(serialized_ids).to contain_exactly(existing_track.id) create(:track, user: user,
expect(serializer.length).to eq(1) distance: 1_000_000.0,
avg_speed: 999.99,
duration: 86_400, # 24 hours in seconds
elevation_gain: 10_000,
elevation_loss: 8_000,
elevation_max: 5_000,
elevation_min: 0)
end
it 'handles large values correctly' do
expect(serialized_track[:distance]).to eq(1_000_000)
expect(serialized_track[:avg_speed]).to eq(999.99)
expect(serialized_track[:duration]).to eq(86_400)
expect(serialized_track[:elevation_gain]).to eq(10_000)
expect(serialized_track[:elevation_loss]).to eq(8_000)
expect(serialized_track[:elevation_max]).to eq(5_000)
expect(serialized_track[:elevation_min]).to eq(0)
end
end
context 'with different timestamp formats' do
let(:start_time) { Time.current }
let(:end_time) { start_time + 1.hour }
let(:track) { create(:track, user: user, start_at: start_time, end_at: end_time) }
it 'formats timestamps consistently' do
expect(serialized_track[:start_at]).to eq(start_time.iso8601)
expect(serialized_track[:end_at]).to eq(end_time.iso8601)
end end
end end
end end
describe '#initialize' do
let(:track) { create(:track) }
it 'accepts a track parameter' do
expect { described_class.new(track) }.not_to raise_error
end
it 'stores the track instance' do
serializer = described_class.new(track)
expect(serializer.instance_variable_get(:@track)).to eq(track)
end
end
end end

View file

@ -0,0 +1,99 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe TracksSerializer do
describe '#call' do
let(:user) { create(:user) }
context 'when serializing user tracks with track IDs' do
subject(:serializer) { described_class.new(user, track_ids).call }
let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) }
let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) }
let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) }
let(:track_ids) { [track1.id, track2.id] }
it 'returns an array of serialized tracks' do
expect(serializer).to be_an(Array)
expect(serializer.length).to eq(2)
end
it 'serializes each track correctly' do
serialized_ids = serializer.map { |track| track[:id] }
expect(serialized_ids).to contain_exactly(track1.id, track2.id)
expect(serialized_ids).not_to include(track3.id)
end
it 'formats timestamps as ISO8601 for all tracks' do
serializer.each do |track|
expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
end
end
it 'includes all required fields for each track' do
serializer.each do |track|
expect(track.keys).to contain_exactly(
:id, :start_at, :end_at, :distance, :avg_speed, :duration,
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path
)
end
end
it 'handles numeric values correctly' do
serializer.each do |track|
expect(track[:distance]).to be_a(Numeric)
expect(track[:avg_speed]).to be_a(Numeric)
expect(track[:duration]).to be_a(Numeric)
expect(track[:elevation_gain]).to be_a(Numeric)
expect(track[:elevation_loss]).to be_a(Numeric)
expect(track[:elevation_max]).to be_a(Numeric)
expect(track[:elevation_min]).to be_a(Numeric)
end
end
it 'orders tracks by start_at in ascending order' do
serialized_tracks = serializer
expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago
expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago
end
end
context 'when track IDs belong to different users' do
subject(:serializer) { described_class.new(user, track_ids).call }
let(:other_user) { create(:user) }
let!(:user_track) { create(:track, user: user) }
let!(:other_user_track) { create(:track, user: other_user) }
let(:track_ids) { [user_track.id, other_user_track.id] }
it 'only returns tracks belonging to the specified user' do
serialized_ids = serializer.map { |track| track[:id] }
expect(serialized_ids).to contain_exactly(user_track.id)
expect(serialized_ids).not_to include(other_user_track.id)
end
end
context 'when track IDs array is empty' do
subject(:serializer) { described_class.new(user, []).call }
it 'returns an empty array' do
expect(serializer).to eq([])
end
end
context 'when track IDs contain non-existent IDs' do
subject(:serializer) { described_class.new(user, track_ids).call }
let!(:existing_track) { create(:track, user: user) }
let(:track_ids) { [existing_track.id, 999999] }
it 'only returns existing tracks' do
serialized_ids = serializer.map { |track| track[:id] }
expect(serialized_ids).to contain_exactly(existing_track.id)
expect(serializer.length).to eq(1)
end
end
end
end

View file

@ -185,5 +185,13 @@ RSpec.describe OwnTracks::Params do
expect(params[:trigger]).to eq('unknown') expect(params[:trigger]).to eq('unknown')
end end
end end
context 'when point is invalid' do
let(:raw_point_params) { super().merge(lon: nil, lat: nil, tst: nil) }
it 'returns parsed params' do
expect(params).to eq(nil)
end
end
end end
end end

View file

@ -30,7 +30,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'processes first point' do it 'processes first point' do
expect(Tracks::CreateJob).to receive(:perform_later) expect(Tracks::CreateJob).to receive(:perform_later)
.with(user.id, start_at: nil, end_at: nil, mode: :none) .with(user.id, start_at: nil, end_at: nil, mode: :incremental)
processor.call processor.call
end end
end end
@ -47,7 +47,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'processes when time threshold exceeded' do it 'processes when time threshold exceeded' do
expect(Tracks::CreateJob).to receive(:perform_later) expect(Tracks::CreateJob).to receive(:perform_later)
.with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :none) .with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental)
processor.call processor.call
end end
end end
@ -65,7 +65,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'uses existing track end time as start_at' do it 'uses existing track end time as start_at' do
expect(Tracks::CreateJob).to receive(:perform_later) expect(Tracks::CreateJob).to receive(:perform_later)
.with(user.id, start_at: existing_track.end_at, end_at: Time.zone.at(previous_point.timestamp), mode: :none) .with(user.id, start_at: existing_track.end_at, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental)
processor.call processor.call
end end
end end
@ -88,7 +88,7 @@ RSpec.describe Tracks::IncrementalProcessor do
it 'processes when distance threshold exceeded' do it 'processes when distance threshold exceeded' do
expect(Tracks::CreateJob).to receive(:perform_later) expect(Tracks::CreateJob).to receive(:perform_later)
.with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :none) .with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental)
processor.call processor.call
end end
end end

View file

@ -75,7 +75,6 @@ RSpec.describe Visits::Suggest do
end end
context 'when reverse geocoding is enabled' do context 'when reverse geocoding is enabled' do
# Use a different time range to avoid interference with main tests
let(:reverse_geocoding_start_at) { Time.zone.local(2020, 6, 1, 0, 0, 0) } let(:reverse_geocoding_start_at) { Time.zone.local(2020, 6, 1, 0, 0, 0) }
let(:reverse_geocoding_end_at) { Time.zone.local(2020, 6, 1, 2, 0, 0) } let(:reverse_geocoding_end_at) { Time.zone.local(2020, 6, 1, 2, 0, 0) }

View file

@ -2,7 +2,6 @@
RSpec.configure do |config| RSpec.configure do |config|
config.before(:each) do config.before(:each) do
# Clear the cache before each test
Rails.cache.clear Rails.cache.clear
end end
end end

View file

@ -21,8 +21,8 @@ describe 'Settings API', type: :request do
'immich_api_key': 'your-immich-api-key', 'immich_api_key': 'your-immich-api-key',
'photoprism_url': 'https://photoprism.example.com', 'photoprism_url': 'https://photoprism.example.com',
'photoprism_api_key': 'your-photoprism-api-key', 'photoprism_api_key': 'your-photoprism-api-key',
'maps': { 'distance_unit': 'km' }, 'speed_color_scale': 'viridis',
'visits_suggestions_enabled': true 'fog_of_war_threshold': 100
} }
} }
tags 'Settings' tags 'Settings'
@ -100,21 +100,15 @@ describe 'Settings API', type: :request do
example: 'your-photoprism-api-key', example: 'your-photoprism-api-key',
description: 'API key for PhotoPrism photo service' description: 'API key for PhotoPrism photo service'
}, },
maps: { speed_color_scale: {
type: :object, type: :string,
properties: { example: 'viridis',
distance_unit: { description: 'Color scale for speed-colored routes'
type: :string,
example: 'km',
description: 'Distance unit preference (km or miles)'
}
},
description: 'Map-related settings'
}, },
visits_suggestions_enabled: { fog_of_war_threshold: {
type: :boolean, type: :number,
example: true, example: 100,
description: 'Whether visit suggestions are enabled' description: 'Fog of war threshold value'
} }
} }
} }
@ -138,33 +132,33 @@ describe 'Settings API', type: :request do
type: :object, type: :object,
properties: { properties: {
route_opacity: { route_opacity: {
type: :string, type: :number,
example: '60', example: 60,
description: 'Route opacity percentage (0-100)' description: 'Route opacity percentage (0-100)'
}, },
meters_between_routes: { meters_between_routes: {
type: :string, type: :number,
example: '500', example: 500,
description: 'Minimum distance between routes in meters' description: 'Minimum distance between routes in meters'
}, },
minutes_between_routes: { minutes_between_routes: {
type: :string, type: :number,
example: '30', example: 30,
description: 'Minimum time between routes in minutes' description: 'Minimum time between routes in minutes'
}, },
fog_of_war_meters: { fog_of_war_meters: {
type: :string, type: :number,
example: '50', example: 50,
description: 'Fog of war radius in meters' description: 'Fog of war radius in meters'
}, },
time_threshold_minutes: { time_threshold_minutes: {
type: :string, type: :number,
example: '30', example: 30,
description: 'Time threshold for grouping points in minutes' description: 'Time threshold for grouping points in minutes'
}, },
merge_threshold_minutes: { merge_threshold_minutes: {
type: :string, type: :number,
example: '15', example: 15,
description: 'Threshold for merging nearby points in minutes' description: 'Threshold for merging nearby points in minutes'
}, },
preferred_map_layer: { preferred_map_layer: {
@ -207,21 +201,15 @@ describe 'Settings API', type: :request do
example: 'your-photoprism-api-key', example: 'your-photoprism-api-key',
description: 'API key for PhotoPrism photo service' description: 'API key for PhotoPrism photo service'
}, },
maps: { speed_color_scale: {
type: :object, type: :string,
properties: { example: 'viridis',
distance_unit: { description: 'Color scale for speed-colored routes'
type: :string,
example: 'km',
description: 'Distance unit preference (km or miles)'
}
},
description: 'Map-related settings'
}, },
visits_suggestions_enabled: { fog_of_war_threshold: {
type: :boolean, type: :number,
example: true, example: 100,
description: 'Whether visit suggestions are enabled' description: 'Fog of war threshold value'
} }
} }
} }

View file

@ -1059,18 +1059,14 @@ paths:
type: string type: string
example: your-photoprism-api-key example: your-photoprism-api-key
description: API key for PhotoPrism photo service description: API key for PhotoPrism photo service
maps: speed_color_scale:
type: object type: string
properties: example: viridis
distance_unit: description: Color scale for speed-colored routes
type: string fog_of_war_threshold:
example: km type: number
description: Distance unit preference (km or miles) example: 100
description: Map-related settings description: Fog of war threshold value
visits_suggestions_enabled:
type: boolean
example: true
description: Whether visit suggestions are enabled
examples: examples:
'0': '0':
summary: Updates user settings summary: Updates user settings
@ -1090,9 +1086,8 @@ paths:
immich_api_key: your-immich-api-key immich_api_key: your-immich-api-key
photoprism_url: https://photoprism.example.com photoprism_url: https://photoprism.example.com
photoprism_api_key: your-photoprism-api-key photoprism_api_key: your-photoprism-api-key
maps: speed_color_scale: viridis
distance_unit: km fog_of_war_threshold: 100
visits_suggestions_enabled: true
get: get:
summary: Retrieves user settings summary: Retrieves user settings
tags: tags:
@ -1116,28 +1111,28 @@ paths:
type: object type: object
properties: properties:
route_opacity: route_opacity:
type: string type: number
example: '60' example: 60
description: Route opacity percentage (0-100) description: Route opacity percentage (0-100)
meters_between_routes: meters_between_routes:
type: string type: number
example: '500' example: 500
description: Minimum distance between routes in meters description: Minimum distance between routes in meters
minutes_between_routes: minutes_between_routes:
type: string type: number
example: '30' example: 30
description: Minimum time between routes in minutes description: Minimum time between routes in minutes
fog_of_war_meters: fog_of_war_meters:
type: string type: number
example: '50' example: 50
description: Fog of war radius in meters description: Fog of war radius in meters
time_threshold_minutes: time_threshold_minutes:
type: string type: number
example: '30' example: 30
description: Time threshold for grouping points in minutes description: Time threshold for grouping points in minutes
merge_threshold_minutes: merge_threshold_minutes:
type: string type: number
example: '15' example: 15
description: Threshold for merging nearby points in minutes description: Threshold for merging nearby points in minutes
preferred_map_layer: preferred_map_layer:
type: string type: string
@ -1172,18 +1167,14 @@ paths:
type: string type: string
example: your-photoprism-api-key example: your-photoprism-api-key
description: API key for PhotoPrism photo service description: API key for PhotoPrism photo service
maps: speed_color_scale:
type: object type: string
properties: example: viridis
distance_unit: description: Color scale for speed-colored routes
type: string fog_of_war_threshold:
example: km type: number
description: Distance unit preference (km or miles) example: 100
description: Map-related settings description: Fog of war threshold value
visits_suggestions_enabled:
type: boolean
example: true
description: Whether visit suggestions are enabled
"/api/v1/stats": "/api/v1/stats":
get: get:
summary: Retrieves all stats summary: Retrieves all stats