- 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
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.