From 3b474704eac1ad86810d1f67e33479bb333ae8e3 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 2 Jul 2025 23:50:32 +0200 Subject: [PATCH] Fixes for visits suggestions. --- CHANGELOG.md | 9 + app/controllers/settings_controller.rb | 8 +- app/javascript/maps/areas.js | 164 ++++++- app/jobs/bulk_visits_suggesting_job.rb | 1 + app/services/users/safe_settings.rb | 10 +- .../settings/background_jobs/index.html.erb | 16 +- spec/jobs/bulk_visits_suggesting_job_spec.rb | 12 + spec/rails_helper.rb | 11 +- spec/requests/settings_spec.rb | 4 +- spec/services/users/safe_settings_spec.rb | 40 +- spec/support/devise.rb | 27 +- .../api/v1/countries/visited_cities_spec.rb | 12 +- spec/swagger/api/v1/health_controller_spec.rb | 12 +- .../v1/overland/batches_controller_spec.rb | 197 +++++---- .../v1/owntracks/points_controller_spec.rb | 6 +- spec/swagger/api/v1/points_controller_spec.rb | 4 +- .../api/v1/settings_controller_spec.rb | 196 +++++++-- swagger/v1/swagger.yaml | 406 ++++++++++++------ 18 files changed, 811 insertions(+), 324 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f4347b9..5ea46f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [0.29.2] - UNRELEASED +## Added + +- In the User Settings -> Background Jobs, you can now 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. + ## Changed - Don't check for new version in production. +- Area popup styles are now more consistent. + +## Fixed + +- Swagger documentation is now valid again. # [0.29.1] - 2025-07-02 diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 82a934af..1a34fed4 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -3,10 +3,13 @@ class SettingsController < ApplicationController before_action :authenticate_user! before_action :authenticate_active_user!, only: %i[update] + def index; end def update - current_user.update(settings: settings_params) + existing_settings = current_user.safe_settings.settings + + current_user.update(settings: existing_settings.merge(settings_params)) flash.now[:notice] = 'Settings updated' @@ -31,7 +34,8 @@ class SettingsController < ApplicationController params.require(:settings).permit( :meters_between_routes, :minutes_between_routes, :fog_of_war_meters, :time_threshold_minutes, :merge_threshold_minutes, :route_opacity, - :immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key + :immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key, + :visits_suggestions_enabled ) end end diff --git a/app/javascript/maps/areas.js b/app/javascript/maps/areas.js index 66d5442b..481f0ba4 100644 --- a/app/javascript/maps/areas.js +++ b/app/javascript/maps/areas.js @@ -1,19 +1,96 @@ import { showFlashMessage } from "./helpers"; +// Add custom CSS for popup styling +const addPopupStyles = () => { + if (!document.querySelector('#area-popup-styles')) { + const style = document.createElement('style'); + style.id = 'area-popup-styles'; + style.textContent = ` + .area-form-popup, + .area-info-popup { + background: transparent !important; + } + + .area-form-popup .leaflet-popup-content-wrapper, + .area-info-popup .leaflet-popup-content-wrapper { + background: transparent !important; + padding: 0 !important; + margin: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + border: none !important; + } + + .area-form-popup .leaflet-popup-content, + .area-info-popup .leaflet-popup-content { + margin: 0 !important; + padding: 0 1rem 0 0 !important; + background: transparent !important; + border-radius: 1rem !important; + overflow: hidden !important; + width: 100% !important; + max-width: none !important; + } + + .area-form-popup .leaflet-popup-tip, + .area-info-popup .leaflet-popup-tip { + background: transparent !important; + border: none !important; + box-shadow: none !important; + } + + .area-form-popup .leaflet-popup, + .area-info-popup .leaflet-popup { + margin-bottom: 0 !important; + } + + .area-form-popup .leaflet-popup-close-button, + .area-info-popup .leaflet-popup-close-button { + right: 1.25rem !important; + top: 1.25rem !important; + width: 1.5rem !important; + height: 1.5rem !important; + padding: 0 !important; + color: oklch(var(--bc) / 0.6) !important; + background: oklch(var(--b2)) !important; + border-radius: 0.5rem !important; + border: 1px solid oklch(var(--bc) / 0.2) !important; + font-size: 1rem !important; + font-weight: bold !important; + line-height: 1 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + transition: all 0.2s ease !important; + } + + .area-form-popup .leaflet-popup-close-button:hover, + .area-info-popup .leaflet-popup-close-button:hover { + background: oklch(var(--b3)) !important; + color: oklch(var(--bc)) !important; + border-color: oklch(var(--bc) / 0.3) !important; + } + `; + document.head.appendChild(style); + } +}; + export function handleAreaCreated(areasLayer, layer, apiKey) { + // Add popup styles + addPopupStyles(); const radius = layer.getRadius(); const center = layer.getLatLng(); const formHtml = ` -
+
-

New Area

+

New Area

