sojorn/sojorn_docs/features/BEACON_SYSTEM.md
Patrick Britton 56a9dd032f feat: Add enhanced video moderation with frame extraction and implement placeholder UI methods
- Add VideoProcessor service to PostHandler for frame-based video moderation
- Implement multi-frame extraction and Azure OpenAI Vision analysis for video content
- Enhance VideoStitchingService with filters, speed control, and text overlays
- Add image upload dialogs for group avatar and banner in GroupCreationModal
- Implement navigation placeholders for mentions, hashtags, and URLs in sojornRichText
2026-02-17 13:32:58 -06:00

25 KiB

Beacon System Documentation

📍 Local Safety & Social Awareness Platform

Version: 3.0
Status: COMPLETED
Last Updated: February 17, 2026


🎯 Overview

The Beacon system transforms local safety and community awareness into an engaging, positive platform that connects neighbors and promotes mutual aid rather than fear-mongering. It combines real-time mapping, categorized alerts, and actionable help items to create a safer, more connected community.

🏗️ Architecture

System Components

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Beacon API      │    │   Map Service     │    │   Notification   │
│                 │◄──►│                 │◄──►│                 │
│ • CRUD Operations│    │ • Geospatial     │    │ • Push Alerts   │
│ • Validation     │    │ • Clustering     │    │ • Email Alerts   │
│ • Scoring        │    │ • Filtering     │    │ • SMS Alerts   │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Database        │    │   External APIs   │    │   User Interface │
│                 │    │                 │    │                 │
│ • Beacon Data     │    │ • Geocoding     │    │ • Map View     │
│ • Categories     │    │ • Reverse Geocoding│    │ • Beacon Feed   │
│ • Relationships  │    │ • Weather API    │    │ • Detail View   │
│ • Analytics      │    │ • Address API    │    │ • Create/Edit  │
└─────────────────    └─────────────────    └─────────────────┘

🎨 Core Features

📍 Map-Based Discovery

  • Interactive Map: Flutter Map with clustered pin visualization
  • Real-time Updates: Live beacon updates without page refresh
  • Geospatial Search: Find beacons by location or radius
  • Neighborhood Filtering: Filter by specific neighborhoods or areas
  • Layer Control: Toggle different beacon categories on map

🏷️ Beacon Categories

  • Safety Alert: Emergency situations, public safety concerns
  • Community Need: Requests for help, volunteer opportunities
  • Lost & Found: Missing persons, pets, or items
  • Events: Community events, meetings, gatherings
  • Mutual Aid: Resource sharing, community support initiatives

Verification System

  • Official Badges: Verified badges for government and official organizations
  • Trust Indicators: Visual indicators for source reliability
  • Confidence Scoring: Community-driven trust metrics
  • Source Validation: API integration for official verification

🤝 Action-Oriented Help

  • How to Help: Specific, actionable help items for each beacon
  • Volunteer Opportunities: Sign-up forms and contact information
  • Resource Sharing: Links to needed resources or donations
  • Community Coordination: Tools for organizing community response

📊 Analytics & Insights

  • Engagement Metrics: Track vouch/report ratios and community response
  • Resolution Tracking: Monitor beacon lifecycle from active to resolved
  • Impact Assessment: Measure community impact of beacon activities
  • Trend Analysis: Identify patterns in local safety and community needs

📱 Implementation Details

Backend Services

Beacon Handler

File: go-backend/internal/handlers/beacon_handler.go

type BeaconHandler struct {
    db          *pgxpool.Pool
    geoService  *GeoService
    notifier    *NotificationService
    validator   *BeaconValidator
}

// Create new beacon
func (h *BeaconHandler) CreateBeacon(c *gin.Context) {
    var req CreateBeaconRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // Validate beacon data
    if err := h.validator.ValidateBeacon(req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // Create beacon with geocoding
    beacon, err := h.geoService.GeocodeLocation(req.Latitude, req.Longitude)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to geocode location"})
        return
    }
    
    // Save to database
    id, err := h.db.Exec(
        `INSERT INTO beacons (title, description, category, latitude, longitude, 
                           author_id, is_official, created_at) 
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
        req.Title, req.Description, req.Category, beacon.Latitude, 
        beacon.Longitude, req.AuthorID, req.IsOfficial, time.Now(),
    )
    
    c.JSON(201, gin.H{"id": id, "status": "created"})
}

// Get beacons with clustering
func (h *BeaconHandler) GetBeacons(c *gin.Context) {
    var filters BeaconFilters
    if err := c.ShouldBindQuery(&filters); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // Get beacons with clustering
    beacons, err := h.geoService.GetClusteredBeacons(filters)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, beacons)
}

Geospatial Service

File: go-backend/internal/services/geo_service.go

type GeoService struct {
    db *pgxpool.Pool
}

// Cluster nearby beacons
func (s *GeoService) GetClusteredBeacons(filters BeaconFilters) ([]BeaconCluster, error) {
    // Get all beacons within radius
    query := `
        SELECT id, title, category, latitude, longitude, author_id, is_official, created_at
        FROM beacons 
        WHERE ST_DWithin(
            ST_MakePoint(longitude, latitude, 4326),
            ST_MakePoint($1, $2, $3),
            $4
        )
        ORDER BY created_at DESC
    `
    
    rows, err := s.db.Query(context.Background(), query, filters.CenterLat, filters.CenterLng, filters.RadiusKm * 1000)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var beacons []EnhancedBeacon
    for rows.Next() {
        var beacon EnhancedBeacon
        if err := rows.Scan(&beacon.ID, &beacon.Title, &beacon.Category, 
            &beacon.Latitude, &beacon.Longitude, &beacon.AuthorID, 
            &beacon.IsOfficial, &beacon.CreatedAt); err != nil {
            return nil, err
        }
        beacons = append(beacons, beacon)
    }
    
    // Cluster beacons
    return s.clusterBeacons(beacons), nil
}

// Geocode address to coordinates
func (s *GeoService) GeocodeLocation(lat, lng float64) (*Location, error) {
    // Use reverse geocoding service
    // This would integrate with Google Geocoding API or similar
    return &Location{
        Latitude:  lat,
        Longitude: lng,
        Address:   "Reverse geocoded address",
        City:      "City name",
        Country:   "Country name",
    }, nil
}

Frontend Components

Enhanced Beacon Map Widget

File: sojorn_app/lib/widgets/beacon/enhanced_beacon_map.dart

class EnhancedBeaconMap extends ConsumerWidget {
  final List<EnhancedBeacon> beacons;
  final Function(EnhancedBeacon)? onBeaconTap;
  final Function(LatLng)? onMapTap;
  final BeaconFilter? filter;
  final bool showUserLocation;
  final bool enableClustering;
  
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return FlutterMap(
      mapController: _mapController,
      options: MapOptions(
        initialCenter: widget.initialCenter ?? _userLocation,
        initialZoom: _currentZoom,
        onMapEvent: (event) => _handleMapEvent(event),
        onTap: (tapPosition, point) => widget.onMapTap?.call(point),
      ),
      children: [
        TileLayer(
          urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
          userAgentPackageName: 'com.example.sojorn',
        ),
        MarkerLayer(
          markers: _buildMapMarkers(),
        ),
        if (widget.showUserLocation)
          MarkerLayer(
            markers: _buildUserLocationMarker(),
          ),
      ],
    );
  }
}

Beacon Detail Screen

File: sojorn_app/lib/screens/beacon/enhanced_beacon_detail_screen.dart

class EnhancedBeaconDetailScreen extends StatelessWidget {
  final EnhancedBeacon beacon;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.black,
        title: Text(beacon.title),
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: CustomScrollView(
        slivers: [
          // Map view with beacon location
          SliverAppBar(
            expandedHeight: 250,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [
                      Colors.transparent,
                      Colors.black.withOpacity(0.7),
                      Colors.black,
                    ],
                  ),
                ),
              ),
              ),
              child: FlutterMap(
                mapController: _mapController,
                options: MapOptions(
                  initialCenter: LatLng(beacon.lat, beacon.lng),
                  initialZoom: 15.0,
                ),
                children: [
                  TileLayer(
                    urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
                  ),
                  MarkerLayer(
                    markers: [
                      Marker(
                        point: LatLng(beacon.lat, beacon.lng),
                        child: _buildBeaconMarker(beacon),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            
            // Beacon content
            SliverToBoxAdapter(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // Title and metadata
                    _buildBeaconHeader(beacon),
                    
                    // Description
                    _buildBeaconDescription(beacon),
                    
                    // Image if available
                    if (beacon.imageUrl != null)
                      _buildBeaconImage(beacon.imageUrl),
                    
                    // Help actions
                    _buildHelpActions(beacon),
                    
                    // Engagement stats
                    _buildEngagementStats(beacon),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

🗂️ Data Models

Enhanced Beacon Model

class EnhancedBeacon {
  final String id;
  final String title;
  final String description;
  final BeaconCategory category;
  final BeaconStatus status;
  final double lat;
  final double lng;
  final String authorId;
  final String authorHandle;
  final String? authorAvatar;
  final bool isVerified;
  final bool isOfficialSource;
  final String? organizationName;
  final DateTime createdAt;
  final DateTime? expiresAt;
  final int vouchCount;
  final int reportCount;
  final double confidenceScore;
  final String? imageUrl;
  final List<String> actionItems;
  final String? neighborhood;
  final double? radiusMeters;
}

Beacon Cluster Model

class BeaconCluster {
  final List<EnhancedBeacon> beacons;
  final double lat;
  final double lng;
  final int count;
  
  BeaconCluster({
    required this.beacons,
    required this.lat,
    required this.lng,
  }) : count = beacons.length;
  
  BeaconCategory get dominantCategory {
    // Find most common category in cluster
    final categoryCount = <BeaconCategory, int>{};
    for (final beacon in beacons) {
      categoryCount[beacon.category] = (categoryCount[beacon.category] ?? 0) + 1;
    }
    
    return categoryCount.entries.reduce((a, b) => 
      a.value > b.value ? a : b
    ).key;
  }
  
  bool get hasOfficialSource {
    return beacons.any((b) => b.isOfficialSource);
  }
  
  EnhancedBeacon get priorityBeacon {
    // Return highest priority beacon
    final officialBeacons = beacons.where((b) => b.isOfficialSource).toList();
    if (officialBeacons.isNotEmpty) {
      return officialBeacons.reduce((a, b) => 
        a.createdAt.isAfter(b.createdAt) ? a : b
      );
    }
    
    final highConfidenceBeacons = beacons.where((b) => b.isHighConfidence).toList();
    if (highConfidenceBeacons.isNotEmpty) {
      return highConfidenceBeacons.reduce((a, b) => 
        a.createdAt.isAfter(b.createdAt) ? a : b
      );
    }
    
    return beacons.reduce((a, b) => 
      a.createdAt.isAfter(b.createdAt) ? a : b
    );
  }
}

Beacon Filter Model

class BeaconFilter {
  final Set<BeaconCategory> categories;
  final Set<BeaconStatus> statuses;
  final bool onlyOfficial;
  final double? radiusKm;
  final String? neighborhood;
  
  bool matches(EnhancedBeacon beacon) {
    // Category filter
    if (categories.isNotEmpty && !categories.contains(beacon.category)) {
      return false;
    }
    
    // Status filter
    if (statuses.isNotEmpty && !statuses.contains(beacon.status)) {
      return false;
    }
    
    // Official filter
    if (onlyOfficial && !beacon.isOfficialSource) {
      return false;
    }
    
    // Neighborhood filter
    if (neighborhood != null && beacon.neighborhood != neighborhood) {
      return false;
    }
    
    return true;
  }
}

🔧 Technical Implementation

Geospatial Database Schema

-- Beacon table with geospatial capabilities
CREATE TABLE IF NOT EXISTS beacons (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title VARCHAR(100) NOT NULL,
    description TEXT,
    category VARCHAR(50) NOT NULL CHECK (category IN ('safety_alert', 'community_need', 'lost_found', 'event', 'mutual_aid')),
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    latitude DECIMAL(10, 8) NOT NULL,
    longitude DECIMAL(10,8) NOT NULL,
    author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    is_official BOOLEAN DEFAULT FALSE,
    is_verified BOOLEAN DEFAULT FALSE,
    organization_name VARCHAR(100),
    image_url TEXT,
    neighborhood VARCHAR(50),
    radius_meters DECIMAL(8,2),
    vouch_count INTEGER DEFAULT 0,
    report_count INTEGER DEFAULT 0,
    confidence_score DECIMAL(5,2) DEFAULT 0.0,
    action_items TEXT[],
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    expires_at TIMESTAMP,
    
    -- Geospatial index for location queries
    INDEX idx_beacon_location ON beacons USING GIST (
      geography(Point(longitude, latitude), 
      Circle(radius_meters)
    );
    
    -- Category index for filtering
    INDEX idx_beacon_category ON beacons(category);
    
    -- Status index for filtering
    INDEX idx_beacon_status ON beacons(status);
    
    -- Created at index for sorting
    INDEX idx_beacon_created_at ON beacons(created_at DESC);
    
    -- Author index for user-specific queries
    INDEX idx_beacon_author_id ON beacons(author_id);
);

-- Beacon relationships
CREATE TABLE IF NOT EXISTS beacon_relationships (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    beacon_id UUID NOT NULL REFERENCES beacons(id) ON DELETE CASCADE,
    related_beacon_id UUID NOT NULL REFERENCES beacons(id) ON DELETE CASCADE,
    relationship_type VARCHAR(20) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    
    UNIQUE (beacon_id, related_beacon_id, relationship_type)
);

Clustering Algorithm

// Cluster nearby beacons based on distance and zoom level
func (s *GeoService) clusterBeacons(beacons []EnhancedBeacon) []BeaconCluster {
    if (!s.enableClustering || _currentZoom >= 15.0) {
        // Show individual beacons at high zoom
        return beacons.map((beacon) => BeaconCluster{
            beacons: []EnhancedBeacon{beacon},
            lat: beacon.lat,
            lng: beacon.lng,
        });
    }
    
    // Calculate cluster radius based on zoom level
    clusterRadius := 0.01 * (16.0 - _currentZoom)
    
    var clusters []BeaconCluster
    processedBeacons := make(map[string]bool)
    
    for _, beacon := range beacons {
        if processedBeacons[beacon.ID] {
            continue
        }
        
        var nearbyBeacons []EnhancedBeacon
        for _, otherBeacon := range beacons {
            if processedBeacons[otherBeacon.ID] {
                continue
            }
            
            distance := calculateDistance(beacon, otherBeacon)
            if distance <= clusterRadius {
                nearbyBeacons = append(nearbyBeacons, otherBeacon)
                processedBeacons[otherBeacon.ID] = true
            }
        }
        
        if len(nearbyBeacons) > 0 {
            // Calculate cluster center (average position)
            avgLat := nearbyBeacons.reduce((sum, beacon) => 
                sum.lat + beacon.lat, 0
            ) / float64(len(nearbyBeacons))
            avgLng := nearbyBeacons.reduce((sum, beacon) => 
                sum.lng + beacon.lng, 0
            ) / float64(nearbyBeacons))
            
            clusters = append(clusters, BeaconCluster{
                beacons: nearbyBeacons,
                lat: avgLat,
                lng: avgLng,
            })
            
            // Mark all beacons in this cluster as processed
            for _, beacon := range nearbyBeacons {
                processedBeacons[beacon.ID] = true
            }
        }
    }
    
    return clusters
}

func calculateDistance(beacon1, beacon2 EnhancedBeacon) float64 {
    // Haversine distance calculation
    lat1 := beacon1.lat * math.Pi / 180
    lng1 := beacon1.lng * math.Pi / 180
    lat2 := beacon2.lat * math.Pi / 180
    lng2 := beacon2.lng * math.Pi / 180
    
    dlat := lat2 - lat1
    dlng := lng2 - lng1
    
    a := math.Sin(dlat/2) * math.Sin(dlng/2)
    c := math.Cos(lat1) * math.Cos(lat2)
    
    return 6371 * 2 * math.Asin(math.Sqrt(a*a + c*c*math.Cos(dlng/2)*math.Cos(dlng/2)))
}

📱 User Interface

Map View

  • Interactive Controls: Zoom, pan, and tap interactions
  • Cluster Visualization: Visual clustering of nearby beacons
  • Filter Controls: Category, status, and official source filters
  • User Location: Current location indicator on map
  • Legend: Clear visual indicators for different beacon types

Beacon Feed

  • Card Layout: Clean, scannable card design
  • Category Badges: Visual category indicators
  • Trust Indicators: Verification and confidence scores
  • Action Buttons: Quick access to help actions
  • Preview Images: Thumbnail previews when available

Detail View

  • Full Context: Complete beacon information and description
  • Map Integration: Embedded map showing beacon location
  • Help Actions: Detailed help items with contact information
  • Engagement: Vouch, report, and share functionality
  • Author Info: Beacon creator information and verification status

Creation Flow

  • Location Selection: Map-based location selection or current location
  • Category Selection: Choose appropriate beacon category
  • Information Entry: Title, description, and details
  • Image Upload: Optional image for visual context
  • Review & Post: Preview and publish beacon

🔒 Security & Privacy

Location Privacy

  • Approximate Location: Beacon locations are approximate to protect exact addresses
  • User Control: Users control location sharing preferences
  • Data Minimization: Only necessary location data is stored
  • Retention Policy: Location data retained according to user preferences

Content Moderation

  • AI Analysis: AI-powered content analysis for safety
  • Community Flagging: User-driven content reporting system
  • Automated Filtering: Automatic detection of inappropriate content
  • Appeal Process: Fair and transparent appeal system

User Safety

  • Anonymous Reporting: Option to report anonymously
  • Block System: Block problematic users from seeing beacons
  • Reporting History: Track and manage reporting history
  • Emergency Contacts: Quick access to emergency services

📊 Analytics & Metrics

Engagement Metrics

  • Vouch/Report Ratio: Community trust indicators
  • Response Time: Average time to first response
  • Resolution Rate: Percentage of beacons marked as resolved
  • Participation Rate: Community engagement levels

Geographic Insights

  • Hotspot Analysis: Areas with high beacon activity
  • Coverage Maps: Geographic coverage visualization
  • Trend Analysis: Patterns in local needs and issues
  • Resource Distribution: Analysis of help requests and offers

Performance Metrics

  • Map Performance: Map rendering and interaction performance
  • API Response: Beacon API response times
  • Database Queries: Database query optimization metrics
  • User Experience: Page load and interaction performance

🚀 Deployment

Environment Configuration

# Geospatial database setup
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;

# Beacon service configuration
BEACON_SERVICE_URL=http://localhost:8080
BEACON_SERVICE_TIMEOUT=30s
BEACON_MAX_RADIUS_KM=50

# Notification settings
BEACON_NOTIFICATION_ENABLED=true
BEACON_PUSH_NOTIFICATIONS=true
BEACON_EMAIL_NOTIFICATIONS=true
BEACON_SMS_NOTIFICATIONS=false

Health Checks

// Beacon service health check
func (s *BeaconService) HealthCheck() HealthStatus {
    // Check database connectivity
    if err := s.db.Ping(context.Background()); err != nil {
        return HealthStatus{
            Status: "unhealthy",
            Message: "Database connection failed",
        }
    }
    
    // Check geospatial extensions
    var result string
    err := s.db.QueryRow(
        context.Background(),
        "SELECT 1 FROM postgis_version",
        &result,
    )
    if err != nil {
        return HealthStatus{
            Status: "degraded",
            Message: "PostGIS extensions not available",
        }
    }
    
    return HealthStatus{
        Status: "healthy",
        Message: "Beacon service ready",
    }
}

📚 Troubleshooting

Common Issues

Map Not Loading

// Check map initialization
if (_mapController == null) {
  _initMap();
}

// Check network connectivity
final connectivity = await Connectivity().checkConnectivity();
if (!connectivity) {
  _showError('No internet connection');
  return;
}

Geocoding Errors

// Check PostGIS extensions
_, err := db.QueryRow(
    context.Background(),
    "SELECT postgis_version",
    &result,
)
if err != nil {
    log.Error("PostGIS not available: %v", err)
    return nil, err
}

Clustering Issues

// Check clustering parameters
if (_currentZoom < 10.0 && _enableClustering) {
  // Clustering disabled at low zoom levels
  return _buildIndividualMarkers();
}

// Check cluster radius
if (clusterRadius < 0.001) {
  // Cluster radius too small
  return _buildIndividualMarkers();
}

📝 Future Enhancements

Version 3.1 (Planned)

  • Real-time Updates: WebSocket-based live beacon updates
  • Advanced Analytics: More detailed engagement metrics
  • Mobile Optimization: Improved mobile performance
  • Offline Support: Offline map caching and sync

Version 4.0 (Long-term)

  • 3D Map View: 3D visualization of beacon locations
  • AR Integration: Augmented reality beacon discovery
  • Voice Commands: Voice-controlled beacon creation
  • Machine Learning: Predictive beacon suggestions

📞 Support & Documentation

User Guides

  • Getting Started: Quick start guide for beacon creation
  • Safety Guidelines: Best practices for beacon usage
  • Community Building: Guide to effective community engagement
  • Troubleshooting: Common issues and solutions

Developer Resources

  • API Documentation: Complete API reference
  • Database Schema: Database design and relationships
  • Integration Guide: Third-party service integration
  • Code Examples: Sample code and implementations

Community Support

  • Discord: Beacon development discussion channel
  • GitHub: Issue tracking and feature requests
  • Documentation: Regular updates and improvements
  • Training: Educational resources and tutorials

📍 The Beacon system transforms local safety into an engaging, positive community platform that connects neighbors and promotes mutual aid rather than fear-mongering. With intelligent clustering, verified sources, and actionable help items, it creates a safer, more connected community.