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

818 lines
25 KiB
Markdown

# 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`
```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`
```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`
```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`
```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
```dart
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
```dart
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
```dart
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
```sql
-- 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
```go
// 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
```bash
# 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
```go
// 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
```dart
// Check map initialization
if (_mapController == null) {
_initMap();
}
// Check network connectivity
final connectivity = await Connectivity().checkConnectivity();
if (!connectivity) {
_showError('No internet connection');
return;
}
```
#### Geocoding Errors
```go
// 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
```dart
// 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.**