@@ -23,7 +100,7 @@ export function handleAreaCreated(areasLayer, layer, apiKey) {
@@ -35,11 +112,14 @@ export function handleAreaCreated(areasLayer, layer, apiKey) { `; layer.bindPopup(formHtml, { - maxWidth: "auto", - minWidth: 300, + maxWidth: 400, + minWidth: 384, + maxHeight: 600, closeButton: true, closeOnClick: false, - className: 'area-form-popup' + className: 'area-form-popup', + autoPan: true, + keepInView: true }).openPopup(); areasLayer.addLayer(layer); @@ -69,7 +149,7 @@ export function handleAreaCreated(areasLayer, layer, apiKey) { e.stopPropagation(); if (!nameInput.value.trim()) { - nameInput.classList.add('input-error'); + nameInput.classList.add('input-error', 'border-error'); return; } @@ -106,10 +186,29 @@ export function saveArea(formData, areasLayer, layer, apiKey) { .then(data => { layer.closePopup(); layer.bindPopup(` - Name: ${data.name}
- Radius: ${Math.round(data.radius)} meters
- [Delete] - `).openPopup(); +
+
+

${data.name}

+
+

Radius: ${Math.round(data.radius)} meters

+
+
+ +
+
+
+ `, { + maxWidth: 340, + minWidth: 320, + className: 'area-info-popup', + closeButton: true, + closeOnClick: false + }).openPopup(); // Add event listener for the delete button layer.on('popupopen', () => { @@ -151,6 +250,9 @@ export function deleteArea(id, areasLayer, layer, apiKey) { } export function fetchAndDrawAreas(areasLayer, apiKey) { + // Add popup styles + addPopupStyles(); + fetch(`/api/v1/areas?api_key=${apiKey}`, { method: 'GET', headers: { @@ -186,20 +288,42 @@ export function fetchAndDrawAreas(areasLayer, apiKey) { pane: 'areasPane' }); - // Bind popup content + // Bind popup content with proper theme-aware styling const popupContent = ` -
+
-

${area.name}

-

Radius: ${Math.round(radius)} meters

-

Center: [${lat.toFixed(4)}, ${lng.toFixed(4)}]

-
- +

${area.name}

+
+
+
+
Radius
+
${Math.round(radius)} meters
+
+
+
Center
+
[${lat.toFixed(4)}, ${lng.toFixed(4)}]
+
+
+
+
+
Area ${area.id}
+
`; - circle.bindPopup(popupContent); + circle.bindPopup(popupContent, { + maxWidth: 400, + minWidth: 384, + className: 'area-info-popup', + closeButton: true, + closeOnClick: false + }); // Add delete button handler when popup opens circle.on('popupopen', () => { diff --git a/app/jobs/bulk_visits_suggesting_job.rb b/app/jobs/bulk_visits_suggesting_job.rb index 54174bca..4384be6a 100644 --- a/app/jobs/bulk_visits_suggesting_job.rb +++ b/app/jobs/bulk_visits_suggesting_job.rb @@ -17,6 +17,7 @@ class BulkVisitsSuggestingJob < ApplicationJob time_chunks = Visits::TimeChunks.new(start_at:, end_at:).call users.active.find_each do |user| + next unless user.safe_settings.visits_suggestions_enabled? next if user.tracked_points.empty? schedule_chunked_jobs(user, time_chunks) diff --git a/app/services/users/safe_settings.rb b/app/services/users/safe_settings.rb index c549dc88..ab5b2181 100644 --- a/app/services/users/safe_settings.rb +++ b/app/services/users/safe_settings.rb @@ -18,7 +18,8 @@ class Users::SafeSettings 'immich_api_key' => nil, 'photoprism_url' => nil, 'photoprism_api_key' => nil, - 'maps' => { 'distance_unit' => 'km' } + 'maps' => { 'distance_unit' => 'km' }, + 'visits_suggestions_enabled' => 'true' }.freeze def initialize(settings = {}) @@ -43,7 +44,8 @@ class Users::SafeSettings photoprism_url: photoprism_url, photoprism_api_key: photoprism_api_key, maps: maps, - distance_unit: distance_unit + distance_unit: distance_unit, + visits_suggestions_enabled: visits_suggestions_enabled? } end # rubocop:enable Metrics/MethodLength @@ -111,4 +113,8 @@ class Users::SafeSettings def distance_unit settings.dig('maps', 'distance_unit') end + + def visits_suggestions_enabled? + settings['visits_suggestions_enabled'] == 'true' + end end diff --git a/app/views/settings/background_jobs/index.html.erb b/app/views/settings/background_jobs/index.html.erb index ebdaaa2c..ba8c1b53 100644 --- a/app/views/settings/background_jobs/index.html.erb +++ b/app/views/settings/background_jobs/index.html.erb @@ -19,7 +19,7 @@ Spamming many new jobs at once is a bad idea. Let them work or clear the queue beforehand.
-
+

Start Reverse Geocoding

@@ -48,6 +48,20 @@ <%= link_to 'Open Dashboard', '/sidekiq', target: '_blank', class: 'btn btn-primary' %>
+
+ +
+
+

Visits suggestions

+

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.

+
+ <% if current_user.safe_settings.visits_suggestions_enabled? %> + <%= link_to 'Disable', settings_path(settings: { 'visits_suggestions_enabled' => false }), method: :patch, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :patch }, class: 'btn btn-error' %> + <% else %> + <%= link_to 'Enable', settings_path(settings: { 'visits_suggestions_enabled' => true }), method: :patch, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :patch }, class: 'btn btn-success' %> + <% end %> +
+
diff --git a/spec/jobs/bulk_visits_suggesting_job_spec.rb b/spec/jobs/bulk_visits_suggesting_job_spec.rb index b4545701..66bf7da6 100644 --- a/spec/jobs/bulk_visits_suggesting_job_spec.rb +++ b/spec/jobs/bulk_visits_suggesting_job_spec.rb @@ -102,5 +102,17 @@ RSpec.describe BulkVisitsSuggestingJob, type: :job do described_class.perform_now(start_at: custom_start, end_at: custom_end) end + + context 'when visits suggestions are disabled' do + before do + allow_any_instance_of(Users::SafeSettings).to receive(:visits_suggestions_enabled?).and_return(false) + end + + it 'does not schedule jobs' do + expect(VisitSuggestingJob).not_to receive(:perform_later) + + described_class.perform_now + end + end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 4e34b6af..99844b0a 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,6 +13,10 @@ require 'super_diff/rspec-rails' require 'rake' Rails.application.load_tasks + +# Ensure Devise is properly configured for tests +require 'devise' + # Add additional requires below this line. Rails is not loaded until this point! Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f } @@ -32,11 +36,14 @@ RSpec.configure do |config| config.filter_rails_from_backtrace! config.include FactoryBot::Syntax::Methods - config.include Devise::Test::IntegrationHelpers, type: :request - config.include Devise::Test::IntegrationHelpers, type: :system config.rswag_dry_run = false + config.before(:suite) do + # Ensure Rails routes are loaded for Devise + Rails.application.reload_routes! + end + config.before do ActiveJob::Base.queue_adapter = :test allow(DawarichSettings).to receive(:store_geodata?).and_return(true) diff --git a/spec/requests/settings_spec.rb b/spec/requests/settings_spec.rb index a06d0b40..0d99f03d 100644 --- a/spec/requests/settings_spec.rb +++ b/spec/requests/settings_spec.rb @@ -80,7 +80,9 @@ RSpec.describe 'Settings', type: :request do it 'updates the user settings' do patch '/settings', params: params - expect(user.reload.settings).to eq(params[:settings]) + user.reload + expect(user.settings['meters_between_routes']).to eq('1000') + expect(user.settings['minutes_between_routes']).to eq('10') end context 'when user is inactive' do diff --git a/spec/services/users/safe_settings_spec.rb b/spec/services/users/safe_settings_spec.rb index ee18406b..11079920 100644 --- a/spec/services/users/safe_settings_spec.rb +++ b/spec/services/users/safe_settings_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'rails_helper' + RSpec.describe Users::SafeSettings do describe '#default_settings' do context 'with default values' do @@ -24,7 +26,8 @@ RSpec.describe Users::SafeSettings do photoprism_url: nil, photoprism_api_key: nil, maps: { "distance_unit" => "km" }, - distance_unit: 'km' + distance_unit: 'km', + visits_suggestions_enabled: true } ) end @@ -47,7 +50,8 @@ RSpec.describe Users::SafeSettings do 'immich_api_key' => 'immich-key', 'photoprism_url' => 'https://photoprism.example.com', 'photoprism_api_key' => 'photoprism-key', - 'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' } + 'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' }, + 'visits_suggestions_enabled' => false } end let(:safe_settings) { described_class.new(settings) } @@ -69,7 +73,32 @@ RSpec.describe Users::SafeSettings do "immich_api_key" => "immich-key", "photoprism_url" => "https://photoprism.example.com", "photoprism_api_key" => "photoprism-key", - "maps" => { "name" => "custom", "url" => "https://custom.example.com" } + "maps" => { "name" => "custom", "url" => "https://custom.example.com" }, + "visits_suggestions_enabled" => false + } + ) + end + + it 'returns custom default_settings configuration' do + expect(safe_settings.default_settings).to eq( + { + fog_of_war_meters: 100, + meters_between_routes: 1000, + preferred_map_layer: "Satellite", + speed_colored_routes: true, + points_rendering_mode: "simplified", + minutes_between_routes: 60, + time_threshold_minutes: 45, + merge_threshold_minutes: 20, + live_map_enabled: false, + route_opacity: 80, + immich_url: "https://immich.example.com", + immich_api_key: "immich-key", + photoprism_url: "https://photoprism.example.com", + photoprism_api_key: "photoprism-key", + maps: { "name" => "custom", "url" => "https://custom.example.com" }, + distance_unit: nil, + visits_suggestions_enabled: false } ) end @@ -98,6 +127,7 @@ RSpec.describe Users::SafeSettings do expect(safe_settings.photoprism_url).to be_nil expect(safe_settings.photoprism_api_key).to be_nil expect(safe_settings.maps).to eq({ "distance_unit" => "km" }) + expect(safe_settings.visits_suggestions_enabled?).to be true end end @@ -118,7 +148,8 @@ RSpec.describe Users::SafeSettings do 'immich_api_key' => 'immich-key', 'photoprism_url' => 'https://photoprism.example.com', 'photoprism_api_key' => 'photoprism-key', - 'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' } + 'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' }, + 'visits_suggestions_enabled' => false } end @@ -138,6 +169,7 @@ RSpec.describe Users::SafeSettings do expect(safe_settings.photoprism_url).to eq('https://photoprism.example.com') expect(safe_settings.photoprism_api_key).to eq('photoprism-key') expect(safe_settings.maps).to eq({ 'name' => 'custom', 'url' => 'https://custom.example.com' }) + expect(safe_settings.visits_suggestions_enabled?).to be false end end end diff --git a/spec/support/devise.rb b/spec/support/devise.rb index 5d8bf8de..a07f0af9 100644 --- a/spec/support/devise.rb +++ b/spec/support/devise.rb @@ -1,22 +1,15 @@ # frozen_string_literal: true -# https://makandracards.com/makandra/37161-rspec-devise-how-to-sign-in-users-in-request-specs - -module DeviseRequestSpecHelpers - include Warden::Test::Helpers - - def sign_in(resource_or_scope, resource = nil) - resource ||= resource_or_scope - scope = Devise::Mapping.find_scope!(resource_or_scope) - login_as(resource, scope:) - end - - def sign_out(resource_or_scope) - scope = Devise::Mapping.find_scope!(resource_or_scope) - logout(scope) - end -end +# Standard Devise test helpers configuration for request specs RSpec.configure do |config| - config.include DeviseRequestSpecHelpers, type: :request + config.include Devise::Test::IntegrationHelpers, type: :request + config.include Devise::Test::IntegrationHelpers, type: :system + + # Ensure Devise routes are loaded before request specs + config.before(:each, type: :request) do + # Reload routes to ensure Devise mappings are available + Rails.application.reload_routes! unless @routes_reloaded + @routes_reloaded = true + end end diff --git a/spec/swagger/api/v1/countries/visited_cities_spec.rb b/spec/swagger/api/v1/countries/visited_cities_spec.rb index 61a7fa43..b0de92d8 100644 --- a/spec/swagger/api/v1/countries/visited_cities_spec.rb +++ b/spec/swagger/api/v1/countries/visited_cities_spec.rb @@ -17,16 +17,20 @@ RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do description: 'Your API authentication key' parameter name: :start_at, in: :query, - type: :string, - format: 'date-time', + schema: { + type: :string, + format: :date + }, required: true, description: 'Start date in YYYY-MM-DD format', example: '2023-01-01' parameter name: :end_at, in: :query, - type: :string, - format: 'date-time', + schema: { + type: :string, + format: :date + }, required: true, description: 'End date in YYYY-MM-DD format', example: '2023-12-31' diff --git a/spec/swagger/api/v1/health_controller_spec.rb b/spec/swagger/api/v1/health_controller_spec.rb index 7305521f..b395fd24 100644 --- a/spec/swagger/api/v1/health_controller_spec.rb +++ b/spec/swagger/api/v1/health_controller_spec.rb @@ -14,14 +14,18 @@ describe 'Health API', type: :request do } header 'X-Dawarich-Response', - type: :string, + schema: { + type: :string, + example: 'Hey, I\'m alive!' + }, required: true, - example: 'Hey, I\'m alive!', description: "Depending on the authentication status of the request, the response will be different. If the request is authenticated, the response will be 'Hey, I'm alive and authenticated!'. If the request is not authenticated, the response will be 'Hey, I'm alive!'." header 'X-Dawarich-Version', - type: :string, + schema: { + type: :string, + example: '1.0.0' + }, required: true, - example: '1.0.0', description: 'The version of the application, for example: 1.0.0' run_test! diff --git a/spec/swagger/api/v1/overland/batches_controller_spec.rb b/spec/swagger/api/v1/overland/batches_controller_spec.rb index 4ba2e0d3..b626c56f 100644 --- a/spec/swagger/api/v1/overland/batches_controller_spec.rb +++ b/spec/swagger/api/v1/overland/batches_controller_spec.rb @@ -40,99 +40,112 @@ describe 'Overland Batches API', type: :request do parameter name: :locations, in: :body, schema: { type: :object, properties: { - type: { type: :string, example: 'Feature' }, - geometry: { - type: :object, - properties: { - type: { type: :string, example: 'Point' }, - coordinates: { type: :array, example: [13.356718, 52.502397] } + locations: { + type: :array, + items: { + type: :object, + properties: { + type: { type: :string, example: 'Feature' }, + geometry: { + type: :object, + properties: { + type: { type: :string, example: 'Point' }, + coordinates: { + type: :array, + items: { type: :number }, + example: [13.356718, 52.502397] + } + } + }, + properties: { + type: :object, + properties: { + timestamp: { + type: :string, + example: '2021-06-01T12:00:00Z', + description: 'Timestamp in ISO 8601 format' + }, + altitude: { + type: :number, + example: 0, + description: 'Altitude in meters' + }, + speed: { + type: :number, + example: 0, + description: 'Speed in meters per second' + }, + horizontal_accuracy: { + type: :number, + example: 0, + description: 'Horizontal accuracy in meters' + }, + vertical_accuracy: { + type: :number, + example: 0, + description: 'Vertical accuracy in meters' + }, + motion: { + type: :array, + items: { type: :string }, + example: %w[walking running driving cycling stationary], + description: 'Motion type, for example: automotive_navigation, fitness, other_navigation or other' + }, + activity: { + type: :string, + example: 'unknown', + description: 'Activity type, for example: automotive_navigation, fitness, other_navigation or other' + }, + desired_accuracy: { + type: :number, + example: 0, + description: 'Desired accuracy in meters' + }, + deferred: { + type: :number, + example: 0, + description: 'the distance in meters to defer location updates' + }, + significant_change: { + type: :string, + example: 'disabled', + description: 'a significant change mode, disabled, enabled or exclusive' + }, + locations_in_payload: { + type: :number, + example: 1, + description: 'the number of locations in the payload' + }, + device_id: { + type: :string, + example: 'iOS device #166', + description: 'the device id' + }, + unique_id: { + type: :string, + example: '1234567890', + description: 'the device\'s Unique ID as set by Apple' + }, + wifi: { + type: :string, + example: 'unknown', + description: 'the WiFi network name' + }, + battery_state: { + type: :string, + example: 'unknown', + description: 'the battery state, unknown, unplugged, charging or full' + }, + battery_level: { + type: :number, + example: 0, + description: 'the battery level percentage, from 0 to 1' + } + } + } + }, + required: %w[geometry properties] } - }, - properties: { - type: :object, - properties: { - timestamp: { - type: :string, - example: '2021-06-01T12:00:00Z', - description: 'Timestamp in ISO 8601 format' - }, - altitude: { - type: :number, - example: 0, - description: 'Altitude in meters' - }, - speed: { - type: :number, - example: 0, - description: 'Speed in meters per second' - }, - horizontal_accuracy: { - type: :number, - example: 0, - description: 'Horizontal accuracy in meters' - }, - vertical_accuracy: { - type: :number, - example: 0, - description: 'Vertical accuracy in meters' - }, - motion: { - type: :array, - example: %w[walking running driving cycling stationary], - description: 'Motion type, for example: automotive_navigation, fitness, other_navigation or other' - }, - activity: { - type: :string, - example: 'unknown', - description: 'Activity type, for example: automotive_navigation, fitness, other_navigation or other' - }, - desired_accuracy: { - type: :number, - example: 0, - description: 'Desired accuracy in meters' - }, - deferred: { - type: :number, - example: 0, - description: 'the distance in meters to defer location updates' - }, - significant_change: { - type: :string, - example: 'disabled', - description: 'a significant change mode, disabled, enabled or exclusive' - }, - locations_in_payload: { - type: :number, - example: 1, - description: 'the number of locations in the payload' - }, - device_id: { - type: :string, - example: 'iOS device #166', - description: 'the device id' - }, - unique_id: { - type: :string, - example: '1234567890', - description: 'the device\'s Unique ID as set by Apple' - }, - wifi: { - type: :string, - example: 'unknown', - description: 'the WiFi network name' - }, - battery_state: { - type: :string, - example: 'unknown', - description: 'the battery state, unknown, unplugged, charging or full' - }, - battery_level: { - type: :number, - example: 0, - description: 'the battery level percentage, from 0 to 1' - } - }, - required: %w[geometry properties] } } } diff --git a/spec/swagger/api/v1/owntracks/points_controller_spec.rb b/spec/swagger/api/v1/owntracks/points_controller_spec.rb index 00157df8..5159a302 100644 --- a/spec/swagger/api/v1/owntracks/points_controller_spec.rb +++ b/spec/swagger/api/v1/owntracks/points_controller_spec.rb @@ -43,11 +43,11 @@ describe 'OwnTracks Points API', type: :request do lon: { type: :number, description: 'Longitude coordinate' }, acc: { type: :number, description: 'Accuracy of position in meters' }, bs: { type: :number, description: 'Battery status (0=unknown, 1=unplugged, 2=charging, 3=full)' }, - inrids: { type: :array, description: 'Array of region IDs device is currently in' }, + inrids: { type: :array, items: { type: :string }, description: 'Array of region IDs device is currently in' }, BSSID: { type: :string, description: 'Connected WiFi access point MAC address' }, SSID: { type: :string, description: 'Connected WiFi network name' }, vac: { type: :number, description: 'Vertical accuracy in meters' }, - inregions: { type: :array, description: 'Array of region names device is currently in' }, + inregions: { type: :array, items: { type: :string }, description: 'Array of region names device is currently in' }, lat: { type: :number, description: 'Latitude coordinate' }, topic: { type: :string, description: 'MQTT topic in format owntracks/user/device' }, t: { type: :string, description: 'Type of message (p=position, c=circle, etc)' }, @@ -63,7 +63,7 @@ describe 'OwnTracks Points API', type: :request do isotst: { type: :string, description: 'ISO 8601 timestamp of the location fix' }, disptst: { type: :string, description: 'Human-readable timestamp of the location fix' } }, - required: %w[owntracks/jane] + required: %w[lat lon tst _type] } parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key' diff --git a/spec/swagger/api/v1/points_controller_spec.rb b/spec/swagger/api/v1/points_controller_spec.rb index 7450df45..2b5fe369 100644 --- a/spec/swagger/api/v1/points_controller_spec.rb +++ b/spec/swagger/api/v1/points_controller_spec.rb @@ -39,8 +39,8 @@ describe 'Points API', type: :request do timestamp: { type: :number }, latitude: { type: :number }, mode: { type: :number }, - inrids: { type: :array }, - in_regions: { type: :array }, + inrids: { type: :array, items: { type: :string } }, + in_regions: { type: :array, items: { type: :string } }, raw_data: { type: :string }, import_id: { type: :string }, city: { type: :string }, diff --git a/spec/swagger/api/v1/settings_controller_spec.rb b/spec/swagger/api/v1/settings_controller_spec.rb index aecba56b..e9716d12 100644 --- a/spec/swagger/api/v1/settings_controller_spec.rb +++ b/spec/swagger/api/v1/settings_controller_spec.rb @@ -7,12 +7,22 @@ describe 'Settings API', type: :request do patch 'Updates user settings' do request_body_example value: { 'settings': { - 'route_opacity': 0.3, - 'meters_between_routes': 100, - 'minutes_between_routes': 100, - 'fog_of_war_meters': 100, - 'time_threshold_minutes': 100, - 'merge_threshold_minutes': 100 + 'route_opacity': 60, + 'meters_between_routes': 500, + 'minutes_between_routes': 30, + 'fog_of_war_meters': 50, + 'time_threshold_minutes': 30, + 'merge_threshold_minutes': 15, + 'preferred_map_layer': 'OpenStreetMap', + 'speed_colored_routes': false, + 'points_rendering_mode': 'raw', + 'live_map_enabled': true, + 'immich_url': 'https://immich.example.com', + 'immich_api_key': 'your-immich-api-key', + 'photoprism_url': 'https://photoprism.example.com', + 'photoprism_api_key': 'your-photoprism-api-key', + 'maps': { 'distance_unit': 'km' }, + 'visits_suggestions_enabled': true } } tags 'Settings' @@ -22,31 +32,95 @@ describe 'Settings API', type: :request do properties: { route_opacity: { type: :number, - example: 0.3, - description: 'the opacity of the route, float between 0 and 1' + example: 60, + description: 'Route opacity percentage (0-100)' }, meters_between_routes: { type: :number, - example: 100, - description: 'the distance between routes in meters' + example: 500, + description: 'Minimum distance between routes in meters' }, minutes_between_routes: { type: :number, - example: 100, - description: 'the time between routes in minutes' + example: 30, + description: 'Minimum time between routes in minutes' }, fog_of_war_meters: { type: :number, - example: 100, - description: 'the fog of war distance in meters' + example: 50, + description: 'Fog of war radius in meters' + }, + time_threshold_minutes: { + type: :number, + example: 30, + description: 'Time threshold for grouping points in minutes' + }, + merge_threshold_minutes: { + type: :number, + example: 15, + description: 'Threshold for merging nearby points in minutes' + }, + preferred_map_layer: { + type: :string, + example: 'OpenStreetMap', + description: 'Preferred map layer/tile provider' + }, + speed_colored_routes: { + type: :boolean, + example: false, + description: 'Whether to color routes based on speed' + }, + points_rendering_mode: { + type: :string, + example: 'raw', + description: 'How to render points on the map (raw, heatmap, etc.)' + }, + live_map_enabled: { + type: :boolean, + example: true, + description: 'Whether live map updates are enabled' + }, + immich_url: { + type: :string, + example: 'https://immich.example.com', + description: 'Immich server URL for photo integration' + }, + immich_api_key: { + type: :string, + example: 'your-immich-api-key', + description: 'API key for Immich photo service' + }, + photoprism_url: { + type: :string, + example: 'https://photoprism.example.com', + description: 'PhotoPrism server URL for photo integration' + }, + photoprism_api_key: { + type: :string, + example: 'your-photoprism-api-key', + description: 'API key for PhotoPrism photo service' + }, + maps: { + type: :object, + properties: { + distance_unit: { + type: :string, + example: 'km', + description: 'Distance unit preference (km or miles)' + } + }, + description: 'Map-related settings' + }, + visits_suggestions_enabled: { + type: :boolean, + example: true, + description: 'Whether visit suggestions are enabled' } - }, - optional: %w[route_opacity meters_between_routes minutes_between_routes fog_of_war_meters - time_threshold_minutes merge_threshold_minutes] + } } parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key' response '200', 'settings updated' do - let(:settings) { { settings: { route_opacity: 0.3 } } } + let(:settings) { { settings: { route_opacity: 60 } } } let(:api_key) { create(:user).api_key } run_test! @@ -65,27 +139,91 @@ describe 'Settings API', type: :request do properties: { route_opacity: { type: :string, - example: 0.3, - description: 'the opacity of the route, float between 0 and 1' + example: '60', + description: 'Route opacity percentage (0-100)' }, meters_between_routes: { type: :string, - example: 100, - description: 'the distance between routes in meters' + example: '500', + description: 'Minimum distance between routes in meters' }, minutes_between_routes: { type: :string, - example: 100, - description: 'the time between routes in minutes' + example: '30', + description: 'Minimum time between routes in minutes' }, fog_of_war_meters: { type: :string, - example: 100, - description: 'the fog of war distance in meters' + example: '50', + description: 'Fog of war radius in meters' + }, + time_threshold_minutes: { + type: :string, + example: '30', + description: 'Time threshold for grouping points in minutes' + }, + merge_threshold_minutes: { + type: :string, + example: '15', + description: 'Threshold for merging nearby points in minutes' + }, + preferred_map_layer: { + type: :string, + example: 'OpenStreetMap', + description: 'Preferred map layer/tile provider' + }, + speed_colored_routes: { + type: :boolean, + example: false, + description: 'Whether to color routes based on speed' + }, + points_rendering_mode: { + type: :string, + example: 'raw', + description: 'How to render points on the map (raw, heatmap, etc.)' + }, + live_map_enabled: { + type: :boolean, + example: true, + description: 'Whether live map updates are enabled' + }, + immich_url: { + type: :string, + example: 'https://immich.example.com', + description: 'Immich server URL for photo integration' + }, + immich_api_key: { + type: :string, + example: 'your-immich-api-key', + description: 'API key for Immich photo service' + }, + photoprism_url: { + type: :string, + example: 'https://photoprism.example.com', + description: 'PhotoPrism server URL for photo integration' + }, + photoprism_api_key: { + type: :string, + example: 'your-photoprism-api-key', + description: 'API key for PhotoPrism photo service' + }, + maps: { + type: :object, + properties: { + distance_unit: { + type: :string, + example: 'km', + description: 'Distance unit preference (km or miles)' + } + }, + description: 'Map-related settings' + }, + visits_suggestions_enabled: { + type: :boolean, + example: true, + description: 'Whether visit suggestions are enabled' } - }, - required: %w[route_opacity meters_between_routes minutes_between_routes fog_of_war_meters - time_threshold_minutes merge_threshold_minutes] + } } } diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index a58bcb10..7a65546f 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -141,20 +141,20 @@ paths: type: string - name: start_at in: query - format: date-time + schema: + type: string + format: date required: true description: Start date in YYYY-MM-DD format example: '2023-01-01' - schema: - type: string - name: end_at in: query - format: date-time + schema: + type: string + format: date required: true description: End date in YYYY-MM-DD format example: '2023-12-31' - schema: - type: string responses: '200': description: cities found @@ -231,17 +231,19 @@ paths: description: Healthy headers: X-Dawarich-Response: - type: string + schema: + type: string + example: Hey, I'm alive! required: true - example: Hey, I'm alive! description: Depending on the authentication status of the request, the response will be different. If the request is authenticated, the response will be 'Hey, I'm alive and authenticated!'. If the request is not authenticated, the response will be 'Hey, I'm alive!'. X-Dawarich-Version: - type: string + schema: + type: string + example: 1.0.0 required: true - example: 1.0.0 description: 'The version of the application, for example: 1.0.0' content: application/json: @@ -273,99 +275,109 @@ paths: schema: type: object properties: - type: - type: string - example: Feature - geometry: - type: object - properties: - type: - type: string - example: Point - coordinates: - type: array - example: - - 13.356718 - - 52.502397 - properties: - type: object - properties: - timestamp: - type: string - example: '2021-06-01T12:00:00Z' - description: Timestamp in ISO 8601 format - altitude: - type: number - example: 0 - description: Altitude in meters - speed: - type: number - example: 0 - description: Speed in meters per second - horizontal_accuracy: - type: number - example: 0 - description: Horizontal accuracy in meters - vertical_accuracy: - type: number - example: 0 - description: Vertical accuracy in meters - motion: - type: array - example: - - walking - - running - - driving - - cycling - - stationary - description: 'Motion type, for example: automotive_navigation, - fitness, other_navigation or other' - activity: - type: string - example: unknown - description: 'Activity type, for example: automotive_navigation, - fitness, other_navigation or other' - desired_accuracy: - type: number - example: 0 - description: Desired accuracy in meters - deferred: - type: number - example: 0 - description: the distance in meters to defer location updates - significant_change: - type: string - example: disabled - description: a significant change mode, disabled, enabled or - exclusive - locations_in_payload: - type: number - example: 1 - description: the number of locations in the payload - device_id: - type: string - example: 'iOS device #166' - description: the device id - unique_id: - type: string - example: '1234567890' - description: the device's Unique ID as set by Apple - wifi: - type: string - example: unknown - description: the WiFi network name - battery_state: - type: string - example: unknown - description: the battery state, unknown, unplugged, charging - or full - battery_level: - type: number - example: 0 - description: the battery level percentage, from 0 to 1 - required: - - geometry - - properties + locations: + type: array + items: + type: object + properties: + type: + type: string + example: Feature + geometry: + type: object + properties: + type: + type: string + example: Point + coordinates: + type: array + items: + type: number + example: + - 13.356718 + - 52.502397 + properties: + type: object + properties: + timestamp: + type: string + example: '2021-06-01T12:00:00Z' + description: Timestamp in ISO 8601 format + altitude: + type: number + example: 0 + description: Altitude in meters + speed: + type: number + example: 0 + description: Speed in meters per second + horizontal_accuracy: + type: number + example: 0 + description: Horizontal accuracy in meters + vertical_accuracy: + type: number + example: 0 + description: Vertical accuracy in meters + motion: + type: array + items: + type: string + example: + - walking + - running + - driving + - cycling + - stationary + description: 'Motion type, for example: automotive_navigation, + fitness, other_navigation or other' + activity: + type: string + example: unknown + description: 'Activity type, for example: automotive_navigation, + fitness, other_navigation or other' + desired_accuracy: + type: number + example: 0 + description: Desired accuracy in meters + deferred: + type: number + example: 0 + description: the distance in meters to defer location + updates + significant_change: + type: string + example: disabled + description: a significant change mode, disabled, enabled + or exclusive + locations_in_payload: + type: number + example: 1 + description: the number of locations in the payload + device_id: + type: string + example: 'iOS device #166' + description: the device id + unique_id: + type: string + example: '1234567890' + description: the device's Unique ID as set by Apple + wifi: + type: string + example: unknown + description: the WiFi network name + battery_state: + type: string + example: unknown + description: the battery state, unknown, unplugged, charging + or full + battery_level: + type: number + example: 0 + description: the battery level percentage, from 0 to 1 + required: + - geometry + - properties examples: '0': summary: Creates a batch of points @@ -433,6 +445,8 @@ paths: 3=full) inrids: type: array + items: + type: string description: Array of region IDs device is currently in BSSID: type: string @@ -445,6 +459,8 @@ paths: description: Vertical accuracy in meters inregions: type: array + items: + type: string description: Array of region names device is currently in lat: type: number @@ -489,7 +505,10 @@ paths: type: string description: Human-readable timestamp of the location fix required: - - owntracks/jane + - lat + - lon + - tst + - _type examples: '0': summary: Creates a point @@ -805,8 +824,12 @@ paths: type: number inrids: type: array + items: + type: string in_regions: type: array + items: + type: string raw_data: type: string import_id: @@ -982,38 +1005,94 @@ paths: properties: route_opacity: type: number - example: 0.3 - description: the opacity of the route, float between 0 and 1 + example: 60 + description: Route opacity percentage (0-100) meters_between_routes: type: number - example: 100 - description: the distance between routes in meters + example: 500 + description: Minimum distance between routes in meters minutes_between_routes: type: number - example: 100 - description: the time between routes in minutes + example: 30 + description: Minimum time between routes in minutes fog_of_war_meters: type: number - example: 100 - description: the fog of war distance in meters - optional: - - route_opacity - - meters_between_routes - - minutes_between_routes - - fog_of_war_meters - - time_threshold_minutes - - merge_threshold_minutes + example: 50 + description: Fog of war radius in meters + time_threshold_minutes: + type: number + example: 30 + description: Time threshold for grouping points in minutes + merge_threshold_minutes: + type: number + example: 15 + description: Threshold for merging nearby points in minutes + preferred_map_layer: + type: string + example: OpenStreetMap + description: Preferred map layer/tile provider + speed_colored_routes: + type: boolean + example: false + description: Whether to color routes based on speed + points_rendering_mode: + type: string + example: raw + description: How to render points on the map (raw, heatmap, etc.) + live_map_enabled: + type: boolean + example: true + description: Whether live map updates are enabled + immich_url: + type: string + example: https://immich.example.com + description: Immich server URL for photo integration + immich_api_key: + type: string + example: your-immich-api-key + description: API key for Immich photo service + photoprism_url: + type: string + example: https://photoprism.example.com + description: PhotoPrism server URL for photo integration + photoprism_api_key: + type: string + example: your-photoprism-api-key + description: API key for PhotoPrism photo service + maps: + type: object + properties: + distance_unit: + type: string + example: km + description: Distance unit preference (km or miles) + description: Map-related settings + visits_suggestions_enabled: + type: boolean + example: true + description: Whether visit suggestions are enabled examples: '0': summary: Updates user settings value: settings: - route_opacity: 0.3 - meters_between_routes: 100 - minutes_between_routes: 100 - fog_of_war_meters: 100 - time_threshold_minutes: 100 - merge_threshold_minutes: 100 + route_opacity: 60 + meters_between_routes: 500 + minutes_between_routes: 30 + fog_of_war_meters: 50 + time_threshold_minutes: 30 + merge_threshold_minutes: 15 + preferred_map_layer: OpenStreetMap + speed_colored_routes: false + points_rendering_mode: raw + live_map_enabled: true + immich_url: https://immich.example.com + immich_api_key: your-immich-api-key + photoprism_url: https://photoprism.example.com + photoprism_api_key: your-photoprism-api-key + maps: + distance_unit: km + visits_suggestions_enabled: true get: summary: Retrieves user settings tags: @@ -1038,28 +1117,73 @@ paths: properties: route_opacity: type: string - example: 0.3 - description: the opacity of the route, float between 0 and - 1 + example: '60' + description: Route opacity percentage (0-100) meters_between_routes: type: string - example: 100 - description: the distance between routes in meters + example: '500' + description: Minimum distance between routes in meters minutes_between_routes: type: string - example: 100 - description: the time between routes in minutes + example: '30' + description: Minimum time between routes in minutes fog_of_war_meters: type: string - example: 100 - description: the fog of war distance in meters - required: - - route_opacity - - meters_between_routes - - minutes_between_routes - - fog_of_war_meters - - time_threshold_minutes - - merge_threshold_minutes + example: '50' + description: Fog of war radius in meters + time_threshold_minutes: + type: string + example: '30' + description: Time threshold for grouping points in minutes + merge_threshold_minutes: + type: string + example: '15' + description: Threshold for merging nearby points in minutes + preferred_map_layer: + type: string + example: OpenStreetMap + description: Preferred map layer/tile provider + speed_colored_routes: + type: boolean + example: false + description: Whether to color routes based on speed + points_rendering_mode: + type: string + example: raw + description: How to render points on the map (raw, heatmap, + etc.) + live_map_enabled: + type: boolean + example: true + description: Whether live map updates are enabled + immich_url: + type: string + example: https://immich.example.com + description: Immich server URL for photo integration + immich_api_key: + type: string + example: your-immich-api-key + description: API key for Immich photo service + photoprism_url: + type: string + example: https://photoprism.example.com + description: PhotoPrism server URL for photo integration + photoprism_api_key: + type: string + example: your-photoprism-api-key + description: API key for PhotoPrism photo service + maps: + type: object + properties: + distance_unit: + type: string + example: km + description: Distance unit preference (km or miles) + description: Map-related settings + visits_suggestions_enabled: + type: boolean + example: true + description: Whether visit suggestions are enabled "/api/v1/stats": get: summary: Retrieves all stats