hot reload

This commit is contained in:
2025-11-02 11:36:37 +01:00
parent 8003bc4a77
commit d8542a10e6
7 changed files with 366 additions and 231 deletions

View File

@@ -107,6 +107,13 @@ func (c *TCPClient) setDefaultParams() {
}
}
func (c *TCPClient) ReloadFilters() {
if c.LoggedIn {
Log.Info("Reloading cluster filters...")
c.SetFilters()
}
}
func (c *TCPClient) calculateBackoff() time.Duration {
// Formule: min(baseDelay * 2^attempts, maxDelay)
delay := time.Duration(float64(c.baseReconnectDelay) * math.Pow(2, float64(c.reconnectAttempts)))

View File

@@ -56,4 +56,5 @@ gotify:
NewDXCC: true
NewBand: false
NewMode: false
NewBandAndMode: false
NewBandAndMode: false
Watchlist: false

115
config.go
View File

@@ -2,9 +2,11 @@ package main
import (
"fmt"
"log"
"os"
"sync"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
@@ -81,9 +83,16 @@ type Config struct {
NewBand bool `yaml:"NewBand"`
NewMode bool `yaml:"NewMode"`
NewBandAndMode bool `yaml:"NewBandAndMode"`
WatchList bool `yaml:"Watchlist"`
} `yaml:"gotify"`
}
type ConfigWatcher struct {
watcher *fsnotify.Watcher
configPath string
mu sync.RWMutex
}
func NewConfig(configPath string) *Config {
Cfg = &Config{}
@@ -111,3 +120,107 @@ func ValidateConfigPath(path string) error {
}
return nil
}
func NewConfigWatcher(configPath string) (*ConfigWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
return &ConfigWatcher{
watcher: watcher,
configPath: configPath,
}, nil
}
func (cw *ConfigWatcher) Start() error {
if err := cw.watcher.Add(cw.configPath); err != nil {
return err
}
go func() {
for {
select {
case event, ok := <-cw.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
Log.Info("Config file modified, reloading...")
cw.reloadConfig()
}
case err, ok := <-cw.watcher.Errors:
if !ok {
return
}
Log.Errorf("Config watcher error: %v", err)
}
}
}()
return nil
}
func (cw *ConfigWatcher) reloadConfig() {
cw.mu.Lock()
defer cw.mu.Unlock()
newCfg := &Config{}
file, err := os.Open(cw.configPath)
if err != nil {
Log.Errorf("Could not reload config: %v", err)
return
}
defer file.Close()
d := yaml.NewDecoder(file)
if err := d.Decode(newCfg); err != nil {
Log.Errorf("Could not decode reloaded config: %v", err)
return
}
// Sauvegarder l'ancienne config
oldCfg := Cfg
// Appliquer la nouvelle config
Cfg = newCfg
// Vérifier les changements qui nécessitent des actions
cw.applyConfigChanges(oldCfg, newCfg)
Log.Info("✅ Config reloaded successfully")
}
func (cw *ConfigWatcher) applyConfigChanges(oldCfg, newCfg *Config) {
// Log level
if oldCfg.General.LogLevel != newCfg.General.LogLevel {
switch newCfg.General.LogLevel {
case "DEBUG":
Log.SetLevel(log.DebugLevel)
case "INFO":
Log.SetLevel(log.InfoLevel)
case "WARN":
Log.SetLevel(log.WarnLevel)
default:
Log.SetLevel(log.InfoLevel)
}
Log.Infof("Log level changed to %s", newCfg.General.LogLevel)
}
// Gotify
if oldCfg.Gotify.Enable != newCfg.Gotify.Enable {
Log.Infof("Gotify notifications %s", map[bool]string{true: "enabled", false: "disabled"}[newCfg.Gotify.Enable])
}
if oldCfg.Cluster.FT8 != newCfg.Cluster.FT8 ||
oldCfg.Cluster.FT4 != newCfg.Cluster.FT4 ||
oldCfg.Cluster.Skimmer != newCfg.Cluster.Skimmer ||
oldCfg.Cluster.Beacon != newCfg.Cluster.Beacon {
Log.Info("Cluster filters changed, applying")
httpServerInstance.TCPClient.ReloadFilters()
}
}
func (cw *ConfigWatcher) Stop() {
cw.watcher.Close()
}

1
go.mod
View File

@@ -12,6 +12,7 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect

2
go.sum
View File

@@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=

11
main.go
View File

@@ -73,10 +73,21 @@ func main() {
NewConfig(cfgPath)
configWatcher, err := NewConfigWatcher(cfgPath)
if err != nil {
log.Fatalf("Could not create config watcher: %v", err)
}
defer configWatcher.Stop()
if err := configWatcher.Start(); err != nil {
log.Fatalf("Could not start config watcher: %v", err)
}
log := NewLog()
defer CloseLog()
log.Info("Running FlexDXCluster version 2.1")
log.Infof("Callsign: %s", Cfg.General.Callsign)
log.Info("Config hot reload enabled")
DeleteDatabase("./flex.sqlite", log)

View File

@@ -1,44 +1,36 @@
[
{
"callsign": "3B8M",
"callsign": "3W9A",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:32.6851135+02:00",
"addedAt": "2025-10-23T20:42:24.5787678+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "PJ6Y",
"lastSeen": "2025-10-28T14:01:51.7023253+01:00",
"lastSeenStr": "4 days ago",
"addedAt": "2025-10-18T17:17:47.7237081+02:00",
"spotCount": 1333,
"callsign": "9U1RU",
"lastSeen": "2025-11-02T11:35:54.7934565+01:00",
"lastSeenStr": "Just now",
"addedAt": "2025-10-28T22:43:38.4903514+01:00",
"spotCount": 540,
"playSound": true
},
{
"callsign": "DP0GVN",
"lastSeen": "2025-11-01T10:58:18.732114+01:00",
"lastSeenStr": "12 hours ago",
"addedAt": "2025-10-20T07:00:51.7088369+02:00",
"spotCount": 234,
"playSound": true
"callsign": "C5R",
"lastSeen": "2025-11-02T11:25:50.8967823+01:00",
"lastSeenStr": "9 minutes ago",
"addedAt": "2025-10-18T17:18:04.5006892+02:00",
"spotCount": 1851,
"playSound": false
},
{
"callsign": "SU0ERA",
"callsign": "C8K",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:45.8848244+02:00",
"addedAt": "2025-10-18T17:18:39.8627992+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "XT2AW",
"lastSeen": "2025-10-24T04:08:09.2640864+02:00",
"lastSeenStr": "8 days ago",
"addedAt": "2025-10-18T17:17:27.3839089+02:00",
"spotCount": 136,
"playSound": true
},
{
"callsign": "5R8",
"lastSeen": "0001-01-01T00:00:00Z",
@@ -48,139 +40,19 @@
"playSound": true
},
{
"callsign": "FW5K",
"lastSeen": "2025-10-31T06:58:37.7867666+01:00",
"lastSeenStr": "1 day ago",
"addedAt": "2025-10-18T17:17:37.9061157+02:00",
"spotCount": 339,
"playSound": true
},
{
"callsign": "TZ4AM",
"lastSeen": "2025-11-01T22:50:24.2026748+01:00",
"lastSeenStr": "1 hour ago",
"addedAt": "2025-10-18T17:19:00.3154177+02:00",
"spotCount": 130,
"playSound": true
},
{
"callsign": "V85NPV",
"lastSeen": "2025-11-01T15:55:54.2255972+01:00",
"lastSeenStr": "7 hours ago",
"addedAt": "2025-10-18T17:18:15.8781583+02:00",
"spotCount": 32,
"playSound": true
},
{
"callsign": "9U1RU",
"lastSeen": "2025-11-02T08:24:11.0845498+01:00",
"lastSeenStr": "Just now",
"addedAt": "2025-10-28T22:43:38.4903514+01:00",
"spotCount": 459,
"playSound": true
},
{
"callsign": "VP8LP",
"lastSeen": "2025-10-30T12:01:48.3242696+01:00",
"lastSeenStr": "2 days ago",
"addedAt": "2025-10-18T17:18:49.0576187+02:00",
"spotCount": 46,
"playSound": true
},
{
"callsign": "7Q1A",
"callsign": "5X2I",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-24T07:36:06.609998+02:00",
"addedAt": "2025-10-18T17:17:14.6598633+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "D2A",
"lastSeen": "2025-10-24T10:08:29.9662677+02:00",
"lastSeenStr": "8 days ago",
"addedAt": "2025-10-20T22:11:35.4767205+02:00",
"spotCount": 536,
"playSound": true
},
{
"callsign": "9L9L",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:53.3401773+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "3W9A",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-23T20:42:24.5787678+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "ZC4RH",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-28T22:43:16.3202825+01:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "4X6TT",
"lastSeen": "2025-10-31T06:35:40.581257+01:00",
"lastSeenStr": "1 day ago",
"addedAt": "2025-10-18T17:18:13.335878+02:00",
"spotCount": 11,
"playSound": true
},
{
"callsign": "XF4B",
"lastSeen": "2025-10-29T20:42:02.8584079+01:00",
"lastSeenStr": "3 days ago",
"addedAt": "2025-10-27T13:11:16.3404549+01:00",
"spotCount": 12,
"playSound": true
},
{
"callsign": "5K0UA",
"lastSeen": "2025-11-02T08:27:01.5669215+01:00",
"lastSeenStr": "Just now",
"addedAt": "2025-10-18T17:17:53.7390559+02:00",
"spotCount": 2670,
"playSound": true
},
{
"callsign": "C5R",
"lastSeen": "2025-11-02T08:27:33.178474+01:00",
"lastSeenStr": "Just now",
"addedAt": "2025-10-18T17:18:04.5006892+02:00",
"spotCount": 1826,
"playSound": false
},
{
"callsign": "YJ0CA",
"lastSeen": "2025-10-23T09:14:00.5419174+02:00",
"lastSeenStr": "9 days ago",
"addedAt": "2025-10-18T17:17:33.3921665+02:00",
"spotCount": 1,
"playSound": true
},
{
"callsign": "H44MS",
"lastSeen": "2025-11-01T08:09:36.8196241+01:00",
"lastSeenStr": "15 hours ago",
"addedAt": "2025-10-18T17:16:49.1572859+02:00",
"spotCount": 33,
"playSound": true
},
{
"callsign": "9L8MD",
"lastSeen": "2025-11-02T08:27:41.3699778+01:00",
"lastSeen": "2025-11-02T11:35:38.2605474+01:00",
"lastSeenStr": "Just now",
"addedAt": "2025-10-18T17:18:56.7896868+02:00",
"spotCount": 1205,
"spotCount": 1307,
"playSound": true
},
{
@@ -192,61 +64,37 @@
"playSound": true
},
{
"callsign": "VP2M",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:17:57.308717+02:00",
"spotCount": 0,
"playSound": false
"callsign": "4X6TT",
"lastSeen": "2025-10-31T06:35:40.581257+01:00",
"lastSeenStr": "2 days ago",
"addedAt": "2025-10-18T17:18:13.335878+02:00",
"spotCount": 11,
"playSound": true
},
{
"callsign": "5H3MB",
"lastSeen": "2025-11-01T20:12:44.5661106+01:00",
"lastSeenStr": "3 hours ago",
"addedAt": "2025-10-18T17:18:42.8402097+02:00",
"spotCount": 56,
"callsign": "EL2BG",
"lastSeen": "2025-11-02T11:27:57.362105+01:00",
"lastSeenStr": "7 minutes ago",
"addedAt": "2025-10-18T17:18:10.2000017+02:00",
"spotCount": 94,
"playSound": true
},
{
"callsign": "H44MS",
"lastSeen": "2025-11-01T08:09:36.8196241+01:00",
"lastSeenStr": "1 day ago",
"addedAt": "2025-10-18T17:16:49.1572859+02:00",
"spotCount": 33,
"playSound": true
},
{
"callsign": "TJ1GD",
"lastSeen": "2025-11-01T15:21:39.1331739+01:00",
"lastSeenStr": "8 hours ago",
"lastSeenStr": "20 hours ago",
"addedAt": "2025-10-18T17:18:27.6004027+02:00",
"spotCount": 338,
"playSound": false
},
{
"callsign": "C5Y",
"lastSeen": "2025-10-30T17:00:04.8264529+01:00",
"lastSeenStr": "2 days ago",
"addedAt": "2025-10-27T19:34:57.6714115+01:00",
"spotCount": 175,
"playSound": true
},
{
"callsign": "YI1MB",
"lastSeen": "2025-11-01T11:07:08.2392713+01:00",
"lastSeenStr": "12 hours ago",
"addedAt": "2025-10-18T17:18:18.825584+02:00",
"spotCount": 1,
"playSound": true
},
{
"callsign": "ZL7IO",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:17:30.7153757+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "XV9",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:24.9155327+02:00",
"spotCount": 0,
"playSound": false
},
{
"callsign": "5X1XA",
"lastSeen": "0001-01-01T00:00:00Z",
@@ -255,30 +103,6 @@
"spotCount": 0,
"playSound": true
},
{
"callsign": "EL2BG",
"lastSeen": "2025-11-02T08:11:59.0624133+01:00",
"lastSeenStr": "Just now",
"addedAt": "2025-10-18T17:18:10.2000017+02:00",
"spotCount": 87,
"playSound": true
},
{
"callsign": "A52AA",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-28T22:44:18.1202597+01:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "PY0FB",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:17:24.3843986+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "5J0EA",
"lastSeen": "0001-01-01T00:00:00Z",
@@ -287,36 +111,164 @@
"spotCount": 0,
"playSound": true
},
{
"callsign": "5H3MB",
"lastSeen": "2025-11-01T20:12:44.5661106+01:00",
"lastSeenStr": "15 hours ago",
"addedAt": "2025-10-18T17:18:42.8402097+02:00",
"spotCount": 56,
"playSound": true
},
{
"callsign": "9L9L",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:53.3401773+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "ZC4RH",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-28T22:43:16.3202825+01:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "A52AA",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-28T22:44:18.1202597+01:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "3B8M",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:32.6851135+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "DP0GVN",
"lastSeen": "2025-11-01T10:58:18.732114+01:00",
"lastSeenStr": "1 day ago",
"addedAt": "2025-10-20T07:00:51.7088369+02:00",
"spotCount": 234,
"playSound": true
},
{
"callsign": "TZ4AM",
"lastSeen": "2025-11-01T22:50:24.2026748+01:00",
"lastSeenStr": "12 hours ago",
"addedAt": "2025-10-18T17:19:00.3154177+02:00",
"spotCount": 130,
"playSound": true
},
{
"callsign": "XV9",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:24.9155327+02:00",
"spotCount": 0,
"playSound": false
},
{
"callsign": "7Q1A",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-24T07:36:06.609998+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "ZL7IO",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:17:30.7153757+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "PJ6Y",
"lastSeen": "2025-10-28T14:01:51.7023253+01:00",
"lastSeenStr": "4 days ago",
"addedAt": "2025-10-18T17:17:47.7237081+02:00",
"spotCount": 1333,
"playSound": true
},
{
"callsign": "XF4B",
"lastSeen": "2025-10-29T20:42:02.8584079+01:00",
"lastSeenStr": "3 days ago",
"addedAt": "2025-10-27T13:11:16.3404549+01:00",
"spotCount": 12,
"playSound": true
},
{
"callsign": "YJ0CA",
"lastSeen": "2025-10-23T09:14:00.5419174+02:00",
"lastSeenStr": "10 days ago",
"addedAt": "2025-10-18T17:17:33.3921665+02:00",
"spotCount": 1,
"playSound": true
},
{
"callsign": "SU0ERA",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:45.8848244+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "CP7DX",
"lastSeen": "2025-11-02T05:20:53.7928254+01:00",
"lastSeenStr": "Just now",
"lastSeenStr": "6 hours ago",
"addedAt": "2025-10-28T22:42:54.1867739+01:00",
"spotCount": 64,
"playSound": true
},
{
"callsign": "5X2I",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:17:14.6598633+02:00",
"spotCount": 0,
"callsign": "VP8LP",
"lastSeen": "2025-10-30T12:01:48.3242696+01:00",
"lastSeenStr": "2 days ago",
"addedAt": "2025-10-18T17:18:49.0576187+02:00",
"spotCount": 46,
"playSound": true
},
{
"callsign": "C8K",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:18:39.8627992+02:00",
"spotCount": 0,
"callsign": "FW5K",
"lastSeen": "2025-10-31T06:58:37.7867666+01:00",
"lastSeenStr": "2 days ago",
"addedAt": "2025-10-18T17:17:37.9061157+02:00",
"spotCount": 339,
"playSound": true
},
{
"callsign": "C5Y",
"lastSeen": "2025-10-30T17:00:04.8264529+01:00",
"lastSeenStr": "2 days ago",
"addedAt": "2025-10-27T19:34:57.6714115+01:00",
"spotCount": 175,
"playSound": true
},
{
"callsign": "YI1MB",
"lastSeen": "2025-11-01T11:07:08.2392713+01:00",
"lastSeenStr": "1 day ago",
"addedAt": "2025-10-18T17:18:18.825584+02:00",
"spotCount": 1,
"playSound": true
},
{
"callsign": "6O3T",
"lastSeen": "2025-11-02T08:24:51.6037533+01:00",
"lastSeenStr": "Just now",
"lastSeen": "2025-11-02T11:29:00.7935157+01:00",
"lastSeenStr": "6 minutes ago",
"addedAt": "2025-10-22T19:31:13.1154881+02:00",
"spotCount": 2188,
"spotCount": 2278,
"playSound": true
},
{
@@ -326,5 +278,53 @@
"addedAt": "2025-10-18T17:17:43.6895454+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "XT2AW",
"lastSeen": "2025-10-24T04:08:09.2640864+02:00",
"lastSeenStr": "9 days ago",
"addedAt": "2025-10-18T17:17:27.3839089+02:00",
"spotCount": 136,
"playSound": true
},
{
"callsign": "D2A",
"lastSeen": "2025-10-24T10:08:29.9662677+02:00",
"lastSeenStr": "9 days ago",
"addedAt": "2025-10-20T22:11:35.4767205+02:00",
"spotCount": 536,
"playSound": true
},
{
"callsign": "5K0UA",
"lastSeen": "2025-11-02T10:36:27.8311341+01:00",
"lastSeenStr": "59 minutes ago",
"addedAt": "2025-10-18T17:17:53.7390559+02:00",
"spotCount": 2691,
"playSound": true
},
{
"callsign": "VP2M",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:17:57.308717+02:00",
"spotCount": 0,
"playSound": false
},
{
"callsign": "PY0FB",
"lastSeen": "0001-01-01T00:00:00Z",
"lastSeenStr": "Never",
"addedAt": "2025-10-18T17:17:24.3843986+02:00",
"spotCount": 0,
"playSound": true
},
{
"callsign": "V85NPV",
"lastSeen": "2025-11-02T09:04:29.9410671+01:00",
"lastSeenStr": "2 hours ago",
"addedAt": "2025-10-18T17:18:15.8781583+02:00",
"spotCount": 33,
"playSound": true
}
]