Merge pull request #1569 from Freika/dev

0.30.4
This commit is contained in:
Evgenii Burmakin 2025-07-26 15:37:30 +02:00 committed by GitHub
commit 0a4756a5fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 150 additions and 26 deletions

View file

@ -1 +1 @@
0.30.3
0.30.4

1
.gitignore vendored
View file

@ -65,6 +65,7 @@
.dotnet/
.cursorrules
.cursormemory.md
.serena/project.yml
/config/credentials/production.key
/config/credentials/production.yml.enc

View file

@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [0.30.4] - 2025-07-26
## Added
- Prometheus metrics are now available at `/metrics`. Configure `METRICS_USERNAME` and `METRICS_PASSWORD` environment variables for basic authentication. All other prometheus-related environment variables are also necessary.
## Fixed
- The Warden error in jobs is now fixed. #1556
- The Live Map setting is now respected.
- The Live Map info modal is now displayed. #665
- GPX from Basecamp is now supported. #790
- The "Delete Selected" button is now hidden when no points are selected. #1025
# [0.30.3] - 2025-07-23
## Changed

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class MetricsController < ApplicationController
http_basic_authenticate_with name: METRICS_USERNAME, password: METRICS_PASSWORD, only: :index
def index
result = PrometheusMetrics.fetch_data
if result[:success]
render plain: result[:data], content_type: 'text/plain'
elsif result[:error] == 'Prometheus exporter not enabled'
head :not_found
else
head :service_unavailable
end
end
end

View file

@ -18,7 +18,14 @@ class PointsController < ApplicationController
end
def bulk_destroy
current_user.tracked_points.where(id: params[:point_ids].compact).destroy_all
point_ids = params[:point_ids]&.compact&.reject(&:blank?)
redirect_to points_url(preserved_params),
alert: 'No points selected.',
status: :see_other and return if point_ids.blank?
current_user.tracked_points.where(id: point_ids).destroy_all
redirect_to points_url(preserved_params),
notice: 'Points were successfully destroyed.',
status: :see_other

View file

@ -2,11 +2,12 @@ import BaseController from "./base_controller"
// Connects to data-controller="checkbox-select-all"
export default class extends BaseController {
static targets = ["parent", "child"]
static targets = ["parent", "child", "deleteButton"]
connect() {
this.parentTarget.checked = false
this.childTargets.map(x => x.checked = false)
this.updateDeleteButtonVisibility()
}
toggleChildren() {
@ -15,6 +16,7 @@ export default class extends BaseController {
} else {
this.childTargets.map(x => x.checked = false)
}
this.updateDeleteButtonVisibility()
}
toggleParent() {
@ -23,5 +25,14 @@ export default class extends BaseController {
} else {
this.parentTarget.checked = true
}
this.updateDeleteButtonVisibility()
}
updateDeleteButtonVisibility() {
const hasCheckedItems = this.childTargets.some(target => target.checked)
if (this.hasDeleteButtonTarget) {
this.deleteButtonTarget.style.display = hasCheckedItems ? 'inline-block' : 'none'
}
}
}

View file

@ -969,6 +969,12 @@ export default class extends BaseController {
this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6;
this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50;
// Update the DOM data attribute to keep it in sync
const mapElement = document.getElementById('map');
if (mapElement) {
mapElement.setAttribute('data-user_settings', JSON.stringify(this.userSettings));
}
// Store current layer states
const layerStates = {
Points: this.map.hasLayer(this.markersLayer),

View file

@ -37,11 +37,6 @@ module DistanceConvertible
distance.to_f / conversion_factor
end
def distance_for_user(user)
user_unit = user.safe_settings.distance_unit
distance_in_unit(user_unit)
end
module ClassMethods
def convert_distance(distance_meters, unit)
return 0.0 unless distance_meters.present?

View file

@ -17,7 +17,7 @@ class Point < ApplicationRecord
index: true
}
enum :battery_status, { unknown: 0, unplugged: 1, charging: 2, full: 3 }, suffix: true
enum :battery_status, { unknown: 0, unplugged: 1, charging: 2, full: 3, connected_not_charging: 4 }, suffix: true
enum :trigger, {
unknown: 0, background_event: 1, circular_region_event: 2, beacon_event: 3,
report_location_message_event: 4, manual_event: 5, timer_based_event: 6,
@ -70,6 +70,8 @@ class Point < ApplicationRecord
# rubocop:disable Metrics/MethodLength Metrics/AbcSize
def broadcast_coordinates
return unless user.safe_settings.live_map_enabled
PointsChannel.broadcast_to(
user,
[

View file

@ -81,8 +81,10 @@ class Gpx::TrackImporter
def speed(point)
return if point['extensions'].blank?
(
point.dig('extensions', 'speed') || point.dig('extensions', 'TrackPointExtension', 'speed')
).to_f.round(1)
value = point.dig('extensions', 'speed')
extensions = point.dig('extensions', 'TrackPointExtension')
value ||= extensions.is_a?(Hash) ? extensions.dig('speed') : nil
value&.to_f&.round(1) || 0.0
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'net/http'
require 'uri'
class PrometheusMetrics
class << self
def fetch_data
return { success: false, error: 'Prometheus exporter not enabled' } unless prometheus_enabled?
host = ENV.fetch('PROMETHEUS_EXPORTER_HOST', 'localhost')
port = ENV.fetch('PROMETHEUS_EXPORTER_PORT', 9394)
begin
response = Net::HTTP.get_response(URI("http://#{host}:#{port}/metrics"))
if response.code == '200'
{ success: true, data: response.body }
else
{ success: false, error: "Prometheus server returned #{response.code}" }
end
rescue => e
Rails.logger.error "Failed to fetch Prometheus metrics: #{e.message}"
{ success: false, error: e.message }
end
end
private
def prometheus_enabled?
DawarichSettings.prometheus_exporter_enabled?
end
end
end

View file

@ -156,6 +156,23 @@
<label class="modal-backdrop" for="speed_colored_routes_info">Close</label>
</div>
<input type="checkbox" id="live_map_enabled_info" class="modal-toggle" />
<div class="modal focus:z-99" role="dialog">
<div class="modal-box">
<h3 class="text-lg font-bold">Live map</h3>
<p class="py-4">
This checkbox will enable the live map.
</p>
<p class="py-4">
Uncheck this checkbox if you want to disable the live map.
</p>
<p class="py-4">
When the live map is enabled, the map will update in real-time with the latest points.
</p>
</div>
<label class="modal-backdrop" for="live_map_enabled_info">Close</label>
</div>
<input type="checkbox" id="speed_color_scale_info" class="modal-toggle" />
<div class="modal focus:z-99" role="dialog">
<div class="modal-box">

View file

@ -6,34 +6,34 @@
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2">
<%= f.label :start_at, class: "text-sm font-semibold" %>
<%= f.datetime_local_field :start_at, class: "rounded-md w-full", value: @start_at %>
<%= f.datetime_local_field :start_at, class: "input input-bordered hover:cursor-pointer hover:input-primary", value: @start_at %>
</div>
</div>
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2">
<%= f.label :end_at, class: "text-sm font-semibold" %>
<%= f.datetime_local_field :end_at, class: "rounded-md w-full", value: @end_at %>
<%= f.datetime_local_field :end_at, class: "input input-bordered hover:cursor-pointer hover:input-primary", value: @end_at %>
</div>
</div>
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2">
<%= f.label :import, class: "text-sm font-semibold" %>
<%= f.select :import_id, options_for_select(@imports.map { |i| [i.name, i.id] }, params[:import_id]), { include_blank: true }, class: "rounded-md w-full" %>
<%= f.select :import_id, options_for_select(@imports.map { |i| [i.name, i.id] }, params[:import_id]), { include_blank: true }, class: "input input-bordered hover:cursor-pointer hover:input-primary" %>
</div>
</div>
<div class="w-full md:w-1/12">
<div class="flex flex-col space-y-2">
<%= f.submit "Search", class: "px-4 py-2 bg-blue-500 text-white rounded-md" %>
<%= f.submit "Search", class: "btn btn-primary" %>
</div>
</div>
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2 text-center">
<%= link_to 'Export as GeoJSON', exports_path(start_at: @start_at, end_at: @end_at, file_format: :json), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points within timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "px-4 py-2 bg-green-500 text-white rounded-md join-item" %>
<%= link_to 'Export as GeoJSON', exports_path(start_at: @start_at, end_at: @end_at, file_format: :json), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points within timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "btn border border-base-300 hover:btn-ghost" %>
</div>
</div>
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2 text-center">
<%= link_to 'Export as GPX', exports_path(start_at: @start_at, end_at: @end_at, file_format: :gpx), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points within timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "px-4 py-2 bg-green-500 text-white rounded-md join-item" %>
<%= link_to 'Export as GPX', exports_path(start_at: @start_at, end_at: @end_at, file_format: :gpx), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points within timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "btn border border-base-300 hover:btn-ghost" %>
</div>
</div>
</div>
@ -46,9 +46,8 @@
<div id="points" class="min-w-full">
<div data-controller='checkbox-select-all'>
<%= form_with url: bulk_destroy_points_path(params.permit!), method: :delete, id: :bulk_destroy_form do |f| %>
<div class="flex justify-between my-5">
<%= f.submit "Delete Selected", class: "px-4 py-2 bg-red-500 text-white rounded-md", data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" } %>
<%= f.submit "Delete Selected", class: "px-4 py-2 bg-red-500 text-white rounded-md", data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", checkbox_select_all_target: "deleteButton" }, style: "display: none;" %>
<div>
<%= page_entries_info @points, entry_name: 'point' %>
</div>
@ -64,14 +63,15 @@
<tr>
<th>
<%= label_tag do %>
Select all
<%= check_box_tag 'Select all',
id: :select_all_points,
data: {
checkbox_select_all_target: 'parent',
action: 'change->checkbox-select-all#toggleChildren'
}
},
class: 'mr-2'
%>
Select all
<% end %>
</div>
</th>

View file

@ -2,7 +2,7 @@
<div class="card bg-base-200 shadow-lg">
<div class="card-body p-4">
<div class="stat-title text-xs">Distance</div>
<div class="stat-value text-lg"><%= trip.distance_for_user(current_user).round %> <%= distance_unit %></div>
<div class="stat-value text-lg"><%= trip.distance_in_unit(distance_unit).round %> <%= distance_unit %></div>
</div>
</div>
<div class="card bg-base-200 shadow-lg">

View file

@ -1,5 +1,5 @@
<% if trip.distance.present? %>
<span class="text-md"><%= trip.distance_for_user(current_user).round %> <%= distance_unit %></span>
<span class="text-md"><%= trip.distance_in_unit(distance_unit).round %> <%= distance_unit %></span>
<% else %>
<span class="text-md">Calculating...</span>
<span class="loading loading-dots loading-sm"></span>

View file

@ -5,7 +5,7 @@
<span class="hover:underline"><%= trip.name %></span>
</h2>
<p class="text-sm text-gray-600 text-center">
<%= "#{human_date(trip.started_at)} #{human_date(trip.ended_at)}, #{trip.distance_for_user(current_user).round} #{current_user.safe_settings.distance_unit}" %>
<%= "#{human_date(trip.started_at)} #{human_date(trip.ended_at)}, #{trip.distance_in_unit(current_user.safe_settings.distance_unit).round} #{distance_unit}" %>
</p>
<div style="width: 100%; aspect-ratio: 1/1;"

View file

@ -31,3 +31,8 @@ STORE_GEODATA = ENV.fetch('STORE_GEODATA', 'true') == 'true'
SENTRY_DSN = ENV.fetch('SENTRY_DSN', nil)
MANAGER_URL = SELF_HOSTED ? nil : ENV.fetch('MANAGER_URL', nil)
# Prometheus metrics
METRICS_USERNAME = ENV.fetch('METRICS_USERNAME', 'prometheus')
METRICS_PASSWORD = ENV.fetch('METRICS_PASSWORD', 'prometheus')
# /Prometheus metrics

View file

@ -87,6 +87,8 @@ Rails.application.routes.draw do
devise_for :users
end
resources :metrics, only: [:index]
get 'map', to: 'map#index'
namespace :api do

View file

@ -63,5 +63,14 @@ RSpec.describe '/points', type: :request do
expect(response).to redirect_to(points_url(start_at: '2021-01-01', end_at: '2021-01-02'))
end
context 'when no points are selected' do
it 'redirects to the points list' do
delete bulk_destroy_points_url, params: { point_ids: [] }
expect(response).to redirect_to(points_url)
expect(flash[:alert]).to eq('No points selected.')
end
end
end
end

View file

@ -435,7 +435,7 @@ RSpec.describe 'Map Interaction', type: :system do
end
end
context 'settings panel functionality' do
xcontext 'settings panel functionality' do
include_context 'authenticated map user'
it 'allows updating route opacity settings' do