mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Compare commits
20 commits
abe0129d03
...
0a4756a5fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a4756a5fd | ||
|
|
b995594b8b | ||
|
|
f8c509912b | ||
|
|
f3d4a1431b | ||
|
|
7f5d84e18f | ||
|
|
60802a6f44 | ||
|
|
460d008152 | ||
|
|
31b23745f8 | ||
|
|
e3d3a92faa | ||
|
|
2e6d1bdef6 | ||
|
|
b2d8f85d35 | ||
|
|
b94be44cbf | ||
|
|
cb9525cb77 | ||
|
|
e127511262 | ||
|
|
6fdecb1724 | ||
|
|
b55b1eb018 | ||
|
|
4e93f60eac | ||
|
|
5090a52f79 | ||
|
|
9c7084a10b | ||
|
|
17340079ce |
20 changed files with 150 additions and 26 deletions
|
|
@ -1 +1 @@
|
|||
0.30.3
|
||||
0.30.4
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -65,6 +65,7 @@
|
|||
.dotnet/
|
||||
.cursorrules
|
||||
.cursormemory.md
|
||||
.serena/project.yml
|
||||
|
||||
/config/credentials/production.key
|
||||
/config/credentials/production.yml.enc
|
||||
|
|
|
|||
16
CHANGELOG.md
16
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
|
||||
|
|
|
|||
17
app/controllers/metrics_controller.rb
Normal file
17
app/controllers/metrics_controller.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
34
app/services/prometheus_metrics.rb
Normal file
34
app/services/prometheus_metrics.rb
Normal 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
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue