mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Compare commits
77 commits
master
...
0.37.2-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dc31a66d4 | ||
|
|
ec524d64a0 | ||
|
|
594b7ffdd2 | ||
|
|
a4b9ed1087 | ||
|
|
966dc01651 | ||
|
|
96a78881f1 | ||
|
|
aa3bf93a45 | ||
|
|
24cbabf3b7 | ||
|
|
8ecb2e3765 | ||
|
|
b037be3299 | ||
|
|
b4c2def2be | ||
|
|
8a1e42a2e8 | ||
|
|
2f11003c29 | ||
|
|
7f277612fc | ||
|
|
2f5487cd35 | ||
|
|
5455228b80 | ||
|
|
26062a1278 | ||
|
|
14a0bb6478 | ||
|
|
18b13fb915 | ||
|
|
e857f520cc | ||
|
|
9e933aff9c | ||
|
|
a722e19a93 | ||
|
|
67d7123e47 | ||
|
|
573d527455 | ||
|
|
4be58d4b4c | ||
|
|
3f436c1d3a | ||
|
|
fe9d7d2f79 | ||
|
|
fab0121113 | ||
|
|
9805c5524c | ||
|
|
f325fd7a4f | ||
|
|
3c1d17b806 | ||
|
|
c9ba7914b6 | ||
|
|
03697ecef2 | ||
|
|
7347be9a87 | ||
|
|
ce74b3d846 | ||
|
|
da9742bf4a | ||
|
|
e12b45f93e | ||
|
|
32f5d2f89a | ||
|
|
ad385f4464 | ||
|
|
d4e87ce830 | ||
|
|
04fbe4d564 | ||
|
|
1471e4de40 | ||
|
|
9ef0da27d6 | ||
|
|
87baf8bb11 | ||
|
|
d40b2a1959 | ||
|
|
35995e7be8 | ||
|
|
20a4553921 | ||
|
|
c1bb7f3d87 | ||
|
|
0b6149bfc0 | ||
|
|
f2d96e50f0 | ||
|
|
1090bcd6e8 | ||
|
|
b7f0b7ebc2 | ||
|
|
b81d2580e3 | ||
|
|
acee848e72 | ||
|
|
88f5e2a6ea | ||
|
|
353837e27f | ||
|
|
2a4ed8bf82 | ||
|
|
8af032a215 | ||
|
|
bb980f2210 | ||
|
|
c6d09c341d | ||
|
|
516cfabb06 | ||
|
|
9ac4566b5a | ||
|
|
1c9843dde7 | ||
|
|
6cc8ba0fbd | ||
|
|
913d60812a | ||
|
|
a7f77b042e | ||
|
|
cdf1428e35 | ||
|
|
9661e8e7f7 | ||
|
|
2debcd88fa | ||
|
|
672c308f67 | ||
|
|
c5ef4d3861 | ||
|
|
9fb4bc517b | ||
|
|
97d52f9edc | ||
|
|
4421a5bf3c | ||
|
|
cebbc28912 | ||
|
|
ac9b668c30 | ||
|
|
6772f2f7b7 |
25 changed files with 122 additions and 409 deletions
|
|
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# [0.37.2] - 2026-01-04
|
||||
# [0.37.2] - 2026-01-03
|
||||
|
||||
## Fixed
|
||||
|
||||
|
|
@ -12,7 +12,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
- Time spent in a country and city is now calculated correctly for the year-end digest email. #2104
|
||||
- Updated Trix to fix a XSS vulnerability. #2102
|
||||
- Map v2 UI no longer blocks when Immich/Photoprism integration has a bad URL or is unreachable. Added 10-second timeout to photo API requests and improved error handling to prevent UI freezing during initial load. #2085
|
||||
- In Map v2 settings, you can now enable map to be rendered as a globe.
|
||||
|
||||
# [0.37.1] - 2025-12-30
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class Api::V1::SettingsController < ApiController
|
|||
:preferred_map_layer, :points_rendering_mode, :live_map_enabled,
|
||||
:immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key,
|
||||
:speed_colored_routes, :speed_color_scale, :fog_of_war_threshold,
|
||||
:maps_v2_style, :maps_maplibre_style, :globe_projection,
|
||||
:maps_v2_style, :maps_maplibre_style,
|
||||
enabled_map_layers: []
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class Users::DigestsController < ApplicationController
|
|||
tracked_years = current_user.stats.select(:year).distinct.pluck(:year)
|
||||
existing_digests = current_user.digests.yearly.pluck(:year)
|
||||
|
||||
(tracked_years - existing_digests - [Time.current.year]).sort.reverse
|
||||
(tracked_years - existing_digests).sort.reverse
|
||||
end
|
||||
|
||||
def valid_year?(year)
|
||||
|
|
|
|||
|
|
@ -2,27 +2,6 @@
|
|||
|
||||
module Users
|
||||
module DigestsHelper
|
||||
PROGRESS_COLORS = %w[
|
||||
progress-primary progress-secondary progress-accent
|
||||
progress-info progress-success progress-warning
|
||||
].freeze
|
||||
|
||||
def progress_color_for_index(index)
|
||||
PROGRESS_COLORS[index % PROGRESS_COLORS.length]
|
||||
end
|
||||
|
||||
def city_progress_value(city_count, max_cities)
|
||||
return 0 unless max_cities&.positive?
|
||||
|
||||
(city_count.to_f / max_cities * 100).round
|
||||
end
|
||||
|
||||
def max_cities_count(toponyms)
|
||||
return 0 if toponyms.blank?
|
||||
|
||||
toponyms.map { |country| country['cities']&.length || 0 }.max
|
||||
end
|
||||
|
||||
def distance_with_unit(distance_meters, unit)
|
||||
value = Users::Digest.convert_distance(distance_meters, unit).round
|
||||
"#{number_with_delimiter(value)} #{unit}"
|
||||
|
|
|
|||
|
|
@ -16,35 +16,17 @@ export class MapInitializer {
|
|||
mapStyle = 'streets',
|
||||
center = [0, 0],
|
||||
zoom = 2,
|
||||
showControls = true,
|
||||
globeProjection = false
|
||||
showControls = true
|
||||
} = settings
|
||||
|
||||
const style = await getMapStyle(mapStyle)
|
||||
|
||||
const mapOptions = {
|
||||
const map = new maplibregl.Map({
|
||||
container,
|
||||
style,
|
||||
center,
|
||||
zoom
|
||||
}
|
||||
|
||||
const map = new maplibregl.Map(mapOptions)
|
||||
|
||||
// Set globe projection after map loads
|
||||
if (globeProjection === true || globeProjection === 'true') {
|
||||
map.on('load', () => {
|
||||
map.setProjection({ type: 'globe' })
|
||||
|
||||
// Add atmosphere effect
|
||||
map.setSky({
|
||||
'atmosphere-blend': [
|
||||
'interpolate', ['linear'], ['zoom'],
|
||||
0, 1, 5, 1, 7, 0
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (showControls) {
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
|
|
|
|||
|
|
@ -91,11 +91,6 @@ export class SettingsController {
|
|||
mapStyleSelect.value = this.settings.mapStyle || 'light'
|
||||
}
|
||||
|
||||
// Sync globe projection toggle
|
||||
if (controller.hasGlobeToggleTarget) {
|
||||
controller.globeToggleTarget.checked = this.settings.globeProjection || false
|
||||
}
|
||||
|
||||
// Sync fog of war settings
|
||||
const fogRadiusInput = controller.element.querySelector('input[name="fogOfWarRadius"]')
|
||||
if (fogRadiusInput) {
|
||||
|
|
@ -183,22 +178,6 @@ export class SettingsController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle globe projection
|
||||
* Requires page reload to apply since projection is set at map initialization
|
||||
*/
|
||||
async toggleGlobe(event) {
|
||||
const enabled = event.target.checked
|
||||
await SettingsManager.updateSetting('globeProjection', enabled)
|
||||
|
||||
Toast.info('Globe view will be applied after page reload')
|
||||
|
||||
// Prompt user to reload
|
||||
if (confirm('Globe view requires a page reload to take effect. Reload now?')) {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update route opacity in real-time
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@ export default class extends Controller {
|
|||
'speedColoredToggle',
|
||||
'speedColorScaleContainer',
|
||||
'speedColorScaleInput',
|
||||
// Globe projection
|
||||
'globeToggle',
|
||||
// Family members
|
||||
'familyMembersList',
|
||||
'familyMembersContainer',
|
||||
|
|
@ -149,8 +147,7 @@ export default class extends Controller {
|
|||
*/
|
||||
async initializeMap() {
|
||||
this.map = await MapInitializer.initialize(this.containerTarget, {
|
||||
mapStyle: this.settings.mapStyle,
|
||||
globeProjection: this.settings.globeProjection
|
||||
mapStyle: this.settings.mapStyle
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +243,6 @@ export default class extends Controller {
|
|||
updateFogThresholdDisplay(event) { return this.settingsController.updateFogThresholdDisplay(event) }
|
||||
updateMetersBetweenDisplay(event) { return this.settingsController.updateMetersBetweenDisplay(event) }
|
||||
updateMinutesBetweenDisplay(event) { return this.settingsController.updateMinutesBetweenDisplay(event) }
|
||||
toggleGlobe(event) { return this.settingsController.toggleGlobe(event) }
|
||||
|
||||
// Area Selection Manager methods
|
||||
startSelectArea() { return this.areaSelectionManager.startSelectArea() }
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ const DEFAULT_SETTINGS = {
|
|||
minutesBetweenRoutes: 60,
|
||||
pointsRenderingMode: 'raw',
|
||||
speedColoredRoutes: false,
|
||||
speedColorScale: '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300',
|
||||
globeProjection: false
|
||||
speedColorScale: '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300'
|
||||
}
|
||||
|
||||
// Mapping between v2 layer names and v1 layer names in enabled_map_layers array
|
||||
|
|
@ -42,8 +41,7 @@ const BACKEND_SETTINGS_MAP = {
|
|||
minutesBetweenRoutes: 'minutes_between_routes',
|
||||
pointsRenderingMode: 'points_rendering_mode',
|
||||
speedColoredRoutes: 'speed_colored_routes',
|
||||
speedColorScale: 'speed_color_scale',
|
||||
globeProjection: 'globe_projection'
|
||||
speedColorScale: 'speed_color_scale'
|
||||
}
|
||||
|
||||
export class SettingsManager {
|
||||
|
|
@ -154,8 +152,6 @@ export class SettingsManager {
|
|||
value = parseInt(value) || DEFAULT_SETTINGS.minutesBetweenRoutes
|
||||
} else if (frontendKey === 'speedColoredRoutes') {
|
||||
value = value === true || value === 'true'
|
||||
} else if (frontendKey === 'globeProjection') {
|
||||
value = value === true || value === 'true'
|
||||
}
|
||||
|
||||
frontendSettings[frontendKey] = value
|
||||
|
|
@ -223,8 +219,6 @@ export class SettingsManager {
|
|||
value = parseInt(value).toString()
|
||||
} else if (frontendKey === 'speedColoredRoutes') {
|
||||
value = Boolean(value)
|
||||
} else if (frontendKey === 'globeProjection') {
|
||||
value = Boolean(value)
|
||||
}
|
||||
|
||||
backendSettings[backendKey] = value
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ class Users::Digests::CalculatingJob < ApplicationJob
|
|||
queue_as :digests
|
||||
|
||||
def perform(user_id, year)
|
||||
recalculate_monthly_stats(user_id, year)
|
||||
Users::Digests::CalculateYear.new(user_id, year).call
|
||||
rescue StandardError => e
|
||||
create_digest_failed_notification(user_id, e)
|
||||
|
|
@ -12,12 +11,6 @@ class Users::Digests::CalculatingJob < ApplicationJob
|
|||
|
||||
private
|
||||
|
||||
def recalculate_monthly_stats(user_id, year)
|
||||
(1..12).each do |month|
|
||||
Stats::CalculateMonth.new(user_id, year, month).call
|
||||
end
|
||||
end
|
||||
|
||||
def create_digest_failed_notification(user_id, error)
|
||||
user = User.find(user_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -45,13 +45,18 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
|||
|
||||
def countries_visited
|
||||
Rails.cache.fetch("dawarich/user_#{id}_countries_visited", expires_in: 1.day) do
|
||||
countries_visited_uncached
|
||||
points
|
||||
.without_raw_data
|
||||
.where.not(country_name: [nil, ''])
|
||||
.distinct
|
||||
.pluck(:country_name)
|
||||
.compact
|
||||
end
|
||||
end
|
||||
|
||||
def cities_visited
|
||||
Rails.cache.fetch("dawarich/user_#{id}_cities_visited", expires_in: 1.day) do
|
||||
cities_visited_uncached
|
||||
points.where.not(city: [nil, '']).distinct.pluck(:city).compact
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -134,47 +139,17 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
|||
Time.zone.name
|
||||
end
|
||||
|
||||
# Aggregate countries from all stats' toponyms
|
||||
# This is more accurate than raw point queries as it uses processed data
|
||||
def countries_visited_uncached
|
||||
countries = Set.new
|
||||
|
||||
stats.find_each do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.each do |toponym|
|
||||
next unless toponym.is_a?(Hash)
|
||||
|
||||
countries.add(toponym['country']) if toponym['country'].present?
|
||||
end
|
||||
end
|
||||
|
||||
countries.to_a.sort
|
||||
points
|
||||
.without_raw_data
|
||||
.where.not(country_name: [nil, ''])
|
||||
.distinct
|
||||
.pluck(:country_name)
|
||||
.compact
|
||||
end
|
||||
|
||||
# Aggregate cities from all stats' toponyms
|
||||
# This respects MIN_MINUTES_SPENT_IN_CITY since toponyms are already filtered
|
||||
def cities_visited_uncached
|
||||
cities = Set.new
|
||||
|
||||
stats.find_each do |stat|
|
||||
toponyms = stat.toponyms
|
||||
next unless toponyms.is_a?(Array)
|
||||
|
||||
toponyms.each do |toponym|
|
||||
next unless toponym.is_a?(Hash)
|
||||
next unless toponym['cities'].is_a?(Array)
|
||||
|
||||
toponym['cities'].each do |city|
|
||||
next unless city.is_a?(Hash)
|
||||
|
||||
cities.add(city['city']) if city['city'].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cities.to_a.sort
|
||||
points.where.not(city: [nil, '']).distinct.pluck(:city).compact
|
||||
end
|
||||
|
||||
def home_place_coordinates
|
||||
|
|
|
|||
|
|
@ -162,9 +162,6 @@ class Users::Digest < ApplicationRecord
|
|||
end
|
||||
|
||||
def total_tracked_minutes
|
||||
# Use total_country_minutes if available (new digests),
|
||||
# fall back to summing top_countries_by_time (existing digests)
|
||||
time_spent_by_location['total_country_minutes'] ||
|
||||
top_countries_by_time.sum { |country| country['minutes'].to_i }
|
||||
top_countries_by_time.sum { |country| country['minutes'].to_i }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@ class Api::UserSerializer
|
|||
photoprism_url: user.safe_settings.photoprism_url,
|
||||
visits_suggestions_enabled: user.safe_settings.visits_suggestions_enabled?,
|
||||
speed_color_scale: user.safe_settings.speed_color_scale,
|
||||
fog_of_war_threshold: user.safe_settings.fog_of_war_threshold,
|
||||
globe_projection: user.safe_settings.globe_projection
|
||||
fog_of_war_threshold: user.safe_settings.fog_of_war_threshold
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -49,17 +49,6 @@ class CountriesAndCities
|
|||
end
|
||||
|
||||
def calculate_duration_in_minutes(timestamps)
|
||||
return 0 if timestamps.size < 2
|
||||
|
||||
sorted = timestamps.sort
|
||||
total_minutes = 0
|
||||
gap_threshold_seconds = ::MIN_MINUTES_SPENT_IN_CITY * 60
|
||||
|
||||
sorted.each_cons(2) do |prev_ts, curr_ts|
|
||||
interval_seconds = curr_ts - prev_ts
|
||||
total_minutes += (interval_seconds / 60) if interval_seconds < gap_threshold_seconds
|
||||
end
|
||||
|
||||
total_minutes
|
||||
((timestamps.max - timestamps.min).to_i / 60)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
module Users
|
||||
module Digests
|
||||
class CalculateYear
|
||||
MINUTES_PER_DAY = 1440
|
||||
MAX_COUNTRY_GAP_SECONDS = 60 * 60 # 60 minutes
|
||||
|
||||
def initialize(user_id, year)
|
||||
@user = ::User.find(user_id)
|
||||
|
|
@ -66,7 +66,7 @@ module Users
|
|||
end
|
||||
end
|
||||
|
||||
country_cities.sort_by { |_country, cities| -cities.size }.map do |country, cities|
|
||||
country_cities.sort_by { |country, _| country }.map do |country, cities|
|
||||
{
|
||||
'country' => country,
|
||||
'cities' => cities.to_a.sort.map { |city| { 'city' => city } }
|
||||
|
|
@ -90,16 +90,15 @@ module Users
|
|||
end
|
||||
|
||||
def calculate_time_spent
|
||||
country_minutes = calculate_actual_country_minutes
|
||||
|
||||
{
|
||||
'countries' => format_top_countries(country_minutes),
|
||||
'cities' => calculate_city_time_spent,
|
||||
'total_country_minutes' => country_minutes.values.sum
|
||||
'countries' => calculate_country_time_spent,
|
||||
'cities' => calculate_city_time_spent
|
||||
}
|
||||
end
|
||||
|
||||
def format_top_countries(country_minutes)
|
||||
def calculate_country_time_spent
|
||||
country_minutes = calculate_actual_country_minutes
|
||||
|
||||
country_minutes
|
||||
.sort_by { |_, minutes| -minutes }
|
||||
.first(10)
|
||||
|
|
@ -107,51 +106,22 @@ module Users
|
|||
end
|
||||
|
||||
def calculate_actual_country_minutes
|
||||
points_by_date = group_points_by_date
|
||||
points = fetch_year_points_with_country_ordered
|
||||
country_minutes = Hash.new(0)
|
||||
|
||||
points_by_date.each do |_date, day_points|
|
||||
countries_on_day = day_points.map(&:country_name).uniq
|
||||
points.each_cons(2) do |point_a, point_b|
|
||||
next if point_a.country_name != point_b.country_name
|
||||
|
||||
if countries_on_day.size == 1
|
||||
# Single country day - assign full day
|
||||
country_minutes[countries_on_day.first] += MINUTES_PER_DAY
|
||||
else
|
||||
# Multi-country day - calculate proportional time
|
||||
calculate_proportional_time(day_points, country_minutes)
|
||||
end
|
||||
gap_seconds = point_b.timestamp - point_a.timestamp
|
||||
next if gap_seconds > MAX_COUNTRY_GAP_SECONDS
|
||||
next if gap_seconds <= 0
|
||||
|
||||
country_minutes[point_a.country_name] += (gap_seconds / 60)
|
||||
end
|
||||
|
||||
country_minutes
|
||||
end
|
||||
|
||||
def group_points_by_date
|
||||
points = fetch_year_points_with_country_ordered
|
||||
|
||||
points.group_by do |point|
|
||||
Time.zone.at(point.timestamp).to_date
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_proportional_time(day_points, country_minutes)
|
||||
country_spans = Hash.new(0)
|
||||
points_by_country = day_points.group_by(&:country_name)
|
||||
|
||||
points_by_country.each do |country, country_points|
|
||||
timestamps = country_points.map(&:timestamp)
|
||||
span_seconds = timestamps.max - timestamps.min
|
||||
# Minimum 60 seconds (1 min) for single-point countries
|
||||
country_spans[country] = [span_seconds, 60].max
|
||||
end
|
||||
|
||||
total_spans = country_spans.values.sum.to_f
|
||||
|
||||
country_spans.each do |country, span|
|
||||
proportional_minutes = (span / total_spans * MINUTES_PER_DAY).round
|
||||
country_minutes[country] += proportional_minutes
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_year_points_with_country_ordered
|
||||
start_of_year = Time.zone.local(year, 1, 1, 0, 0, 0)
|
||||
end_of_year = start_of_year.end_of_year
|
||||
|
|
@ -216,8 +186,8 @@ module Users
|
|||
|
||||
def calculate_all_time_stats
|
||||
{
|
||||
'total_countries' => user.countries_visited_uncached.size,
|
||||
'total_cities' => user.cities_visited_uncached.size,
|
||||
'total_countries' => user.countries_visited.count,
|
||||
'total_cities' => user.cities_visited.count,
|
||||
'total_distance' => user.stats.sum(:distance).to_s
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class Users::ExportData::Points
|
|||
|
||||
output_file.write('[')
|
||||
|
||||
user.points.find_in_batches(batch_size: BATCH_SIZE).with_index do |batch, _batch_index|
|
||||
user.points.find_in_batches(batch_size: BATCH_SIZE).with_index do |batch, batch_index|
|
||||
batch_sql = build_batch_query(batch.map(&:id))
|
||||
result = ActiveRecord::Base.connection.exec_query(batch_sql, 'Points Export Batch')
|
||||
|
||||
|
|
@ -188,13 +188,13 @@ class Users::ExportData::Points
|
|||
}
|
||||
end
|
||||
|
||||
return unless row['visit_name']
|
||||
|
||||
point_hash['visit_reference'] = {
|
||||
'name' => row['visit_name'],
|
||||
'started_at' => row['visit_started_at'],
|
||||
'ended_at' => row['visit_ended_at']
|
||||
}
|
||||
if row['visit_name']
|
||||
point_hash['visit_reference'] = {
|
||||
'name' => row['visit_name'],
|
||||
'started_at' => row['visit_started_at'],
|
||||
'ended_at' => row['visit_ended_at']
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def log_progress(processed, total)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ class Users::SafeSettings
|
|||
'visits_suggestions_enabled' => 'true',
|
||||
'enabled_map_layers' => %w[Routes Heatmap],
|
||||
'maps_maplibre_style' => 'light',
|
||||
'digest_emails_enabled' => true,
|
||||
'globe_projection' => false
|
||||
'digest_emails_enabled' => true
|
||||
}.freeze
|
||||
|
||||
def initialize(settings = {})
|
||||
|
|
@ -53,8 +52,7 @@ class Users::SafeSettings
|
|||
speed_color_scale: speed_color_scale,
|
||||
fog_of_war_threshold: fog_of_war_threshold,
|
||||
enabled_map_layers: enabled_map_layers,
|
||||
maps_maplibre_style: maps_maplibre_style,
|
||||
globe_projection: globe_projection
|
||||
maps_maplibre_style: maps_maplibre_style
|
||||
}
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
|
@ -143,10 +141,6 @@ class Users::SafeSettings
|
|||
settings['maps_maplibre_style']
|
||||
end
|
||||
|
||||
def globe_projection
|
||||
ActiveModel::Type::Boolean.new.cast(settings['globe_projection'])
|
||||
end
|
||||
|
||||
def digest_emails_enabled?
|
||||
value = settings['digest_emails_enabled']
|
||||
return true if value.nil?
|
||||
|
|
|
|||
|
|
@ -365,19 +365,6 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Globe Projection -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox"
|
||||
name="globeProjection"
|
||||
class="toggle toggle-primary"
|
||||
data-maps--maplibre-target="globeToggle"
|
||||
data-action="change->maps--maplibre#toggleGlobe" />
|
||||
<span class="label-text font-medium">Globe View</span>
|
||||
</label>
|
||||
<p class="text-sm text-base-content/60 mt-1">Render map as a 3D globe (requires page reload)</p>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Route Opacity -->
|
||||
|
|
|
|||
|
|
@ -168,7 +168,14 @@
|
|||
</h2>
|
||||
<div class="space-y-4 w-full">
|
||||
<% if @digest.toponyms.present? %>
|
||||
<% max_cities = @digest.toponyms.map { |country| country['cities']&.length || 0 }.max %>
|
||||
<% progress_colors = ['progress-primary', 'progress-secondary', 'progress-accent', 'progress-info', 'progress-success', 'progress-warning'] %>
|
||||
|
||||
<% @digest.toponyms.each_with_index do |country, index| %>
|
||||
<% cities_count = country['cities']&.length || 0 %>
|
||||
<% progress_value = max_cities&.positive? ? (cities_count.to_f / max_cities * 100).round : 0 %>
|
||||
<% color_class = progress_colors[index % progress_colors.length] %>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold">
|
||||
|
|
@ -176,10 +183,10 @@
|
|||
<%= country['country'] %>
|
||||
</span>
|
||||
<span class="text-sm">
|
||||
<%= pluralize(country['cities']&.length || 0, 'city') %>
|
||||
<%= pluralize(cities_count, 'city') %>
|
||||
</span>
|
||||
</div>
|
||||
<progress class="progress <%= progress_color_for_index(index) %> w-full" value="<%= city_progress_value(country['cities']&.length || 0, max_cities_count(@digest.toponyms)) %>" max="100"></progress>
|
||||
<progress class="progress <%= color_class %> w-full" value="<%= progress_value %>" max="100"></progress>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { test as setup, expect } from '@playwright/test';
|
||||
import { disableGlobeProjection } from '../v2/helpers/setup.js';
|
||||
|
||||
const authFile = 'e2e/temp/.auth/user.json';
|
||||
|
||||
|
|
@ -20,9 +19,6 @@ setup('authenticate', async ({ page }) => {
|
|||
// Wait for successful navigation to map (v1 or v2 depending on user preference)
|
||||
await page.waitForURL(/\/map(\/v[12])?/, { timeout: 10000 });
|
||||
|
||||
// Disable globe projection to ensure consistent E2E test behavior
|
||||
await disableGlobeProjection(page);
|
||||
|
||||
// Save authentication state
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,33 +2,6 @@
|
|||
* Helper functions for Maps V2 E2E tests
|
||||
*/
|
||||
|
||||
/**
|
||||
* Disable globe projection setting via API
|
||||
* This ensures consistent map rendering for E2E tests
|
||||
* @param {Page} page - Playwright page object
|
||||
*/
|
||||
export async function disableGlobeProjection(page) {
|
||||
// Get API key from the page (requires being logged in)
|
||||
const apiKey = await page.evaluate(() => {
|
||||
const metaTag = document.querySelector('meta[name="api-key"]');
|
||||
return metaTag?.content;
|
||||
});
|
||||
|
||||
if (apiKey) {
|
||||
await page.request.patch('/api/v1/settings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
settings: {
|
||||
globe_projection: false
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to Maps V2 page
|
||||
* @param {Page} page - Playwright page object
|
||||
|
|
|
|||
|
|
@ -163,16 +163,12 @@ RSpec.describe User, type: :model do
|
|||
describe '#countries_visited' do
|
||||
subject { user.countries_visited }
|
||||
|
||||
let!(:stat) do
|
||||
create(:stat, user:, toponyms: [
|
||||
{ 'country' => 'Germany', 'cities' => [{ 'city' => 'Berlin', 'stayed_for' => 120 }] },
|
||||
{ 'country' => 'France', 'cities' => [{ 'city' => 'Paris', 'stayed_for' => 90 }] },
|
||||
{ 'country' => nil, 'cities' => [] },
|
||||
{ 'country' => '', 'cities' => [] }
|
||||
])
|
||||
end
|
||||
let!(:point1) { create(:point, user:, country_name: 'Germany') }
|
||||
let!(:point2) { create(:point, user:, country_name: 'France') }
|
||||
let!(:point3) { create(:point, user:, country_name: nil) }
|
||||
let!(:point4) { create(:point, user:, country_name: '') }
|
||||
|
||||
it 'returns array of countries from stats toponyms' do
|
||||
it 'returns array of countries' do
|
||||
expect(subject).to include('Germany', 'France')
|
||||
expect(subject.count).to eq(2)
|
||||
end
|
||||
|
|
@ -185,18 +181,12 @@ RSpec.describe User, type: :model do
|
|||
describe '#cities_visited' do
|
||||
subject { user.cities_visited }
|
||||
|
||||
let!(:stat) do
|
||||
create(:stat, user:, toponyms: [
|
||||
{ 'country' => 'Germany', 'cities' => [
|
||||
{ 'city' => 'Berlin', 'stayed_for' => 120 },
|
||||
{ 'city' => nil, 'stayed_for' => 60 },
|
||||
{ 'city' => '', 'stayed_for' => 60 }
|
||||
] },
|
||||
{ 'country' => 'France', 'cities' => [{ 'city' => 'Paris', 'stayed_for' => 90 }] }
|
||||
])
|
||||
end
|
||||
let!(:point1) { create(:point, user:, city: 'Berlin') }
|
||||
let!(:point2) { create(:point, user:, city: 'Paris') }
|
||||
let!(:point3) { create(:point, user:, city: nil) }
|
||||
let!(:point4) { create(:point, user:, city: '') }
|
||||
|
||||
it 'returns array of cities from stats toponyms' do
|
||||
it 'returns array of cities' do
|
||||
expect(subject).to include('Berlin', 'Paris')
|
||||
expect(subject.count).to eq(2)
|
||||
end
|
||||
|
|
@ -220,15 +210,11 @@ RSpec.describe User, type: :model do
|
|||
describe '#total_countries' do
|
||||
subject { user.total_countries }
|
||||
|
||||
let!(:stat) do
|
||||
create(:stat, user:, toponyms: [
|
||||
{ 'country' => 'Germany', 'cities' => [] },
|
||||
{ 'country' => 'France', 'cities' => [] },
|
||||
{ 'country' => nil, 'cities' => [] }
|
||||
])
|
||||
end
|
||||
let!(:point1) { create(:point, user:, country_name: 'Germany') }
|
||||
let!(:point2) { create(:point, user:, country_name: 'France') }
|
||||
let!(:point3) { create(:point, user:, country_name: nil) }
|
||||
|
||||
it 'returns number of countries from stats toponyms' do
|
||||
it 'returns number of countries' do
|
||||
expect(subject).to eq(2)
|
||||
end
|
||||
end
|
||||
|
|
@ -236,17 +222,11 @@ RSpec.describe User, type: :model do
|
|||
describe '#total_cities' do
|
||||
subject { user.total_cities }
|
||||
|
||||
let!(:stat) do
|
||||
create(:stat, user:, toponyms: [
|
||||
{ 'country' => 'Germany', 'cities' => [
|
||||
{ 'city' => 'Berlin', 'stayed_for' => 120 },
|
||||
{ 'city' => 'Paris', 'stayed_for' => 90 },
|
||||
{ 'city' => nil, 'stayed_for' => 60 }
|
||||
] }
|
||||
])
|
||||
end
|
||||
let!(:point1) { create(:point, user:, city: 'Berlin') }
|
||||
let!(:point2) { create(:point, user:, city: 'Paris') }
|
||||
let!(:point3) { create(:point, user:, city: nil) }
|
||||
|
||||
it 'returns number of cities from stats toponyms' do
|
||||
it 'returns number of cities' do
|
||||
expect(subject).to eq(2)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -79,58 +79,6 @@ RSpec.describe CountriesAndCities do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when points have a gap larger than threshold (passing through)' do
|
||||
let(:points) do
|
||||
[
|
||||
# User in Berlin at 9:00, leaves, returns at 11:00
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp:),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 15.minutes),
|
||||
# 105-minute gap here (user left the city)
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 120.minutes),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 130.minutes)
|
||||
]
|
||||
end
|
||||
|
||||
it 'only counts time between consecutive points within threshold' do
|
||||
# Old logic would count 130 minutes (span from first to last)
|
||||
# New logic counts: 15 min (0->15) + 10 min (120->130) = 25 minutes
|
||||
# Since 25 < 60, Berlin should be filtered out
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Germany',
|
||||
cities: []
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when points span a long time but have continuous presence' do
|
||||
let(:points) do
|
||||
# Points every 30 minutes for 2.5 hours = continuous presence
|
||||
(0..5).map do |i|
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + (i * 30).minutes)
|
||||
end
|
||||
end
|
||||
|
||||
it 'counts the full duration when all intervals are within threshold' do
|
||||
# 5 intervals of 30 minutes each = 150 minutes total
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Germany',
|
||||
cities: [
|
||||
CountriesAndCities::CityData.new(
|
||||
city: 'Berlin', points: 6, timestamp: (timestamp + 150.minutes).to_i, stayed_for: 150
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -155,14 +155,10 @@ RSpec.describe Stats::CalculateMonth do
|
|||
context 'when user visited multiple cities with mixed durations' do
|
||||
let!(:mixed_points) do
|
||||
[
|
||||
# Berlin: 70 minutes with continuous presence (should be included)
|
||||
# Points every 35 minutes: 0, 35, 70 = 70 min total
|
||||
# Berlin: 70 minutes (should be included)
|
||||
create(:point, user:, import:, timestamp: timestamp_base,
|
||||
city: 'Berlin', country_name: 'Germany',
|
||||
lonlat: 'POINT(13.404954 52.520008)'),
|
||||
create(:point, user:, import:, timestamp: timestamp_base + 35.minutes,
|
||||
city: 'Berlin', country_name: 'Germany',
|
||||
lonlat: 'POINT(13.404954 52.520008)'),
|
||||
create(:point, user:, import:, timestamp: timestamp_base + 70.minutes,
|
||||
city: 'Berlin', country_name: 'Germany',
|
||||
lonlat: 'POINT(13.404954 52.520008)'),
|
||||
|
|
@ -175,17 +171,10 @@ RSpec.describe Stats::CalculateMonth do
|
|||
city: 'Prague', country_name: 'Czech Republic',
|
||||
lonlat: 'POINT(14.4378 50.0755)'),
|
||||
|
||||
# Vienna: 90 minutes with continuous presence (should be included)
|
||||
# Points every 30 minutes: 150, 180, 210, 240 = 90 min total
|
||||
# Vienna: 90 minutes (should be included)
|
||||
create(:point, user:, import:, timestamp: timestamp_base + 150.minutes,
|
||||
city: 'Vienna', country_name: 'Austria',
|
||||
lonlat: 'POINT(16.3738 48.2082)'),
|
||||
create(:point, user:, import:, timestamp: timestamp_base + 180.minutes,
|
||||
city: 'Vienna', country_name: 'Austria',
|
||||
lonlat: 'POINT(16.3738 48.2082)'),
|
||||
create(:point, user:, import:, timestamp: timestamp_base + 210.minutes,
|
||||
city: 'Vienna', country_name: 'Austria',
|
||||
lonlat: 'POINT(16.3738 48.2082)'),
|
||||
create(:point, user:, import:, timestamp: timestamp_base + 240.minutes,
|
||||
city: 'Vienna', country_name: 'Austria',
|
||||
lonlat: 'POINT(16.3738 48.2082)')
|
||||
|
|
|
|||
|
|
@ -76,13 +76,11 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
expect(calculate_digest.monthly_distances['3']).to eq('0') # Missing month
|
||||
end
|
||||
|
||||
it 'calculates time spent by location using hybrid day-based approach' do
|
||||
# Create points to test hybrid calculation
|
||||
# Jan 1: single country day (Germany) -> full 1440 minutes
|
||||
it 'calculates time spent by location using actual minutes between consecutive points' do
|
||||
# Create points with specific gaps to test actual minute calculation
|
||||
jan_1_10am = Time.zone.local(2024, 1, 1, 10, 0, 0).to_i
|
||||
jan_1_11am = Time.zone.local(2024, 1, 1, 11, 0, 0).to_i
|
||||
jan_1_12pm = Time.zone.local(2024, 1, 1, 12, 0, 0).to_i
|
||||
# Feb 1: single country day (France) -> full 1440 minutes
|
||||
jan_1_11am = Time.zone.local(2024, 1, 1, 11, 0, 0).to_i # 60 min later
|
||||
jan_1_12pm = Time.zone.local(2024, 1, 1, 12, 0, 0).to_i # 60 min later
|
||||
feb_1_10am = Time.zone.local(2024, 2, 1, 10, 0, 0).to_i
|
||||
|
||||
create(:point, user: user, timestamp: jan_1_10am, country_name: 'Germany', city: 'Berlin')
|
||||
|
|
@ -93,13 +91,13 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
countries = calculate_digest.time_spent_by_location['countries']
|
||||
cities = calculate_digest.time_spent_by_location['cities']
|
||||
|
||||
# Germany: 1 full day = 1440 minutes
|
||||
# Germany: 60 min (10am->11am) + 60 min (11am->12pm) = 120 minutes
|
||||
germany_country = countries.find { |c| c['name'] == 'Germany' }
|
||||
expect(germany_country['minutes']).to eq(1440)
|
||||
expect(germany_country['minutes']).to eq(120)
|
||||
|
||||
# France: 1 full day = 1440 minutes
|
||||
# France: only 1 point, so 0 minutes (no consecutive pair)
|
||||
france_country = countries.find { |c| c['name'] == 'France' }
|
||||
expect(france_country['minutes']).to eq(1440)
|
||||
expect(france_country).to be_nil # No time counted for single point
|
||||
|
||||
# Cities: based on stayed_for from monthly stats (sum across months)
|
||||
expect(cities.first['name']).to eq('Berlin')
|
||||
|
|
@ -111,12 +109,12 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
end
|
||||
|
||||
context 'when user visits same country across multiple months' do
|
||||
it 'counts each day as a full day for single-country days' do
|
||||
it 'calculates actual minutes from consecutive point pairs' do
|
||||
# Create hourly points across multiple days in March and July
|
||||
mar_start = Time.zone.local(2024, 3, 1, 10, 0, 0).to_i
|
||||
jul_start = Time.zone.local(2024, 7, 1, 10, 0, 0).to_i
|
||||
|
||||
# Create 3 days of hourly points in March
|
||||
# Create 3 days of hourly points in March (3 points per day = 2 gaps of 60 min each)
|
||||
3.times do |day|
|
||||
3.times do |hour|
|
||||
timestamp = mar_start + (day * 24 * 60 * 60) + (hour * 60 * 60)
|
||||
|
|
@ -132,7 +130,7 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
end
|
||||
end
|
||||
|
||||
# Create the monthly stats
|
||||
# Create the monthly stats (simulating what would be created by the stats calculation)
|
||||
create(:stat, user: user, year: 2024, month: 3, distance: 10_000, toponyms: [
|
||||
{ 'country' => 'Germany', 'cities' => [
|
||||
{ 'city' => 'Berlin', 'stayed_for' => 14_400 }
|
||||
|
|
@ -149,21 +147,22 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
countries = digest.time_spent_by_location['countries']
|
||||
germany = countries.find { |c| c['name'] == 'Germany' }
|
||||
|
||||
# Each single-country day = 1440 minutes
|
||||
# 6 days total (3 in March + 3 in July) = 6 * 1440 = 8640 minutes
|
||||
expect(germany['minutes']).to eq(6 * 1440)
|
||||
# Each day: 2 gaps of 60 minutes = 120 minutes
|
||||
# 6 days total (3 in March + 3 in July) = 720 minutes
|
||||
# But gaps between days are > 60 min threshold, so not counted
|
||||
expect(germany['minutes']).to eq(6 * 2 * 60)
|
||||
|
||||
# Total should equal exactly 6 days
|
||||
total_days = germany['minutes'] / 1440.0
|
||||
expect(total_days).to eq(6)
|
||||
# Total should be much less than 365 days
|
||||
total_hours = germany['minutes'] / 60.0
|
||||
expect(total_hours).to eq(12) # 12 hours of tracked time
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are large gaps between points on same day' do
|
||||
it 'still counts the full day for single-country day' do
|
||||
context 'when there are large gaps between points' do
|
||||
it 'does not count time during gaps exceeding 60 minute threshold' do
|
||||
point_1 = Time.zone.local(2024, 1, 1, 10, 0, 0).to_i
|
||||
point_2 = Time.zone.local(2024, 1, 1, 12, 0, 0).to_i # 2 hours later
|
||||
point_3 = Time.zone.local(2024, 1, 1, 18, 0, 0).to_i # 6 hours later
|
||||
point_2 = Time.zone.local(2024, 1, 1, 12, 0, 0).to_i # 2 hours later (> 1 hour threshold)
|
||||
point_3 = Time.zone.local(2024, 1, 1, 13, 0, 0).to_i # 1 hour after point_2
|
||||
|
||||
create(:point, user: user, timestamp: point_1, country_name: 'Germany')
|
||||
create(:point, user: user, timestamp: point_2, country_name: 'Germany')
|
||||
|
|
@ -172,15 +171,14 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
digest = calculate_digest
|
||||
germany = digest.time_spent_by_location['countries'].find { |c| c['name'] == 'Germany' }
|
||||
|
||||
# Hybrid approach: single-country day = full 1440 minutes
|
||||
# regardless of gaps between points
|
||||
expect(germany['minutes']).to eq(1440)
|
||||
# Only point_2 -> point_3 gap (60 min) should be counted
|
||||
# point_1 -> point_2 gap (120 min) exceeds threshold
|
||||
expect(germany['minutes']).to eq(60)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when transitioning between countries on same day' do
|
||||
it 'calculates proportional time based on time spans' do
|
||||
# Multi-country day: Germany 10:00-10:30, France 11:00-11:30
|
||||
context 'when transitioning between countries' do
|
||||
it 'does not count transition time' do
|
||||
point_1 = Time.zone.local(2024, 1, 1, 10, 0, 0).to_i
|
||||
point_2 = Time.zone.local(2024, 1, 1, 10, 30, 0).to_i # In Germany
|
||||
point_3 = Time.zone.local(2024, 1, 1, 11, 0, 0).to_i # Now in France
|
||||
|
|
@ -197,22 +195,15 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
germany = countries.find { |c| c['name'] == 'Germany' }
|
||||
france = countries.find { |c| c['name'] == 'France' }
|
||||
|
||||
# Germany span: 10:30 - 10:00 = 30 min = 1800 seconds
|
||||
# France span: 11:30 - 11:00 = 30 min = 1800 seconds
|
||||
# Total spans = 3600 seconds
|
||||
# Each country gets 50% of 1440 = 720 minutes
|
||||
expect(germany['minutes']).to eq(720)
|
||||
expect(france['minutes']).to eq(720)
|
||||
# Total = 1440 (exactly one day)
|
||||
expect(germany['minutes'] + france['minutes']).to eq(1440)
|
||||
expect(germany['minutes']).to eq(30) # point_1 -> point_2
|
||||
expect(france['minutes']).to eq(30) # point_3 -> point_4
|
||||
# Transition time (point_2 -> point_3) is NOT counted
|
||||
end
|
||||
end
|
||||
|
||||
context 'when visiting multiple countries on same day' do
|
||||
it 'calculates proportional time and never exceeds one day total' do
|
||||
it 'does not exceed the actual time in the day' do
|
||||
# This tests the fix for the original bug: border crossing should not count double
|
||||
# France: 8am-9am (1 hour span = 3600 seconds)
|
||||
# Germany: 10am-11am (1 hour span = 3600 seconds)
|
||||
jan_1_8am = Time.zone.local(2024, 1, 1, 8, 0, 0).to_i
|
||||
jan_1_9am = Time.zone.local(2024, 1, 1, 9, 0, 0).to_i
|
||||
jan_1_10am = Time.zone.local(2024, 1, 1, 10, 0, 0).to_i # Border crossing
|
||||
|
|
@ -229,13 +220,12 @@ RSpec.describe Users::Digests::CalculateYear do
|
|||
france = countries.find { |c| c['name'] == 'France' }
|
||||
germany = countries.find { |c| c['name'] == 'Germany' }
|
||||
|
||||
# France span: 3600 seconds, Germany span: 3600 seconds
|
||||
# Total spans: 7200 seconds
|
||||
# Each gets 50% of 1440 = 720 minutes
|
||||
expect(france['minutes']).to eq(720)
|
||||
expect(germany['minutes']).to eq(720)
|
||||
# Total = 1440 (exactly one day) - NOT 2 days as the bug would have caused
|
||||
expect(france['minutes'] + germany['minutes']).to eq(1440)
|
||||
# France: 60 min (8am->9am)
|
||||
# Germany: 60 min (10am->11am)
|
||||
# Total: 120 min (2 hours) - NOT 2 days (2880 min) as the bug would have caused
|
||||
expect(france['minutes']).to eq(60)
|
||||
expect(germany['minutes']).to eq(60)
|
||||
expect(france['minutes'] + germany['minutes']).to eq(120)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ RSpec.describe Users::SafeSettings do
|
|||
speed_color_scale: nil,
|
||||
fog_of_war_threshold: nil,
|
||||
enabled_map_layers: %w[Routes Heatmap],
|
||||
maps_maplibre_style: 'light',
|
||||
globe_projection: false
|
||||
maps_maplibre_style: 'light'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
@ -83,8 +82,7 @@ RSpec.describe Users::SafeSettings do
|
|||
'visits_suggestions_enabled' => false,
|
||||
'enabled_map_layers' => %w[Points Routes Areas Photos],
|
||||
'maps_maplibre_style' => 'light',
|
||||
'digest_emails_enabled' => true,
|
||||
'globe_projection' => false
|
||||
'digest_emails_enabled' => true
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
@ -112,8 +110,7 @@ RSpec.describe Users::SafeSettings do
|
|||
speed_color_scale: nil,
|
||||
fog_of_war_threshold: nil,
|
||||
enabled_map_layers: %w[Points Routes Areas Photos],
|
||||
maps_maplibre_style: 'light',
|
||||
globe_projection: false
|
||||
maps_maplibre_style: 'light'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue