diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b69af1f..6aa48f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Changed - Onboarding modal window now features a link to the App Store and a QR code to configure the Dawarich iOS app. +- A permanent option was removed from stats sharing options. Now, stats can be shared for 1, 12 or 24 hours only. # [0.32.0] - 2025-09-13 diff --git a/app/controllers/shared/stats_controller.rb b/app/controllers/shared/stats_controller.rb index ff8d19d7..a9b83862 100644 --- a/app/controllers/shared/stats_controller.rb +++ b/app/controllers/shared/stats_controller.rb @@ -30,7 +30,7 @@ class Shared::StatsController < ApplicationController return head :not_found unless @stat if params[:enabled] == '1' - @stat.enable_sharing!(expiration: params[:expiration] || 'permanent') + @stat.enable_sharing!(expiration: params[:expiration] || '24h') sharing_url = shared_stat_url(@stat.sharing_uuid) render json: { diff --git a/app/models/stat.rb b/app/models/stat.rb index 9d25da89..6c5d592b 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -38,7 +38,7 @@ class Stat < ApplicationRecord def sharing_expired? expiration = sharing_settings['expiration'] - return false if expiration.blank? || expiration == 'permanent' + return false if expiration.blank? expires_at_value = sharing_settings['expires_at'] return true if expires_at_value.blank? @@ -67,6 +67,9 @@ class Stat < ApplicationRecord end def enable_sharing!(expiration: '1h') + # Default to 24h if an invalid expiration is provided + expiration = '24h' unless %w[1h 12h 24h].include?(expiration) + expires_at = case expiration when '1h' then 1.hour.from_now when '12h' then 12.hours.from_now @@ -77,7 +80,7 @@ class Stat < ApplicationRecord sharing_settings: { 'enabled' => true, 'expiration' => expiration, - 'expires_at' => expires_at&.iso8601 + 'expires_at' => expires_at.iso8601 }, sharing_uuid: sharing_uuid || SecureRandom.uuid ) diff --git a/app/views/shared/_sharing_modal.html.erb b/app/views/shared/_sharing_modal.html.erb index beb120d0..926719cb 100644 --- a/app/views/shared/_sharing_modal.html.erb +++ b/app/views/shared/_sharing_modal.html.erb @@ -43,8 +43,7 @@ <%= options_for_select([ ['1 hour', '1h'], ['12 hours', '12h'], - ['24 hours', '24h'], - ['Permanent', 'permanent'] + ['24 hours', '24h'] ], @stat&.sharing_settings&.dig('expiration') || '1h') %> diff --git a/docs/SHAREABLE_STATS_FEATURE.md b/docs/SHAREABLE_STATS_FEATURE.md deleted file mode 100644 index 285c511d..00000000 --- a/docs/SHAREABLE_STATS_FEATURE.md +++ /dev/null @@ -1,502 +0,0 @@ -# Shareable Stats Feature Documentation - -## Overview - -The Shareable Stats feature allows Dawarich users to publicly share their monthly location statistics without requiring authentication. This system provides a secure, time-limited way to share location insights while maintaining user privacy through configurable expiration settings and unguessable UUID-based access. - -## Key Features - -- **Time-based expiration**: Share links can expire after 1 hour, 12 hours, 24 hours, or be permanent -- **UUID-based security**: Each shared stat has a unique, unguessable UUID for secure access -- **Public API access**: Hexagon map data can be accessed via API without authentication when sharing is enabled -- **H3 Hexagon visualization**: Enhanced geographic data visualization using Uber's H3 hexagonal hierarchical spatial index -- **Automatic expiration**: Expired shares are automatically inaccessible -- **Privacy controls**: Users can enable/disable sharing and regenerate sharing URLs at any time - -## Database Schema - -### Stats Table Extensions - -The sharing functionality extends the `stats` table with the following columns: - -```sql --- Public sharing configuration -sharing_settings JSONB DEFAULT {} -sharing_uuid UUID - --- Pre-calculated H3 hexagon data for performance -h3_hex_ids JSONB DEFAULT {} - --- Indexes for performance -INDEX ON h3_hex_ids USING GIN WHERE (h3_hex_ids IS NOT NULL AND h3_hex_ids != '{}'::jsonb) -``` - -### Sharing Settings Structure - -```json -{ - "enabled": true, - "expiration": "24h", // "1h", "12h", "24h", or "permanent" - "expires_at": "2024-01-15T12:00:00Z" -} -``` - -### H3 Hex IDs Data Format - -The `h3_hex_ids` column stores pre-calculated H3 hexagon data as a hash: - -```json -{ - "8a1fb46622dffff": [15, 1640995200, 1640998800], - "8a1fb46622e7fff": [8, 1640996400, 1640999200], - // ... more H3 index entries - // Format: { "h3_index_string": [point_count, earliest_timestamp, latest_timestamp] } -} -``` - -## Architecture Components - -### Models - -#### Stat Model (`app/models/stat.rb`) - -**Key Methods:** -- `sharing_enabled?`: Checks if sharing is enabled -- `sharing_expired?`: Validates expiration status -- `public_accessible?`: Combined check for sharing availability -- `hexagons_available?`: Verifies pre-calculated H3 hex data exists -- `enable_sharing!(expiration:)`: Enables sharing with expiration -- `disable_sharing!`: Disables sharing -- `generate_new_sharing_uuid!`: Regenerates sharing UUID -- `calculate_data_bounds`: Calculates geographic bounds for the month - -### Controllers - -#### Shared::StatsController (`app/controllers/shared/stats_controller.rb`) - -Handles public sharing functionality: - -**Routes:** -- `GET /shared/stats/:uuid` - Public view of shared stats -- `PATCH /stats/:year/:month/sharing` - Sharing management (authenticated) - -**Key Methods:** -- `show`: Renders public stats view without authentication -- `update`: Manages sharing settings (enable/disable, expiration) - -#### Api::V1::Maps::HexagonsController (`app/controllers/api/v1/maps/hexagons_controller.rb`) - -Provides hexagon data for both authenticated and public access: - -**Features:** -- Skip authentication for public sharing requests (`uuid` parameter) -- Context resolution for public vs. authenticated access -- Error handling for missing or expired shares - -```ruby -# Public access via UUID -GET /api/v1/maps/hexagons?uuid=SHARING_UUID - -# Authenticated access -GET /api/v1/maps/hexagons?start_date=2024-01-01&end_date=2024-01-31 -``` - -### Services - -#### Maps::HexagonRequestHandler (`app/services/maps/hexagon_request_handler.rb`) - -Central service for processing hexagon requests: - -**Workflow:** -1. Attempts to find matching stat for the request -2. Delegates to `HexagonCenterManager` for pre-calculated data -3. Returns empty feature collection if no data available - -#### Maps::HexagonCenterManager (`app/services/maps/hexagon_center_manager.rb`) - -Manages pre-calculated H3 hexagon data: - -**Responsibilities:** -- Retrieves pre-calculated H3 hex IDs from database -- Converts stored H3 indexes to GeoJSON polygons -- Builds hexagon features with point counts and timestamps -- Handles efficient polygon generation from H3 indexes - -**Data Flow:** -1. Check if pre-calculated H3 hex IDs are available -2. Convert H3 indexes to hexagon polygons using `HexagonPolygonGenerator` -3. Build GeoJSON FeatureCollection with metadata and point counts - -#### Stats::CalculateMonth (`app/services/stats/calculate_month.rb`) - -Responsible for calculating and storing hexagon data during stats processing: - -**H3 Configuration:** -- `DEFAULT_H3_RESOLUTION = 8`: Small hexagons for good detail -- `MAX_HEXAGONS = 10_000`: Maximum to prevent memory issues - -**Key Methods:** -- `calculate_h3_hex_ids`: Main method for H3 calculation and storage -- `calculate_h3_hexagon_centers`: Internal H3 calculation logic -- `calculate_h3_indexes`: Groups points into H3 hexagons -- `fetch_user_points_for_period`: Retrieves points for date range - -**Algorithm:** -1. Fetch user points for the specified month -2. Convert each point to H3 index at specified resolution -3. Aggregate points per hexagon with count and timestamp bounds -4. Apply resolution reduction if hexagon count exceeds maximum -5. Store as hash of { h3_index_string => [count, earliest, latest] } - -#### Maps::HexagonPolygonGenerator (`app/services/maps/hexagon_polygon_generator.rb`) - -Converts H3 indexes back to polygon geometry: - -**Features:** -- Uses H3 library for accurate hexagon boundaries -- Converts coordinates to GeoJSON Polygon format -- H3-index-only generation for maximum efficiency -- Direct H3 index to polygon conversion with coordinate transformation - -**Usage:** -- **H3-index only**: `new(h3_index: h3_index_string_or_integer)` -- Supports both hex string (`"8a1fb46622dffff"`) and integer formats -- Converts H3 boundary coordinates to [lng, lat] GeoJSON format - -## H3 Hexagon System - -### What is H3? - -H3 is Uber's Hexagonal Hierarchical Spatial Index that provides: -- **Uniform coverage**: Earth divided into hexagonal cells -- **Hierarchical resolution**: 16 levels from global to local -- **Efficient indexing**: Fast spatial queries and aggregations -- **Consistent shape**: Hexagons have uniform neighbors - -### Resolution Levels - -Dawarich uses H3 resolution 8 by default: -- **Resolution 8**: ~737m average hexagon edge length -- **Fallback mechanism**: Reduces resolution if too many hexagons -- **Maximum limit**: 10,000 hexagons to prevent memory issues - -### Performance Benefits - -1. **Pre-calculation**: H3 hexagons calculated once during stats processing -2. **Efficient storage**: Hash-based storage with H3 index as key -3. **Fast retrieval**: Database lookup instead of real-time calculation -4. **Reduced bandwidth**: Compact JSON hash format for API responses -5. **Direct polygon generation**: H3 index directly converts to polygon boundaries - -## Workflow - -### 1. Stats Calculation Process - -```mermaid -graph TD - A[User Data Import] --> B[Stats::CalculateMonth Service] - B --> C[Calculate H3 Hex IDs] - C --> D[Store in h3_hex_ids Column] - D --> E[Stats Available for Sharing] -``` - -**Detailed Steps:** -1. User imports location data (GPX, JSON, etc.) -2. Background job triggers `Stats::CalculateMonth` -3. Service calculates monthly statistics including H3 hex IDs -4. H3 indexes are calculated for all points in the month -5. Results stored in `stats.h3_hex_ids` as JSON hash with format `{"h3_index": [count, earliest, latest]}` - -### 2. Sharing Activation - -```mermaid -graph TD - A[User Visits Stats Page] --> B[Enable Sharing Toggle] - B --> C[Select Expiration Duration] - C --> D[PATCH /stats/:year/:month/sharing] - D --> E[Generate/Update sharing_uuid] - E --> F[Set sharing_settings] - F --> G[Return Public URL] -``` - -**Sharing Settings:** -- **Expiration options**: 1h, 12h, 24h, permanent -- **UUID generation**: Secure random UUID for each stat -- **Expiration timestamp**: Calculated and stored in sharing_settings - -### 3. Public Access Flow - -```mermaid -graph TD - A[Public User Visits Shared URL] --> B[Validate UUID & Expiration] - B --> C{Valid & Not Expired?} - C -->|Yes| D[Load Public Stats View] - C -->|No| E[Redirect with Error] - D --> F[Render Map with Hexagons] - F --> G[Load Hexagon Data via API] - G --> H[Display Interactive Map] -``` - -**Security Checks:** -1. Verify sharing UUID exists in database -2. Check `sharing_settings.enabled = true` -3. Validate expiration timestamp if not permanent -4. Return 404 if any check fails - -### 4. Hexagon Data Retrieval - -```mermaid -graph TD - A[Map Requests Hexagon Data] --> B[GET /api/v1/maps/hexagons?uuid=UUID] - B --> C[HexagonsController] - C --> D[Skip Authentication for UUID Request] - D --> E[HexagonRequestHandler] - E --> F[Find Stat by UUID] - F --> G[HexagonCenterManager] - G --> H[Load Pre-calculated H3 Hex IDs] - H --> I[Convert to GeoJSON Polygons] - I --> J[Return FeatureCollection] -``` - -**Data Transformation:** -1. Retrieve stored H3 hex IDs hash from database -2. For each H3 index, use H3 library to get hexagon boundary coordinates -3. Convert coordinates to GeoJSON Polygon format ([lng, lat] ordering) -4. Build GeoJSON Feature with properties (point count, earliest/latest timestamps) -5. Return complete FeatureCollection for map rendering - -## API Endpoints - -### Public Sharing - -#### View Shared Stats -```http -GET /shared/stats/:uuid -``` -- **Authentication**: None required -- **Response**: HTML page with public stats view -- **Error Handling**: Redirects to root with alert if invalid/expired - -#### Get Hexagon Data -```http -GET /api/v1/maps/hexagons?uuid=:uuid -``` -- **Authentication**: None required for UUID access -- **Response**: GeoJSON FeatureCollection -- **Features**: Each feature represents one hexagon with point count and timestamps - -### Authenticated Management - -#### Toggle Sharing -```http -PATCH /stats/:year/:month/sharing -``` -**Parameters:** -- `enabled`: "1" to enable, "0" to disable -- `expiration`: "1h", "12h", "24h", or "permanent" (when enabling) - -**Response:** -```json -{ - "success": true, - "sharing_url": "https://domain.com/shared/stats/uuid", - "message": "Sharing enabled successfully" -} -``` - -## Security Features - -### UUID-based Access -- **Unguessable URLs**: Uses secure random UUIDs -- **No enumeration**: Can't guess valid sharing links -- **Automatic generation**: New UUID created for each sharing activation - -### Time-based Expiration -- **Configurable duration**: Multiple expiration options -- **Automatic enforcement**: Expired shares become inaccessible -- **Precise timestamping**: ISO8601 format with timezone awareness - -### Limited Data Exposure -- **No user identification**: Public view doesn't expose user details -- **Aggregated data only**: Only statistical summaries are shared -- **No raw location points**: Individual coordinates not exposed - -### Privacy Controls -- **User control**: Users can enable/disable sharing at any time -- **UUID regeneration**: Can generate new sharing URL to invalidate old ones -- **Granular permissions**: Per-month sharing control - -## Frontend Integration - -### Public View Template (`app/views/stats/public_month.html.erb`) - -**Features:** -- **Responsive design**: Mobile-friendly layout with Tailwind CSS -- **Monthly statistics**: Distance, active days, countries visited -- **Interactive hexagon map**: Leaflet.js with H3 hexagon overlay -- **Activity charts**: Daily distance visualization -- **Location summary**: Countries and cities visited - -**Map Integration:** -```erb -
-
-``` - -### JavaScript Controller - -**Stimulus Controller**: `public-stat-map` -- **Leaflet initialization**: Sets up interactive map -- **Hexagon layer**: Loads and renders hexagon data from API -- **User interaction**: Click handlers, zoom controls -- **Loading states**: Shows loading spinner during data fetch - -## Performance Considerations - -### Pre-calculation Strategy -- **Background processing**: H3 hex IDs calculated during stats job -- **Storage efficiency**: H3 indexes are compact and stored as hash keys -- **Query optimization**: GIN index on h3_hex_ids column -- **Caching**: Pre-calculated data serves multiple requests - -### Memory Management -- **Hexagon limits**: Maximum 10,000 hexagons per month -- **Resolution fallback**: Automatically reduces detail for large areas -- **Lazy loading**: Only calculate when stats are processed -- **Efficient formats**: JSON storage optimized for size - -### Database Optimization -```sql --- Optimized queries for H3 hex data -SELECT h3_hex_ids FROM stats -WHERE sharing_uuid = ? AND sharing_settings->>'enabled' = 'true'; - --- GIN index for efficient JSONB queries -CREATE INDEX index_stats_on_h3_hex_ids -ON stats USING gin (h3_hex_ids) -WHERE (h3_hex_ids IS NOT NULL AND h3_hex_ids != '{}'::jsonb); - --- Example H3 hex data structure in database --- h3_hex_ids: {"8a1fb46622dffff": [15, 1640995200, 1640998800], ...} -``` - -## Error Handling - -### Validation Errors -- **Missing UUID**: 404 response with user-friendly message -- **Expired sharing**: Redirect with appropriate alert -- **Invalid parameters**: Bad request with error details - -### Service Errors -- **H3 calculation failures**: Graceful degradation, logs warning -- **Database errors**: Transaction rollback, user notification -- **Memory issues**: Resolution reduction, retry mechanism - -### Frontend Resilience -- **Loading states**: User feedback during data fetching -- **Fallback content**: Display stats even if hexagons fail -- **Error messages**: Clear communication of issues - -## Configuration - -### Environment Variables -```bash -# H3 hexagon settings (optional, defaults shown) -H3_DEFAULT_RESOLUTION=8 -H3_MAX_HEXAGONS=10000 - -# Feature flags -ENABLE_PUBLIC_SHARING=true -``` - -### Runtime Configuration -- **Resolution adaptation**: Automatic based on data size -- **Expiration options**: Configurable in sharing settings -- **Security headers**: CORS configuration for API access - -## Monitoring and Analytics - -### Logging -- **Share creation**: Log when sharing is enabled -- **Public access**: Log UUID-based requests (without exposing UUID) -- **Performance metrics**: H3 calculation timing -- **Error tracking**: Failed calculations and API errors - -### Metrics -- **Sharing adoption**: How many users enable sharing -- **Expiration preferences**: Popular expiration durations -- **Performance**: Hexagon calculation and rendering times -- **Error rates**: Failed sharing requests - -## Troubleshooting - -### Common Issues - -#### No Hexagons Displayed -1. Check if `hexagons_available?` returns true for the stat -2. Verify `h3_hex_ids` column contains non-empty hash data -3. Confirm H3 gem is properly installed and accessible -4. Check API endpoint returns valid GeoJSON FeatureCollection -5. Verify H3 indexes are valid and can be converted to boundaries - -#### Sharing Link Not Working -1. Verify UUID exists in database -2. Check sharing_settings.enabled = true -3. Validate expiration timestamp -4. Confirm public routes are properly configured - -#### Performance Issues -1. Monitor hexagon count (should be < 10,000) -2. Check if resolution is too high for large areas -3. Verify database indexes are present -4. Consider increasing H3_MAX_HEXAGONS if needed - -### Debug Commands - -```bash -# Check sharing status for a stat -rails runner " -stat = Stat.find_by(sharing_uuid: 'UUID_HERE') -puts stat.public_accessible? -puts stat.hexagons_available? -" - -# Verify H3 hex data format and structure -rails runner " -stat = Stat.where.not(h3_hex_ids: {}).first -puts \"Data type: #{stat.h3_hex_ids.class}\" -puts \"Sample entry: #{stat.h3_hex_ids.first}\" -puts \"Total hexagons: #{stat.h3_hex_ids.size}\" -puts \"Available: #{stat.hexagons_available?}\" - -# Test H3 polygon generation -h3_index, data = stat.h3_hex_ids.first -polygon = Maps::HexagonPolygonGenerator.new(h3_index: h3_index).call -puts \"Generated polygon type: #{polygon['type']}\" -" -``` - -## Future Enhancements - -### Planned Features -- **Social sharing**: Integration with social media platforms -- **Embedding**: Iframe widgets for external sites -- **Analytics**: View count and engagement metrics -- **Custom styling**: User-configurable map themes - -### Technical Improvements -- **CDN integration**: Faster global access to shared stats -- **Compression**: Further optimize H3 hex data storage format -- **Real-time updates**: Live sharing for ongoing activities -- **API versioning**: Stable API contracts for external integration -- **Adaptive H3 resolution**: Dynamic resolution based on geographic area and zoom level -- **Polygon caching**: Cache generated polygons for frequently accessed stats - -## Conclusion - -The Shareable Stats feature provides a robust, secure, and performant way for Dawarich users to share their location insights. The H3 hexagon system offers excellent visualization while maintaining privacy through aggregated data. The UUID-based security model ensures that only intended recipients can access shared statistics, while the configurable expiration system gives users complete control over data visibility. - -The architecture is designed for scalability and performance, with pre-calculated H3 hex data reducing server load and providing fast response times for public viewers. The streamlined H3-only implementation ensures consistent polygon generation and efficient storage. The comprehensive error handling and monitoring ensure reliable operation in production environments. diff --git a/spec/factories/stats.rb b/spec/factories/stats.rb index 724ddbfa..16be6795 100644 --- a/spec/factories/stats.rb +++ b/spec/factories/stats.rb @@ -21,7 +21,7 @@ FactoryBot.define do trait :with_sharing_enabled do after(:create) do |stat, _evaluator| - stat.enable_sharing!(expiration: 'permanent') + stat.enable_sharing!(expiration: '24h') end end