diff --git a/.app_version b/.app_version index e8262eb5..db287d4a 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.30.3 +0.30.4 diff --git a/.gitignore b/.gitignore index 1510b45b..ce9010e1 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ .dotnet/ .cursorrules .cursormemory.md +.serena/project.yml /config/credentials/production.key /config/credentials/production.yml.enc diff --git a/CHANGELOG.md b/CHANGELOG.md index 2046bacf..4d04e16b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb new file mode 100644 index 00000000..785044c0 --- /dev/null +++ b/app/controllers/metrics_controller.rb @@ -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 diff --git a/app/controllers/points_controller.rb b/app/controllers/points_controller.rb index 55d56315..a78c97c4 100644 --- a/app/controllers/points_controller.rb +++ b/app/controllers/points_controller.rb @@ -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 diff --git a/app/javascript/controllers/checkbox_select_all_controller.js b/app/javascript/controllers/checkbox_select_all_controller.js index 1b542f84..1aaf26bc 100644 --- a/app/javascript/controllers/checkbox_select_all_controller.js +++ b/app/javascript/controllers/checkbox_select_all_controller.js @@ -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' + } } } diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 12092891..1efff1e7 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -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), diff --git a/app/models/concerns/distance_convertible.rb b/app/models/concerns/distance_convertible.rb index 2a757303..30c7693b 100644 --- a/app/models/concerns/distance_convertible.rb +++ b/app/models/concerns/distance_convertible.rb @@ -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? diff --git a/app/models/point.rb b/app/models/point.rb index 7be8524a..a36a0019 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -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, [ diff --git a/app/services/gpx/track_importer.rb b/app/services/gpx/track_importer.rb index 0bb0d516..e0207292 100644 --- a/app/services/gpx/track_importer.rb +++ b/app/services/gpx/track_importer.rb @@ -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 diff --git a/app/services/prometheus_metrics.rb b/app/services/prometheus_metrics.rb new file mode 100644 index 00000000..a4077b97 --- /dev/null +++ b/app/services/prometheus_metrics.rb @@ -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 diff --git a/app/views/map/_settings_modals.html.erb b/app/views/map/_settings_modals.html.erb index 24d965a5..31bb3d12 100644 --- a/app/views/map/_settings_modals.html.erb +++ b/app/views/map/_settings_modals.html.erb @@ -156,6 +156,23 @@ + +
+ This checkbox will enable the live map. +
++ Uncheck this checkbox if you want to disable the live map. +
++ When the live map is enabled, the map will update in real-time with the latest points. +
+- <%= "#{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}" %>