Update stuff, fix stuff

This commit is contained in:
Eugene Burmakin 2025-09-29 22:27:07 +02:00
parent 4287fee93d
commit 0728c21c61
28 changed files with 213 additions and 406 deletions

View file

@ -141,7 +141,7 @@ Dawarich includes a comprehensive public sharing system that allows users to sha
### Technical Implementation ### Technical Implementation
- **Database**: `sharing_settings` (JSONB) and `sharing_uuid` (UUID) columns on `stats` table - **Database**: `sharing_settings` (JSONB) and `sharing_uuid` (UUID) columns on `stats` table
- **Routes**: `/shared/stats/:uuid` for public viewing, `/stats/:year/:month/sharing` for management - **Routes**: `/shared/month/:uuid` for public viewing, `/stats/:year/:month/sharing` for management
- **API**: `/api/v1/maps/hexagons` supports public access via `uuid` parameter - **API**: `/api/v1/maps/hexagons` supports public access via `uuid` parameter
- **Controllers**: `Shared::StatsController` handles public views, sharing management integrated into existing stats flow - **Controllers**: `Shared::StatsController` handles public views, sharing management integrated into existing stats flow

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,7 @@
*/ */
@import 'actiontext.css'; @import 'actiontext.css';
@import 'leaflet_theme.css';
@layer components { @layer components {
.fade-out { .fade-out {
@ -71,14 +72,6 @@
right: 310px; right: 310px;
} }
.leaflet-control-button {
background-color: white !important;
color: #374151 !important;
}
.leaflet-control-button:hover {
background-color: #f3f4f6 !important;
}
/* Drawer Panel Styles */ /* Drawer Panel Styles */
.leaflet-drawer { .leaflet-drawer {

View file

@ -0,0 +1,141 @@
/* Leaflet Theme Styles - Light and Dark mode support */
/* CSS Custom Properties for Light Theme */
[data-theme="light"] {
--leaflet-bg-color: #ffffff;
--leaflet-text-color: #000000;
--leaflet-border-color: #e5e7eb;
--leaflet-shadow-color: rgba(0, 0, 0, 0.1);
--leaflet-hover-color: #f3f4f6;
--leaflet-link-color: #0066cc;
--leaflet-scale-bg: rgba(255, 255, 255, 0.9);
}
/* CSS Custom Properties for Dark Theme */
[data-theme="dark"] {
--leaflet-bg-color: #374151;
--leaflet-text-color: #ffffff;
--leaflet-border-color: #4b5563;
--leaflet-shadow-color: rgba(0, 0, 0, 0.3);
--leaflet-hover-color: #4b5563;
--leaflet-link-color: #66b3ff;
--leaflet-scale-bg: rgba(55, 65, 81, 0.9);
}
/* Leaflet default controls theme override */
.leaflet-control-layers,
.leaflet-control-zoom,
.leaflet-control-attribution,
.leaflet-bar a,
.leaflet-control-layers-toggle,
.leaflet-control-layers-list,
.leaflet-control-draw {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
border-color: var(--leaflet-border-color) !important;
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
}
/* Leaflet zoom buttons */
.leaflet-control-zoom a {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
border-bottom: 1px solid var(--leaflet-border-color) !important;
}
.leaflet-control-zoom a:hover {
background-color: var(--leaflet-hover-color) !important;
}
/* Leaflet layer control */
.leaflet-control-layers-toggle {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
}
.leaflet-control-layers-expanded {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
}
.leaflet-control-layers label {
color: var(--leaflet-text-color) !important;
}
/* Leaflet Draw controls */
.leaflet-draw-toolbar a {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
border-bottom: 1px solid var(--leaflet-border-color) !important;
}
.leaflet-draw-toolbar a:hover {
background-color: var(--leaflet-hover-color) !important;
}
.leaflet-draw-actions a {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
}
/* Leaflet popups */
.leaflet-popup-content-wrapper {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
}
.leaflet-popup-tip {
background-color: var(--leaflet-bg-color) !important;
}
/* Attribution control */
.leaflet-control-attribution a {
color: var(--leaflet-link-color) !important;
}
/* Custom control buttons */
.leaflet-control-button,
.add-visit-button,
.leaflet-bar button {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
border: 1px solid var(--leaflet-border-color) !important;
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
}
.leaflet-control-button:hover,
.add-visit-button:hover,
.leaflet-bar button:hover {
background-color: var(--leaflet-hover-color) !important;
}
/* Any other custom controls */
.leaflet-top .leaflet-control button,
.leaflet-bottom .leaflet-control button,
.leaflet-left .leaflet-control button,
.leaflet-right .leaflet-control button {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
border: 1px solid var(--leaflet-border-color) !important;
}
/* Location search button */
.location-search-toggle,
#location-search-toggle {
background-color: var(--leaflet-bg-color) !important;
color: var(--leaflet-text-color) !important;
border: 1px solid var(--leaflet-border-color) !important;
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
}
.location-search-toggle:hover,
#location-search-toggle:hover {
background-color: var(--leaflet-hover-color) !important;
}
/* Distance scale control */
.leaflet-control-scale {
background: var(--leaflet-scale-bg) !important;
border-radius: 3px !important;
padding: 2px !important;
}

View file

@ -40,7 +40,6 @@ class ApplicationController < ActionController::Base
end end
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
# Check both current request header and stored session value
client_type = request.headers['X-Dawarich-Client'] || session[:dawarich_client] client_type = request.headers['X-Dawarich-Client'] || session[:dawarich_client]
case client_type case client_type

View file

@ -59,23 +59,8 @@ class Settings::UsersController < ApplicationController
return return
end end
archive_param = params[:archive] import =
create_import_from_signed_archive_id(params[:archive])
# Handle both direct upload (signed_id) and traditional upload (file)
if archive_param.is_a?(String)
# Direct upload: archive_param is a signed blob ID
import = create_import_from_signed_archive_id(archive_param)
else
# Traditional upload: archive_param is an uploaded file
validate_archive_file(archive_param)
import = current_user.imports.build(
name: archive_param.original_filename,
source: :user_data_archive
)
import.file.attach(archive_param)
end
if import.save if import.save
redirect_to edit_user_registration_path, redirect_to edit_user_registration_path,

View file

@ -19,16 +19,12 @@ export default class extends Controller {
this.currentPopup = null; this.currentPopup = null;
this.mapsController = null; this.mapsController = null;
// Listen for theme changes
document.addEventListener('theme:changed', this.handleThemeChange.bind(this));
// Wait for the map to be initialized // Wait for the map to be initialized
this.waitForMap(); this.waitForMap();
} }
disconnect() { disconnect() {
this.cleanup(); this.cleanup();
document.removeEventListener('theme:changed', this.handleThemeChange.bind(this));
console.log("Add visit controller disconnected"); console.log("Add visit controller disconnected");
} }
@ -435,16 +431,6 @@ export default class extends Controller {
}); });
} }
handleThemeChange(event) {
console.log('Add visit controller: Theme changed to', event.detail.theme);
this.userThemeValue = event.detail.theme;
// Update button theme if it exists
if (this.addVisitButton && !this.isAddingVisit) {
applyThemeToButton(this.addVisitButton, this.userThemeValue);
}
}
cleanup() { cleanup() {
if (this.map) { if (this.map) {
this.map.off('click', this.onMapClick, this); this.map.off('click', this.onMapClick, this);

View file

@ -44,7 +44,6 @@ import { TileMonitor } from "../maps/tile_monitor";
import BaseController from "./base_controller"; import BaseController from "./base_controller";
import { createAllMapLayers } from "../maps/layers"; import { createAllMapLayers } from "../maps/layers";
import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils"; import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils";
import { injectThemeStyles } from "../maps/theme_styles";
export default class extends BaseController { export default class extends BaseController {
static targets = ["container"]; static targets = ["container"];
@ -65,9 +64,6 @@ export default class extends BaseController {
this.selfHosted = this.element.dataset.self_hosted; this.selfHosted = this.element.dataset.self_hosted;
this.userTheme = this.element.dataset.user_theme || 'dark'; this.userTheme = this.element.dataset.user_theme || 'dark';
// Inject theme styles for Leaflet controls
injectThemeStyles(this.userTheme);
try { try {
this.markers = this.element.dataset.coordinates ? JSON.parse(this.element.dataset.coordinates) : []; this.markers = this.element.dataset.coordinates ? JSON.parse(this.element.dataset.coordinates) : [];
} catch (error) { } catch (error) {
@ -1005,7 +1001,6 @@ export default class extends BaseController {
if (newSettings.theme && newSettings.theme !== this.userTheme) { if (newSettings.theme && newSettings.theme !== this.userTheme) {
this.userTheme = newSettings.theme; this.userTheme = newSettings.theme;
mapElement.setAttribute('data-user_theme', this.userTheme); mapElement.setAttribute('data-user_theme', this.userTheme);
injectThemeStyles(this.userTheme);
// Dispatch theme change event for other controllers // Dispatch theme change event for other controllers
document.dispatchEvent(new CustomEvent('theme:changed', { document.dispatchEvent(new CustomEvent('theme:changed', {

View file

@ -1159,7 +1159,6 @@ class LocationSearch {
return new Date(dateString).toLocaleDateString() + ' ' + return new Date(dateString).toLocaleDateString() + ' ' +
new Date(dateString).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); new Date(dateString).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
} }
} }
export { LocationSearch }; export { LocationSearch };

View file

@ -1,156 +0,0 @@
// Dynamic CSS injection for theme-aware Leaflet controls
export function injectThemeStyles(userTheme) {
// Remove existing theme styles if any
const existingStyle = document.getElementById('leaflet-theme-styles');
if (existingStyle) {
existingStyle.remove();
}
const themeColors = getThemeColors(userTheme);
const css = `
/* Leaflet default controls theme override */
.leaflet-control-layers,
.leaflet-control-zoom,
.leaflet-control-attribution,
.leaflet-bar a,
.leaflet-control-layers-toggle,
.leaflet-control-layers-list,
.leaflet-control-draw {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
border-color: ${themeColors.borderColor} !important;
box-shadow: 0 1px 4px ${themeColors.shadowColor} !important;
}
/* Leaflet zoom buttons */
.leaflet-control-zoom a {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
border-bottom: 1px solid ${themeColors.borderColor} !important;
}
.leaflet-control-zoom a:hover {
background-color: ${themeColors.hoverColor} !important;
}
/* Leaflet layer control */
.leaflet-control-layers-toggle {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
}
.leaflet-control-layers-expanded {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
}
.leaflet-control-layers label {
color: ${themeColors.textColor} !important;
}
/* Leaflet Draw controls */
.leaflet-draw-toolbar a {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
border-bottom: 1px solid ${themeColors.borderColor} !important;
}
.leaflet-draw-toolbar a:hover {
background-color: ${themeColors.hoverColor} !important;
}
.leaflet-draw-actions a {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
}
/* Leaflet popups */
.leaflet-popup-content-wrapper {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
}
.leaflet-popup-tip {
background-color: ${themeColors.backgroundColor} !important;
}
/* Attribution control */
.leaflet-control-attribution a {
color: ${userTheme === 'light' ? '#0066cc' : '#66b3ff'} !important;
}
/* Custom control buttons */
.leaflet-control-button,
.add-visit-button,
.leaflet-bar button {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
border: 1px solid ${themeColors.borderColor} !important;
box-shadow: 0 1px 4px ${themeColors.shadowColor} !important;
}
.leaflet-control-button:hover,
.add-visit-button:hover,
.leaflet-bar button:hover {
background-color: ${themeColors.hoverColor} !important;
}
/* Any other custom controls */
.leaflet-top .leaflet-control button,
.leaflet-bottom .leaflet-control button,
.leaflet-left .leaflet-control button,
.leaflet-right .leaflet-control button {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
border: 1px solid ${themeColors.borderColor} !important;
}
/* Location search button */
.location-search-toggle,
#location-search-toggle {
background-color: ${themeColors.backgroundColor} !important;
color: ${themeColors.textColor} !important;
border: 1px solid ${themeColors.borderColor} !important;
box-shadow: 0 1px 4px ${themeColors.shadowColor} !important;
}
.location-search-toggle:hover,
#location-search-toggle:hover {
background-color: ${themeColors.hoverColor} !important;
}
/* Distance scale control - minimal theming to avoid duplication */
.leaflet-control-scale {
background: rgba(${userTheme === 'light' ? '255, 255, 255' : '55, 65, 81'}, 0.9) !important;
border-radius: 3px !important;
padding: 2px !important;
}
`;
// Inject the CSS
const style = document.createElement('style');
style.id = 'leaflet-theme-styles';
style.textContent = css;
document.head.appendChild(style);
}
function getThemeColors(userTheme) {
if (userTheme === 'light') {
return {
backgroundColor: '#ffffff',
textColor: '#000000',
borderColor: '#e5e7eb',
shadowColor: 'rgba(0, 0, 0, 0.1)',
hoverColor: '#f3f4f6'
};
} else {
return {
backgroundColor: '#374151',
textColor: '#ffffff',
borderColor: '#4b5563',
shadowColor: 'rgba(0, 0, 0, 0.3)',
hoverColor: '#4b5563'
};
}
}

View file

@ -15,8 +15,8 @@ class Api::PointSerializer
lat = point.lat lat = point.lat
lon = point.lon lon = point.lon
attributes['latitude'] = lat.nil? ? nil : lat.to_s attributes['latitude'] = lat&.to_s
attributes['longitude'] = lon.nil? ? nil : lon.to_s attributes['longitude'] = lon&.to_s
end end
end end

View file

@ -39,8 +39,10 @@ module Maps
def execute_bounds_query(start_timestamp, end_timestamp) def execute_bounds_query(start_timestamp, end_timestamp)
ActiveRecord::Base.connection.exec_query( ActiveRecord::Base.connection.exec_query(
"SELECT MIN(latitude) as min_lat, MAX(latitude) as max_lat, "SELECT ST_YMin(ST_Extent(lonlat::geometry)) as min_lat,
MIN(longitude) as min_lng, MAX(longitude) as max_lng ST_YMax(ST_Extent(lonlat::geometry)) as max_lat,
ST_XMin(ST_Extent(lonlat::geometry)) as min_lng,
ST_XMax(ST_Extent(lonlat::geometry)) as max_lng
FROM points FROM points
WHERE user_id = $1 WHERE user_id = $1
AND timestamp BETWEEN $2 AND $3", AND timestamp BETWEEN $2 AND $3",

View file

@ -1,39 +0,0 @@
# frozen_string_literal: true
module Maps
class DateParameterCoercer
class InvalidDateFormatError < StandardError; end
def initialize(param)
@param = param
end
def call
coerce_date(@param)
end
private
attr_reader :param
def coerce_date(param)
case param
when String
coerce_string_param(param)
when Integer
param
else
param.to_i
end
rescue ArgumentError => e
Rails.logger.error "Invalid date format: #{param} - #{e.message}"
raise InvalidDateFormatError, "Invalid date format: #{param}"
end
def coerce_string_param(param)
return param.to_i if param.match?(/^\d+$/)
Time.parse(param).to_i
end
end
end

View file

@ -32,8 +32,7 @@ module Maps
end end
def recalculate_h3_hex_ids def recalculate_h3_hex_ids
service = Stats::CalculateMonth.new(user.id, stat.year, stat.month) Stats::HexagonCalculator.new(user.id, stat.year, stat.month).call
service.send(:calculate_h3_hex_ids)
end end
def update_stat_with_new_hex_ids(new_hex_ids) def update_stat_with_new_hex_ids(new_hex_ids)

View file

@ -40,7 +40,8 @@ class Stats::CalculateMonth
toponyms: toponyms, toponyms: toponyms,
h3_hex_ids: calculate_h3_hex_ids h3_hex_ids: calculate_h3_hex_ids
) )
stat.save
stat.save!
end end
end end

View file

@ -17,13 +17,6 @@ class Stats::HexagonCalculator
calculate_h3_hexagon_centers(h3_resolution) calculate_h3_hexagon_centers(h3_resolution)
end end
def calculate_h3_hex_ids
result = calculate_hexagons(DEFAULT_H3_RESOLUTION)
return {} if result.nil?
result
end
private private
attr_reader :user, :year, :month attr_reader :user, :year, :month

View file

@ -73,17 +73,49 @@ class Users::ImportData
zip_file.each do |entry| zip_file.each do |entry|
next if entry.directory? next if entry.directory?
extraction_path = File.join(@import_directory, entry.name) # Sanitize entry name to prevent path traversal attacks
sanitized_name = sanitize_zip_entry_name(entry.name)
next if sanitized_name.nil?
# Compute absolute destination path
extraction_path = File.expand_path(File.join(@import_directory, sanitized_name))
# Verify the extraction path is within the import directory
safe_import_dir = File.expand_path(@import_directory) + File::SEPARATOR
unless extraction_path.start_with?(safe_import_dir) || extraction_path == File.expand_path(@import_directory)
Rails.logger.warn "Skipping potentially malicious ZIP entry: #{entry.name} (would extract to #{extraction_path})"
next
end
Rails.logger.debug "Extracting #{entry.name} to #{extraction_path}" Rails.logger.debug "Extracting #{entry.name} to #{extraction_path}"
FileUtils.mkdir_p(File.dirname(extraction_path)) FileUtils.mkdir_p(File.dirname(extraction_path))
# Use destination_directory parameter for rubyzip 3.x compatibility # Use destination_directory parameter for rubyzip 3.x compatibility
entry.extract(entry.name, destination_directory: @import_directory) entry.extract(sanitized_name, destination_directory: @import_directory)
end end
end end
end end
def sanitize_zip_entry_name(entry_name)
# Remove leading slashes, backslashes, and dots
sanitized = entry_name.gsub(%r{^[/\\]+}, '')
# Reject entries with path traversal attempts
if sanitized.include?('..') || sanitized.start_with?('/') || sanitized.start_with?('\\')
Rails.logger.warn "Rejecting potentially malicious ZIP entry name: #{entry_name}"
return nil
end
# Reject absolute paths
if Pathname.new(sanitized).absolute?
Rails.logger.warn "Rejecting absolute path in ZIP entry: #{entry_name}"
return nil
end
sanitized
end
def load_json_data def load_json_data
json_path = @import_directory.join('data.json') json_path = @import_directory.join('data.json')

View file

@ -76,10 +76,16 @@
<div class="join"> <div class="join">
<%= link_to "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}" do %> <%= link_to "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}" do %>
<span class="join-item btn btn-sm <%= trial_button_class(current_user) %>"> <span class="join-item btn btn-sm <%= trial_button_class(current_user) %>">
<% if current_user.active_until.past? %> <% expiry = current_user.active_until %>
<span class="tooltip tooltip-bottom">Trial expired 🥺</span> <% if expiry.blank? || expiry.past? %>
<span class="tooltip tooltip-bottom" data-tip="Trial expired" title="Trial expired">Trial expired 🥺</span>
<% else %> <% else %>
<span class="tooltip tooltip-bottom" data-tip="Your trial will end in <%= distance_of_time_in_words(current_user.active_until, Time.current) %>"><%= (current_user.active_until.to_date - Time.current.to_date).to_i %> days remaining</span> <% days_left = [(expiry.to_date - Time.zone.today).to_i, 0].max %>
<span class="tooltip tooltip-bottom"
data-tip="Your trial will end in <%= distance_of_time_in_words(expiry, Time.current) %>"
title="Your trial will end in <%= distance_of_time_in_words(expiry, Time.current) %>">
<%= pluralize(days_left, 'day') %> remaining
</span>
<% end %> <% end %>
</span><span class="join-item btn btn-sm btn-success"> </span><span class="join-item btn btn-sm btn-success">
Subscribe Subscribe

View file

@ -1,6 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
if defined?(Rails::Server) && !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled? # Initialize Prometheus exporter for web processes, but exclude console, rake tasks, and tests
should_initialize = DawarichSettings.prometheus_exporter_enabled? &&
!Rails.env.test? &&
!defined?(Rails::Console) &&
!File.basename($PROGRAM_NAME).include?('rake')
if should_initialize
require 'prometheus_exporter/middleware' require 'prometheus_exporter/middleware'
require 'prometheus_exporter/instrumentation' require 'prometheus_exporter/instrumentation'

View file

@ -75,7 +75,7 @@ Rails.application.routes.draw do
to: 'stats#update', to: 'stats#update',
as: :update_year_month_stats, as: :update_year_month_stats,
constraints: { year: /\d{4}/, month: /\d{1,2}|all/ } constraints: { year: /\d{4}/, month: /\d{1,2}|all/ }
get 'shared/stats/:uuid', to: 'shared/stats#show', as: :shared_stat get 'shared/month/:uuid', to: 'shared/stats#show', as: :shared_stat
# Sharing management endpoint (requires auth) # Sharing management endpoint (requires auth)
patch 'stats/:year/:month/sharing', patch 'stats/:year/:month/sharing',
@ -85,7 +85,6 @@ Rails.application.routes.draw do
root to: 'home#index' root to: 'home#index'
# iOS mobile auth success endpoint
get 'auth/ios/success', to: 'auth/ios#success', as: :ios_success get 'auth/ios/success', to: 'auth/ios#success', as: :ios_success
if SELF_HOSTED if SELF_HOSTED

View file

@ -43,4 +43,4 @@ daily_track_generation_job:
nightly_reverse_geocoding_job: nightly_reverse_geocoding_job:
cron: "15 1 * * *" # every day at 01:15 cron: "15 1 * * *" # every day at 01:15
class: "Points::NightlyReverseGeocodingJob" class: "Points::NightlyReverseGeocodingJob"
queue: tracks queue: reverse_geocoding

View file

@ -1,13 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class AddSharingFieldsToStats < ActiveRecord::Migration[8.0] class AddSharingFieldsToStats < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def up def up
add_column :stats, :sharing_settings, :jsonb add_column :stats, :sharing_settings, :jsonb
add_column :stats, :sharing_uuid, :uuid add_column :stats, :sharing_uuid, :uuid
change_column_default :stats, :sharing_settings, {} change_column_default :stats, :sharing_settings, {}
BulkStatsCalculatingJob.set(wait: 5.minutes).perform_later
end end
def down def down

View file

@ -1,19 +1 @@
{ {"name":"Dawarich","short_name":"Dawarich","icons":[{"src":"/assets/favicon/android-chrome-192x192-f9610e2af28e4e48ff0472572c0cb9e3902d29bccc2b07f8f03aabf684822355.png","sizes":"192x192","type":"image/png"},{"src":"/assets/favicon/android-chrome-512x512-c2ec8132d773ae99f53955360cdd5691bb38e0ed141bddebd39d896b78b5afb6.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
"name": "Dawarich",
"short_name": "Dawarich",
"icons": [
{
"src": "/assets/favicon/android-chrome-192x192-f9610e2af28e4e48ff0472572c0cb9e3902d29bccc2b07f8f03aabf684822355.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/assets/favicon/android-chrome-512x512-c2ec8132d773ae99f53955360cdd5691bb38e0ed141bddebd39d896b78b5afb6.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View file

@ -26,10 +26,8 @@ RSpec.describe Tracks::DailyGenerationJob, type: :job do
active_user.update!(points_count: active_user.points.count) active_user.update!(points_count: active_user.points.count)
trial_user.update!(points_count: trial_user.points.count) trial_user.update!(points_count: trial_user.points.count)
# Mock User.active_or_trial to only return test users allow(User).to receive(:active_or_trial)
active_or_trial_mock = double('ActiveRecord::Relation') .and_return(User.where(id: [active_user.id, trial_user.id]))
allow(User).to receive(:active_or_trial).and_return(active_or_trial_mock)
allow(active_or_trial_mock).to receive(:find_each).and_yield(active_user).and_yield(trial_user)
ActiveJob::Base.queue_adapter.enqueued_jobs.clear ActiveJob::Base.queue_adapter.enqueued_jobs.clear
end end

View file

@ -12,7 +12,7 @@ RSpec.describe 'Shared::Stats', type: :request do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) } let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) }
describe 'GET /shared/stats/:uuid' do describe 'GET /shared/month/:uuid' do
context 'with valid sharing UUID' do context 'with valid sharing UUID' do
before do before do
# Create some test points for data bounds calculation # Create some test points for data bounds calculation

View file

@ -10,7 +10,6 @@ RSpec.describe Api::PointSerializer do
let(:all_excluded) { Api::PointSerializer::EXCLUDED_ATTRIBUTES } let(:all_excluded) { Api::PointSerializer::EXCLUDED_ATTRIBUTES }
let(:expected_json) do let(:expected_json) do
point.attributes.except(*all_excluded).tap do |attributes| point.attributes.except(*all_excluded).tap do |attributes|
# API serializer extracts coordinates from PostGIS geometry
attributes['latitude'] = point.lat.to_s attributes['latitude'] = point.lat.to_s
attributes['longitude'] = point.lon.to_s attributes['longitude'] = point.lon.to_s
end end

View file

@ -1,70 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Maps::DateParameterCoercer do
describe '.call' do
subject(:coerce_date) { described_class.new(param).call }
context 'with integer parameter' do
let(:param) { 1_717_200_000 }
it 'returns the integer unchanged' do
expect(coerce_date).to eq(1_717_200_000)
end
end
context 'with numeric string parameter' do
let(:param) { '1717200000' }
it 'converts to integer' do
expect(coerce_date).to eq(1_717_200_000)
end
end
context 'with ISO date string parameter' do
let(:param) { '2024-06-01T00:00:00Z' }
it 'parses and converts to timestamp' do
expected_timestamp = Time.parse('2024-06-01T00:00:00Z').to_i
expect(coerce_date).to eq(expected_timestamp)
end
end
context 'with date string parameter' do
let(:param) { '2024-06-01' }
it 'parses and converts to timestamp' do
expected_timestamp = Time.parse('2024-06-01').to_i
expect(coerce_date).to eq(expected_timestamp)
end
end
context 'with invalid date string' do
let(:param) { 'invalid-date' }
it 'raises InvalidDateFormatError' do
expect { coerce_date }.to raise_error(
Maps::DateParameterCoercer::InvalidDateFormatError,
'Invalid date format: invalid-date'
)
end
end
context 'with nil parameter' do
let(:param) { nil }
it 'converts to 0' do
expect(coerce_date).to eq(0)
end
end
context 'with float parameter' do
let(:param) { 1_717_200_000.5 }
it 'converts to integer' do
expect(coerce_date).to eq(1_717_200_000)
end
end
end
end

View file

@ -81,43 +81,4 @@ RSpec.describe Stats::HexagonCalculator do
end end
end end
end end
describe '#calculate_h3_hex_ids' do
subject(:calculate_hex_ids) { described_class.new(user.id, year, month).calculate_h3_hex_ids }
let(:user) { create(:user) }
let(:year) { 2024 }
let(:month) { 1 }
context 'when there are no points' do
it 'returns empty hash' do
expect(calculate_hex_ids).to eq({})
end
end
context 'when there are points' do
let(:timestamp1) { DateTime.new(year, month, 1, 12).to_i }
let!(:import) { create(:import, user:) }
let!(:point1) do
create(:point,
user:,
import:,
timestamp: timestamp1,
lonlat: 'POINT(14.452712811406352 52.107902115161316)')
end
it 'returns hash with H3 hex IDs' do
result = calculate_hex_ids
expect(result).to be_a(Hash)
expect(result).not_to be_empty
result.each do |h3_index, data|
expect(h3_index).to be_a(String)
expect(data).to be_an(Array)
expect(data.size).to eq(3) # [count, earliest, latest]
end
end
end
end
end end