mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
commit
2ecd1b7763
37 changed files with 804 additions and 57 deletions
|
|
@ -1 +1 @@
|
||||||
0.24.0
|
0.24.1
|
||||||
|
|
|
||||||
33
CHANGELOG.md
33
CHANGELOG.md
|
|
@ -4,7 +4,38 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
# 0.24.0 - 2025-02-09
|
# 0.24.1 - 2025-02-13
|
||||||
|
|
||||||
|
## Custom map tiles
|
||||||
|
|
||||||
|
In the user settings, you can now set a custom tile URL for the map. This is useful if you want to use a custom map tile provider or if you want to use a map tile provider that is not listed in the dropdown.
|
||||||
|
|
||||||
|
To set a custom tile URL, go to the user settings and set the `Maps` section to your liking. Be mindful that currently, only raster tiles are supported. The URL should be a valid tile URL, like `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`. You, as the user, are responsible for any extra costs that may occur due to using a custom tile URL.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Safe settings for user with default values.
|
||||||
|
- Nominatim API is now supported as a reverse geocoding provider.
|
||||||
|
- In the user settings, you can now set a custom tile URL for the map. #429 #715
|
||||||
|
- In the user map settings, you can now see a chart of map tiles usage.
|
||||||
|
- If you have Prometheus exporter enabled, you can now see a `ruby_dawarich_map_tiles` metric in Prometheus, which shows the total number of map tiles loaded. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
# HELP ruby_dawarich_map_tiles_usage
|
||||||
|
# TYPE ruby_dawarich_map_tiles_usage counter
|
||||||
|
ruby_dawarich_map_tiles_usage 99
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Speed on the Points page is now being displayed in kilometers per hour. #700
|
||||||
|
- Fog of war displacement #774
|
||||||
|
|
||||||
|
### Reverted
|
||||||
|
|
||||||
|
- #748
|
||||||
|
|
||||||
|
# 0.24.0 - 2025-02-10
|
||||||
|
|
||||||
## Points speed units
|
## Points speed units
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ Donate using crypto: [0x6bAd13667692632f1bF926cA9B421bEe7EaEB8D4](https://ethers
|
||||||
- Explore statistics like the number of countries and cities visited, total distance traveled, and more!
|
- Explore statistics like the number of countries and cities visited, total distance traveled, and more!
|
||||||
|
|
||||||
📄 **Changelog**: Find the latest updates [here](CHANGELOG.md).
|
📄 **Changelog**: Find the latest updates [here](CHANGELOG.md).
|
||||||
|
|
||||||
👩💻 **Contribute**: See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute to Dawarich.
|
👩💻 **Contribute**: See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute to Dawarich.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
15
app/controllers/api/v1/maps/tile_usage_controller.rb
Normal file
15
app/controllers/api/v1/maps/tile_usage_controller.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Maps::TileUsageController < ApiController
|
||||||
|
def create
|
||||||
|
Maps::TileUsage::Track.new(current_api_user.id, tile_usage_params[:count].to_i).call
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def tile_usage_params
|
||||||
|
params.require(:tile_usage).permit(:count)
|
||||||
|
end
|
||||||
|
end
|
||||||
29
app/controllers/settings/maps_controller.rb
Normal file
29
app/controllers/settings/maps_controller.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::MapsController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def index
|
||||||
|
@maps = current_user.safe_settings.maps
|
||||||
|
|
||||||
|
@tile_usage = 7.days.ago.to_date.upto(Time.zone.today).map do |date|
|
||||||
|
[
|
||||||
|
date.to_s,
|
||||||
|
Rails.cache.read("dawarich_map_tiles_usage:#{current_user.id}:#{date}") || 0
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
current_user.settings['maps'] = settings_params
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
redirect_to settings_maps_path, notice: 'Settings updated'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def settings_params
|
||||||
|
params.require(:maps).permit(:name, :url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -120,4 +120,10 @@ module ApplicationHelper
|
||||||
|
|
||||||
'text-red-500'
|
'text-red-500'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def point_speed(speed)
|
||||||
|
return speed if speed.to_i <= 0
|
||||||
|
|
||||||
|
speed * 3.6
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
67
app/javascript/controllers/map_preview_controller.js
Normal file
67
app/javascript/controllers/map_preview_controller.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import L from "leaflet"
|
||||||
|
import { showFlashMessage } from "../maps/helpers"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["urlInput", "mapContainer", "saveButton"]
|
||||||
|
|
||||||
|
DEFAULT_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log("Controller connected!")
|
||||||
|
// Wait for the next frame to ensure the DOM is ready
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Force container height
|
||||||
|
this.mapContainerTarget.style.height = '500px'
|
||||||
|
this.initializeMap()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeMap() {
|
||||||
|
console.log("Initializing map...")
|
||||||
|
if (!this.map) {
|
||||||
|
this.map = L.map(this.mapContainerTarget).setView([51.505, -0.09], 13)
|
||||||
|
// Invalidate size after initialization
|
||||||
|
setTimeout(() => {
|
||||||
|
this.map.invalidateSize()
|
||||||
|
}, 0)
|
||||||
|
this.updatePreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePreview() {
|
||||||
|
console.log("Updating preview...")
|
||||||
|
const url = this.urlInputTarget.value || this.DEFAULT_TILE_URL
|
||||||
|
|
||||||
|
// Only animate if save button target exists
|
||||||
|
if (this.hasSaveButtonTarget) {
|
||||||
|
this.saveButtonTarget.classList.add('btn-animate')
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saveButtonTarget.classList.remove('btn-animate')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentLayer) {
|
||||||
|
this.map.removeLayer(this.currentLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.currentLayer = L.tileLayer(url, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(this.map)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid tile URL:', e)
|
||||||
|
showFlashMessage('error', 'Invalid tile URL. Reverting to OpenStreetMap.')
|
||||||
|
|
||||||
|
// Reset input to default OSM URL
|
||||||
|
this.urlInputTarget.value = this.DEFAULT_TILE_URL
|
||||||
|
|
||||||
|
// Create default layer
|
||||||
|
this.currentLayer = L.tileLayer(this.DEFAULT_TILE_URL, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(this.map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,9 +8,7 @@ import { createMarkersArray } from "../maps/markers";
|
||||||
import {
|
import {
|
||||||
createPolylinesLayer,
|
createPolylinesLayer,
|
||||||
updatePolylinesOpacity,
|
updatePolylinesOpacity,
|
||||||
updatePolylinesColors,
|
updatePolylinesColors
|
||||||
calculateSpeed,
|
|
||||||
getSpeedColor
|
|
||||||
} from "../maps/polylines";
|
} from "../maps/polylines";
|
||||||
|
|
||||||
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
|
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
|
||||||
|
|
@ -32,6 +30,7 @@ import { countryCodesMap } from "../maps/country_codes";
|
||||||
|
|
||||||
import "leaflet-draw";
|
import "leaflet-draw";
|
||||||
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
|
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
|
||||||
|
import { TileMonitor } from "../maps/tile_monitor";
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["container"];
|
static targets = ["container"];
|
||||||
|
|
@ -245,6 +244,19 @@ export default class extends Controller {
|
||||||
if (this.liveMapEnabled) {
|
if (this.liveMapEnabled) {
|
||||||
this.setupSubscription();
|
this.setupSubscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize tile monitor
|
||||||
|
this.tileMonitor = new TileMonitor(this.apiKey);
|
||||||
|
|
||||||
|
// Add tile load event handlers to each base layer
|
||||||
|
Object.entries(this.baseMaps()).forEach(([name, layer]) => {
|
||||||
|
layer.on('tileload', () => {
|
||||||
|
this.tileMonitor.recordTileLoad(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start monitoring
|
||||||
|
this.tileMonitor.startMonitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
|
@ -260,6 +272,11 @@ export default class extends Controller {
|
||||||
if (this.map) {
|
if (this.map) {
|
||||||
this.map.remove();
|
this.map.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop tile monitoring
|
||||||
|
if (this.tileMonitor) {
|
||||||
|
this.tileMonitor.stopMonitoring();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSubscription() {
|
setupSubscription() {
|
||||||
|
|
@ -385,8 +402,7 @@ export default class extends Controller {
|
||||||
|
|
||||||
baseMaps() {
|
baseMaps() {
|
||||||
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
|
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
|
||||||
|
let maps = {
|
||||||
return {
|
|
||||||
OpenStreetMap: osmMapLayer(this.map, selectedLayerName),
|
OpenStreetMap: osmMapLayer(this.map, selectedLayerName),
|
||||||
"OpenStreetMap.HOT": osmHotMapLayer(this.map, selectedLayerName),
|
"OpenStreetMap.HOT": osmHotMapLayer(this.map, selectedLayerName),
|
||||||
OPNV: OPNVMapLayer(this.map, selectedLayerName),
|
OPNV: OPNVMapLayer(this.map, selectedLayerName),
|
||||||
|
|
@ -397,6 +413,33 @@ export default class extends Controller {
|
||||||
esriWorldImagery: esriWorldImageryMapLayer(this.map, selectedLayerName),
|
esriWorldImagery: esriWorldImageryMapLayer(this.map, selectedLayerName),
|
||||||
esriWorldGrayCanvas: esriWorldGrayCanvasMapLayer(this.map, selectedLayerName)
|
esriWorldGrayCanvas: esriWorldGrayCanvasMapLayer(this.map, selectedLayerName)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add custom map if it exists in settings
|
||||||
|
if (this.userSettings.maps && this.userSettings.maps.url) {
|
||||||
|
const customLayer = L.tileLayer(this.userSettings.maps.url, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: "© OpenStreetMap contributors"
|
||||||
|
});
|
||||||
|
|
||||||
|
// If this is the preferred layer, add it to the map immediately
|
||||||
|
if (selectedLayerName === this.userSettings.maps.name) {
|
||||||
|
customLayer.addTo(this.map);
|
||||||
|
// Remove any other base layers that might be active
|
||||||
|
Object.values(maps).forEach(layer => {
|
||||||
|
if (this.map.hasLayer(layer)) {
|
||||||
|
this.map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
maps[this.userSettings.maps.name] = customLayer;
|
||||||
|
} else {
|
||||||
|
// If no custom map is set, ensure a default layer is added
|
||||||
|
const defaultLayer = maps[selectedLayerName] || maps["OpenStreetMap"];
|
||||||
|
defaultLayer.addTo(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
return maps;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEventListeners() {
|
removeEventListeners() {
|
||||||
|
|
|
||||||
|
|
@ -85,15 +85,12 @@ export function createFogOverlay() {
|
||||||
onAdd: (map) => {
|
onAdd: (map) => {
|
||||||
initializeFogCanvas(map);
|
initializeFogCanvas(map);
|
||||||
|
|
||||||
// Add drag event handlers to update fog during marker movement
|
// Add resize event handlers to update fog size
|
||||||
map.on('drag', () => {
|
map.on('resize', () => {
|
||||||
const fog = document.getElementById('fog');
|
// Set canvas size to match map container
|
||||||
if (fog) {
|
const mapSize = map.getSize();
|
||||||
// Update fog canvas position to match map position
|
fog.width = mapSize.x;
|
||||||
const mapPos = map.getContainer().getBoundingClientRect();
|
fog.height = mapSize.y;
|
||||||
fog.style.left = `${mapPos.left}px`;
|
|
||||||
fog.style.top = `${mapPos.top}px`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onRemove: (map) => {
|
onRemove: (map) => {
|
||||||
|
|
@ -102,7 +99,7 @@ export function createFogOverlay() {
|
||||||
fog.remove();
|
fog.remove();
|
||||||
}
|
}
|
||||||
// Clean up event listener
|
// Clean up event listener
|
||||||
map.off('drag');
|
map.off('resize');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
app/javascript/maps/tile_monitor.js
Normal file
63
app/javascript/maps/tile_monitor.js
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
export class TileMonitor {
|
||||||
|
constructor(apiKey) {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.tileQueue = 0;
|
||||||
|
this.tileUpdateInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startMonitoring() {
|
||||||
|
// Clear any existing interval
|
||||||
|
if (this.tileUpdateInterval) {
|
||||||
|
clearInterval(this.tileUpdateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a regular interval to send stats
|
||||||
|
this.tileUpdateInterval = setInterval(() => {
|
||||||
|
this.sendTileUsage();
|
||||||
|
}, 5000); // Exactly every 5 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
stopMonitoring() {
|
||||||
|
if (this.tileUpdateInterval) {
|
||||||
|
clearInterval(this.tileUpdateInterval);
|
||||||
|
this.sendTileUsage(); // Send any remaining stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recordTileLoad() {
|
||||||
|
this.tileQueue += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTileUsage() {
|
||||||
|
if (this.tileQueue === 0) return;
|
||||||
|
|
||||||
|
const currentCount = this.tileQueue;
|
||||||
|
console.log('Sending tile usage batch:', currentCount);
|
||||||
|
|
||||||
|
fetch('/api/v1/maps/tile_usage', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
tile_usage: {
|
||||||
|
count: currentCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
// Only subtract sent count if it hasn't changed
|
||||||
|
if (this.tileQueue === currentCount) {
|
||||||
|
this.tileQueue = 0;
|
||||||
|
} else {
|
||||||
|
this.tileQueue -= currentCount;
|
||||||
|
}
|
||||||
|
console.log('Tile usage batch sent successfully');
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error recording tile usage:', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,13 +16,18 @@ class User < ApplicationRecord
|
||||||
has_many :trips, dependent: :destroy
|
has_many :trips, dependent: :destroy
|
||||||
|
|
||||||
after_create :create_api_key
|
after_create :create_api_key
|
||||||
before_save :strip_trailing_slashes
|
before_save :sanitize_input
|
||||||
|
|
||||||
validates :email, presence: true
|
validates :email, presence: true
|
||||||
|
|
||||||
validates :reset_password_token, uniqueness: true, allow_nil: true
|
validates :reset_password_token, uniqueness: true, allow_nil: true
|
||||||
|
|
||||||
attribute :admin, :boolean, default: false
|
attribute :admin, :boolean, default: false
|
||||||
|
|
||||||
|
def safe_settings
|
||||||
|
Users::SafeSettings.new(settings)
|
||||||
|
end
|
||||||
|
|
||||||
def countries_visited
|
def countries_visited
|
||||||
stats.pluck(:toponyms).flatten.map { _1['country'] }.uniq.compact
|
stats.pluck(:toponyms).flatten.map { _1['country'] }.uniq.compact
|
||||||
end
|
end
|
||||||
|
|
@ -99,8 +104,9 @@ class User < ApplicationRecord
|
||||||
save
|
save
|
||||||
end
|
end
|
||||||
|
|
||||||
def strip_trailing_slashes
|
def sanitize_input
|
||||||
settings['immich_url']&.gsub!(%r{/+\z}, '')
|
settings['immich_url']&.gsub!(%r{/+\z}, '')
|
||||||
settings['photoprism_url']&.gsub!(%r{/+\z}, '')
|
settings['photoprism_url']&.gsub!(%r{/+\z}, '')
|
||||||
|
settings.try(:[], 'maps')&.try(:[], 'url')&.strip!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ class Areas::Visits::Create
|
||||||
def initialize(user, areas)
|
def initialize(user, areas)
|
||||||
@user = user
|
@user = user
|
||||||
@areas = areas
|
@areas = areas
|
||||||
@time_threshold_minutes = 30 || user.settings['time_threshold_minutes']
|
@time_threshold_minutes = 30 || user.safe_settings.time_threshold_minutes
|
||||||
@merge_threshold_minutes = 15 || user.settings['merge_threshold_minutes']
|
@merge_threshold_minutes = 15 || user.safe_settings.merge_threshold_minutes
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,15 @@ class Immich::RequestPhotos
|
||||||
|
|
||||||
def initialize(user, start_date: '1970-01-01', end_date: nil)
|
def initialize(user, start_date: '1970-01-01', end_date: nil)
|
||||||
@user = user
|
@user = user
|
||||||
@immich_api_base_url = URI.parse("#{user.settings['immich_url']}/api/search/metadata")
|
@immich_api_base_url = URI.parse("#{user.safe_settings.immich_url}/api/search/metadata")
|
||||||
@immich_api_key = user.settings['immich_api_key']
|
@immich_api_key = user.safe_settings.immich_api_key
|
||||||
@start_date = start_date
|
@start_date = start_date
|
||||||
@end_date = end_date
|
@end_date = end_date
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
raise ArgumentError, 'Immich API key is missing' if immich_api_key.blank?
|
raise ArgumentError, 'Immich API key is missing' if immich_api_key.blank?
|
||||||
raise ArgumentError, 'Immich URL is missing' if user.settings['immich_url'].blank?
|
raise ArgumentError, 'Immich URL is missing' if user.safe_settings.immich_url.blank?
|
||||||
|
|
||||||
data = retrieve_immich_data
|
data = retrieve_immich_data
|
||||||
|
|
||||||
|
|
|
||||||
36
app/services/maps/tile_usage/track.rb
Normal file
36
app/services/maps/tile_usage/track.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Maps::TileUsage::Track
|
||||||
|
def initialize(user_id, count = 1)
|
||||||
|
@user_id = user_id
|
||||||
|
@count = count
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
report_to_prometheus
|
||||||
|
report_to_cache
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("Failed to send tile usage metric: #{e.message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def report_to_prometheus
|
||||||
|
return unless DawarichSettings.prometheus_exporter_enabled?
|
||||||
|
|
||||||
|
metric_data = {
|
||||||
|
type: 'counter',
|
||||||
|
name: 'dawarich_map_tiles_usage',
|
||||||
|
value: @count
|
||||||
|
}
|
||||||
|
|
||||||
|
PrometheusExporter::Client.default.send_json(metric_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def report_to_cache
|
||||||
|
today_key = "dawarich_map_tiles_usage:#{@user_id}:#{Time.zone.today}"
|
||||||
|
|
||||||
|
current_value = (Rails.cache.read(today_key) || 0).to_i
|
||||||
|
Rails.cache.write(today_key, current_value + @count, expires_in: 7.days)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -9,14 +9,14 @@ class Photoprism::RequestPhotos
|
||||||
|
|
||||||
def initialize(user, start_date: '1970-01-01', end_date: nil)
|
def initialize(user, start_date: '1970-01-01', end_date: nil)
|
||||||
@user = user
|
@user = user
|
||||||
@photoprism_api_base_url = URI.parse("#{user.settings['photoprism_url']}/api/v1/photos")
|
@photoprism_api_base_url = URI.parse("#{user.safe_settings.photoprism_url}/api/v1/photos")
|
||||||
@photoprism_api_key = user.settings['photoprism_api_key']
|
@photoprism_api_key = user.safe_settings.photoprism_api_key
|
||||||
@start_date = start_date
|
@start_date = start_date
|
||||||
@end_date = end_date
|
@end_date = end_date
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
raise ArgumentError, 'Photoprism URL is missing' if user.settings['photoprism_url'].blank?
|
raise ArgumentError, 'Photoprism URL is missing' if user.safe_settings.photoprism_url.blank?
|
||||||
raise ArgumentError, 'Photoprism API key is missing' if photoprism_api_key.blank?
|
raise ArgumentError, 'Photoprism API key is missing' if photoprism_api_key.blank?
|
||||||
|
|
||||||
data = retrieve_photoprism_data
|
data = retrieve_photoprism_data
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Photos::Thumbnail
|
class Photos::Thumbnail
|
||||||
|
SUPPORTED_SOURCES = %w[immich photoprism].freeze
|
||||||
|
|
||||||
def initialize(user, source, id)
|
def initialize(user, source, id)
|
||||||
@user = user
|
@user = user
|
||||||
@source = source
|
@source = source
|
||||||
|
|
@ -8,6 +10,8 @@ class Photos::Thumbnail
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
raise unsupported_source_error unless SUPPORTED_SOURCES.include?(source)
|
||||||
|
|
||||||
HTTParty.get(request_url, headers: headers)
|
HTTParty.get(request_url, headers: headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -16,11 +20,11 @@ class Photos::Thumbnail
|
||||||
attr_reader :user, :source, :id
|
attr_reader :user, :source, :id
|
||||||
|
|
||||||
def source_url
|
def source_url
|
||||||
user.settings["#{source}_url"]
|
user.safe_settings.public_send("#{source}_url")
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_api_key
|
def source_api_key
|
||||||
user.settings["#{source}_api_key"]
|
user.safe_settings.public_send("#{source}_api_key")
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_path
|
def source_path
|
||||||
|
|
@ -30,8 +34,6 @@ class Photos::Thumbnail
|
||||||
when 'photoprism'
|
when 'photoprism'
|
||||||
preview_token = Rails.cache.read("#{Photoprism::CachePreviewToken::TOKEN_CACHE_KEY}_#{user.id}")
|
preview_token = Rails.cache.read("#{Photoprism::CachePreviewToken::TOKEN_CACHE_KEY}_#{user.id}")
|
||||||
"/api/v1/t/#{id}/#{preview_token}/tile_500"
|
"/api/v1/t/#{id}/#{preview_token}/tile_500"
|
||||||
else
|
|
||||||
raise "Unsupported source: #{source}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -48,4 +50,8 @@ class Photos::Thumbnail
|
||||||
|
|
||||||
request_headers
|
request_headers
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unsupported_source_error
|
||||||
|
raise ArgumentError, "Unsupported source: #{source}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,13 @@ class ReverseGeocoding::Places::FetchData
|
||||||
end
|
end
|
||||||
|
|
||||||
def reverse_geocoded_places
|
def reverse_geocoded_places
|
||||||
data = Geocoder.search([place.latitude, place.longitude], limit: 10, distance_sort: true)
|
data = Geocoder.search(
|
||||||
|
[place.latitude, place.longitude],
|
||||||
|
limit: 10,
|
||||||
|
distance_sort: true,
|
||||||
|
radius: 1,
|
||||||
|
units: DISTANCE_UNITS
|
||||||
|
)
|
||||||
|
|
||||||
data.reject do |place|
|
data.reject do |place|
|
||||||
place.data['properties']['osm_value'].in?(IGNORED_OSM_VALUES) ||
|
place.data['properties']['osm_value'].in?(IGNORED_OSM_VALUES) ||
|
||||||
|
|
|
||||||
93
app/services/users/safe_settings.rb
Normal file
93
app/services/users/safe_settings.rb
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Users::SafeSettings
|
||||||
|
attr_reader :settings
|
||||||
|
|
||||||
|
def initialize(settings)
|
||||||
|
@settings = settings
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
def config
|
||||||
|
{
|
||||||
|
fog_of_war_meters: fog_of_war_meters,
|
||||||
|
meters_between_routes: meters_between_routes,
|
||||||
|
preferred_map_layer: preferred_map_layer,
|
||||||
|
speed_colored_routes: speed_colored_routes,
|
||||||
|
points_rendering_mode: points_rendering_mode,
|
||||||
|
minutes_between_routes: minutes_between_routes,
|
||||||
|
time_threshold_minutes: time_threshold_minutes,
|
||||||
|
merge_threshold_minutes: merge_threshold_minutes,
|
||||||
|
live_map_enabled: live_map_enabled,
|
||||||
|
route_opacity: route_opacity,
|
||||||
|
immich_url: immich_url,
|
||||||
|
immich_api_key: immich_api_key,
|
||||||
|
photoprism_url: photoprism_url,
|
||||||
|
photoprism_api_key: photoprism_api_key,
|
||||||
|
maps: maps
|
||||||
|
}
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
|
|
||||||
|
def fog_of_war_meters
|
||||||
|
settings['fog_of_war_meters'] || 50
|
||||||
|
end
|
||||||
|
|
||||||
|
def meters_between_routes
|
||||||
|
settings['meters_between_routes'] || 500
|
||||||
|
end
|
||||||
|
|
||||||
|
def preferred_map_layer
|
||||||
|
settings['preferred_map_layer'] || 'OpenStreetMap'
|
||||||
|
end
|
||||||
|
|
||||||
|
def speed_colored_routes
|
||||||
|
settings['speed_colored_routes'] || false
|
||||||
|
end
|
||||||
|
|
||||||
|
def points_rendering_mode
|
||||||
|
settings['points_rendering_mode'] || 'raw'
|
||||||
|
end
|
||||||
|
|
||||||
|
def minutes_between_routes
|
||||||
|
settings['minutes_between_routes'] || 30
|
||||||
|
end
|
||||||
|
|
||||||
|
def time_threshold_minutes
|
||||||
|
settings['time_threshold_minutes'] || 30
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_threshold_minutes
|
||||||
|
settings['merge_threshold_minutes'] || 15
|
||||||
|
end
|
||||||
|
|
||||||
|
def live_map_enabled
|
||||||
|
return settings['live_map_enabled'] if settings.key?('live_map_enabled')
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def route_opacity
|
||||||
|
settings['route_opacity'] || 0.6
|
||||||
|
end
|
||||||
|
|
||||||
|
def immich_url
|
||||||
|
settings['immich_url']
|
||||||
|
end
|
||||||
|
|
||||||
|
def immich_api_key
|
||||||
|
settings['immich_api_key']
|
||||||
|
end
|
||||||
|
|
||||||
|
def photoprism_url
|
||||||
|
settings['photoprism_url']
|
||||||
|
end
|
||||||
|
|
||||||
|
def photoprism_api_key
|
||||||
|
settings['photoprism_api_key']
|
||||||
|
end
|
||||||
|
|
||||||
|
def maps
|
||||||
|
settings['maps'] || {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -5,12 +5,12 @@
|
||||||
<h1 class="font-bold text-4xl">Imports</h1>
|
<h1 class="font-bold text-4xl">Imports</h1>
|
||||||
<%= link_to "New import", new_import_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
|
<%= link_to "New import", new_import_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
|
||||||
|
|
||||||
<% if current_user.settings['immich_url'] && current_user.settings['immich_api_key'] %>
|
<% if current_user.safe_settings.immich_url && current_user.safe_settings.immich_api_key %>
|
||||||
<%= link_to 'Import Immich data', settings_background_jobs_path(job_name: 'start_immich_import'), method: :post, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :post }, class: 'rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium' %>
|
<%= link_to 'Import Immich data', settings_background_jobs_path(job_name: 'start_immich_import'), method: :post, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :post }, class: 'rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium' %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<a href='' class="rounded-lg py-3 px-5 bg-blue-900 text-gray block font-medium tooltip cursor-not-allowed" data-tip="You need to provide your Immich instance data in the Settings">Import Immich data</a>
|
<a href='' class="rounded-lg py-3 px-5 bg-blue-900 text-gray block font-medium tooltip cursor-not-allowed" data-tip="You need to provide your Immich instance data in the Settings">Import Immich data</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if current_user.settings['photoprism_url'] && current_user.settings['photoprism_api_key'] %>
|
<% if current_user.safe_settings.photoprism_url && current_user.safe_settings.photoprism_api_key %>
|
||||||
<%= link_to 'Import Photoprism data', settings_background_jobs_path(job_name: 'start_photoprism_import'), method: :post, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :post }, class: 'rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium' %>
|
<%= link_to 'Import Photoprism data', settings_background_jobs_path(job_name: 'start_photoprism_import'), method: :post, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :post }, class: 'rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium' %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<a href='' class="rounded-lg py-3 px-5 bg-blue-900 text-gray block font-medium tooltip cursor-not-allowed" data-tip="You need to provide your Photoprism instance data in the Settings">Import Photoprism data</a>
|
<a href='' class="rounded-lg py-3 px-5 bg-blue-900 text-gray block font-medium tooltip cursor-not-allowed" data-tip="You need to provide your Photoprism instance data in the Settings">Import Photoprism data</a>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
data-points-target="map"
|
data-points-target="map"
|
||||||
data-distance_unit="<%= DISTANCE_UNIT %>"
|
data-distance_unit="<%= DISTANCE_UNIT %>"
|
||||||
data-api_key="<%= current_user.api_key %>"
|
data-api_key="<%= current_user.api_key %>"
|
||||||
data-user_settings=<%= current_user.settings.to_json %>
|
data-user_settings='<%= current_user.settings.to_json.html_safe %>'
|
||||||
data-coordinates="<%= @coordinates %>"
|
data-coordinates="<%= @coordinates %>"
|
||||||
data-distance="<%= @distance %>"
|
data-distance="<%= @distance %>"
|
||||||
data-points_number="<%= @points_number %>"
|
data-points_number="<%= @points_number %>"
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@
|
||||||
<%= link_to 'Users', settings_users_path, role: 'tab', class: "tab #{active_tab?(settings_users_path)}" %>
|
<%= link_to 'Users', settings_users_path, role: 'tab', class: "tab #{active_tab?(settings_users_path)}" %>
|
||||||
<%= link_to 'Background Jobs', settings_background_jobs_path, role: 'tab', class: "tab #{active_tab?(settings_background_jobs_path)}" %>
|
<%= link_to 'Background Jobs', settings_background_jobs_path, role: 'tab', class: "tab #{active_tab?(settings_background_jobs_path)}" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= link_to 'Map', settings_maps_path, role: 'tab', class: "tab #{active_tab?(settings_maps_path)}" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,20 @@
|
||||||
<%= form_for :settings, url: settings_path, method: :patch, data: { turbo_method: :patch, turbo: false } do |f| %>
|
<%= form_for :settings, url: settings_path, method: :patch, data: { turbo_method: :patch, turbo: false } do |f| %>
|
||||||
<div class="form-control my-2">
|
<div class="form-control my-2">
|
||||||
<%= f.label :immich_url %>
|
<%= f.label :immich_url %>
|
||||||
<%= f.text_field :immich_url, value: current_user.settings['immich_url'], class: "input input-bordered", placeholder: 'http://192.168.0.1:2283' %>
|
<%= f.text_field :immich_url, value: current_user.safe_settings.immich_url, class: "input input-bordered", placeholder: 'http://192.168.0.1:2283' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control my-2">
|
<div class="form-control my-2">
|
||||||
<%= f.label :immich_api_key %>
|
<%= f.label :immich_api_key %>
|
||||||
<%= f.text_field :immich_api_key, value: current_user.settings['immich_api_key'], class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
<%= f.text_field :immich_api_key, value: current_user.safe_settings.immich_api_key, class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="form-control my-2">
|
<div class="form-control my-2">
|
||||||
<%= f.label :photoprism_url %>
|
<%= f.label :photoprism_url %>
|
||||||
<%= f.text_field :photoprism_url, value: current_user.settings['photoprism_url'], class: "input input-bordered", placeholder: 'http://192.168.0.1:2342' %>
|
<%= f.text_field :photoprism_url, value: current_user.safe_settings.photoprism_url, class: "input input-bordered", placeholder: 'http://192.168.0.1:2342' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control my-2">
|
<div class="form-control my-2">
|
||||||
<%= f.label :photoprism_api_key %>
|
<%= f.label :photoprism_api_key %>
|
||||||
<%= f.text_field :photoprism_api_key, value: current_user.settings['photoprism_api_key'], class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
<%= f.text_field :photoprism_api_key, value: current_user.safe_settings.photoprism_api_key, class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control my-2">
|
<div class="form-control my-2">
|
||||||
|
|
|
||||||
71
app/views/settings/maps/index.html.erb
Normal file
71
app/views/settings/maps/index.html.erb
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<% content_for :title, "Background jobs" %>
|
||||||
|
|
||||||
|
<div class="min-h-content w-full my-5">
|
||||||
|
<%= render 'settings/navigation' %>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center my-5">
|
||||||
|
<h1 class="font-bold text-4xl">Maps settings</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="alert" class="alert alert-info">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-6 w-6 shrink-0 stroke-current">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Please remember, that using a custom tile URL may result in extra costs. Check your map tile provider's terms of service for more information.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-5" data-controller="map-preview">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<%= form_for :maps,
|
||||||
|
url: settings_maps_path,
|
||||||
|
method: :patch,
|
||||||
|
autocomplete: "off",
|
||||||
|
data: { turbo_method: :patch, turbo: false } do |f| %>
|
||||||
|
<div class="form-control my-2">
|
||||||
|
<%= f.label :name %>
|
||||||
|
<%= f.text_field :name, value: @maps['name'], placeholder: 'Example: OpenStreetMap', class: "input input-bordered" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control my-2">
|
||||||
|
<%= f.label :url, 'URL' %>
|
||||||
|
<%= f.text_field :url,
|
||||||
|
value: @maps['url'],
|
||||||
|
autocomplete: "off",
|
||||||
|
placeholder: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
class: "input input-bordered",
|
||||||
|
data: {
|
||||||
|
map_preview_target: "urlInput",
|
||||||
|
action: "input->map-preview#updatePreview"
|
||||||
|
} %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= f.submit 'Save', class: "btn btn-primary", data: { map_preview_target: "saveButton" } %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<h2 class="text-lg font-bold">Tile usage</h2>
|
||||||
|
|
||||||
|
<%= line_chart(
|
||||||
|
@tile_usage,
|
||||||
|
height: '200px',
|
||||||
|
xtitle: 'Days',
|
||||||
|
ytitle: 'Tiles',
|
||||||
|
suffix: ' tiles loaded'
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 500px;">
|
||||||
|
<div
|
||||||
|
data-map-preview-target="mapContainer"
|
||||||
|
class="w-full h-full rounded-lg border"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -13,5 +13,9 @@ PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil)
|
||||||
PHOTON_API_KEY = ENV.fetch('PHOTON_API_KEY', nil)
|
PHOTON_API_KEY = ENV.fetch('PHOTON_API_KEY', nil)
|
||||||
PHOTON_API_USE_HTTPS = ENV.fetch('PHOTON_API_USE_HTTPS', 'true') == 'true'
|
PHOTON_API_USE_HTTPS = ENV.fetch('PHOTON_API_USE_HTTPS', 'true') == 'true'
|
||||||
|
|
||||||
|
NOMINATIM_API_HOST = ENV.fetch('NOMINATIM_API_HOST', nil)
|
||||||
|
NOMINATIM_API_KEY = ENV.fetch('NOMINATIM_API_KEY', nil)
|
||||||
|
NOMINATIM_API_USE_HTTPS = ENV.fetch('NOMINATIM_API_USE_HTTPS', 'true') == 'true'
|
||||||
|
|
||||||
GEOAPIFY_API_KEY = ENV.fetch('GEOAPIFY_API_KEY', nil)
|
GEOAPIFY_API_KEY = ENV.fetch('GEOAPIFY_API_KEY', nil)
|
||||||
# /Reverse geocoding settings
|
# /Reverse geocoding settings
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class DawarichSettings
|
class DawarichSettings
|
||||||
class << self
|
class << self
|
||||||
def reverse_geocoding_enabled?
|
def reverse_geocoding_enabled?
|
||||||
@reverse_geocoding_enabled ||= photon_enabled? || geoapify_enabled?
|
@reverse_geocoding_enabled ||= photon_enabled? || geoapify_enabled? || nominatim_enabled?
|
||||||
end
|
end
|
||||||
|
|
||||||
def photon_enabled?
|
def photon_enabled?
|
||||||
|
|
@ -18,12 +18,15 @@ class DawarichSettings
|
||||||
@geoapify_enabled ||= GEOAPIFY_API_KEY.present?
|
@geoapify_enabled ||= GEOAPIFY_API_KEY.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def meters_between_tracks
|
def prometheus_exporter_enabled?
|
||||||
@meters_between_tracks ||= 300
|
@prometheus_exporter_enabled ||=
|
||||||
|
ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true' &&
|
||||||
|
ENV['PROMETHEUS_EXPORTER_HOST'].present? &&
|
||||||
|
ENV['PROMETHEUS_EXPORTER_PORT'].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def minutes_between_tracks
|
def nominatim_enabled?
|
||||||
@minutes_between_tracks ||= 20
|
@nominatim_enabled ||= NOMINATIM_API_HOST.present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,12 @@ if PHOTON_API_HOST.present?
|
||||||
elsif GEOAPIFY_API_KEY.present?
|
elsif GEOAPIFY_API_KEY.present?
|
||||||
settings[:lookup] = :geoapify
|
settings[:lookup] = :geoapify
|
||||||
settings[:api_key] = GEOAPIFY_API_KEY
|
settings[:api_key] = GEOAPIFY_API_KEY
|
||||||
|
elsif NOMINATIM_API_HOST.present?
|
||||||
|
settings[:lookup] = :nominatim
|
||||||
|
settings[:nominatim] = { use_https: NOMINATIM_API_USE_HTTPS, host: NOMINATIM_API_HOST }
|
||||||
|
if NOMINATIM_API_KEY.present?
|
||||||
|
settings[:api_key] = NOMINATIM_API_KEY
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Geocoder.configure(settings)
|
Geocoder.configure(settings)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
if !Rails.env.test? && ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true'
|
if !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled?
|
||||||
require 'prometheus_exporter/middleware'
|
require 'prometheus_exporter/middleware'
|
||||||
require 'prometheus_exporter/instrumentation'
|
require 'prometheus_exporter/instrumentation'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Reddis
|
|
||||||
def self.client
|
|
||||||
@client ||= Redis.new(url: ENV['REDIS_URL'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -20,6 +20,8 @@ Rails.application.routes.draw do
|
||||||
namespace :settings do
|
namespace :settings do
|
||||||
resources :background_jobs, only: %i[index create destroy]
|
resources :background_jobs, only: %i[index create destroy]
|
||||||
resources :users, only: %i[index create destroy edit update]
|
resources :users, only: %i[index create destroy edit update]
|
||||||
|
resources :maps, only: %i[index]
|
||||||
|
patch 'maps', to: 'maps#update'
|
||||||
end
|
end
|
||||||
|
|
||||||
patch 'settings', to: 'settings#update'
|
patch 'settings', to: 'settings#update'
|
||||||
|
|
@ -94,6 +96,10 @@ Rails.application.routes.draw do
|
||||||
get 'thumbnail', constraints: { id: %r{[^/]+} }
|
get 'thumbnail', constraints: { id: %r{[^/]+} }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :maps do
|
||||||
|
resources :tile_usage, only: [:create]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ fi
|
||||||
|
|
||||||
# Wait for the database to become available
|
# Wait for the database to become available
|
||||||
echo "⏳ Waiting for database to be ready..."
|
echo "⏳ Waiting for database to be ready..."
|
||||||
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
|
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
|
||||||
>&2 echo "Postgres is unavailable - retrying..."
|
>&2 echo "Postgres is unavailable - retrying..."
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,14 @@ rm -f $APP_PATH/tmp/pids/server.pid
|
||||||
|
|
||||||
# Wait for the database to become available
|
# Wait for the database to become available
|
||||||
echo "⏳ Waiting for database to be ready..."
|
echo "⏳ Waiting for database to be ready..."
|
||||||
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
|
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
|
||||||
>&2 echo "Postgres is unavailable - retrying..."
|
>&2 echo "Postgres is unavailable - retrying..."
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
echo "✅ PostgreSQL is ready!"
|
echo "✅ PostgreSQL is ready!"
|
||||||
|
|
||||||
# Create database if it doesn't exist
|
# Create database if it doesn't exist
|
||||||
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
|
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
|
||||||
echo "Creating database $DATABASE_NAME..."
|
echo "Creating database $DATABASE_NAME..."
|
||||||
bundle exec rails db:create
|
bundle exec rails db:create
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
36
spec/requests/api/v1/maps/tile_usage_spec.rb
Normal file
36
spec/requests/api/v1/maps/tile_usage_spec.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Api::V1::Maps::TileUsage', type: :request do
|
||||||
|
describe 'POST /api/v1/maps/tile_usage' do
|
||||||
|
let(:tile_count) { 5 }
|
||||||
|
let(:track_service) { instance_double(Maps::TileUsage::Track) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Maps::TileUsage::Track).to receive(:new).with(user.id, tile_count).and_return(track_service)
|
||||||
|
allow(track_service).to receive(:call)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is authenticated' do
|
||||||
|
it 'tracks tile usage' do
|
||||||
|
post '/api/v1/maps/tile_usage',
|
||||||
|
params: { tile_usage: { count: tile_count } },
|
||||||
|
headers: { 'Authorization' => "Bearer #{user.api_key}" }
|
||||||
|
|
||||||
|
expect(Maps::TileUsage::Track).to have_received(:new).with(user.id, tile_count)
|
||||||
|
expect(track_service).to have_received(:call)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is not authenticated' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post '/api/v1/maps/tile_usage', params: { tile_usage: { count: tile_count } }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
43
spec/requests/settings/maps_spec.rb
Normal file
43
spec/requests/settings/maps_spec.rb
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'settings/maps', type: :request do
|
||||||
|
before do
|
||||||
|
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||||
|
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is authenticated' do
|
||||||
|
let!(:user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /index' do
|
||||||
|
it 'returns a success response' do
|
||||||
|
get settings_maps_url
|
||||||
|
|
||||||
|
expect(response).to be_successful
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /update' do
|
||||||
|
it 'returns a success response' do
|
||||||
|
patch settings_maps_path, params: { maps: { name: 'Test', url: 'https://test.com' } }
|
||||||
|
|
||||||
|
expect(response).to redirect_to(settings_maps_path)
|
||||||
|
expect(user.settings['maps']).to eq({ 'name' => 'Test', 'url' => 'https://test.com' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is not authenticated' do
|
||||||
|
it 'redirects to the sign in page' do
|
||||||
|
get settings_maps_path
|
||||||
|
|
||||||
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
42
spec/services/maps/tile_usage/track_spec.rb
Normal file
42
spec/services/maps/tile_usage/track_spec.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'prometheus_exporter/client'
|
||||||
|
|
||||||
|
RSpec.describe Maps::TileUsage::Track do
|
||||||
|
describe '#call' do
|
||||||
|
subject(:track) { described_class.new(user_id, tile_count).call }
|
||||||
|
|
||||||
|
let(:user_id) { 1 }
|
||||||
|
let(:tile_count) { 5 }
|
||||||
|
let(:prometheus_client) { instance_double(PrometheusExporter::Client) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(PrometheusExporter::Client).to receive(:default).and_return(prometheus_client)
|
||||||
|
allow(prometheus_client).to receive(:send_json)
|
||||||
|
allow(DawarichSettings).to receive(:prometheus_exporter_enabled?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tracks tile usage in prometheus' do
|
||||||
|
expect(prometheus_client).to receive(:send_json).with(
|
||||||
|
{
|
||||||
|
type: 'counter',
|
||||||
|
name: 'dawarich_map_tiles_usage',
|
||||||
|
value: tile_count
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
track
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'tracks tile usage in cache' do
|
||||||
|
expect(Rails.cache).to receive(:write).with(
|
||||||
|
"dawarich_map_tiles_usage:#{user_id}:#{Time.zone.today}",
|
||||||
|
tile_count,
|
||||||
|
expires_in: 7.days
|
||||||
|
)
|
||||||
|
|
||||||
|
track
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -70,7 +70,7 @@ RSpec.describe Photos::Thumbnail do
|
||||||
let(:source) { 'unsupported' }
|
let(:source) { 'unsupported' }
|
||||||
|
|
||||||
it 'raises an error' do
|
it 'raises an error' do
|
||||||
expect { subject }.to raise_error(RuntimeError, 'Unsupported source: unsupported')
|
expect { subject }.to raise_error(ArgumentError, 'Unsupported source: unsupported')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
143
spec/services/users/safe_settings_spec.rb
Normal file
143
spec/services/users/safe_settings_spec.rb
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe Users::SafeSettings do
|
||||||
|
describe '#config' do
|
||||||
|
context 'with default values' do
|
||||||
|
let(:settings) { {} }
|
||||||
|
let(:safe_settings) { described_class.new(settings) }
|
||||||
|
|
||||||
|
it 'returns default configuration' do
|
||||||
|
expect(safe_settings.config).to eq(
|
||||||
|
{
|
||||||
|
fog_of_war_meters: 50,
|
||||||
|
meters_between_routes: 500,
|
||||||
|
preferred_map_layer: 'OpenStreetMap',
|
||||||
|
speed_colored_routes: false,
|
||||||
|
points_rendering_mode: 'raw',
|
||||||
|
minutes_between_routes: 30,
|
||||||
|
time_threshold_minutes: 30,
|
||||||
|
merge_threshold_minutes: 15,
|
||||||
|
live_map_enabled: true,
|
||||||
|
route_opacity: 0.6,
|
||||||
|
immich_url: nil,
|
||||||
|
immich_api_key: nil,
|
||||||
|
photoprism_url: nil,
|
||||||
|
photoprism_api_key: nil,
|
||||||
|
maps: {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with custom values' do
|
||||||
|
let(:settings) do
|
||||||
|
{
|
||||||
|
'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' => 0.8,
|
||||||
|
'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' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:safe_settings) { described_class.new(settings) }
|
||||||
|
|
||||||
|
it 'returns custom configuration' do
|
||||||
|
expect(safe_settings.config).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: 0.8,
|
||||||
|
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' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'individual settings' do
|
||||||
|
let(:safe_settings) { described_class.new(settings) }
|
||||||
|
|
||||||
|
context 'with default values' do
|
||||||
|
let(:settings) { {} }
|
||||||
|
|
||||||
|
it 'returns default values for each setting' do
|
||||||
|
expect(safe_settings.fog_of_war_meters).to eq(50)
|
||||||
|
expect(safe_settings.meters_between_routes).to eq(500)
|
||||||
|
expect(safe_settings.preferred_map_layer).to eq('OpenStreetMap')
|
||||||
|
expect(safe_settings.speed_colored_routes).to be false
|
||||||
|
expect(safe_settings.points_rendering_mode).to eq('raw')
|
||||||
|
expect(safe_settings.minutes_between_routes).to eq(30)
|
||||||
|
expect(safe_settings.time_threshold_minutes).to eq(30)
|
||||||
|
expect(safe_settings.merge_threshold_minutes).to eq(15)
|
||||||
|
expect(safe_settings.live_map_enabled).to be true
|
||||||
|
expect(safe_settings.route_opacity).to eq(0.6)
|
||||||
|
expect(safe_settings.immich_url).to be_nil
|
||||||
|
expect(safe_settings.immich_api_key).to be_nil
|
||||||
|
expect(safe_settings.photoprism_url).to be_nil
|
||||||
|
expect(safe_settings.photoprism_api_key).to be_nil
|
||||||
|
expect(safe_settings.maps).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with custom values' do
|
||||||
|
let(:settings) do
|
||||||
|
{
|
||||||
|
'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' => 0.8,
|
||||||
|
'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' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns custom values for each setting' do
|
||||||
|
expect(safe_settings.fog_of_war_meters).to eq(100)
|
||||||
|
expect(safe_settings.meters_between_routes).to eq(1000)
|
||||||
|
expect(safe_settings.preferred_map_layer).to eq('Satellite')
|
||||||
|
expect(safe_settings.speed_colored_routes).to be true
|
||||||
|
expect(safe_settings.points_rendering_mode).to eq('simplified')
|
||||||
|
expect(safe_settings.minutes_between_routes).to eq(60)
|
||||||
|
expect(safe_settings.time_threshold_minutes).to eq(45)
|
||||||
|
expect(safe_settings.merge_threshold_minutes).to eq(20)
|
||||||
|
expect(safe_settings.live_map_enabled).to be false
|
||||||
|
expect(safe_settings.route_opacity).to eq(0.8)
|
||||||
|
expect(safe_settings.immich_url).to eq('https://immich.example.com')
|
||||||
|
expect(safe_settings.immich_api_key).to eq('immich-key')
|
||||||
|
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' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue