mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Merge pull request #1486 from Freika/feature/disable-visits-suggestion
Feature/disable visits suggestion
This commit is contained in:
commit
fd4b785a19
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
|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="card w-96">
|
||||
<div class="card w-96 bg-base-100 border border-base-300 shadow-xl">
|
||||
<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">
|
||||
<div class="form-control">
|
||||
<input type="text"
|
||||
id="circle-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"
|
||||
autofocus
|
||||
required>
|
||||
|
|
@ -23,7 +100,7 @@ export function handleAreaCreated(areasLayer, layer, apiKey) {
|
|||
<input type="hidden" name="area[radius]" value="${radius}">
|
||||
<div class="flex justify-between mt-4">
|
||||
<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()">
|
||||
Cancel
|
||||
</button>
|
||||
|
|
@ -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}<br>
|
||||
Radius: ${Math.round(data.radius)} meters<br>
|
||||
<a href="#" data-id="${data.id}" class="delete-area">[Delete]</a>
|
||||
`).openPopup();
|
||||
<div class="card w-80 bg-base-100 border border-base-300 shadow-lg">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-base-content text-lg">${data.name}</h3>
|
||||
<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
|
||||
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 = `
|
||||
<div class="card w-full">
|
||||
<div class="card w-96 bg-base-100 border border-base-300 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">${area.name}</h2>
|
||||
<p>Radius: ${Math.round(radius)} meters</p>
|
||||
<p>Center: [${lat.toFixed(4)}, ${lng.toFixed(4)}]</p>
|
||||
<div class="flex justify-end mt-4">
|
||||
<button class="btn btn-sm btn-error delete-area" data-id="${area.id}">Delete</button>
|
||||
<h2 class="card-title text-base-content text-xl">${area.name}</h2>
|
||||
<div class="space-y-3">
|
||||
<div class="stats stats-vertical shadow bg-base-200">
|
||||
<div class="stat py-2">
|
||||
<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>
|
||||
`;
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<span>Spamming many new jobs at once is a bad idea. Let them work or clear the queue beforehand.</span>
|
||||
</div>
|
||||
|
||||
<div class='flex'>
|
||||
<div class='flex flex-wrap'>
|
||||
<div class="card bg-base-300 w-96 shadow-xl m-5">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Start Reverse Geocoding</h2>
|
||||
|
|
@ -48,6 +48,20 @@
|
|||
<%= link_to 'Open Dashboard', '/sidekiq', target: '_blank', class: 'btn btn-primary' %>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue