From 2bd0390d1ad9b3354decedec0d1c49fa6e4bcea5 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 19 Sep 2025 00:23:12 +0200 Subject: [PATCH] Rename hexagon_centers to h3_hex_ids and update related logic --- app/models/stat.rb | 6 +- app/services/maps/hexagon_center_manager.rb | 68 +-- .../maps/hexagon_polygon_generator.rb | 5 +- app/services/stats/calculate_month.rb | 24 +- ...0250913194134_add_hexagon_data_to_stats.rb | 7 - ...0914094851_add_hexagon_centers_to_stats.rb | 5 - ...0914095157_add_index_to_hexagon_centers.rb | 7 - .../20250918215512_add_h3_hex_ids_to_stats.rb | 8 + db/schema.rb | 5 +- docs/SHAREABLE_STATS_FEATURE.md | 487 ++++++++++++++++++ 10 files changed, 541 insertions(+), 81 deletions(-) delete mode 100644 db/migrate/20250913194134_add_hexagon_data_to_stats.rb delete mode 100644 db/migrate/20250914094851_add_hexagon_centers_to_stats.rb delete mode 100644 db/migrate/20250914095157_add_index_to_hexagon_centers.rb create mode 100644 db/migrate/20250918215512_add_h3_hex_ids_to_stats.rb create mode 100644 docs/SHAREABLE_STATS_FEATURE.md diff --git a/app/models/stat.rb b/app/models/stat.rb index 38babb8a..9d25da89 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -57,9 +57,9 @@ class Stat < ApplicationRecord end def hexagons_available? - hexagon_centers.present? && - hexagon_centers.is_a?(Array) && - hexagon_centers.any? + h3_hex_ids.present? && + h3_hex_ids.is_a?(Hash) && + h3_hex_ids.any? end def generate_new_sharing_uuid! diff --git a/app/services/maps/hexagon_center_manager.rb b/app/services/maps/hexagon_center_manager.rb index 9c3d83be..fd699be8 100644 --- a/app/services/maps/hexagon_center_manager.rb +++ b/app/services/maps/hexagon_center_manager.rb @@ -9,7 +9,6 @@ module Maps def call return build_response_from_centers if pre_calculated_centers_available? - return handle_legacy_area_too_large if legacy_area_too_large? nil # No pre-calculated data available end @@ -19,78 +18,59 @@ module Maps attr_reader :stat, :user def pre_calculated_centers_available? - return false if stat&.hexagon_centers.blank? + return false if stat&.h3_hex_ids.blank? - # Handle legacy hash format - if stat.hexagon_centers.is_a?(Hash) - !stat.hexagon_centers['area_too_large'] - else - # Handle array format (actual hexagon centers) - stat.hexagon_centers.is_a?(Array) && stat.hexagon_centers.any? - end - end - - def legacy_area_too_large? - stat&.hexagon_centers.is_a?(Hash) && stat.hexagon_centers['area_too_large'] + stat.h3_hex_ids.is_a?(Hash) && stat.h3_hex_ids.any? end def build_response_from_centers - centers = stat.hexagon_centers - Rails.logger.debug "Using pre-calculated hexagon centers: #{centers.size} centers" + hex_ids = stat.h3_hex_ids + Rails.logger.debug "Using pre-calculated H3 hex IDs: #{hex_ids.size} hexagons" - result = build_hexagons_from_centers(centers) + result = build_hexagons_from_h3_ids(hex_ids) { success: true, data: result, pre_calculated: true } end - def handle_legacy_area_too_large - Rails.logger.info "Recalculating previously skipped large area hexagons for stat #{stat.id}" - - new_centers = recalculate_hexagon_centers - return nil unless new_centers.is_a?(Array) - - update_stat_with_new_centers(new_centers) - end - - def recalculate_hexagon_centers + def recalculate_h3_hex_ids service = Stats::CalculateMonth.new(user.id, stat.year, stat.month) - service.send(:calculate_hexagon_centers) + service.send(:calculate_h3_hex_ids) end - def update_stat_with_new_centers(new_centers) - stat.update(hexagon_centers: new_centers) - result = build_hexagons_from_centers(new_centers) - Rails.logger.debug "Successfully recalculated hexagon centers: #{new_centers.size} centers" + def update_stat_with_new_hex_ids(new_hex_ids) + stat.update(h3_hex_ids: new_hex_ids) + result = build_hexagons_from_h3_ids(new_hex_ids) + Rails.logger.debug "Successfully recalculated H3 hex IDs: #{new_hex_ids.size} hexagons" { success: true, data: result, pre_calculated: true } end - def build_hexagons_from_centers(centers) - # Convert stored centers back to hexagon polygons - hexagon_features = centers.map.with_index { |center, index| build_hexagon_feature(center, index) } + def build_hexagons_from_h3_ids(hex_ids) + # Convert stored H3 IDs back to hexagon polygons + hexagon_features = hex_ids.map.with_index do |(h3_index, data), index| + build_hexagon_feature_from_h3(h3_index, data, index) + end build_feature_collection(hexagon_features) end - def build_hexagon_feature(center, index) - lng, lat, earliest, latest = center + def build_hexagon_feature_from_h3(h3_index, data, index) + count, earliest, latest = data { 'type' => 'Feature', 'id' => index + 1, - 'geometry' => generate_hexagon_geometry(lng, lat), - 'properties' => build_hexagon_properties(index, earliest, latest) + 'geometry' => generate_hexagon_geometry_from_h3(h3_index), + 'properties' => build_hexagon_properties(index, count, earliest, latest) } end - def generate_hexagon_geometry(lng, lat) - Maps::HexagonPolygonGenerator.new( - center_lng: lng, - center_lat: lat - ).call + def generate_hexagon_geometry_from_h3(h3_index) + Maps::HexagonPolygonGenerator.new(h3_index: h3_index).call end - def build_hexagon_properties(index, earliest, latest) + def build_hexagon_properties(index, count, earliest, latest) { 'hex_id' => index + 1, + 'point_count' => count, 'earliest_point' => earliest ? Time.zone.at(earliest).iso8601 : nil, 'latest_point' => latest ? Time.zone.at(latest).iso8601 : nil } diff --git a/app/services/maps/hexagon_polygon_generator.rb b/app/services/maps/hexagon_polygon_generator.rb index b6700aab..29c7efff 100644 --- a/app/services/maps/hexagon_polygon_generator.rb +++ b/app/services/maps/hexagon_polygon_generator.rb @@ -2,10 +2,11 @@ module Maps class HexagonPolygonGenerator - def initialize(center_lng:, center_lat:, h3_resolution: 5) + def initialize(center_lng: nil, center_lat: nil, h3_resolution: 5, h3_index: nil) @center_lng = center_lng @center_lat = center_lat @h3_resolution = h3_resolution + @h3_index = h3_index end def call @@ -14,7 +15,7 @@ module Maps private - attr_reader :center_lng, :center_lat, :h3_resolution + attr_reader :center_lng, :center_lat, :h3_resolution, :h3_index def generate_h3_hexagon_polygon # Convert coordinates to H3 format [lat, lng] diff --git a/app/services/stats/calculate_month.rb b/app/services/stats/calculate_month.rb index 28dd0a39..bd66d4be 100644 --- a/app/services/stats/calculate_month.rb +++ b/app/services/stats/calculate_month.rb @@ -86,7 +86,7 @@ class Stats::CalculateMonth daily_distance: distance_by_day, distance: distance(distance_by_day), toponyms: toponyms, - hexagon_centers: calculate_hexagon_centers + h3_hex_ids: calculate_h3_hex_ids ) stat.save end @@ -132,22 +132,28 @@ class Stats::CalculateMonth Stat.where(year:, month:, user:).destroy_all end - def calculate_hexagon_centers - return nil if points.empty? + def calculate_h3_hex_ids + return {} if points.empty? begin result = calculate_h3_hexagon_centers if result.empty? - Rails.logger.info "No H3 hexagon centers calculated for user #{user.id}, #{year}-#{month} (no data)" - return nil + Rails.logger.info "No H3 hex IDs calculated for user #{user.id}, #{year}-#{month} (no data)" + return {} end - Rails.logger.info "Pre-calculated #{result.size} H3 hexagon centers for user #{user.id}, #{year}-#{month}" - result + # Convert array format to hash format: { h3_index => [count, earliest, latest] } + hex_hash = result.each_with_object({}) do |hex_data, hash| + h3_index, count, earliest, latest = hex_data + hash[h3_index] = [count, earliest, latest] + end + + Rails.logger.info "Pre-calculated #{hex_hash.size} H3 hex IDs for user #{user.id}, #{year}-#{month}" + hex_hash rescue PostGISError => e - Rails.logger.warn "H3 hexagon centers calculation failed for user #{user.id}, #{year}-#{month}: #{e.message}" - nil + Rails.logger.warn "H3 hex IDs calculation failed for user #{user.id}, #{year}-#{month}: #{e.message}" + {} end end diff --git a/db/migrate/20250913194134_add_hexagon_data_to_stats.rb b/db/migrate/20250913194134_add_hexagon_data_to_stats.rb deleted file mode 100644 index f5c1b97a..00000000 --- a/db/migrate/20250913194134_add_hexagon_data_to_stats.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class AddHexagonDataToStats < ActiveRecord::Migration[8.0] - def change - add_column :stats, :hexagon_data, :jsonb - end -end diff --git a/db/migrate/20250914094851_add_hexagon_centers_to_stats.rb b/db/migrate/20250914094851_add_hexagon_centers_to_stats.rb deleted file mode 100644 index 9dbc5232..00000000 --- a/db/migrate/20250914094851_add_hexagon_centers_to_stats.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddHexagonCentersToStats < ActiveRecord::Migration[8.0] - def change - add_column :stats, :hexagon_centers, :jsonb - end -end diff --git a/db/migrate/20250914095157_add_index_to_hexagon_centers.rb b/db/migrate/20250914095157_add_index_to_hexagon_centers.rb deleted file mode 100644 index 9e301543..00000000 --- a/db/migrate/20250914095157_add_index_to_hexagon_centers.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AddIndexToHexagonCenters < ActiveRecord::Migration[8.0] - disable_ddl_transaction! - - def change - add_index :stats, :hexagon_centers, using: :gin, where: "hexagon_centers IS NOT NULL", algorithm: :concurrently - end -end diff --git a/db/migrate/20250918215512_add_h3_hex_ids_to_stats.rb b/db/migrate/20250918215512_add_h3_hex_ids_to_stats.rb new file mode 100644 index 00000000..0ab8a90c --- /dev/null +++ b/db/migrate/20250918215512_add_h3_hex_ids_to_stats.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddH3HexIdsToStats < ActiveRecord::Migration[8.0] + def change + add_column :stats, :h3_hex_ids, :jsonb, default: {} + add_index :stats, :h3_hex_ids, using: :gin, where: "(h3_hex_ids IS NOT NULL AND h3_hex_ids != '{}'::jsonb)" + end +end diff --git a/db/schema.rb b/db/schema.rb index 071c1860..cfcab1ea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_14_095157) do +ActiveRecord::Schema[8.0].define(version: 2025_09_10_224714) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "postgis" @@ -222,10 +222,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_14_095157) do t.jsonb "daily_distance", default: {} t.jsonb "sharing_settings", default: {} t.uuid "sharing_uuid" - t.jsonb "hexagon_data" - t.jsonb "hexagon_centers" t.index ["distance"], name: "index_stats_on_distance" - t.index ["hexagon_centers"], name: "index_stats_on_hexagon_centers", where: "(hexagon_centers IS NOT NULL)", using: :gin t.index ["month"], name: "index_stats_on_month" t.index ["sharing_uuid"], name: "index_stats_on_sharing_uuid", unique: true t.index ["user_id"], name: "index_stats_on_user_id" diff --git a/docs/SHAREABLE_STATS_FEATURE.md b/docs/SHAREABLE_STATS_FEATURE.md new file mode 100644 index 00000000..56ddfe19 --- /dev/null +++ b/docs/SHAREABLE_STATS_FEATURE.md @@ -0,0 +1,487 @@ +# 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 +- Supports both center-based and H3-index-based generation +- Direct H3 index to polygon conversion for efficiency + +**Usage Modes:** +- **Center-based**: `new(center_lng: lng, center_lat: lat)` + +## 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 Hexagon Centers] + C --> D[Store in hexagon_centers 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 + +### 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 Centers] + H --> I[Convert to GeoJSON Polygons] + I --> J[Return FeatureCollection] +``` + +**Data Transformation:** +1. Retrieve stored H3 hex IDs hash from database +2. Convert each H3 index to hexagon boundary coordinates +3. Build GeoJSON Feature with properties (point count, timestamps) +4. 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**: Hexagons calculated during stats job +- **Storage efficiency**: H3 indexes are compact +- **Query optimization**: GIN index on hexagon_centers 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 +SELECT h3_hex_ids FROM stats +WHERE sharing_uuid = ? AND sharing_settings->>'enabled' = 'true'; + +-- Index for performance +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); +``` + +## 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 +2. Verify `h3_hex_ids` column has data +3. Confirm H3 library is properly installed +4. Check API endpoint returns valid GeoJSON + +#### 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 +rails runner " +stat = Stat.first +puts stat.h3_hex_ids.class +puts stat.h3_hex_ids.first +" +``` + +## 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 +- **Real-time updates**: Live sharing for ongoing activities +- **API versioning**: Stable API contracts for external integration +- **H3 resolution optimization**: Dynamic resolution based on geographic area + +## 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 data reducing server load and providing fast response times for public viewers. The comprehensive error handling and monitoring ensure reliable operation in production environments.