mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
Update stuff, fix stuff
This commit is contained in:
parent
4287fee93d
commit
0728c21c61
28 changed files with 213 additions and 406 deletions
|
|
@ -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
|
|
@ -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 {
|
||||||
|
|
|
||||||
141
app/assets/stylesheets/leaflet_theme.css
Normal file
141
app/assets/stylesheets/leaflet_theme.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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', {
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue