Files
openfsd/fsd/postoffice.go
Reese Norris 335409c4b4 Add ConfigRepository and enhance server configuration management
1. Database Enhancements (db/repositories.go):
   - Added ConfigRepository interface and implementations for PostgreSQL and SQLite
   - Updated Repositories struct to include ConfigRepository
   - Modified NewRepositories to initialize both UserRepo and ConfigRepo

2. FSD Server Improvements:
   - Removed hardcoded jwtSecret, now retrieved from ConfigRepository (fsd/conn.go, web/auth.go)
   - Added dynamic welcome message retrieval from ConfigRepository (fsd/conn.go)
   - Optimized METAR buffer size from 4096 to 512 bytes (fsd/metar.go)
   - Reduced minimum fields for DeleteATC and DeletePilot packets (fsd/packet.go)
   - Improved Haversine distance calculation with constants (fsd/postoffice.go)
   - Added thread-safety documentation for sendError (fsd/client.go)

3. Server Configuration (fsd/server.go):
   - Added NewDefaultServer to initialize server with environment-based config
   - Implemented automatic database migration and default admin user creation
   - Added configurable METAR worker count
   - Improved logging with slog and environment-based debug level

4. Web Interface Enhancements:
   - Added user and config editor frontend routes (web/frontend.go, web/routes.go)
   - Improved JWT handling by retrieving secret from ConfigRepository (web/auth.go)
   - Enhanced user management API endpoints (web/user.go)
   - Updated dashboard to display CID and conditional admin links (web/templates/dashboard.html)
   - Embedded templates using go:embed (web/templates.go)

5. Frontend JavaScript Improvements:
   - Added networkRatingFromInt helper for readable ratings (web/static/js/openfsd/dashboard.js)
   - Improved API request handling with auth/no-auth variants (web/static/js/openfsd/api.js)

6. Miscellaneous:
   - Added sethvargo/go-envconfig dependency for environment variable parsing
   - Fixed parseVisRange to use 64-bit float parsing (fsd/util.go)
   - Added strPtr utility function (fsd/util.go, web/main.go)
   - Improved SVG logo rendering in layout (web/templates/layout.html)
2025-05-16 22:27:26 -07:00

189 lines
4.9 KiB
Go

package fsd
import (
"errors"
"github.com/tidwall/rtree"
"math"
"sync"
)
type postOffice struct {
clientMap map[string]*Client // Callsign -> *Client
clientMapLock *sync.RWMutex
tree *rtree.RTreeG[*Client] // Geospatial rtree
treeLock *sync.RWMutex
}
func newPostOffice() *postOffice {
return &postOffice{
clientMap: make(map[string]*Client, 128),
clientMapLock: &sync.RWMutex{},
tree: &rtree.RTreeG[*Client]{},
treeLock: &sync.RWMutex{},
}
}
var ErrCallsignInUse = errors.New("callsign in use")
var ErrCallsignDoesNotExist = errors.New("callsign does not exist")
// register adds a new Client to the post office. Returns ErrCallsignInUse when the callsign is taken.
func (p *postOffice) register(client *Client) (err error) {
p.clientMapLock.Lock()
if _, exists := p.clientMap[client.callsign]; exists {
p.clientMapLock.Unlock()
err = ErrCallsignInUse
return
}
p.clientMap[client.callsign] = client
p.clientMapLock.Unlock()
// Insert into R-tree
clientMin, clientMax := calculateBoundingBox(client.latLon, client.visRange)
p.treeLock.Lock()
p.tree.Insert(clientMin, clientMax, client)
p.treeLock.Unlock()
return
}
// release removes a Client from the post office.
func (p *postOffice) release(client *Client) {
clientMin, clientMax := calculateBoundingBox(client.latLon, client.visRange)
p.treeLock.Lock()
p.tree.Delete(clientMin, clientMax, client)
p.treeLock.Unlock()
p.clientMapLock.Lock()
delete(p.clientMap, client.callsign)
p.clientMapLock.Unlock()
return
}
// updatePosition updates the geospatial position of a Client.
// The referenced client's latLon and visRange are rewritten.
func (p *postOffice) updatePosition(client *Client, newCenter [2]float64, newVisRange float64) {
oldMin, oldMax := calculateBoundingBox(client.latLon, client.visRange)
newMin, newMax := calculateBoundingBox(newCenter, newVisRange)
client.latLon = newCenter
client.visRange = newVisRange
// Avoid redundant updates
if oldMin == newMin && oldMax == newMax {
return
}
p.treeLock.Lock()
p.tree.Delete(oldMin, oldMax, client)
p.tree.Insert(newMin, newMax, client)
p.treeLock.Unlock()
return
}
// search calls `callback` for every other Client within geographical range of the provided Client
func (p *postOffice) search(client *Client, callback func(recipient *Client) bool) {
clientMin, clientMax := calculateBoundingBox(client.latLon, client.visRange)
p.treeLock.RLock()
p.tree.Search(clientMin, clientMax, func(foundMin [2]float64, foundMax [2]float64, foundClient *Client) bool {
if foundClient == client {
return true // Ignore self
}
return callback(foundClient)
})
p.treeLock.RUnlock()
}
// send sends a packet to a client with a given callsign.
//
// Returns ErrCallsignDoesNotExist if the callsign does not exist.
func (p *postOffice) send(callsign string, packet string) (err error) {
p.clientMapLock.RLock()
client, exists := p.clientMap[callsign]
p.clientMapLock.RUnlock()
if !exists {
err = ErrCallsignDoesNotExist
return
}
return client.send(packet)
}
// find finds a Client with a given callsign.
//
// Returns ErrCallsignDoesNotExist if the callsign does not exist.
func (p *postOffice) find(callsign string) (client *Client, err error) {
p.clientMapLock.RLock()
client, exists := p.clientMap[callsign]
p.clientMapLock.RUnlock()
if !exists {
err = ErrCallsignDoesNotExist
}
return
}
// all calls `callback` for every single client registered to the post office.
func (p *postOffice) all(client *Client, callback func(recipient *Client) bool) {
p.clientMapLock.RLock()
for _, recipient := range p.clientMap {
if recipient == client {
continue
}
if !callback(recipient) {
break
}
}
p.clientMapLock.RUnlock()
}
const (
earthRadius = 6371000.0 // meters, approximate mean radius of Earth
degToRad = math.Pi / 180
)
func calculateBoundingBox(center [2]float64, radius float64) (min [2]float64, max [2]float64) {
latRad := center[0] * degToRad
const metersPerDegreeLat = (math.Pi * earthRadius) / 180
deltaLat := radius / metersPerDegreeLat
metersPerDegreeLon := metersPerDegreeLat * math.Cos(latRad)
deltaLon := radius / metersPerDegreeLon
minLat := center[0] - deltaLat
maxLat := center[0] + deltaLat
minLon := center[1] - deltaLon
maxLon := center[1] + deltaLon
min = [2]float64{minLat, minLon}
max = [2]float64{maxLat, maxLon}
return min, max
}
// distance calculates the great-circle distance between two points using the Haversine formula.
func distance(lat1, lon1, lat2, lon2 float64) float64 {
dLat := (lat2 - lat1) * degToRad
dLon := (lon2 - lon1) * degToRad
sinDLat2 := math.Sin(dLat * 0.5)
sinDLon2 := math.Sin(dLon * 0.5)
cosLat1 := math.Cos(lat1 * degToRad)
cosLat2 := math.Cos(lat2 * degToRad)
a := sinDLat2*sinDLat2 + cosLat1*cosLat2*sinDLon2*sinDLon2
sqrtA := math.Sqrt(a)
sqrt1MinusA := math.Sqrt(1 - a)
c := 2 * math.Atan2(sqrtA, sqrt1MinusA)
return earthRadius * c
}