mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Fixes for visits suggestions.
This commit is contained in:
parent
12a53aac20
commit
3b474704ea
18 changed files with 811 additions and 324 deletions
|
|
@ -6,9 +6,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
# [0.29.2] - UNRELEASED
|
# [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
|
## Changed
|
||||||
|
|
||||||
- Don't check for new version in production.
|
- 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
|
# [0.29.1] - 2025-07-02
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@
|
||||||
class SettingsController < ApplicationController
|
class SettingsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :authenticate_active_user!, only: %i[update]
|
before_action :authenticate_active_user!, only: %i[update]
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def update
|
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'
|
flash.now[:notice] = 'Settings updated'
|
||||||
|
|
||||||
|
|
@ -31,7 +34,8 @@ class SettingsController < ApplicationController
|
||||||
params.require(:settings).permit(
|
params.require(:settings).permit(
|
||||||
:meters_between_routes, :minutes_between_routes, :fog_of_war_meters,
|
:meters_between_routes, :minutes_between_routes, :fog_of_war_meters,
|
||||||
:time_threshold_minutes, :merge_threshold_minutes, :route_opacity,
|
: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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,96 @@
|
||||||
import { showFlashMessage } from "./helpers";
|
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) {
|
export function handleAreaCreated(areasLayer, layer, apiKey) {
|
||||||
|
// Add popup styles
|
||||||
|
addPopupStyles();
|
||||||
const radius = layer.getRadius();
|
const radius = layer.getRadius();
|
||||||
const center = layer.getLatLng();
|
const center = layer.getLatLng();
|
||||||
|
|
||||||
const formHtml = `
|
const formHtml = `
|
||||||
<div class="card w-96">
|
<div class="card w-96 bg-base-100 border border-base-300 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">New Area</h2>
|
<h2 class="card-title text-gray-500">New Area</h2>
|
||||||
<form id="circle-form" class="space-y-4">
|
<form id="circle-form" class="space-y-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="circle-name"
|
id="circle-name"
|
||||||
name="area[name]"
|
name="area[name]"
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered input-primary w-full bg-base-200 text-base-content placeholder-base-content/70 border-base-300 focus:border-primary focus:bg-base-100"
|
||||||
placeholder="Enter area name"
|
placeholder="Enter area name"
|
||||||
autofocus
|
autofocus
|
||||||
required>
|
required>
|
||||||
|
|
@ -23,7 +100,7 @@ export function handleAreaCreated(areasLayer, layer, apiKey) {
|
||||||
<input type="hidden" name="area[radius]" value="${radius}">
|
<input type="hidden" name="area[radius]" value="${radius}">
|
||||||
<div class="flex justify-between mt-4">
|
<div class="flex justify-between mt-4">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-outline"
|
class="btn btn-outline btn-neutral text-base-content border-base-300 hover:bg-base-200"
|
||||||
onclick="this.closest('.leaflet-popup').querySelector('.leaflet-popup-close-button').click()">
|
onclick="this.closest('.leaflet-popup').querySelector('.leaflet-popup-close-button').click()">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -35,11 +112,14 @@ export function handleAreaCreated(areasLayer, layer, apiKey) {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
layer.bindPopup(formHtml, {
|
layer.bindPopup(formHtml, {
|
||||||
maxWidth: "auto",
|
maxWidth: 400,
|
||||||
minWidth: 300,
|
minWidth: 384,
|
||||||
|
maxHeight: 600,
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
closeOnClick: false,
|
closeOnClick: false,
|
||||||
className: 'area-form-popup'
|
className: 'area-form-popup',
|
||||||
|
autoPan: true,
|
||||||
|
keepInView: true
|
||||||
}).openPopup();
|
}).openPopup();
|
||||||
|
|
||||||
areasLayer.addLayer(layer);
|
areasLayer.addLayer(layer);
|
||||||
|
|
@ -69,7 +149,7 @@ export function handleAreaCreated(areasLayer, layer, apiKey) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!nameInput.value.trim()) {
|
if (!nameInput.value.trim()) {
|
||||||
nameInput.classList.add('input-error');
|
nameInput.classList.add('input-error', 'border-error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,10 +186,29 @@ export function saveArea(formData, areasLayer, layer, apiKey) {
|
||||||
.then(data => {
|
.then(data => {
|
||||||
layer.closePopup();
|
layer.closePopup();
|
||||||
layer.bindPopup(`
|
layer.bindPopup(`
|
||||||
Name: ${data.name}<br>
|
<div class="card w-80 bg-base-100 border border-base-300 shadow-lg">
|
||||||
Radius: ${Math.round(data.radius)} meters<br>
|
<div class="card-body">
|
||||||
<a href="#" data-id="${data.id}" class="delete-area">[Delete]</a>
|
<h3 class="card-title text-base-content text-lg">${data.name}</h3>
|
||||||
`).openPopup();
|
<div class="space-y-2 text-base-content/80">
|
||||||
|
<p><span class="font-medium text-base-content">Radius:</span> ${Math.round(data.radius)} meters</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions justify-end mt-4">
|
||||||
|
<button class="btn btn-sm btn-error delete-area" data-id="${data.id}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`, {
|
||||||
|
maxWidth: 340,
|
||||||
|
minWidth: 320,
|
||||||
|
className: 'area-info-popup',
|
||||||
|
closeButton: true,
|
||||||
|
closeOnClick: false
|
||||||
|
}).openPopup();
|
||||||
|
|
||||||
// Add event listener for the delete button
|
// Add event listener for the delete button
|
||||||
layer.on('popupopen', () => {
|
layer.on('popupopen', () => {
|
||||||
|
|
@ -151,6 +250,9 @@ export function deleteArea(id, areasLayer, layer, apiKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAndDrawAreas(areasLayer, apiKey) {
|
export function fetchAndDrawAreas(areasLayer, apiKey) {
|
||||||
|
// Add popup styles
|
||||||
|
addPopupStyles();
|
||||||
|
|
||||||
fetch(`/api/v1/areas?api_key=${apiKey}`, {
|
fetch(`/api/v1/areas?api_key=${apiKey}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -186,20 +288,42 @@ export function fetchAndDrawAreas(areasLayer, apiKey) {
|
||||||
pane: 'areasPane'
|
pane: 'areasPane'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bind popup content
|
// Bind popup content with proper theme-aware styling
|
||||||
const popupContent = `
|
const popupContent = `
|
||||||
<div class="card w-full">
|
<div class="card w-96 bg-base-100 border border-base-300 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">${area.name}</h2>
|
<h2 class="card-title text-base-content text-xl">${area.name}</h2>
|
||||||
<p>Radius: ${Math.round(radius)} meters</p>
|
<div class="space-y-3">
|
||||||
<p>Center: [${lat.toFixed(4)}, ${lng.toFixed(4)}]</p>
|
<div class="stats stats-vertical shadow bg-base-200">
|
||||||
<div class="flex justify-end mt-4">
|
<div class="stat py-2">
|
||||||
<button class="btn btn-sm btn-error delete-area" data-id="${area.id}">Delete</button>
|
<div class="stat-title text-base-content/70 text-sm">Radius</div>
|
||||||
|
<div class="stat-value text-base-content text-lg">${Math.round(radius)} meters</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat py-2">
|
||||||
|
<div class="stat-title text-base-content/70 text-sm">Center</div>
|
||||||
|
<div class="stat-value text-base-content text-sm">[${lat.toFixed(4)}, ${lng.toFixed(4)}]</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions justify-between items-center mt-6">
|
||||||
|
<div class="badge badge-primary badge-outline">Area ${area.id}</div>
|
||||||
|
<button class="btn btn-error btn-sm delete-area" data-id="${area.id}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
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
|
// Add delete button handler when popup opens
|
||||||
circle.on('popupopen', () => {
|
circle.on('popupopen', () => {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class BulkVisitsSuggestingJob < ApplicationJob
|
||||||
time_chunks = Visits::TimeChunks.new(start_at:, end_at:).call
|
time_chunks = Visits::TimeChunks.new(start_at:, end_at:).call
|
||||||
|
|
||||||
users.active.find_each do |user|
|
users.active.find_each do |user|
|
||||||
|
next unless user.safe_settings.visits_suggestions_enabled?
|
||||||
next if user.tracked_points.empty?
|
next if user.tracked_points.empty?
|
||||||
|
|
||||||
schedule_chunked_jobs(user, time_chunks)
|
schedule_chunked_jobs(user, time_chunks)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ class Users::SafeSettings
|
||||||
'immich_api_key' => nil,
|
'immich_api_key' => nil,
|
||||||
'photoprism_url' => nil,
|
'photoprism_url' => nil,
|
||||||
'photoprism_api_key' => nil,
|
'photoprism_api_key' => nil,
|
||||||
'maps' => { 'distance_unit' => 'km' }
|
'maps' => { 'distance_unit' => 'km' },
|
||||||
|
'visits_suggestions_enabled' => 'true'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def initialize(settings = {})
|
def initialize(settings = {})
|
||||||
|
|
@ -43,7 +44,8 @@ class Users::SafeSettings
|
||||||
photoprism_url: photoprism_url,
|
photoprism_url: photoprism_url,
|
||||||
photoprism_api_key: photoprism_api_key,
|
photoprism_api_key: photoprism_api_key,
|
||||||
maps: maps,
|
maps: maps,
|
||||||
distance_unit: distance_unit
|
distance_unit: distance_unit,
|
||||||
|
visits_suggestions_enabled: visits_suggestions_enabled?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
@ -111,4 +113,8 @@ class Users::SafeSettings
|
||||||
def distance_unit
|
def distance_unit
|
||||||
settings.dig('maps', 'distance_unit')
|
settings.dig('maps', 'distance_unit')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visits_suggestions_enabled?
|
||||||
|
settings['visits_suggestions_enabled'] == 'true'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<span>Spamming many new jobs at once is a bad idea. Let them work or clear the queue beforehand.</span>
|
<span>Spamming many new jobs at once is a bad idea. Let them work or clear the queue beforehand.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='flex'>
|
<div class='flex flex-wrap'>
|
||||||
<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">Start Reverse Geocoding</h2>
|
<h2 class="card-title">Start Reverse Geocoding</h2>
|
||||||
|
|
@ -48,6 +48,20 @@
|
||||||
<%= link_to 'Open Dashboard', '/sidekiq', target: '_blank', class: 'btn btn-primary' %>
|
<%= link_to 'Open Dashboard', '/sidekiq', target: '_blank', class: 'btn btn-primary' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-300 w-96 shadow-xl m-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<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>
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<% 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 %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -102,5 +102,17 @@ RSpec.describe BulkVisitsSuggestingJob, type: :job do
|
||||||
|
|
||||||
described_class.perform_now(start_at: custom_start, end_at: custom_end)
|
described_class.perform_now(start_at: custom_start, end_at: custom_end)
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ require 'super_diff/rspec-rails'
|
||||||
require 'rake'
|
require 'rake'
|
||||||
|
|
||||||
Rails.application.load_tasks
|
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!
|
# 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 }
|
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.filter_rails_from_backtrace!
|
||||||
|
|
||||||
config.include FactoryBot::Syntax::Methods
|
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.rswag_dry_run = false
|
||||||
|
|
||||||
|
config.before(:suite) do
|
||||||
|
# Ensure Rails routes are loaded for Devise
|
||||||
|
Rails.application.reload_routes!
|
||||||
|
end
|
||||||
|
|
||||||
config.before do
|
config.before do
|
||||||
ActiveJob::Base.queue_adapter = :test
|
ActiveJob::Base.queue_adapter = :test
|
||||||
allow(DawarichSettings).to receive(:store_geodata?).and_return(true)
|
allow(DawarichSettings).to receive(:store_geodata?).and_return(true)
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,9 @@ RSpec.describe 'Settings', type: :request do
|
||||||
it 'updates the user settings' do
|
it 'updates the user settings' do
|
||||||
patch '/settings', params: params
|
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
|
end
|
||||||
|
|
||||||
context 'when user is inactive' do
|
context 'when user is inactive' do
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Users::SafeSettings do
|
RSpec.describe Users::SafeSettings do
|
||||||
describe '#default_settings' do
|
describe '#default_settings' do
|
||||||
context 'with default values' do
|
context 'with default values' do
|
||||||
|
|
@ -24,7 +26,8 @@ RSpec.describe Users::SafeSettings do
|
||||||
photoprism_url: nil,
|
photoprism_url: nil,
|
||||||
photoprism_api_key: nil,
|
photoprism_api_key: nil,
|
||||||
maps: { "distance_unit" => "km" },
|
maps: { "distance_unit" => "km" },
|
||||||
distance_unit: 'km'
|
distance_unit: 'km',
|
||||||
|
visits_suggestions_enabled: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
@ -47,7 +50,8 @@ RSpec.describe Users::SafeSettings do
|
||||||
'immich_api_key' => 'immich-key',
|
'immich_api_key' => 'immich-key',
|
||||||
'photoprism_url' => 'https://photoprism.example.com',
|
'photoprism_url' => 'https://photoprism.example.com',
|
||||||
'photoprism_api_key' => 'photoprism-key',
|
'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
|
end
|
||||||
let(:safe_settings) { described_class.new(settings) }
|
let(:safe_settings) { described_class.new(settings) }
|
||||||
|
|
@ -69,7 +73,32 @@ RSpec.describe Users::SafeSettings do
|
||||||
"immich_api_key" => "immich-key",
|
"immich_api_key" => "immich-key",
|
||||||
"photoprism_url" => "https://photoprism.example.com",
|
"photoprism_url" => "https://photoprism.example.com",
|
||||||
"photoprism_api_key" => "photoprism-key",
|
"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
|
end
|
||||||
|
|
@ -98,6 +127,7 @@ RSpec.describe Users::SafeSettings do
|
||||||
expect(safe_settings.photoprism_url).to be_nil
|
expect(safe_settings.photoprism_url).to be_nil
|
||||||
expect(safe_settings.photoprism_api_key).to be_nil
|
expect(safe_settings.photoprism_api_key).to be_nil
|
||||||
expect(safe_settings.maps).to eq({ "distance_unit" => "km" })
|
expect(safe_settings.maps).to eq({ "distance_unit" => "km" })
|
||||||
|
expect(safe_settings.visits_suggestions_enabled?).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -118,7 +148,8 @@ RSpec.describe Users::SafeSettings do
|
||||||
'immich_api_key' => 'immich-key',
|
'immich_api_key' => 'immich-key',
|
||||||
'photoprism_url' => 'https://photoprism.example.com',
|
'photoprism_url' => 'https://photoprism.example.com',
|
||||||
'photoprism_api_key' => 'photoprism-key',
|
'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
|
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_url).to eq('https://photoprism.example.com')
|
||||||
expect(safe_settings.photoprism_api_key).to eq('photoprism-key')
|
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.maps).to eq({ 'name' => 'custom', 'url' => 'https://custom.example.com' })
|
||||||
|
expect(safe_settings.visits_suggestions_enabled?).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,15 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# https://makandracards.com/makandra/37161-rspec-devise-how-to-sign-in-users-in-request-specs
|
# Standard Devise test helpers configuration for 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
|
|
||||||
|
|
||||||
RSpec.configure do |config|
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,20 @@ RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do
|
||||||
description: 'Your API authentication key'
|
description: 'Your API authentication key'
|
||||||
parameter name: :start_at,
|
parameter name: :start_at,
|
||||||
in: :query,
|
in: :query,
|
||||||
type: :string,
|
schema: {
|
||||||
format: 'date-time',
|
type: :string,
|
||||||
|
format: :date
|
||||||
|
},
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Start date in YYYY-MM-DD format',
|
description: 'Start date in YYYY-MM-DD format',
|
||||||
example: '2023-01-01'
|
example: '2023-01-01'
|
||||||
|
|
||||||
parameter name: :end_at,
|
parameter name: :end_at,
|
||||||
in: :query,
|
in: :query,
|
||||||
type: :string,
|
schema: {
|
||||||
format: 'date-time',
|
type: :string,
|
||||||
|
format: :date
|
||||||
|
},
|
||||||
required: true,
|
required: true,
|
||||||
description: 'End date in YYYY-MM-DD format',
|
description: 'End date in YYYY-MM-DD format',
|
||||||
example: '2023-12-31'
|
example: '2023-12-31'
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,18 @@ describe 'Health API', type: :request do
|
||||||
}
|
}
|
||||||
|
|
||||||
header 'X-Dawarich-Response',
|
header 'X-Dawarich-Response',
|
||||||
type: :string,
|
schema: {
|
||||||
|
type: :string,
|
||||||
|
example: 'Hey, I\'m alive!'
|
||||||
|
},
|
||||||
required: true,
|
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!'."
|
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',
|
header 'X-Dawarich-Version',
|
||||||
type: :string,
|
schema: {
|
||||||
|
type: :string,
|
||||||
|
example: '1.0.0'
|
||||||
|
},
|
||||||
required: true,
|
required: true,
|
||||||
example: '1.0.0',
|
|
||||||
description: 'The version of the application, for example: 1.0.0'
|
description: 'The version of the application, for example: 1.0.0'
|
||||||
|
|
||||||
run_test!
|
run_test!
|
||||||
|
|
|
||||||
|
|
@ -40,99 +40,112 @@ describe 'Overland Batches API', type: :request do
|
||||||
parameter name: :locations, in: :body, schema: {
|
parameter name: :locations, in: :body, schema: {
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: {
|
properties: {
|
||||||
type: { type: :string, example: 'Feature' },
|
locations: {
|
||||||
geometry: {
|
type: :array,
|
||||||
type: :object,
|
items: {
|
||||||
properties: {
|
type: :object,
|
||||||
type: { type: :string, example: 'Point' },
|
properties: {
|
||||||
coordinates: { type: :array, example: [13.356718, 52.502397] }
|
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]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,11 @@ describe 'OwnTracks Points API', type: :request do
|
||||||
lon: { type: :number, description: 'Longitude coordinate' },
|
lon: { type: :number, description: 'Longitude coordinate' },
|
||||||
acc: { type: :number, description: 'Accuracy of position in meters' },
|
acc: { type: :number, description: 'Accuracy of position in meters' },
|
||||||
bs: { type: :number, description: 'Battery status (0=unknown, 1=unplugged, 2=charging, 3=full)' },
|
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' },
|
BSSID: { type: :string, description: 'Connected WiFi access point MAC address' },
|
||||||
SSID: { type: :string, description: 'Connected WiFi network name' },
|
SSID: { type: :string, description: 'Connected WiFi network name' },
|
||||||
vac: { type: :number, description: 'Vertical accuracy in meters' },
|
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' },
|
lat: { type: :number, description: 'Latitude coordinate' },
|
||||||
topic: { type: :string, description: 'MQTT topic in format owntracks/user/device' },
|
topic: { type: :string, description: 'MQTT topic in format owntracks/user/device' },
|
||||||
t: { type: :string, description: 'Type of message (p=position, c=circle, etc)' },
|
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' },
|
isotst: { type: :string, description: 'ISO 8601 timestamp of the location fix' },
|
||||||
disptst: { type: :string, description: 'Human-readable 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'
|
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ describe 'Points API', type: :request do
|
||||||
timestamp: { type: :number },
|
timestamp: { type: :number },
|
||||||
latitude: { type: :number },
|
latitude: { type: :number },
|
||||||
mode: { type: :number },
|
mode: { type: :number },
|
||||||
inrids: { type: :array },
|
inrids: { type: :array, items: { type: :string } },
|
||||||
in_regions: { type: :array },
|
in_regions: { type: :array, items: { type: :string } },
|
||||||
raw_data: { type: :string },
|
raw_data: { type: :string },
|
||||||
import_id: { type: :string },
|
import_id: { type: :string },
|
||||||
city: { type: :string },
|
city: { type: :string },
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,22 @@ describe 'Settings API', type: :request do
|
||||||
patch 'Updates user settings' do
|
patch 'Updates user settings' do
|
||||||
request_body_example value: {
|
request_body_example value: {
|
||||||
'settings': {
|
'settings': {
|
||||||
'route_opacity': 0.3,
|
'route_opacity': 60,
|
||||||
'meters_between_routes': 100,
|
'meters_between_routes': 500,
|
||||||
'minutes_between_routes': 100,
|
'minutes_between_routes': 30,
|
||||||
'fog_of_war_meters': 100,
|
'fog_of_war_meters': 50,
|
||||||
'time_threshold_minutes': 100,
|
'time_threshold_minutes': 30,
|
||||||
'merge_threshold_minutes': 100
|
'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'
|
tags 'Settings'
|
||||||
|
|
@ -22,31 +32,95 @@ describe 'Settings API', type: :request do
|
||||||
properties: {
|
properties: {
|
||||||
route_opacity: {
|
route_opacity: {
|
||||||
type: :number,
|
type: :number,
|
||||||
example: 0.3,
|
example: 60,
|
||||||
description: 'the opacity of the route, float between 0 and 1'
|
description: 'Route opacity percentage (0-100)'
|
||||||
},
|
},
|
||||||
meters_between_routes: {
|
meters_between_routes: {
|
||||||
type: :number,
|
type: :number,
|
||||||
example: 100,
|
example: 500,
|
||||||
description: 'the distance between routes in meters'
|
description: 'Minimum distance between routes in meters'
|
||||||
},
|
},
|
||||||
minutes_between_routes: {
|
minutes_between_routes: {
|
||||||
type: :number,
|
type: :number,
|
||||||
example: 100,
|
example: 30,
|
||||||
description: 'the time between routes in minutes'
|
description: 'Minimum time between routes in minutes'
|
||||||
},
|
},
|
||||||
fog_of_war_meters: {
|
fog_of_war_meters: {
|
||||||
type: :number,
|
type: :number,
|
||||||
example: 100,
|
example: 50,
|
||||||
description: 'the fog of war distance in meters'
|
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'
|
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||||
response '200', 'settings updated' do
|
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 }
|
let(:api_key) { create(:user).api_key }
|
||||||
|
|
||||||
run_test!
|
run_test!
|
||||||
|
|
@ -65,27 +139,91 @@ describe 'Settings API', type: :request do
|
||||||
properties: {
|
properties: {
|
||||||
route_opacity: {
|
route_opacity: {
|
||||||
type: :string,
|
type: :string,
|
||||||
example: 0.3,
|
example: '60',
|
||||||
description: 'the opacity of the route, float between 0 and 1'
|
description: 'Route opacity percentage (0-100)'
|
||||||
},
|
},
|
||||||
meters_between_routes: {
|
meters_between_routes: {
|
||||||
type: :string,
|
type: :string,
|
||||||
example: 100,
|
example: '500',
|
||||||
description: 'the distance between routes in meters'
|
description: 'Minimum distance between routes in meters'
|
||||||
},
|
},
|
||||||
minutes_between_routes: {
|
minutes_between_routes: {
|
||||||
type: :string,
|
type: :string,
|
||||||
example: 100,
|
example: '30',
|
||||||
description: 'the time between routes in minutes'
|
description: 'Minimum time between routes in minutes'
|
||||||
},
|
},
|
||||||
fog_of_war_meters: {
|
fog_of_war_meters: {
|
||||||
type: :string,
|
type: :string,
|
||||||
example: 100,
|
example: '50',
|
||||||
description: 'the fog of war distance in meters'
|
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]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,20 +141,20 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
- name: start_at
|
- name: start_at
|
||||||
in: query
|
in: query
|
||||||
format: date-time
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
required: true
|
required: true
|
||||||
description: Start date in YYYY-MM-DD format
|
description: Start date in YYYY-MM-DD format
|
||||||
example: '2023-01-01'
|
example: '2023-01-01'
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: end_at
|
- name: end_at
|
||||||
in: query
|
in: query
|
||||||
format: date-time
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
required: true
|
required: true
|
||||||
description: End date in YYYY-MM-DD format
|
description: End date in YYYY-MM-DD format
|
||||||
example: '2023-12-31'
|
example: '2023-12-31'
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: cities found
|
description: cities found
|
||||||
|
|
@ -231,17 +231,19 @@ paths:
|
||||||
description: Healthy
|
description: Healthy
|
||||||
headers:
|
headers:
|
||||||
X-Dawarich-Response:
|
X-Dawarich-Response:
|
||||||
type: string
|
schema:
|
||||||
|
type: string
|
||||||
|
example: Hey, I'm alive!
|
||||||
required: true
|
required: true
|
||||||
example: Hey, I'm alive!
|
|
||||||
description: Depending on the authentication status of the request,
|
description: Depending on the authentication status of the request,
|
||||||
the response will be different. If the request is authenticated, the
|
the response will be different. If the request is authenticated, the
|
||||||
response will be 'Hey, I'm alive and authenticated!'. If the request
|
response will be 'Hey, I'm alive and authenticated!'. If the request
|
||||||
is not authenticated, the response will be 'Hey, I'm alive!'.
|
is not authenticated, the response will be 'Hey, I'm alive!'.
|
||||||
X-Dawarich-Version:
|
X-Dawarich-Version:
|
||||||
type: string
|
schema:
|
||||||
|
type: string
|
||||||
|
example: 1.0.0
|
||||||
required: true
|
required: true
|
||||||
example: 1.0.0
|
|
||||||
description: 'The version of the application, for example: 1.0.0'
|
description: 'The version of the application, for example: 1.0.0'
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
|
@ -273,99 +275,109 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
type:
|
locations:
|
||||||
type: string
|
type: array
|
||||||
example: Feature
|
items:
|
||||||
geometry:
|
type: object
|
||||||
type: object
|
properties:
|
||||||
properties:
|
type:
|
||||||
type:
|
type: string
|
||||||
type: string
|
example: Feature
|
||||||
example: Point
|
geometry:
|
||||||
coordinates:
|
type: object
|
||||||
type: array
|
properties:
|
||||||
example:
|
type:
|
||||||
- 13.356718
|
type: string
|
||||||
- 52.502397
|
example: Point
|
||||||
properties:
|
coordinates:
|
||||||
type: object
|
type: array
|
||||||
properties:
|
items:
|
||||||
timestamp:
|
type: number
|
||||||
type: string
|
example:
|
||||||
example: '2021-06-01T12:00:00Z'
|
- 13.356718
|
||||||
description: Timestamp in ISO 8601 format
|
- 52.502397
|
||||||
altitude:
|
properties:
|
||||||
type: number
|
type: object
|
||||||
example: 0
|
properties:
|
||||||
description: Altitude in meters
|
timestamp:
|
||||||
speed:
|
type: string
|
||||||
type: number
|
example: '2021-06-01T12:00:00Z'
|
||||||
example: 0
|
description: Timestamp in ISO 8601 format
|
||||||
description: Speed in meters per second
|
altitude:
|
||||||
horizontal_accuracy:
|
type: number
|
||||||
type: number
|
example: 0
|
||||||
example: 0
|
description: Altitude in meters
|
||||||
description: Horizontal accuracy in meters
|
speed:
|
||||||
vertical_accuracy:
|
type: number
|
||||||
type: number
|
example: 0
|
||||||
example: 0
|
description: Speed in meters per second
|
||||||
description: Vertical accuracy in meters
|
horizontal_accuracy:
|
||||||
motion:
|
type: number
|
||||||
type: array
|
example: 0
|
||||||
example:
|
description: Horizontal accuracy in meters
|
||||||
- walking
|
vertical_accuracy:
|
||||||
- running
|
type: number
|
||||||
- driving
|
example: 0
|
||||||
- cycling
|
description: Vertical accuracy in meters
|
||||||
- stationary
|
motion:
|
||||||
description: 'Motion type, for example: automotive_navigation,
|
type: array
|
||||||
fitness, other_navigation or other'
|
items:
|
||||||
activity:
|
type: string
|
||||||
type: string
|
example:
|
||||||
example: unknown
|
- walking
|
||||||
description: 'Activity type, for example: automotive_navigation,
|
- running
|
||||||
fitness, other_navigation or other'
|
- driving
|
||||||
desired_accuracy:
|
- cycling
|
||||||
type: number
|
- stationary
|
||||||
example: 0
|
description: 'Motion type, for example: automotive_navigation,
|
||||||
description: Desired accuracy in meters
|
fitness, other_navigation or other'
|
||||||
deferred:
|
activity:
|
||||||
type: number
|
type: string
|
||||||
example: 0
|
example: unknown
|
||||||
description: the distance in meters to defer location updates
|
description: 'Activity type, for example: automotive_navigation,
|
||||||
significant_change:
|
fitness, other_navigation or other'
|
||||||
type: string
|
desired_accuracy:
|
||||||
example: disabled
|
type: number
|
||||||
description: a significant change mode, disabled, enabled or
|
example: 0
|
||||||
exclusive
|
description: Desired accuracy in meters
|
||||||
locations_in_payload:
|
deferred:
|
||||||
type: number
|
type: number
|
||||||
example: 1
|
example: 0
|
||||||
description: the number of locations in the payload
|
description: the distance in meters to defer location
|
||||||
device_id:
|
updates
|
||||||
type: string
|
significant_change:
|
||||||
example: 'iOS device #166'
|
type: string
|
||||||
description: the device id
|
example: disabled
|
||||||
unique_id:
|
description: a significant change mode, disabled, enabled
|
||||||
type: string
|
or exclusive
|
||||||
example: '1234567890'
|
locations_in_payload:
|
||||||
description: the device's Unique ID as set by Apple
|
type: number
|
||||||
wifi:
|
example: 1
|
||||||
type: string
|
description: the number of locations in the payload
|
||||||
example: unknown
|
device_id:
|
||||||
description: the WiFi network name
|
type: string
|
||||||
battery_state:
|
example: 'iOS device #166'
|
||||||
type: string
|
description: the device id
|
||||||
example: unknown
|
unique_id:
|
||||||
description: the battery state, unknown, unplugged, charging
|
type: string
|
||||||
or full
|
example: '1234567890'
|
||||||
battery_level:
|
description: the device's Unique ID as set by Apple
|
||||||
type: number
|
wifi:
|
||||||
example: 0
|
type: string
|
||||||
description: the battery level percentage, from 0 to 1
|
example: unknown
|
||||||
required:
|
description: the WiFi network name
|
||||||
- geometry
|
battery_state:
|
||||||
- properties
|
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:
|
examples:
|
||||||
'0':
|
'0':
|
||||||
summary: Creates a batch of points
|
summary: Creates a batch of points
|
||||||
|
|
@ -433,6 +445,8 @@ paths:
|
||||||
3=full)
|
3=full)
|
||||||
inrids:
|
inrids:
|
||||||
type: array
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
description: Array of region IDs device is currently in
|
description: Array of region IDs device is currently in
|
||||||
BSSID:
|
BSSID:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -445,6 +459,8 @@ paths:
|
||||||
description: Vertical accuracy in meters
|
description: Vertical accuracy in meters
|
||||||
inregions:
|
inregions:
|
||||||
type: array
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
description: Array of region names device is currently in
|
description: Array of region names device is currently in
|
||||||
lat:
|
lat:
|
||||||
type: number
|
type: number
|
||||||
|
|
@ -489,7 +505,10 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
description: Human-readable timestamp of the location fix
|
description: Human-readable timestamp of the location fix
|
||||||
required:
|
required:
|
||||||
- owntracks/jane
|
- lat
|
||||||
|
- lon
|
||||||
|
- tst
|
||||||
|
- _type
|
||||||
examples:
|
examples:
|
||||||
'0':
|
'0':
|
||||||
summary: Creates a point
|
summary: Creates a point
|
||||||
|
|
@ -805,8 +824,12 @@ paths:
|
||||||
type: number
|
type: number
|
||||||
inrids:
|
inrids:
|
||||||
type: array
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
in_regions:
|
in_regions:
|
||||||
type: array
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
raw_data:
|
raw_data:
|
||||||
type: string
|
type: string
|
||||||
import_id:
|
import_id:
|
||||||
|
|
@ -982,38 +1005,94 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
route_opacity:
|
route_opacity:
|
||||||
type: number
|
type: number
|
||||||
example: 0.3
|
example: 60
|
||||||
description: the opacity of the route, float between 0 and 1
|
description: Route opacity percentage (0-100)
|
||||||
meters_between_routes:
|
meters_between_routes:
|
||||||
type: number
|
type: number
|
||||||
example: 100
|
example: 500
|
||||||
description: the distance between routes in meters
|
description: Minimum distance between routes in meters
|
||||||
minutes_between_routes:
|
minutes_between_routes:
|
||||||
type: number
|
type: number
|
||||||
example: 100
|
example: 30
|
||||||
description: the time between routes in minutes
|
description: Minimum time between routes in minutes
|
||||||
fog_of_war_meters:
|
fog_of_war_meters:
|
||||||
type: number
|
type: number
|
||||||
example: 100
|
example: 50
|
||||||
description: the fog of war distance in meters
|
description: Fog of war radius in meters
|
||||||
optional:
|
time_threshold_minutes:
|
||||||
- route_opacity
|
type: number
|
||||||
- meters_between_routes
|
example: 30
|
||||||
- minutes_between_routes
|
description: Time threshold for grouping points in minutes
|
||||||
- fog_of_war_meters
|
merge_threshold_minutes:
|
||||||
- time_threshold_minutes
|
type: number
|
||||||
- merge_threshold_minutes
|
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:
|
examples:
|
||||||
'0':
|
'0':
|
||||||
summary: Updates user settings
|
summary: Updates user settings
|
||||||
value:
|
value:
|
||||||
settings:
|
settings:
|
||||||
route_opacity: 0.3
|
route_opacity: 60
|
||||||
meters_between_routes: 100
|
meters_between_routes: 500
|
||||||
minutes_between_routes: 100
|
minutes_between_routes: 30
|
||||||
fog_of_war_meters: 100
|
fog_of_war_meters: 50
|
||||||
time_threshold_minutes: 100
|
time_threshold_minutes: 30
|
||||||
merge_threshold_minutes: 100
|
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:
|
get:
|
||||||
summary: Retrieves user settings
|
summary: Retrieves user settings
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -1038,28 +1117,73 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
route_opacity:
|
route_opacity:
|
||||||
type: string
|
type: string
|
||||||
example: 0.3
|
example: '60'
|
||||||
description: the opacity of the route, float between 0 and
|
description: Route opacity percentage (0-100)
|
||||||
1
|
|
||||||
meters_between_routes:
|
meters_between_routes:
|
||||||
type: string
|
type: string
|
||||||
example: 100
|
example: '500'
|
||||||
description: the distance between routes in meters
|
description: Minimum distance between routes in meters
|
||||||
minutes_between_routes:
|
minutes_between_routes:
|
||||||
type: string
|
type: string
|
||||||
example: 100
|
example: '30'
|
||||||
description: the time between routes in minutes
|
description: Minimum time between routes in minutes
|
||||||
fog_of_war_meters:
|
fog_of_war_meters:
|
||||||
type: string
|
type: string
|
||||||
example: 100
|
example: '50'
|
||||||
description: the fog of war distance in meters
|
description: Fog of war radius in meters
|
||||||
required:
|
time_threshold_minutes:
|
||||||
- route_opacity
|
type: string
|
||||||
- meters_between_routes
|
example: '30'
|
||||||
- minutes_between_routes
|
description: Time threshold for grouping points in minutes
|
||||||
- fog_of_war_meters
|
merge_threshold_minutes:
|
||||||
- time_threshold_minutes
|
type: string
|
||||||
- merge_threshold_minutes
|
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":
|
"/api/v1/stats":
|
||||||
get:
|
get:
|
||||||
summary: Retrieves all stats
|
summary: Retrieves all stats
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue