diff --git a/.github/workflows/tracky-publish.yml b/.github/workflows/tracky-publish.yml new file mode 100644 index 00000000..8b956f5a --- /dev/null +++ b/.github/workflows/tracky-publish.yml @@ -0,0 +1,58 @@ +name: Build and Push Tracky Image + +on: + push: + branches: ["**"] + paths: + - "tracky/**" + workflow_dispatch: + +jobs: + detect: + runs-on: ubuntu-latest + outputs: + images: ${{ steps.filter.outputs.changes }} + steps: + - uses: actions/checkout@v4 + + - name: Detect changed images + id: filter + uses: dorny/paths-filter@v3 + with: + list-files: shell + filters: | + tracky: + - 'tracky/**' + + build: + needs: detect + if: needs.detect.outputs.images != '[]' + runs-on: ubuntu-latest + strategy: + matrix: + include: + - image: tracky + context: ./tracky/src + dockerfile: ./tracky/src/Dockerfile + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push Docker image + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:latest + ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ github.sha }} diff --git a/bootstrap/cloud-init/specs.yaml b/bootstrap/cloud-init/specs.yaml index b6a204e8..432f82eb 100644 --- a/bootstrap/cloud-init/specs.yaml +++ b/bootstrap/cloud-init/specs.yaml @@ -46,9 +46,9 @@ - machine_type: node name: k3s-node-hermes cpu: 8 - ram: 6144 + ram: 10240 os_storage: 20 - longhorn_storage: 100 + longhorn_storage: 150 networks: - net_type: direct source: vlan.k3s @@ -61,9 +61,9 @@ - machine_type: node name: k3s-node-achilles cpu: 8 - ram: 6144 + ram: 10240 os_storage: 20 - longhorn_storage: 100 + longhorn_storage: 150 networks: - net_type: direct source: vlan.k3s @@ -76,9 +76,9 @@ - machine_type: node name: k3s-node-odysseus cpu: 8 - ram: 6144 + ram: 10240 os_storage: 20 - longhorn_storage: 100 + longhorn_storage: 150 networks: - net_type: direct source: vlan.k3s diff --git a/brokeroo/cmd/brokeroo/main.go b/brokeroo/cmd/brokeroo/main.go index ab7ad338..310ffd17 100644 --- a/brokeroo/cmd/brokeroo/main.go +++ b/brokeroo/cmd/brokeroo/main.go @@ -9,40 +9,48 @@ import ( "os" "os/signal" "strings" + "sync" + "sync/atomic" "syscall" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" _ "github.com/lib/pq" + amqp "github.com/rabbitmq/amqp091-go" "github.com/segmentio/kafka-go" ) type Config struct { - MQTTBroker string - MQTTClientID string - MQTTUsername string - MQTTPassword string - PostgresURL string - TopicPattern string - KafkaBroker string -} - -type MQTTData struct { - TSUnix int64 `json:"ts_unix"` - DevID string `json:"dev_id"` - Tag string `json:"tag"` - Payload json.RawMessage `json:"payload"` + RabbitMQURL string + QueueName string + ExchangeName string + RoutingKey string + PostgresURL string + KafkaBroker string + PrefetchCount int + BatchSize int + BatchTimeout time.Duration + MinPrefetch int + MaxPrefetch int } type Service struct { config *Config - mqttClient mqtt.Client + amqpConn *amqp.Connection + amqpChannel *amqp.Channel db *sql.DB kafkaWriter *kafka.Writer - knownTopics map[string]bool /* for caching kafka topics */ + knownTopics map[string]bool + mu sync.RWMutex + + // metrics + dbLatencies []time.Duration + saturatedBatches atomic.Int64 + timeoutBatches atomic.Int64 + totalBatches atomic.Int64 } type Envelope struct { + Msg amqp.Delivery `json:"-"` TsUnix int64 `json:"ts_unix"` Ts string `json:"ts"` DevID string `json:"dev_id"` @@ -50,6 +58,15 @@ type Envelope struct { PayloadJSON json.RawMessage `json:"payloadJson"` } +// type Envelope struct { +// Msg amqp.Delivery +// DevID string +// Tag string +// TS int64 +// TSStr string +// Body json.RawMessage +// } + func NewService(config *Config) *Service { return &Service{ config: config, @@ -63,37 +80,9 @@ func (s *Service) connectPostgres() error { if err != nil { return fmt.Errorf("failed to connect to postgres: %w", err) } - if err = s.db.Ping(); err != nil { return fmt.Errorf("failed to ping postgres: %w", err) } - - return nil -} - -func (s *Service) connectKafka() error { - s.kafkaWriter = kafka.NewWriter(kafka.WriterConfig{ - Brokers: []string{s.config.KafkaBroker}, - Async: true, - }) - - if err := s.ensureTopicExists("health-check"); err != nil { - log.Printf("failed to create first topic for healt-check: %v", err) - } - - err := s.kafkaWriter.WriteMessages(context.Background(), - kafka.Message{ - Topic: "health-check", - Value: []byte("ping"), - }, - ) - - if err != nil { - log.Printf("Failed to write test message to Kafka: %v", err) - return err - } - - log.Println("Successfully connected to Kafka at", s.config.KafkaBroker) return nil } @@ -131,236 +120,390 @@ func (s *Service) ensureTopicExists(topic string) error { } s.knownTopics[topic] = true - log.Printf("Topic pronto: %s", topic) + log.Printf("topic ready: %s", topic) return nil } -func (s *Service) connectMQTT() error { - opts := mqtt.NewClientOptions() - opts.AddBroker(s.config.MQTTBroker) - opts.SetClientID(s.config.MQTTClientID) - opts.SetUsername(s.config.MQTTUsername) - opts.SetPassword(s.config.MQTTPassword) - opts.SetCleanSession(false) // Keep unacked messages - opts.SetAutoReconnect(true) - opts.SetKeepAlive(60 * time.Second) - opts.SetPingTimeout(10 * time.Second) - opts.SetConnectTimeout(10 * time.Second) - opts.SetOrderMatters(false) - opts.SetAutoAckDisabled(true) - opts.SetProtocolVersion(4) - - // Set connection lost handler - opts.SetConnectionLostHandler(func(client mqtt.Client, err error) { - log.Printf("MQTT connection lost: %v", err) +func (s *Service) connectKafka() error { + s.kafkaWriter = kafka.NewWriter(kafka.WriterConfig{ + Brokers: []string{s.config.KafkaBroker}, + Async: true, }) - // Set reconnect handler - opts.SetOnConnectHandler(func(client mqtt.Client) { - log.Println("MQTT connected/reconnected") - s.subscribeToTopics() - }) + if err := s.ensureTopicExists("health-check"); err != nil { + log.Printf("failed to create first topic for healt-check: %v", err) + } - s.mqttClient = mqtt.NewClient(opts) + err := s.kafkaWriter.WriteMessages(context.Background(), + kafka.Message{ + Topic: "health-check", + Value: []byte("ping"), + }, + ) - c := 0 - for { - if token := s.mqttClient.Connect(); token.Wait() && token.Error() != nil { - if c == 10 { - return fmt.Errorf("failed to connect to MQTT broker: %w", token.Error()) - } - c += 1 - log.Println("Connessione fallita, retry tra 2s:", token.Error()) - time.Sleep(2 * time.Second) - continue - } - break + if err != nil { + log.Printf("failed to write test message to Kafka: %v", err) + return err } - log.Println("Successfully connected to MQTT broker") + log.Println("Successfully connected to Kafka at", s.config.KafkaBroker) return nil } -func (s *Service) subscribeToTopics() { - token := s.mqttClient.Subscribe(s.config.TopicPattern, 1, s.messageHandler) - if token.Wait() && token.Error() != nil { - log.Printf("Failed to subscribe to topics: %v", token.Error()) - return +func (s *Service) connectRabbitMQ() error { + var err error + s.amqpConn, err = amqp.Dial(s.config.RabbitMQURL) + if err != nil { + return fmt.Errorf("failed to connect to RabbitMQ: %w", err) + } + + s.amqpChannel, err = s.amqpConn.Channel() + if err != nil { + return fmt.Errorf("failed to open channel: %w", err) + } + + // QoS: start with configured prefetch + if err := s.amqpChannel.Qos(s.config.PrefetchCount, 0, false); err != nil { + return fmt.Errorf("failed to set QoS: %w", err) } - log.Printf("Subscribed to topic pattern: %s", s.config.TopicPattern) + args := amqp.Table{ + "x-queue-type": "quorum", + } + q, err := s.amqpChannel.QueueDeclare( + s.config.QueueName, + true, // durable + false, // auto-delete + false, // exclusive + false, // no-wait + args, + ) + if err != nil { + _ = s.amqpChannel.Close() + _ = s.amqpConn.Close() + time.Sleep(5 * time.Second) + return fmt.Errorf("Queue declare failed: %v", err) + } + err = s.amqpChannel.QueueBind( + q.Name, // queue name + s.config.RoutingKey, // routing key (use "" for fanout exchanges) + s.config.ExchangeName, // exchange name + false, // no-wait + nil, // arguments + ) + if err != nil { + _ = s.amqpChannel.Close() + _ = s.amqpConn.Close() + return fmt.Errorf("failed to bind queue: %w", err) + } + + return nil } -func (s *Service) messageHandler(client mqtt.Client, msg mqtt.Message) { - topic := msg.Topic() - payload := msg.Payload() +func (s *Service) startConsuming(ctx context.Context) error { + msgs, err := s.amqpChannel.Consume( + s.config.QueueName, "", false, false, false, false, nil, + ) + if err != nil { + return fmt.Errorf("failed to register consumer: %w", err) + } - log.Printf("Received message on topic %s", topic) + batch := make([]Envelope, 0, s.config.BatchSize) + timer := time.NewTimer(s.config.BatchTimeout) + + go func() { + defer timer.Stop() + for { + select { + case <-ctx.Done(): + return + case msg, ok := <-msgs: + if !ok { + return + } + incoming, valid := s.parseMessage(msg) + if !valid { + msg.Ack(false) + continue + } + batch = append(batch, incoming) + + if len(batch) >= s.config.BatchSize { + s.processBatch(batch) + batch = batch[:0] + if !timer.Stop() { + <-timer.C + } + timer.Reset(s.config.BatchTimeout) + } + case <-timer.C: + if len(batch) > 0 { + s.processBatch(batch) + batch = batch[:0] + } + timer.Reset(s.config.BatchTimeout) + } + } + }() + return nil +} - // Parse topic: j/data/DEVID/TAG +func (s *Service) parseMessage(msg amqp.Delivery) (Envelope, bool) { + topic := strings.ReplaceAll(msg.RoutingKey, ".", "/") parts := strings.Split(topic, "/") - if len(parts) != 4 || parts[0] != "j" || parts[1] != "data" { - log.Printf("Invalid topic format: %s, expected j/data/DEVID/TAG", topic) - msg.Ack() - return + if len(parts) != 4 { + return Envelope{}, false } - devID := parts[2] - tag := parts[3] - - // Parse JSON payload + devID, tag := parts[2], parts[3] var data map[string]any - if err := json.Unmarshal(payload, &data); err != nil { - log.Printf("Invalid JSON payload for topic %s: %v", topic, err) - msg.Ack() - return + if err := json.Unmarshal(msg.Body, &data); err != nil { + return Envelope{}, false } - tsUnix, ok := data["ts"].(int64) - if !ok { + var tsUnix int64 + if tsFloat, ok := data["ts"].(float64); ok { + tsUnix = int64(tsFloat) + } else { tsUnix = time.Now().Unix() } - ts := time.Unix(tsUnix, 0).UTC() - start := time.Now() - jsonPayload := json.RawMessage(payload) - if err := s.insertData(tsUnix, ts, devID, tag, jsonPayload); err != nil { - log.Printf("Failed to insert data: %v", err) - return - } - log.Printf("Time to write to DB %v", time.Since(start)) - start = time.Now() - - /* kafka */ - kafkaTopic := sanitizeTopic(topic) - if err := s.ensureTopicExists(kafkaTopic); err != nil { - log.Printf("Error creating topic %s: %v", kafkaTopic, err) - return - } - log.Printf("Time to create topic %v", time.Since(start)) - start = time.Now() - envelope := Envelope{ - TsUnix: tsUnix, - Ts: ts.Format(time.RFC3339), // is this right? + return Envelope{ + Msg: msg, DevID: devID, + TsUnix: tsUnix, Tag: tag, - PayloadJSON: payload, // il payload originale come stringa JSON - } + Ts: ts.Format(time.RFC3339), + PayloadJSON: msg.Body, + }, true +} - envelopeBytes, err := json.Marshal(envelope) +func (s *Service) processBatch(batch []Envelope) { + if len(batch) >= s.config.BatchSize { + s.saturatedBatches.Add(1) + } else { + s.timeoutBatches.Add(1) + } + s.totalBatches.Add(1) + start := time.Now() + tx, err := s.db.Begin() if err != nil { - log.Printf("Failed to marshal envelope: %v", err) + s.nackAll(batch) return } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) /* 5 second timeout */ - defer cancel() + // Build multi-values INSERT + valueStrings := make([]string, 0, len(batch)) + valueArgs := make([]interface{}, 0, len(batch)*5) - err = s.kafkaWriter.WriteMessages(ctx, kafka.Message{ - Topic: kafkaTopic, - Value: envelopeBytes, - }) + for i, m := range batch { + valueStrings = append(valueStrings, + fmt.Sprintf("($%d,$%d,$%d,$%d,$%d)", i*5+1, i*5+2, i*5+3, i*5+4, i*5+5)) + valueArgs = append(valueArgs, m.TsUnix, m.Ts, m.DevID, m.Tag, m.PayloadJSON) + } + + stmt := fmt.Sprintf(`INSERT INTO trackeroo.data (ts_unix, ts, dev_id, tag, payload) + VALUES %s ON CONFLICT (ts, dev_id) DO NOTHING`, strings.Join(valueStrings, ",")) + _, err = tx.Exec(stmt, valueArgs...) if err != nil { - log.Printf("Failed to write to Kafka: %v", err) + _ = tx.Rollback() + log.Println("Batch insert error:", err) + s.nackAll(batch) return } - log.Printf("Message forwarded to Kafka topic: %s", kafkaTopic) - log.Printf("Time to write to Kafka %v", time.Since(start)) - log.Printf("Successfully inserted data for dev_id: %s, tag: %s", devID, tag) - msg.Ack() + if err := tx.Commit(); err != nil { + s.nackAll(batch) + return + } + + // Kafka forward + ack + for _, m := range batch { + kafkaTopic := sanitizeTopic(m.Msg.RoutingKey) + if err := s.ensureTopicExists(kafkaTopic); err != nil { + log.Printf("error creating topic %s: %v", kafkaTopic, err) + s.nackAll(batch) + return + } + envelopeBytes, err := json.Marshal(m) + if err != nil { + log.Printf("failed to marshal envelope: %v", err) + s.nackAll(batch) + return + } + _ = s.kafkaWriter.WriteMessages(context.Background(), kafka.Message{ + Topic: kafkaTopic, + Value: envelopeBytes, + }) + m.Msg.Ack(false) + } + + latency := time.Since(start) + s.mu.Lock() + defer s.mu.Unlock() + s.dbLatencies = append(s.dbLatencies, latency) + if len(s.dbLatencies) > 100 { + s.dbLatencies = s.dbLatencies[1:] + } + // log.Printf("batch of %d inserted in %v", len(batch), latency) } -func (s *Service) insertData(tsUnix int64, ts time.Time, devID, tag string, payload json.RawMessage) error { - query := `INSERT INTO trackeroo.data (ts_unix, ts, dev_id, tag, payload) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (ts, dev_id) DO NOTHING` - _, err := s.db.Exec(query, tsUnix, ts, devID, tag, payload) - if err != nil { - return fmt.Errorf("failed to insert into database: %w", err) +func (s *Service) nackAll(batch []Envelope) { + for _, m := range batch { + m.Msg.Nack(false, true) } - return nil } -func (s *Service) Start() error { - // Connect to PostgreSQL - if err := s.connectPostgres(); err != nil { - return err +// Auto prefetch adjuster +func (s *Service) startPrefetchTuner(ctx context.Context) { + go func() { + current := s.config.PrefetchCount + last := s.config.PrefetchCount + ticker := time.NewTicker(10 * time.Second) + lastLog := time.Now() + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + avgLatency := s.avgDbLatency() + batchFillRate := s.getBatchFillRate() // New metric + + current = s.calculateOptimalPrefetch( + avgLatency, + batchFillRate, + current, + ) + if time.Since(lastLog) > 30*time.Second { + log.Printf("prefetch=%d, db_latency=%v, batch_fill=%.2f%%", current, avgLatency, batchFillRate*100) + lastLog = time.Now() + } + + if last == current { + continue + } + + if err := s.amqpChannel.Qos(current, 0, false); err == nil { + log.Printf(">>> adjusted prefetch=%d (db_latency=%v, batch_fill=%.2f%%)", current, avgLatency, batchFillRate*100) + last = current + } + } + } + }() +} + +func (s *Service) calculateOptimalPrefetch( + avgLatency time.Duration, + batchFillRate float64, + current int, +) int { + + if batchFillRate > 0.95 && current > s.config.MinPrefetch { + return max(current-5, s.config.MinPrefetch) } - // Connect to MQTT - if err := s.connectMQTT(); err != nil { - return err + if batchFillRate < 0.5 && avgLatency < 10*time.Millisecond && + current < s.config.MaxPrefetch { + return min(current+5, s.config.MaxPrefetch) } - // Connect to Kafka - if err := s.connectKafka(); err != nil { - return err + if avgLatency > 25*time.Millisecond && current > s.config.MinPrefetch { + return max(current-5, s.config.MinPrefetch) } - return nil + + return current } -func (s *Service) Stop() { - log.Println("Shutting down service...") +func (s *Service) getBatchFillRate() float64 { + s.mu.RLock() + defer s.mu.RUnlock() - if s.mqttClient != nil && s.mqttClient.IsConnected() { - s.mqttClient.Unsubscribe(s.config.TopicPattern) - s.mqttClient.Disconnect(250) - log.Println("Disconnected from MQTT broker") + if s.totalBatches.Load() == 0 { + return 0 } + return float64(s.saturatedBatches.Load()) / float64(s.totalBatches.Load()) +} - if s.db != nil { - s.db.Close() - log.Println("Closed PostgreSQL connection") +func (s *Service) avgDbLatency() time.Duration { + if len(s.dbLatencies) == 0 { + return 0 } - - if s.kafkaWriter != nil { - s.kafkaWriter.Close() - log.Println("Closed Kafka writer") + var sum time.Duration + for _, l := range s.dbLatencies { + sum += l } + return sum / time.Duration(len(s.dbLatencies)) +} + +func sanitizeTopic(topic string) string { + return strings.ReplaceAll(topic, ".", "-") } func loadConfig() *Config { - return &Config{ - MQTTBroker: getEnvOrDefault("MQTT_BROKER", "tcp://localhost:1883"), - MQTTClientID: getEnvOrDefault("MQTT_CLIENT_ID", "brokeroo"), - MQTTUsername: getEnvOrDefault("MQTT_USERNAME", ""), - MQTTPassword: getEnvOrDefault("MQTT_PASSWORD", ""), - PostgresURL: getEnvOrDefault("POSTGRES_URL", "postgres://user:password@localhost/dbname?sslmode=disable"), - TopicPattern: getEnvOrDefault("TOPIC_PATTERN", "j/data/+/+"), - KafkaBroker: getEnvOrDefault("KAFKA_BROKER", "kafka:9092"), + batchSize := getEnvOrDefaultInt("BATCH_SIZE", 50) + config := &Config{ + RabbitMQURL: getEnvOrDefault("RABBITMQ_URL", "amqp://guest:guest@localhost:5672/"), + QueueName: getEnvOrDefault("QUEUE_NAME", "brokeroo"), + ExchangeName: getEnvOrDefault("EXCHANGE_NAME", "amq.topic"), + RoutingKey: getEnvOrDefault("ROUTING_KEY", "j.data.*.*"), + PostgresURL: getEnvOrDefault("POSTGRES_URL", "postgres://user:password@localhost/dbname?sslmode=disable"), + KafkaBroker: getEnvOrDefault("KAFKA_BROKER", "kafka:9092"), + PrefetchCount: batchSize * 3, + BatchSize: batchSize, + BatchTimeout: time.Duration(getEnvOrDefaultInt("BATCH_TIMEOUT_MS", 100)) * time.Millisecond, + MinPrefetch: batchSize * 2, + MaxPrefetch: batchSize * 4, } + log.Printf("config -> (queue_name=%s, exchange_name=%s, routing_key=%s, prefetch_count=%d, batch_size=%d)", config.QueueName, config.ExchangeName, config.RoutingKey, config.PrefetchCount, config.BatchSize) + return config } -func getEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value +func getEnvOrDefault(key, def string) string { + if v := os.Getenv(key); v != "" { + return v } - return defaultValue + return def } -func sanitizeTopic(topic string) string { - return strings.ReplaceAll(topic, "/", "-") +func getEnvOrDefaultInt(key string, def int) int { + if v := os.Getenv(key); v != "" { + var iv int + if _, err := fmt.Sscanf(v, "%d", &iv); err == nil { + return iv + } + } + return def } func main() { - log.Println("Starting MQTT to Kafka and PostgreSQL service...") - config := loadConfig() service := NewService(config) - if err := service.Start(); err != nil { - log.Fatalf("Failed to start service: %v", err) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := service.connectPostgres(); err != nil { + log.Fatal(err) + } + if err := service.connectRabbitMQ(); err != nil { + log.Fatal(err) } + service.kafkaWriter = kafka.NewWriter(kafka.WriterConfig{ + Brokers: []string{config.KafkaBroker}, + Async: true, + }) - log.Println("Service started successfully") + if err := service.startConsuming(ctx); err != nil { + log.Fatal(err) + } + service.startPrefetchTuner(ctx) - // Wait for interrupt signal to gracefully shutdown + log.Println("Service running...") c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c - service.Stop() - log.Println("Service stopped") + cancel() } diff --git a/brokeroo/go.mod b/brokeroo/go.mod index 9dbc4773..3f3ad5f5 100644 --- a/brokeroo/go.mod +++ b/brokeroo/go.mod @@ -11,6 +11,7 @@ require ( github.com/eclipse/paho.golang v0.23.0 // indirect github.com/klauspost/compress v1.15.9 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect ) require ( diff --git a/brokeroo/go.sum b/brokeroo/go.sum index f1d2009a..32f5262c 100644 --- a/brokeroo/go.sum +++ b/brokeroo/go.sum @@ -10,6 +10,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/segmentio/kafka-go v0.4.49 h1:GJiNX1d/g+kG6ljyJEoi9++PUMdXGAxb7JGPiDCuNmk= github.com/segmentio/kafka-go v0.4.49/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= diff --git a/docker-compose-pull.yml b/docker-compose-pull.yml index f4c8f3bd..b679467b 100644 --- a/docker-compose-pull.yml +++ b/docker-compose-pull.yml @@ -138,7 +138,7 @@ services: TOPIC_PATTERN: j/data/+/+ KAFKA_BROKER: kafka:9092 depends_on: - kafka: + kafka: condition: service_started tsdb: condition: service_healthy @@ -182,7 +182,7 @@ services: KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT depends_on: - zookeeper @@ -231,7 +231,7 @@ volumes: tsdb-data: pgadmin-data: grafana-data: - redis-data: + redis-data: secrets: users_key: diff --git a/flink_job/bin/main/com/example/job/AggregationHandler.class b/flink_job/bin/main/com/example/job/AggregationHandler.class index a3d34df0..70875bce 100644 Binary files a/flink_job/bin/main/com/example/job/AggregationHandler.class and b/flink_job/bin/main/com/example/job/AggregationHandler.class differ diff --git a/mongo/init.js b/mongo/init.js index c084bb01..209dde5f 100644 --- a/mongo/init.js +++ b/mongo/init.js @@ -3,62 +3,67 @@ db.users.insertMany([ { username: "leonardo", role: "admin", - password_hash: "$2b$12$PHQyHC3QX7hSaSNk3otnJe90Htsf5nIqNeqSMKCWrmCHwbDvEUrwm", + password_hash: + "$2b$12$PHQyHC3QX7hSaSNk3otnJe90Htsf5nIqNeqSMKCWrmCHwbDvEUrwm", last_login: new Date(), session_token: "", issued_at: new Date(), - expires_at: new Date(Date.now() + 24*60*60*1000) // 24 hours from now + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours from now }, { username: "simone", role: "admin", - password_hash: "$2b$12$CCBM/Xy54gPzHkJyMWovm.fTmUUqjg73GWb1cBDTKPaMU4JsfSXd6", + password_hash: + "$2b$12$CCBM/Xy54gPzHkJyMWovm.fTmUUqjg73GWb1cBDTKPaMU4JsfSXd6", last_login: new Date(), session_token: "", issued_at: new Date(), - expires_at: new Date(Date.now() + 24*60*60*1000) + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000), }, { username: "admin", role: "admin", - password_hash: "$2b$12$3CaXyt.SvoUeQiR5TAFdReV4AjnAlNe46/oL6SBd75souC5SvKLAu", + password_hash: + "$2b$12$3CaXyt.SvoUeQiR5TAFdReV4AjnAlNe46/oL6SBd75souC5SvKLAu", last_login: new Date(), session_token: "", issued_at: new Date(), - expires_at: new Date(Date.now() + 24*60*60*1000) + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000), }, { username: "apps", role: "admin", - password_hash: "$2b$12$ZhKtxqqkFfrcEXiKjgvM0.BqM9sVXTEZAQCP5/qhRxMBZn2cWWbJ6", + password_hash: + "$2b$12$ZhKtxqqkFfrcEXiKjgvM0.BqM9sVXTEZAQCP5/qhRxMBZn2cWWbJ6", last_login: new Date(), session_token: "", issued_at: new Date(), - expires_at: new Date(Date.now() + 24*60*60*1000) + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000), }, { username: "trackeroo", role: "admin", - password_hash: "$2b$12$NAfj6rvgE20aWfxfK9Y0j.aVUaA1k3eJak2a0.u11nO0/9ijsFX4W", // trackeroo + password_hash: + "$2b$12$NAfj6rvgE20aWfxfK9Y0j.aVUaA1k3eJak2a0.u11nO0/9ijsFX4W", // trackeroo last_login: new Date(), session_token: "", issued_at: new Date(), - expires_at: new Date(Date.now() + 24*60*60*1000) - } + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, ]); db.devices.insertMany([ { _id: "trk-18608d12c8414acfcb9165b1", name: "wonderful_wirth", - status: { connected: true, last_message: "2025-08-02T16:40:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "m9mFoEWSL+GwWtoyPZsF2q4HpwLddf7JfsxmedN8h+Y=", created_at: ISODate("2025-08-18T03:36:46Z"), }, { _id: "trk-18608d12c843cfc86452c8bc", name: "xenodochial_carmack", - status: { connected: false, last_message: "2025-08-18T20:34:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "QNiUX1DdBq9hNGToa8ynDbXZRF9aDbO4LWvm7QspLo8=", created_at: ISODate("2025-08-08T07:04:46Z"), @@ -66,7 +71,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8443834a667d6c2", name: "merry_schwinger", - status: { connected: false, last_message: "2025-08-10T14:36:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "mS+C+G6Ut4mrbmz6v3EA5Fm7OvtcwrcRzvLOoIyIRDw=", created_at: ISODate("2025-08-08T17:05:46Z"), @@ -74,7 +79,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8445101b3d3a3b8", name: "thirsty_henry", - status: { connected: false, last_message: "2025-08-22T18:58:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "4kKuUQSMJE9RckmbCFReQy5gtbRpY3sCgDjcTnxBxwI=", created_at: ISODate("2025-08-04T05:03:46Z"), @@ -82,7 +87,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8446943ffbf2df9", name: "admiring_bohr", - status: { connected: true, last_message: "2025-08-06T01:33:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "5mp9W9wUVd50eqGqC/JpW/0/pG6wBrAsivSPg0bcC2M=", created_at: ISODate("2025-08-25T16:35:46Z"), @@ -90,7 +95,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84480bd0ad4656f", name: "keen_bohr", - status: { connected: true, last_message: "2025-08-16T05:31:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "/3tiwn6woUzwRGe/TUqOZQNGvYWHYSHj4d/Fpq9eTS8=", created_at: ISODate("2025-08-01T14:09:46Z"), @@ -98,7 +103,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8449838a8eb28eb", name: "clever_fermi", - status: { connected: true, last_message: "2025-08-08T23:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "pveqq6guV0EAQANRpMOQq5Yd8iSaZSH6QUMynO0ke6g=", created_at: ISODate("2025-08-05T16:46:46Z"), @@ -106,7 +111,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844b01d0d668fce", name: "brave_schrodinger", - status: { connected: false, last_message: "2025-08-04T16:28:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "+NrhtHayxZYPZnqA+C1C6uth3bORje/ka5uxXZ3E2zQ=", created_at: ISODate("2025-08-04T20:21:46Z"), @@ -114,7 +119,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844cb262515236b", name: "youthful_stallman", - status: { connected: false, last_message: "2025-08-22T21:24:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "dkvOCYpkWXr3WZs62jH9LfjOAbhm0TapSNQvcIzijLA=", created_at: ISODate("2025-08-04T10:00:46Z"), @@ -122,7 +127,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844e58e640b9ea1", name: "friendly_curie", - status: { connected: true, last_message: "2025-08-21T03:51:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "1l0SNynykdBFZVNfGlkTYiniGlmk5KVgyW3bp8HI6xg=", created_at: ISODate("2025-08-05T23:57:46Z"), @@ -130,7 +135,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844fccd26144714", name: "charming_dirac", - status: { connected: false, last_message: "2025-07-31T23:08:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "/Vi9D/RMiWilNw0jHA/fv4DbGwAEbqRAgQJulPl4DTQ=", created_at: ISODate("2025-08-08T18:10:46Z"), @@ -138,7 +143,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8451452004ec96d", name: "iron_franklin", - status: { connected: true, last_message: "2025-08-02T12:47:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "V0exZwne86ptOGifjuWX7QmGjpN1W62szV6+WxX9m5s=", created_at: ISODate("2025-08-07T12:55:46Z"), @@ -146,15 +151,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8456a397013ce6b", name: "gentle_church", - status: { connected: false, last_message: "2025-08-11T23:09:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "KH+3BS3QnEec8vME0ToemUn3mK9i0bwCg1ZlvgnRfkU=", created_at: ISODate("2025-08-15T20:23:46Z"), }, { _id: "trk-18608d12c84581a0b80ca1c5", name: "practical_michelson", - status: { connected: false, last_message: "2025-08-11T11:15:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "ab3Wu7Glx9xhfSFUNRKzi1CZPfPeiCI7ujc/iJmYWpI=", created_at: ISODate("2025-08-04T21:37:46Z"), @@ -162,7 +167,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c845ad105b9a60db", name: "phenomenal_born", - status: { connected: false, last_message: "2025-08-23T09:48:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "h3dT1JuyA/MhH1/o/n/IiPRFEyyEvXlMl3ZNtrCHZwY=", created_at: ISODate("2025-08-06T01:48:46Z"), @@ -170,7 +175,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c845c8050d2973c1", name: "agitated_planck", - status: { connected: false, last_message: "2025-08-20T06:43:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "l/TRQN08IpMfwVHCWHpcNQ/SBuRWLeNcuJZDf28697c=", created_at: ISODate("2025-08-12T01:20:46Z"), @@ -178,7 +183,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c845df659619c7e6", name: "determined_kelvin", - status: { connected: true, last_message: "2025-08-16T14:01:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "MlxmbBIfcGNpReS6GUgLDmTRrDDQQ6rlLxj4Eggrfjo=", created_at: ISODate("2025-08-10T07:50:46Z"), @@ -186,7 +191,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c845f5d103e908ad", name: "thoughtful_weber", - status: { connected: false, last_message: "2025-08-18T05:54:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "WGmlcdPBvesFWds8BZ4UR4k7ze/25P1GpWhXaSmgNDQ=", created_at: ISODate("2025-08-04T22:08:46Z"), @@ -194,15 +199,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8460bf33167dbdc", name: "hyper_shockley", - status: { connected: false, last_message: "2025-08-17T20:07:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "BzHUYgawXJvf458ZW6fLaKxbTEoOXBI1pgI4Z5ebMuM=", created_at: ISODate("2025-08-17T07:01:46Z"), }, { _id: "trk-18608d12c8462309d5e746cc", name: "pious_planck", - status: { connected: true, last_message: "2025-08-12T19:59:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "DMAF8lJbDW/hdBvKttHueeokLXgQ9nlcTQBDMfypO4w=", created_at: ISODate("2025-08-15T02:45:46Z"), @@ -210,7 +215,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84640a54b9bd528", name: "heuristic_fermi", - status: { connected: true, last_message: "2025-08-02T15:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "+crZGHsZ6r5ZN0UMsB0JFK/hveE6E267uzcAGdkQAZQ=", created_at: ISODate("2025-08-09T02:32:46Z"), @@ -218,7 +223,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c846577d4ec97cfb", name: "sharp_edison", - status: { connected: true, last_message: "2025-08-14T03:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "eW16LWeR4CcKm0mRqkBnv9lU6WYKXyzS8csOmoTwt+c=", created_at: ISODate("2025-08-21T01:54:46Z"), @@ -226,7 +231,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8466e529b599dc4", name: "romantic_hertz", - status: { connected: true, last_message: "2025-08-30T03:20:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "kbRJ702ADyEPki+ko7MbOZ7AGvhKPSql5A7fK6MGFGI=", created_at: ISODate("2025-08-17T20:13:46Z"), @@ -234,7 +239,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84685251cdaf35c", name: "gallant_whitehead", - status: { connected: true, last_message: "2025-08-05T15:55:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "ZeGHStsNcbp56gDEk+nasq/BaaLklfzXfuG8BcX+lrs=", created_at: ISODate("2025-08-28T00:41:46Z"), @@ -242,7 +247,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8469c00840cc49b", name: "laughing_rutherford", - status: { connected: false, last_message: "2025-08-13T10:56:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "n0LNbNhaIaW42aBkqCwDM0cJSwcwL8McL8im5CP91v4=", created_at: ISODate("2025-08-29T17:24:46Z"), @@ -250,7 +255,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c846b3600123a417", name: "sad_marconi", - status: { connected: true, last_message: "2025-08-05T23:52:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "g2D4mb6ezOrfK/ft41lACByyeU4IQVF6RWlB/0P7zhc=", created_at: ISODate("2025-08-15T07:38:46Z"), @@ -258,7 +263,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c846ca7bab489e30", name: "clever_newton", - status: { connected: true, last_message: "2025-08-18T06:39:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "hGz/4/osEsLT/5Q53TTlID+wA54Xb2p8Cxpj05jZsik=", created_at: ISODate("2025-08-18T00:25:46Z"), @@ -266,15 +271,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c846e0a3e38974e5", name: "dazzling_maxwell", - status: { connected: false, last_message: "2025-08-10T04:46:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "KSG9WiZ6pWINfm9FH9vS/GSo3t1TmLf+eE5eGhU+Hik=", created_at: ISODate("2025-08-05T06:51:46Z"), }, { _id: "trk-18608d12c847064ce1bdf7ee", name: "elegant_lagrange", - status: { connected: true, last_message: "2025-08-28T18:21:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "tlrnwpslaBwOChP+dAFPQ2sfItsoKERU+MR33NHk9yQ=", created_at: ISODate("2025-08-19T09:04:46Z"), @@ -282,7 +287,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8472b9f0905b3ad", name: "ecstatic_riemann", - status: { connected: true, last_message: "2025-08-05T19:27:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "5v94oQP8pNQZrOZrYNTTl5c67qyrPDUJE4eZTfuBjVU=", created_at: ISODate("2025-08-08T11:59:46Z"), @@ -290,15 +295,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84742df27b88f6b", name: "cranky_ampere", - status: { connected: false, last_message: "2025-08-19T19:18:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "cS34G/an+Z+MM1M5P5dh48B+y4kjyy0eI/8cw4ClkT4=", created_at: ISODate("2025-08-08T18:36:46Z"), }, { _id: "trk-18608d12c84759c8a9ef04f8", name: "energetic_hilbert", - status: { connected: true, last_message: "2025-08-25T05:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "EInJWD1jh2t06pz3D5WZjspYvxzqs57NRKAxVhe9uIk=", created_at: ISODate("2025-08-28T01:57:46Z"), @@ -306,15 +311,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84770f5153b97ef", name: "amazing_turing", - status: { connected: false, last_message: "2025-08-10T10:51:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "X9Lx6+KllLFtNDyvj/466CrOQmF6UewS+3xs9j18ILA=", created_at: ISODate("2025-08-03T11:49:46Z"), }, { _id: "trk-18608d12c84786f21e4edc5e", name: "happy_einstein", - status: { connected: true, last_message: "2025-08-21T12:01:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "wzTOvpERsSN6QO4jgUD3XuRtcdqQLItdjSxSgrKmcoo=", created_at: ISODate("2025-08-17T21:31:46Z"), @@ -322,7 +327,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8479d828433ff9a", name: "funny_peano", - status: { connected: true, last_message: "2025-08-11T05:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "cqseF4l1F5lAgCe7bD6EP+rEE+L1wFhc+10MQV48UJo=", created_at: ISODate("2025-08-24T04:47:46Z"), @@ -330,7 +335,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c847b3e74258eb03", name: "vibrant_thompson", - status: { connected: false, last_message: "2025-08-15T07:02:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "O0XXGhe7rp/EsEYyXNAGIKjJk5T+FXCAdebGqh/dluo=", created_at: ISODate("2025-08-16T09:54:46Z"), @@ -338,7 +343,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8480b18fe88b4af", name: "patient_wu", - status: { connected: false, last_message: "2025-08-21T22:36:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "bHzGkyOqJLG9GAnkC25B2OLoXhQhIUu3bPVIE9sBlzU=", created_at: ISODate("2025-08-24T18:56:46Z"), @@ -346,7 +351,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84823aa3e06b7da", name: "epic_cantor", - status: { connected: true, last_message: "2025-08-07T10:15:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "G7MRIMBP24R0xdEXZRIV3me9dDDFRdxtTXuLXe28494=", created_at: ISODate("2025-08-02T10:17:46Z"), @@ -354,7 +359,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8483aeb4dce2724", name: "hopeful_bardeen", - status: { connected: true, last_message: "2025-08-30T06:16:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "Xd5a7XzvWCVjDtduORbuAH1sykFtGHcvAhK/4BadcaI=", created_at: ISODate("2025-08-06T23:01:46Z"), @@ -362,7 +367,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8486259917b5535", name: "frosty_dedekind", - status: { connected: false, last_message: "2025-08-07T10:35:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "I0ahyNHzwY2ax1RTIwkdAW1+4eErU6s1WISzCED2ImI=", created_at: ISODate("2025-08-05T02:16:46Z"), @@ -370,15 +375,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8487a58b5c5651d", name: "heartwarming_bose", - status: { connected: true, last_message: "2025-08-15T11:05:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "LsgbCoMQG+opSUt9JrsiDIL4Z1k5VU+WnTGajUsfRMU=", created_at: ISODate("2025-08-04T01:01:46Z"), }, { _id: "trk-18608d12c848919ae0a5ac8f", name: "kind_torvalds", - status: { connected: false, last_message: "2025-08-02T19:55:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "ij0mk0VXVo7nsb0cqpGJX0V5oChhDa830+CJ72ZW8xk=", created_at: ISODate("2025-08-20T23:21:46Z"), @@ -386,15 +391,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c848b620cb14aa25", name: "fascinated_noether", - status: { connected: false, last_message: "2025-08-20T21:05:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "XJVLbd6UO12QkuIGo3EbZGXjG4xPKSvliiOLRLnMZmI=", created_at: ISODate("2025-08-09T07:11:46Z"), }, { _id: "trk-18608d12c848d48480a5d5b6", name: "puzzled_fizeau", - status: { connected: true, last_message: "2025-08-22T09:43:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "ceSr36+tty8/YgIQuT7V66kMAXhi+K2JPN7EqEF7r3Y=", created_at: ISODate("2025-08-02T11:41:46Z"), @@ -402,7 +407,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c848eb34f8760e19", name: "competent_rutherford", - status: { connected: true, last_message: "2025-08-04T03:29:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "e8kkc8+QotgP718tO8zL2+ukDwUiNfSmRl3UQFKnEwc=", created_at: ISODate("2025-08-02T23:24:46Z"), @@ -410,7 +415,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8490334a6303366", name: "pedantic_pauli", - status: { connected: true, last_message: "2025-08-30T09:45:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "kXVOIOGZGLfPzYCPK55aVbR15kbwTZr9ezA6zSxHEig=", created_at: ISODate("2025-08-05T01:38:46Z"), @@ -418,7 +423,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8491a91b1f888a9", name: "magical_dirac", - status: { connected: false, last_message: "2025-08-08T15:21:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "C9biy8JKSu1nQCKQxGTjuxkjHr4W46086h3KwjVBr/I=", created_at: ISODate("2025-08-05T22:46:46Z"), @@ -426,7 +431,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84931d6fa2d6232", name: "serene_tesla", - status: { connected: false, last_message: "2025-08-03T19:17:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "xPGoSwT+dTYd6ZX1++0+CJC8iEwVscx3VoDDI+LXWw8=", created_at: ISODate("2025-08-05T07:06:46Z"), @@ -434,15 +439,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8494897112ba78e", name: "peaceful_pascal", - status: { connected: true, last_message: "2025-08-05T08:37:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "WAVVSnHd8206VRwJOtgHDuUGDf0orqqYBIqodJA9L1I=", created_at: ISODate("2025-08-15T05:44:46Z"), }, { _id: "trk-18608d12c8495fd71857ce3f", name: "stoic_ohm", - status: { connected: false, last_message: "2025-08-22T12:36:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "vzDwSMp/L6oyVEyYj3RKTY9UKyBj3V7LvhCD6cqe/nI=", created_at: ISODate("2025-08-22T20:43:46Z"), @@ -450,7 +455,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84976fefd57d1ff", name: "fearless_galois", - status: { connected: false, last_message: "2025-08-10T16:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "PoHtJ2l8pTjJPJctSMrIII20agqTIk4LpVoiIdNLzgo=", created_at: ISODate("2025-08-03T14:47:46Z"), @@ -458,7 +463,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8498d777941e066", name: "distracted_planck", - status: { connected: true, last_message: "2025-08-24T07:49:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "W8F6g6HmjQAP3lwJjRqAfdQOIOsSq1vzEVbHMS8ZUAo=", created_at: ISODate("2025-08-21T20:41:46Z"), @@ -466,7 +471,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c849a50c845c25a9", name: "optimistic_babbage", - status: { connected: true, last_message: "2025-08-12T09:06:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "zpX14uHmth5/ZzzNaL68qeoHLJRZjT3huaYns0LmnWU=", created_at: ISODate("2025-08-06T17:19:46Z"), @@ -474,7 +479,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c849bd6d485879c1", name: "tender_faraday", - status: { connected: true, last_message: "2025-08-10T12:57:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "O8XxluA24EC4bsgPrBHWCwS8q8GmBK7mv0byordbYc8=", created_at: ISODate("2025-08-13T13:09:46Z"), @@ -482,7 +487,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c849d409351613c2", name: "jaunty_pauling", - status: { connected: true, last_message: "2025-08-27T07:44:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "OLbAlVzxkhzr2wlaO/WV8fCB/m4jJQ5AIfrJ0tUgF0A=", created_at: ISODate("2025-08-20T22:21:46Z"), @@ -490,7 +495,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84a2e54a6ffdd76", name: "suspicious_volta", - status: { connected: true, last_message: "2025-08-03T18:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "S30Z2mlkvsUR6yH8nULY8RqPhmUcxEHEcEVuP7ePmSM=", created_at: ISODate("2025-08-10T10:53:46Z"), @@ -498,7 +503,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84a5f766256c9e2", name: "serene_hopper", - status: { connected: true, last_message: "2025-08-13T10:31:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "DTOj/Y2DiO0Ju5vwxOUgBtwXB5579pz30EEMYun7z00=", created_at: ISODate("2025-08-18T16:45:46Z"), @@ -506,7 +511,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84a892c3aad3955", name: "eager_gauss", - status: { connected: false, last_message: "2025-08-06T19:07:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "Dl4fLpzCBXqNHjtrgm+5jrj+tSXPZRxttltQPvr/JcY=", created_at: ISODate("2025-08-29T10:30:46Z"), @@ -514,15 +519,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84aa049b09a2169", name: "eager_darwin", - status: { connected: true, last_message: "2025-08-10T19:58:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "EC7AoSY05EyPbRVEl/eGTSfESGXZZ4OWF5FsPcaKdTU=", created_at: ISODate("2025-08-24T18:39:46Z"), }, { _id: "trk-18608d12c84ab6d05b8082ad", name: "hardcore_bell", - status: { connected: true, last_message: "2025-08-25T03:22:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "Ar3WbxWHU8l2uyOZPnqJffGQ9vJpXAovM/ZoNRYd9RA=", created_at: ISODate("2025-08-20T22:33:46Z"), @@ -530,7 +535,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84acd0d1c7fc74a", name: "enchanting_poincare", - status: { connected: false, last_message: "2025-08-12T07:20:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "0HbmI0I5esubDm4b6XvzgLgHJtXVxGTeIt4EiHJAKc0=", created_at: ISODate("2025-08-08T23:41:46Z"), @@ -538,7 +543,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ae3dffff2e52d", name: "proud_morley", - status: { connected: true, last_message: "2025-08-17T10:23:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "NYrTqrlsGqPo9r6nKF2+bVd+QshvHb00Z676LpK3R00=", created_at: ISODate("2025-08-30T11:59:46Z"), @@ -546,15 +551,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84afa2c10aa6c55", name: "inspiring_bardeen", - status: { connected: false, last_message: "2025-08-15T18:29:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "6dU2H5RdD5VaZyXi5ZWaj5rkydlEV5f2PS1l9+fvL+c=", created_at: ISODate("2025-08-07T16:21:46Z"), }, { _id: "trk-18608d12c84b11c367999861", name: "objective_glashow", - status: { connected: true, last_message: "2025-08-26T14:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "uW2kufM6EeR+tpJHDYRmd/5CPGLMn84jMBsu6mhWtRU=", created_at: ISODate("2025-08-19T03:27:46Z"), @@ -562,7 +567,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b2feca3792861", name: "modest_dyson", - status: { connected: false, last_message: "2025-08-25T10:25:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "5XYG/w/clJolar2hAO3n4D9+fJdckv7i0ZO0S2m2KRo=", created_at: ISODate("2025-08-02T00:09:46Z"), @@ -570,7 +575,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b469627a640d9", name: "boring_heisenberg", - status: { connected: false, last_message: "2025-08-25T03:19:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "EHPvNh8KnmQNbD8Kef2UZh+07Y6DiHKpNwTuuIcyemo=", created_at: ISODate("2025-08-10T16:52:46Z"), @@ -578,7 +583,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b5cae4cff3133", name: "strange_ampere", - status: { connected: true, last_message: "2025-08-28T04:31:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "fUqc7PB2fR3likQIUigerur96bUzz7dQZE/lMUiMJHY=", created_at: ISODate("2025-08-08T07:02:46Z"), @@ -586,7 +591,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b74702fe3e85c", name: "faithful_hardy", - status: { connected: false, last_message: "2025-08-27T20:37:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "etflzQEpsxaJqAlO2o/G45T+s80cWWaSV0KPobuWWFE=", created_at: ISODate("2025-08-02T15:50:46Z"), @@ -594,7 +599,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b8b83746dbc8b", name: "eloquent_leibniz", - status: { connected: true, last_message: "2025-08-14T14:03:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "nwdfO4d3o5nmJsO/i8xQrgcjsa2KNSQQ9PQzgMNbEc8=", created_at: ISODate("2025-08-13T23:17:46Z"), @@ -602,23 +607,23 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ba2911c4a675e", name: "cool_ohm", - status: { connected: false, last_message: "2025-08-12T06:22:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "7TXa0utsnnJXx7tHWYt5KC8nwv1xThn8iy12KI2zOvE=", created_at: ISODate("2025-08-12T01:49:46Z"), }, { _id: "trk-18608d12c84bde9e8b791f18", name: "fervent_abel", - status: { connected: true, last_message: "2025-08-03T23:36:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "1doRBvIJsrXQIG1Ui9albOMUHxMjtrjU6VvNLZTZQak=", created_at: ISODate("2025-08-29T22:05:46Z"), }, { _id: "trk-18608d12c84bf63dcc1fb3e7", name: "groovy_shannon", - status: { connected: true, last_message: "2025-08-10T16:58:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "4NnTHCSG64xQf807OjIJlzc+MdgrOv9/a83gDgRzUwQ=", created_at: ISODate("2025-08-16T23:38:46Z"), @@ -626,7 +631,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c23101fa0a8b6", name: "grieving_wiener", - status: { connected: true, last_message: "2025-08-14T09:38:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "BpChhURWcEyErTfXhnx6J9gg4L+Qy4FyWzdeL6e27u4=", created_at: ISODate("2025-08-14T21:20:46Z"), @@ -634,15 +639,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c3ae0fd8aad7e", name: "optimized_higgs", - status: { connected: false, last_message: "2025-08-21T14:05:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "csXqVuqw2AXm2A46aCt97ytsUzs5pA6t5Jnit25JFkI=", created_at: ISODate("2025-08-15T19:27:46Z"), }, { _id: "trk-18608d12c84c5151bb792931", name: "boring_wozniak", - status: { connected: true, last_message: "2025-08-08T02:27:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "X3CHEXehWzXvRfQAJ6GGCoeEbVPcdX95JCzT3uEBYSs=", created_at: ISODate("2025-08-02T10:22:46Z"), @@ -650,7 +655,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c678173241105", name: "curious_feynman", - status: { connected: false, last_message: "2025-08-21T18:29:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "XxN0HpCbfzUE2AMRPcteDPSRLT5YgnAy4ILOxwIzm/g=", created_at: ISODate("2025-08-26T19:07:46Z"), @@ -658,7 +663,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c7e6385d059d6", name: "noble_weinberg", - status: { connected: true, last_message: "2025-08-07T01:06:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "SZzpdjeGSaaWEBBa8apWtQJdwscbjyJ4jC/nXgnxJu4=", created_at: ISODate("2025-08-21T16:34:46Z"), @@ -666,7 +671,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c9470140d32f6", name: "upbeat_ritchie", - status: { connected: false, last_message: "2025-08-10T09:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "65mhzIpZvOx51yE6nPxQgjgy3rwlv13lMSM5X7vEFeQ=", created_at: ISODate("2025-08-01T12:55:46Z"), @@ -674,7 +679,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84caba1375500b4", name: "quizzical_doppler", - status: { connected: true, last_message: "2025-08-11T14:42:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "t/9N9FYcN12suJ/nZl5q6jC7OAWv0h2Pow6vJBSr4dA=", created_at: ISODate("2025-08-29T00:09:46Z"), @@ -682,7 +687,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84cc3de436f7bde", name: "silly_westinghouse", - status: { connected: true, last_message: "2025-08-28T04:22:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "Ba4MVIb1YQL6dm2eYRiJOX0CljrdeuFtAI2HZ3nBtsE=", created_at: ISODate("2025-08-02T07:07:46Z"), @@ -690,15 +695,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84cdbbc4c9327d2", name: "dreamy_tesla", - status: { connected: true, last_message: "2025-08-12T22:50:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "/kDru3xkiT7oAfKFD4Mp6nVl5IIFEMXponchC0ojKlo=", created_at: ISODate("2025-08-01T00:21:46Z"), }, { _id: "trk-18608d12c84cf3df4741680f", name: "jovial_mendeleev", - status: { connected: true, last_message: "2025-08-06T08:10:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "voqhD27X8L+VW4e/Op93AyazBBx/a7mqhTbAsvn6tnc=", created_at: ISODate("2025-08-26T23:38:46Z"), @@ -706,7 +711,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d0b5fc332f041", name: "great_kolmogorov", - status: { connected: false, last_message: "2025-08-14T09:59:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "TH/tO2mmlgP3dnH6h+ickwu8IaEajnm+DpvDLcAZDC8=", created_at: ISODate("2025-08-29T11:27:46Z"), @@ -714,15 +719,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d2247a477b6ff", name: "interesting_watson", - status: { connected: true, last_message: "2025-08-20T09:29:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "ZBNn287pTaxPwGShBOOICVcZDmQbcdr2vtmLYbelgoU=", created_at: ISODate("2025-08-19T19:46:46Z"), }, { _id: "trk-18608d12c84d385e1d5c7bbc", name: "zealous_berners", - status: { connected: true, last_message: "2025-08-04T11:23:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "5Wx/7819jHalM4yOsTrhtswnTS8ItR5/n0BEaYFotQM=", created_at: ISODate("2025-08-18T18:58:46Z"), @@ -730,7 +735,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d62f4adb3c923", name: "condescending_volta", - status: { connected: true, last_message: "2025-08-15T00:32:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "CKwOOv0LRsGpDURy9iXE9tnUFdjtI6ZqtAFoJkal360=", created_at: ISODate("2025-08-26T02:07:46Z"), @@ -738,7 +743,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d9ce75be19956", name: "adoring_morse", - status: { connected: false, last_message: "2025-08-28T04:09:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "iL0T7kaXpwPrO6Z0mA7Kk2Gs+tHEBl6fNSbFV3rSgVM=", created_at: ISODate("2025-08-04T14:20:46Z"), @@ -746,7 +751,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84db5aa45ada38e", name: "elastic_fourier", - status: { connected: true, last_message: "2025-08-22T06:00:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "8loHVYewF31YJGSlkcOrMZpfc5eQ2DFhuvpne8M68Xc=", created_at: ISODate("2025-08-09T06:44:46Z"), @@ -754,7 +759,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84dccaa3ac843bd", name: "thrilled_gauss", - status: { connected: false, last_message: "2025-08-14T07:07:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "jhESuO3AwXCtX0TcH5E9l91DDRUUx3SIErqpjt+SpUk=", created_at: ISODate("2025-08-25T19:51:46Z"), @@ -762,7 +767,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84de3a8df5efbcc", name: "goofy_markov", - status: { connected: false, last_message: "2025-08-20T02:12:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "1bmj0Qvq4D9EsyHZJpITpEqEBBrOcD9KsiUhDX4X7R4=", created_at: ISODate("2025-08-27T19:33:46Z"), @@ -770,7 +775,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84dfa6590d8fbda", name: "relaxed_turing", - status: { connected: true, last_message: "2025-08-29T02:47:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "bMvQyF6Lu/h5zYPEn4TBAOAxjVj67QTXl3ifZYf3S7s=", created_at: ISODate("2025-08-09T23:16:46Z"), @@ -778,7 +783,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e12b161fb52f1", name: "exciting_godel", - status: { connected: true, last_message: "2025-08-19T23:11:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "L+ZecOMBWt0L7Ix8sPW8MZpDskk30MZydLOV1av++fk=", created_at: ISODate("2025-08-30T12:59:46Z"), @@ -786,7 +791,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e2a4a52da79d9", name: "sweet_galvani", - status: { connected: false, last_message: "2025-08-29T21:21:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "dDVOk5b+i7+npM3o7icJocZSNFaed64RKX6HuxK5TTk=", created_at: ISODate("2025-08-22T08:08:46Z"), @@ -794,7 +799,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e473a6d897698", name: "fabulous_ramanujan", - status: { connected: false, last_message: "2025-08-18T06:15:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "n/7uqrA1Xh7uzYaX4BouodyLFlU/HWXMF3Gjd5gRN6g=", created_at: ISODate("2025-08-19T16:05:46Z"), @@ -802,7 +807,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e67b817e2a8fd", name: "trusting_knuth", - status: { connected: true, last_message: "2025-08-28T09:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "frrz+vzXLVQV4HHhZf7MFMPo4iGIJQWsgSPIkHKJ8OE=", created_at: ISODate("2025-08-01T15:20:46Z"), @@ -810,7 +815,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e7f725b9dd529", name: "quirky_shannon", - status: { connected: false, last_message: "2025-08-16T17:49:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "443NJMtr5p+i0QIA8th2B9QTsh3aKhI1p51G0uwN1iQ=", created_at: ISODate("2025-08-09T21:36:46Z"), @@ -818,7 +823,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e95effa9dc925", name: "nervous_hawking", - status: { connected: true, last_message: "2025-08-15T23:40:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "XTAux5J/tThXxN+5AoXR61Pxm0nfvJ92SOpCVo9HVpk=", created_at: ISODate("2025-08-18T09:24:46Z"), @@ -826,7 +831,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84eaef489342f7e", name: "exotic_turing", - status: { connected: true, last_message: "2025-08-03T15:10:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "rrNb0cdFEBeDwGfwZXv8U1VqHz4QfxT6Re+o+HR8vGA=", created_at: ISODate("2025-08-16T03:12:46Z"), @@ -834,7 +839,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ec5eea3a8dae1", name: "gifted_kleene", - status: { connected: true, last_message: "2025-08-12T00:58:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "EaBPc3d6Edkfi3YYxsqc+sAMILGwn2RbsIkB4zmnSU0=", created_at: ISODate("2025-08-24T06:35:46Z"), @@ -842,7 +847,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ee5a2e49c6f83", name: "lucid_heisenberg", - status: { connected: true, last_message: "2025-08-08T11:32:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "Rb/XilBAyjcoGfgM4JkwZbZNEZsdf5pNWUXtF9/NqR8=", created_at: ISODate("2025-08-19T05:39:46Z"), diff --git a/services/inventory/hosts.yml b/services/inventory/hosts.yml index 1b6959d9..e4bd261d 100644 --- a/services/inventory/hosts.yml +++ b/services/inventory/hosts.yml @@ -4,6 +4,11 @@ all: ansible_become: true rabbitmq_service_ip: 10.20.30.120 +kubernetes: + hosts: + kubernetes-vip: + ansible_host: 10.20.30.40 + servers: hosts: k3s-server-cronus: diff --git a/services/playbooks/roles/brokeroo/defaults/main.yml b/services/playbooks/roles/brokeroo/defaults/main.yml index 45b73a25..8d0f4e3f 100644 --- a/services/playbooks/roles/brokeroo/defaults/main.yml +++ b/services/playbooks/roles/brokeroo/defaults/main.yml @@ -1,7 +1,9 @@ brokeroo_namespace: trackeroo brokeroo_app_name: brokeroo - +brokeroo_rabbitmq_connection_string: "{{ brokeroo_rabbitmq_connection_string_secret }}" +brokeroo_tsdb_connection_string: "{{ brokeroo_tsdb_connection_string_secret }}" +brokeroo_kafka_broker: kafka.data.svc.cluster.local:9092 brokeroo_image: ghcr.io/skiby7/brokeroo:latest - -brokeroo_replicas: 3 +brokeroo_amqp_queue_name: brokeroo +brokeroo_replicas: 1 brokeroo_port: 8080 diff --git a/services/playbooks/roles/brokeroo/tasks/main.yml b/services/playbooks/roles/brokeroo/tasks/main.yml index b4bb75ef..6ae46e17 100644 --- a/services/playbooks/roles/brokeroo/tasks/main.yml +++ b/services/playbooks/roles/brokeroo/tasks/main.yml @@ -6,6 +6,33 @@ kind: Namespace state: present +- name: "Create Brokeroo Secrets (RabbitMQ and TSDB connection strings)" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + state: present + namespace: "{{ brokeroo_namespace }}" + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ brokeroo_app_name }}-secrets" + stringData: + brokeroo_rabbitmq_connection_string: "{{ brokeroo_rabbitmq_connection_string }}" + brokeroo_tsdb_connection_string: "{{ brokeroo_tsdb_connection_string }}" + +- name: "Create Secrets for KEDA (RabbitMQ connection strings)" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + state: present + namespace: "{{ brokeroo_namespace }}" + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ brokeroo_app_name }}-keda-secrets" + data: + brokeroo_rabbitmq_connection_string: "{{ brokeroo_rabbitmq_connection_string | b64encode }}" + - name: "Deploy Brokeroo Deployment and Service" kubernetes.core.k8s: kubeconfig: /etc/rancher/k3s/k3s.yaml @@ -15,3 +42,6 @@ loop: - "01-deployment.yml.j2" - "02-service.yml.j2" + - "03-triggerauthentication.yml.j2" + - "04-scaledobject.yml.j2" + - "05-poddistruptionbudget.yml.j2" diff --git a/services/playbooks/roles/brokeroo/templates/01-deployment.yml.j2 b/services/playbooks/roles/brokeroo/templates/01-deployment.yml.j2 index cc1ae1e9..17cc709c 100644 --- a/services/playbooks/roles/brokeroo/templates/01-deployment.yml.j2 +++ b/services/playbooks/roles/brokeroo/templates/01-deployment.yml.j2 @@ -1,12 +1,10 @@ apiVersion: apps/v1 -kind: StatefulSet +kind: Deployment metadata: name: {{ brokeroo_app_name }} labels: app: {{ brokeroo_app_name }} spec: - nodeSelector: - workload-type: apps replicas: {{ brokeroo_replicas }} selector: matchLabels: @@ -25,26 +23,30 @@ spec: ports: - containerPort: {{ brokeroo_port }} env: - - name: MQTT_BROKER - value: "mqtt://rabbitmq:1883" - - name: MQTT_CLIENT_ID + - name: RABBITMQ_URL valueFrom: - fieldRef: - fieldPath: metadata.name - - name: MQTT_USERNAME - value: "apps" - - name: MQTT_PASSWORD - value: "apps" + secretKeyRef: + name: "{{ brokeroo_app_name }}-secrets" + key: brokeroo_rabbitmq_connection_string - name: POSTGRES_URL - value: "postgres://apps:apps@tsdb-rw.data.svc.cluster.local:5432/tracker_db?sslmode=disable" - - name: TOPIC_PATTERN - value: "j/data/+/+" + valueFrom: + secretKeyRef: + name: "{{ brokeroo_app_name }}-secrets" + key: brokeroo_tsdb_connection_string + - name: QUEUE_NAME + value: "{{ brokeroo_amqp_queue_name }}" + - name: ROUTING_KEY + value: "j.data.*.*" - name: KAFKA_BROKER - value: "kafka.data.svc.cluster.local:9092" + value: "{{ brokeroo_kafka_broker }}" + - name: BATCH_SIZE + value: "50" + - name: BATCH_TIMEOUT_MS + value: "100" resources: requests: - cpu: "250m" - memory: "256Mi" + cpu: 500m + memory: 512Mi limits: - cpu: "1" - memory: "512Mi" + cpu: 2000m + memory: 2Gi diff --git a/services/playbooks/roles/brokeroo/templates/03-hpa.yml.j2.old b/services/playbooks/roles/brokeroo/templates/03-hpa.yml.j2.old new file mode 100644 index 00000000..fb5a6b4f --- /dev/null +++ b/services/playbooks/roles/brokeroo/templates/03-hpa.yml.j2.old @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ brokeroo_app_name }}-hpa + labels: + app: {{ brokeroo_app_name }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ brokeroo_app_name }} + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 diff --git a/services/playbooks/roles/brokeroo/templates/03-triggerauthentication.yml.j2 b/services/playbooks/roles/brokeroo/templates/03-triggerauthentication.yml.j2 new file mode 100644 index 00000000..e5aa001b --- /dev/null +++ b/services/playbooks/roles/brokeroo/templates/03-triggerauthentication.yml.j2 @@ -0,0 +1,10 @@ +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: rabbitmq-auth + namespace: "{{ brokeroo_namespace }}" +spec: + secretTargetRef: + - parameter: host + name: "{{ brokeroo_app_name }}-keda-secrets" + key: brokeroo_rabbitmq_connection_string diff --git a/services/playbooks/roles/brokeroo/templates/04-scaledobject.yml.j2 b/services/playbooks/roles/brokeroo/templates/04-scaledobject.yml.j2 new file mode 100644 index 00000000..d04d73b5 --- /dev/null +++ b/services/playbooks/roles/brokeroo/templates/04-scaledobject.yml.j2 @@ -0,0 +1,49 @@ +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: "{{ brokeroo_app_name }}-scaler" + namespace: "{{ brokeroo_namespace }}" +spec: + scaleTargetRef: + name: brokeroo + minReplicaCount: 3 + maxReplicaCount: 20 + pollingInterval: 10 + cooldownPeriod: 60 + advanced: + horizontalPodAutoscalerConfig: + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 10 + periodSeconds: 60 + - type: Pods + value: 2 + periodSeconds: 60 + selectPolicy: Min + scaleUp: + stabilizationWindowSeconds: 0 + policies: + - type: Percent + value: 100 + periodSeconds: 15 + - type: Pods + value: 4 + periodSeconds: 15 + selectPolicy: Max + triggers: + - type: rabbitmq + metadata: + protocol: amqp + mode: QueueLength + value: "500" + queueName: "{{ brokeroo_amqp_queue_name }}" + activationValue: "10" + authenticationRef: + name: rabbitmq-auth + - type: cpu + metricType: Utilization + metadata: + value: "70" diff --git a/services/playbooks/roles/brokeroo/templates/05-poddistruptionbudget.yml.j2 b/services/playbooks/roles/brokeroo/templates/05-poddistruptionbudget.yml.j2 new file mode 100644 index 00000000..b698dcda --- /dev/null +++ b/services/playbooks/roles/brokeroo/templates/05-poddistruptionbudget.yml.j2 @@ -0,0 +1,10 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: "{{ brokeroo_app_name }}-pdb" + namespace: "{{ brokeroo_namespace }}" +spec: + minAvailable: 1 + selector: + matchLabels: + app: "{{ brokeroo_app_name }}" diff --git a/services/playbooks/roles/grafana/defaults/main.yml b/services/playbooks/roles/grafana/defaults/main.yml index 7453eeff..5f08e69d 100644 --- a/services/playbooks/roles/grafana/defaults/main.yml +++ b/services/playbooks/roles/grafana/defaults/main.yml @@ -7,10 +7,10 @@ grafana_admin_user: admin grafana_admin_password: admin123 grafana_db_type: postgres -grafana_db_host: tsdb-rw.data.svc.cluster.local:5432 -grafana_db_name: tracker_db -grafana_db_user: admin -grafana_db_password: administrator +grafana_db_host: grafana-postgres:5432 +grafana_db_name: grafana +grafana_db_user: grafana +grafana_db_password: grafana grafana_port: 3000 grafana_service_ip: 10.20.30.56 diff --git a/services/playbooks/roles/grafana/tasks/main.yml b/services/playbooks/roles/grafana/tasks/main.yml index 45d9183f..40d2cdf7 100644 --- a/services/playbooks/roles/grafana/tasks/main.yml +++ b/services/playbooks/roles/grafana/tasks/main.yml @@ -6,6 +6,110 @@ kind: Namespace state: present +- name: "Create Postgres Secret" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + namespace: "{{ grafana_namespace }}" + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: grafana-postgres-secret + type: Opaque + data: + postgres-user: "{{ 'grafana' | b64encode }}" + postgres-password: "{{ 'grafana' | b64encode }}" + postgres-db: "{{ 'grafana' | b64encode }}" + +- name: "Create Postgres PVC" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + namespace: "{{ grafana_namespace }}" + state: present + definition: + apiVersion: v1 + kind: "PersistentVolumeClaim" + metadata: + name: grafana-postgres-pvc + spec: + accessModes: + - ReadWriteOnce + + storageClassName: "{{ grafana_storage_class }}" + resources: + requests: + storage: 5Gi + +- name: "Deploy Postgres Deployment" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + namespace: "{{ grafana_namespace }}" + state: present + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: grafana-postgres + spec: + replicas: 1 + selector: + matchLabels: + app: grafana-postgres + template: + metadata: + labels: + app: grafana-postgres + spec: + containers: + - name: postgres + image: postgres:17 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: grafana-postgres-secret + key: postgres-user + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: grafana-postgres-secret + key: postgres-password + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: grafana-postgres-secret + key: postgres-db + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: grafana-postgres-pvc + +- name: "Create Postgres Service" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + namespace: "{{ grafana_namespace }}" + state: present + definition: + apiVersion: v1 + kind: Service + metadata: + name: grafana-postgres + spec: + type: ClusterIP + selector: + app: grafana-postgres + ports: + - port: 5432 + targetPort: 5432 + - name: "Create Grafana Persistent Volume Claim" kubernetes.core.k8s: kubeconfig: /etc/rancher/k3s/k3s.yaml diff --git a/services/playbooks/roles/grafana/templates/01-deployment.yml.j2 b/services/playbooks/roles/grafana/templates/01-deployment.yml.j2 index 5ca30ed0..6926b4fe 100644 --- a/services/playbooks/roles/grafana/templates/01-deployment.yml.j2 +++ b/services/playbooks/roles/grafana/templates/01-deployment.yml.j2 @@ -23,7 +23,7 @@ spec: containers: - name: {{ grafana_app_name }} image: {{ grafana_image }} - imagePullPolicy: Always + imagePullPolicy: IfNotPresent ports: - containerPort: {{ grafana_port }} env: @@ -41,6 +41,13 @@ spec: value: "{{ grafana_db_user }}" - name: GF_DATABASE_PASSWORD value: "{{ grafana_db_password }}" + - name: GF_SERVER_DOMAIN + value: "trackeroo.rblabs.net" + - name: GF_SERVER_ROOT_URL + value: "https://trackeroo.rblabs.net/dashboards/" + - name: GF_SERVER_SERVE_FROM_SUB_PATH + value: "true" + volumeMounts: - name: grafana-storage mountPath: /var/lib/grafana diff --git a/services/playbooks/roles/kafka/templates/03-kafka-deployment.yml.j2 b/services/playbooks/roles/kafka/templates/03-kafka-deployment.yml.j2 index 5bceea53..342e2e5b 100644 --- a/services/playbooks/roles/kafka/templates/03-kafka-deployment.yml.j2 +++ b/services/playbooks/roles/kafka/templates/03-kafka-deployment.yml.j2 @@ -25,6 +25,13 @@ spec: containerPort: {{ kafka_port }} - name: plaintext-host containerPort: {{ kafka_host_port }} + resources: + requests: + memory: "2Gi" + cpu: "1" + limits: + memory: "3Gi" + cpu: "2" env: - name: KAFKA_BROKER_ID value: "{{ kafka_broker_id }}" @@ -38,6 +45,8 @@ spec: value: "PLAINTEXT" - name: KAFKA_LOG4J_ROOT_LOGLEVEL value: "INFO" + - name: KAFKA_HEAP_OPTS + value: "-Xms1G -Xmx2G" readinessProbe: tcpSocket: port: {{ kafka_port }} diff --git a/services/playbooks/roles/keda/defaults/main.yml b/services/playbooks/roles/keda/defaults/main.yml new file mode 100644 index 00000000..d24d2bb0 --- /dev/null +++ b/services/playbooks/roles/keda/defaults/main.yml @@ -0,0 +1,2 @@ +keda_namespace: keda +keda_version: 2.18.0 diff --git a/services/playbooks/roles/keda/tasks/main.yml b/services/playbooks/roles/keda/tasks/main.yml new file mode 100644 index 00000000..21023d92 --- /dev/null +++ b/services/playbooks/roles/keda/tasks/main.yml @@ -0,0 +1,35 @@ +- name: Install KEDA to autoscale + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + state: present + src: "https://github.com/kedacore/keda/releases/download/v{{ keda_version }}/keda-{{ keda_version }}.yaml" + +- name: Wait for KEDA operator to be ready + kubernetes.core.k8s_info: + kubeconfig: /etc/rancher/k3s/k3s.yaml + api_version: v1 + kind: Pod + namespace: "{{ keda_namespace }}" + label_selectors: + - app=keda-operator + register: keda_pods + until: + - keda_pods.resources | length > 0 + - keda_pods.resources[0].status.phase == "Running" + retries: 30 + delay: 10 + +- name: Wait for KEDA metrics server to be ready + kubernetes.core.k8s_info: + kubeconfig: /etc/rancher/k3s/k3s.yaml + api_version: v1 + kind: Pod + namespace: "{{ keda_namespace }}" + label_selectors: + - app=keda-metrics-apiserver + register: keda_metrics_pods + until: + - keda_metrics_pods.resources | length > 0 + - keda_metrics_pods.resources[0].status.phase == "Running" + retries: 30 + delay: 10 diff --git a/services/playbooks/roles/mongo/files/init.js b/services/playbooks/roles/mongo/files/init.js index cc60bb57..eb941942 100644 --- a/services/playbooks/roles/mongo/files/init.js +++ b/services/playbooks/roles/mongo/files/init.js @@ -55,15 +55,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8414acfcb9165b1", name: "wonderful_wirth", - status: { connected: true, last_message: "2025-08-02T16:40:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", private_key: "m9mFoEWSL+GwWtoyPZsF2q4HpwLddf7JfsxmedN8h+Y=", created_at: ISODate("2025-08-18T03:36:46Z"), }, { _id: "trk-18608d12c843cfc86452c8bc", name: "xenodochial_carmack", - status: { connected: false, last_message: "2025-08-18T20:34:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "QNiUX1DdBq9hNGToa8ynDbXZRF9aDbO4LWvm7QspLo8=", created_at: ISODate("2025-08-08T07:04:46Z"), @@ -71,7 +71,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8443834a667d6c2", name: "merry_schwinger", - status: { connected: false, last_message: "2025-08-10T14:36:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "mS+C+G6Ut4mrbmz6v3EA5Fm7OvtcwrcRzvLOoIyIRDw=", created_at: ISODate("2025-08-08T17:05:46Z"), @@ -79,7 +79,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8445101b3d3a3b8", name: "thirsty_henry", - status: { connected: false, last_message: "2025-08-22T18:58:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "4kKuUQSMJE9RckmbCFReQy5gtbRpY3sCgDjcTnxBxwI=", created_at: ISODate("2025-08-04T05:03:46Z"), @@ -87,7 +87,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8446943ffbf2df9", name: "admiring_bohr", - status: { connected: true, last_message: "2025-08-06T01:33:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "5mp9W9wUVd50eqGqC/JpW/0/pG6wBrAsivSPg0bcC2M=", created_at: ISODate("2025-08-25T16:35:46Z"), @@ -95,7 +95,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84480bd0ad4656f", name: "keen_bohr", - status: { connected: true, last_message: "2025-08-16T05:31:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "/3tiwn6woUzwRGe/TUqOZQNGvYWHYSHj4d/Fpq9eTS8=", created_at: ISODate("2025-08-01T14:09:46Z"), @@ -103,7 +103,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8449838a8eb28eb", name: "clever_fermi", - status: { connected: true, last_message: "2025-08-08T23:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "pveqq6guV0EAQANRpMOQq5Yd8iSaZSH6QUMynO0ke6g=", created_at: ISODate("2025-08-05T16:46:46Z"), @@ -111,7 +111,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844b01d0d668fce", name: "brave_schrodinger", - status: { connected: false, last_message: "2025-08-04T16:28:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "+NrhtHayxZYPZnqA+C1C6uth3bORje/ka5uxXZ3E2zQ=", created_at: ISODate("2025-08-04T20:21:46Z"), @@ -119,7 +119,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844cb262515236b", name: "youthful_stallman", - status: { connected: false, last_message: "2025-08-22T21:24:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "dkvOCYpkWXr3WZs62jH9LfjOAbhm0TapSNQvcIzijLA=", created_at: ISODate("2025-08-04T10:00:46Z"), @@ -127,7 +127,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844e58e640b9ea1", name: "friendly_curie", - status: { connected: true, last_message: "2025-08-21T03:51:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "1l0SNynykdBFZVNfGlkTYiniGlmk5KVgyW3bp8HI6xg=", created_at: ISODate("2025-08-05T23:57:46Z"), @@ -135,7 +135,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c844fccd26144714", name: "charming_dirac", - status: { connected: false, last_message: "2025-07-31T23:08:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "/Vi9D/RMiWilNw0jHA/fv4DbGwAEbqRAgQJulPl4DTQ=", created_at: ISODate("2025-08-08T18:10:46Z"), @@ -143,7 +143,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8451452004ec96d", name: "iron_franklin", - status: { connected: true, last_message: "2025-08-02T12:47:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "V0exZwne86ptOGifjuWX7QmGjpN1W62szV6+WxX9m5s=", created_at: ISODate("2025-08-07T12:55:46Z"), @@ -151,23 +151,23 @@ db.devices.insertMany([ { _id: "trk-18608d12c8456a397013ce6b", name: "gentle_church", - status: { connected: false, last_message: "2025-08-11T23:09:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "KH+3BS3QnEec8vME0ToemUn3mK9i0bwCg1ZlvgnRfkU=", created_at: ISODate("2025-08-15T20:23:46Z"), }, { _id: "trk-18608d12c84581a0b80ca1c5", name: "practical_michelson", - status: { connected: false, last_message: "2025-08-11T11:15:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "ab3Wu7Glx9xhfSFUNRKzi1CZPfPeiCI7ujc/iJmYWpI=", created_at: ISODate("2025-08-04T21:37:46Z"), }, { _id: "trk-18608d12c845ad105b9a60db", name: "phenomenal_born", - status: { connected: false, last_message: "2025-08-23T09:48:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "h3dT1JuyA/MhH1/o/n/IiPRFEyyEvXlMl3ZNtrCHZwY=", created_at: ISODate("2025-08-06T01:48:46Z"), @@ -175,7 +175,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c845c8050d2973c1", name: "agitated_planck", - status: { connected: false, last_message: "2025-08-20T06:43:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "l/TRQN08IpMfwVHCWHpcNQ/SBuRWLeNcuJZDf28697c=", created_at: ISODate("2025-08-12T01:20:46Z"), @@ -183,7 +183,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c845df659619c7e6", name: "determined_kelvin", - status: { connected: true, last_message: "2025-08-16T14:01:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "MlxmbBIfcGNpReS6GUgLDmTRrDDQQ6rlLxj4Eggrfjo=", created_at: ISODate("2025-08-10T07:50:46Z"), @@ -191,7 +191,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c845f5d103e908ad", name: "thoughtful_weber", - status: { connected: false, last_message: "2025-08-18T05:54:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "WGmlcdPBvesFWds8BZ4UR4k7ze/25P1GpWhXaSmgNDQ=", created_at: ISODate("2025-08-04T22:08:46Z"), @@ -199,15 +199,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8460bf33167dbdc", name: "hyper_shockley", - status: { connected: false, last_message: "2025-08-17T20:07:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "BzHUYgawXJvf458ZW6fLaKxbTEoOXBI1pgI4Z5ebMuM=", created_at: ISODate("2025-08-17T07:01:46Z"), }, { _id: "trk-18608d12c8462309d5e746cc", name: "pious_planck", - status: { connected: true, last_message: "2025-08-12T19:59:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "DMAF8lJbDW/hdBvKttHueeokLXgQ9nlcTQBDMfypO4w=", created_at: ISODate("2025-08-15T02:45:46Z"), @@ -215,7 +215,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84640a54b9bd528", name: "heuristic_fermi", - status: { connected: true, last_message: "2025-08-02T15:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "+crZGHsZ6r5ZN0UMsB0JFK/hveE6E267uzcAGdkQAZQ=", created_at: ISODate("2025-08-09T02:32:46Z"), @@ -223,7 +223,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c846577d4ec97cfb", name: "sharp_edison", - status: { connected: true, last_message: "2025-08-14T03:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "eW16LWeR4CcKm0mRqkBnv9lU6WYKXyzS8csOmoTwt+c=", created_at: ISODate("2025-08-21T01:54:46Z"), @@ -231,7 +231,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8466e529b599dc4", name: "romantic_hertz", - status: { connected: true, last_message: "2025-08-30T03:20:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "kbRJ702ADyEPki+ko7MbOZ7AGvhKPSql5A7fK6MGFGI=", created_at: ISODate("2025-08-17T20:13:46Z"), @@ -239,7 +239,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84685251cdaf35c", name: "gallant_whitehead", - status: { connected: true, last_message: "2025-08-05T15:55:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "ZeGHStsNcbp56gDEk+nasq/BaaLklfzXfuG8BcX+lrs=", created_at: ISODate("2025-08-28T00:41:46Z"), @@ -247,7 +247,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8469c00840cc49b", name: "laughing_rutherford", - status: { connected: false, last_message: "2025-08-13T10:56:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "n0LNbNhaIaW42aBkqCwDM0cJSwcwL8McL8im5CP91v4=", created_at: ISODate("2025-08-29T17:24:46Z"), @@ -255,7 +255,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c846b3600123a417", name: "sad_marconi", - status: { connected: true, last_message: "2025-08-05T23:52:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "g2D4mb6ezOrfK/ft41lACByyeU4IQVF6RWlB/0P7zhc=", created_at: ISODate("2025-08-15T07:38:46Z"), @@ -263,7 +263,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c846ca7bab489e30", name: "clever_newton", - status: { connected: true, last_message: "2025-08-18T06:39:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "hGz/4/osEsLT/5Q53TTlID+wA54Xb2p8Cxpj05jZsik=", created_at: ISODate("2025-08-18T00:25:46Z"), @@ -271,23 +271,23 @@ db.devices.insertMany([ { _id: "trk-18608d12c846e0a3e38974e5", name: "dazzling_maxwell", - status: { connected: false, last_message: "2025-08-10T04:46:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "KSG9WiZ6pWINfm9FH9vS/GSo3t1TmLf+eE5eGhU+Hik=", created_at: ISODate("2025-08-05T06:51:46Z"), }, { _id: "trk-18608d12c847064ce1bdf7ee", name: "elegant_lagrange", - status: { connected: true, last_message: "2025-08-28T18:21:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "tlrnwpslaBwOChP+dAFPQ2sfItsoKERU+MR33NHk9yQ=", created_at: ISODate("2025-08-19T09:04:46Z"), }, { _id: "trk-18608d12c8472b9f0905b3ad", name: "ecstatic_riemann", - status: { connected: true, last_message: "2025-08-05T19:27:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "5v94oQP8pNQZrOZrYNTTl5c67qyrPDUJE4eZTfuBjVU=", created_at: ISODate("2025-08-08T11:59:46Z"), @@ -295,31 +295,31 @@ db.devices.insertMany([ { _id: "trk-18608d12c84742df27b88f6b", name: "cranky_ampere", - status: { connected: false, last_message: "2025-08-19T19:18:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "cS34G/an+Z+MM1M5P5dh48B+y4kjyy0eI/8cw4ClkT4=", created_at: ISODate("2025-08-08T18:36:46Z"), }, { _id: "trk-18608d12c84759c8a9ef04f8", name: "energetic_hilbert", - status: { connected: true, last_message: "2025-08-25T05:04:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "EInJWD1jh2t06pz3D5WZjspYvxzqs57NRKAxVhe9uIk=", created_at: ISODate("2025-08-28T01:57:46Z"), }, { _id: "trk-18608d12c84770f5153b97ef", name: "amazing_turing", - status: { connected: false, last_message: "2025-08-10T10:51:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "X9Lx6+KllLFtNDyvj/466CrOQmF6UewS+3xs9j18ILA=", created_at: ISODate("2025-08-03T11:49:46Z"), }, { _id: "trk-18608d12c84786f21e4edc5e", name: "happy_einstein", - status: { connected: true, last_message: "2025-08-21T12:01:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "wzTOvpERsSN6QO4jgUD3XuRtcdqQLItdjSxSgrKmcoo=", created_at: ISODate("2025-08-17T21:31:46Z"), @@ -327,7 +327,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8479d828433ff9a", name: "funny_peano", - status: { connected: true, last_message: "2025-08-11T05:13:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "cqseF4l1F5lAgCe7bD6EP+rEE+L1wFhc+10MQV48UJo=", created_at: ISODate("2025-08-24T04:47:46Z"), @@ -335,7 +335,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c847b3e74258eb03", name: "vibrant_thompson", - status: { connected: false, last_message: "2025-08-15T07:02:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "O0XXGhe7rp/EsEYyXNAGIKjJk5T+FXCAdebGqh/dluo=", created_at: ISODate("2025-08-16T09:54:46Z"), @@ -343,15 +343,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8480b18fe88b4af", name: "patient_wu", - status: { connected: false, last_message: "2025-08-21T22:36:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "bHzGkyOqJLG9GAnkC25B2OLoXhQhIUu3bPVIE9sBlzU=", created_at: ISODate("2025-08-24T18:56:46Z"), }, { _id: "trk-18608d12c84823aa3e06b7da", name: "epic_cantor", - status: { connected: true, last_message: "2025-08-07T10:15:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "G7MRIMBP24R0xdEXZRIV3me9dDDFRdxtTXuLXe28494=", created_at: ISODate("2025-08-02T10:17:46Z"), @@ -359,15 +359,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8483aeb4dce2724", name: "hopeful_bardeen", - status: { connected: true, last_message: "2025-08-30T06:16:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "Xd5a7XzvWCVjDtduORbuAH1sykFtGHcvAhK/4BadcaI=", created_at: ISODate("2025-08-06T23:01:46Z"), }, { _id: "trk-18608d12c8486259917b5535", name: "frosty_dedekind", - status: { connected: false, last_message: "2025-08-07T10:35:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "I0ahyNHzwY2ax1RTIwkdAW1+4eErU6s1WISzCED2ImI=", created_at: ISODate("2025-08-05T02:16:46Z"), @@ -375,15 +375,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8487a58b5c5651d", name: "heartwarming_bose", - status: { connected: true, last_message: "2025-08-15T11:05:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "LsgbCoMQG+opSUt9JrsiDIL4Z1k5VU+WnTGajUsfRMU=", created_at: ISODate("2025-08-04T01:01:46Z"), }, { _id: "trk-18608d12c848919ae0a5ac8f", name: "kind_torvalds", - status: { connected: false, last_message: "2025-08-02T19:55:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "ij0mk0VXVo7nsb0cqpGJX0V5oChhDa830+CJ72ZW8xk=", created_at: ISODate("2025-08-20T23:21:46Z"), @@ -391,15 +391,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c848b620cb14aa25", name: "fascinated_noether", - status: { connected: false, last_message: "2025-08-20T21:05:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "XJVLbd6UO12QkuIGo3EbZGXjG4xPKSvliiOLRLnMZmI=", created_at: ISODate("2025-08-09T07:11:46Z"), }, { _id: "trk-18608d12c848d48480a5d5b6", name: "puzzled_fizeau", - status: { connected: true, last_message: "2025-08-22T09:43:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "ceSr36+tty8/YgIQuT7V66kMAXhi+K2JPN7EqEF7r3Y=", created_at: ISODate("2025-08-02T11:41:46Z"), @@ -407,7 +407,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c848eb34f8760e19", name: "competent_rutherford", - status: { connected: true, last_message: "2025-08-04T03:29:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "e8kkc8+QotgP718tO8zL2+ukDwUiNfSmRl3UQFKnEwc=", created_at: ISODate("2025-08-02T23:24:46Z"), @@ -415,7 +415,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8490334a6303366", name: "pedantic_pauli", - status: { connected: true, last_message: "2025-08-30T09:45:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "kXVOIOGZGLfPzYCPK55aVbR15kbwTZr9ezA6zSxHEig=", created_at: ISODate("2025-08-05T01:38:46Z"), @@ -423,7 +423,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8491a91b1f888a9", name: "magical_dirac", - status: { connected: false, last_message: "2025-08-08T15:21:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "C9biy8JKSu1nQCKQxGTjuxkjHr4W46086h3KwjVBr/I=", created_at: ISODate("2025-08-05T22:46:46Z"), @@ -431,7 +431,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84931d6fa2d6232", name: "serene_tesla", - status: { connected: false, last_message: "2025-08-03T19:17:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "xPGoSwT+dTYd6ZX1++0+CJC8iEwVscx3VoDDI+LXWw8=", created_at: ISODate("2025-08-05T07:06:46Z"), @@ -439,15 +439,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c8494897112ba78e", name: "peaceful_pascal", - status: { connected: true, last_message: "2025-08-05T08:37:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "WAVVSnHd8206VRwJOtgHDuUGDf0orqqYBIqodJA9L1I=", created_at: ISODate("2025-08-15T05:44:46Z"), }, { _id: "trk-18608d12c8495fd71857ce3f", name: "stoic_ohm", - status: { connected: false, last_message: "2025-08-22T12:36:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "vzDwSMp/L6oyVEyYj3RKTY9UKyBj3V7LvhCD6cqe/nI=", created_at: ISODate("2025-08-22T20:43:46Z"), @@ -455,7 +455,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84976fefd57d1ff", name: "fearless_galois", - status: { connected: false, last_message: "2025-08-10T16:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "PoHtJ2l8pTjJPJctSMrIII20agqTIk4LpVoiIdNLzgo=", created_at: ISODate("2025-08-03T14:47:46Z"), @@ -463,7 +463,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c8498d777941e066", name: "distracted_planck", - status: { connected: true, last_message: "2025-08-24T07:49:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "W8F6g6HmjQAP3lwJjRqAfdQOIOsSq1vzEVbHMS8ZUAo=", created_at: ISODate("2025-08-21T20:41:46Z"), @@ -471,7 +471,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c849a50c845c25a9", name: "optimistic_babbage", - status: { connected: true, last_message: "2025-08-12T09:06:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "zpX14uHmth5/ZzzNaL68qeoHLJRZjT3huaYns0LmnWU=", created_at: ISODate("2025-08-06T17:19:46Z"), @@ -479,23 +479,23 @@ db.devices.insertMany([ { _id: "trk-18608d12c849bd6d485879c1", name: "tender_faraday", - status: { connected: true, last_message: "2025-08-10T12:57:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "O8XxluA24EC4bsgPrBHWCwS8q8GmBK7mv0byordbYc8=", created_at: ISODate("2025-08-13T13:09:46Z"), }, { _id: "trk-18608d12c849d409351613c2", name: "jaunty_pauling", - status: { connected: true, last_message: "2025-08-27T07:44:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "OLbAlVzxkhzr2wlaO/WV8fCB/m4jJQ5AIfrJ0tUgF0A=", created_at: ISODate("2025-08-20T22:21:46Z"), }, { _id: "trk-18608d12c84a2e54a6ffdd76", name: "suspicious_volta", - status: { connected: true, last_message: "2025-08-03T18:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "S30Z2mlkvsUR6yH8nULY8RqPhmUcxEHEcEVuP7ePmSM=", created_at: ISODate("2025-08-10T10:53:46Z"), @@ -503,7 +503,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84a5f766256c9e2", name: "serene_hopper", - status: { connected: true, last_message: "2025-08-13T10:31:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "DTOj/Y2DiO0Ju5vwxOUgBtwXB5579pz30EEMYun7z00=", created_at: ISODate("2025-08-18T16:45:46Z"), @@ -511,23 +511,23 @@ db.devices.insertMany([ { _id: "trk-18608d12c84a892c3aad3955", name: "eager_gauss", - status: { connected: false, last_message: "2025-08-06T19:07:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "Dl4fLpzCBXqNHjtrgm+5jrj+tSXPZRxttltQPvr/JcY=", created_at: ISODate("2025-08-29T10:30:46Z"), }, { _id: "trk-18608d12c84aa049b09a2169", name: "eager_darwin", - status: { connected: true, last_message: "2025-08-10T19:58:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "EC7AoSY05EyPbRVEl/eGTSfESGXZZ4OWF5FsPcaKdTU=", created_at: ISODate("2025-08-24T18:39:46Z"), }, { _id: "trk-18608d12c84ab6d05b8082ad", name: "hardcore_bell", - status: { connected: true, last_message: "2025-08-25T03:22:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "Ar3WbxWHU8l2uyOZPnqJffGQ9vJpXAovM/ZoNRYd9RA=", created_at: ISODate("2025-08-20T22:33:46Z"), @@ -535,7 +535,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84acd0d1c7fc74a", name: "enchanting_poincare", - status: { connected: false, last_message: "2025-08-12T07:20:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "0HbmI0I5esubDm4b6XvzgLgHJtXVxGTeIt4EiHJAKc0=", created_at: ISODate("2025-08-08T23:41:46Z"), @@ -543,7 +543,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ae3dffff2e52d", name: "proud_morley", - status: { connected: true, last_message: "2025-08-17T10:23:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "NYrTqrlsGqPo9r6nKF2+bVd+QshvHb00Z676LpK3R00=", created_at: ISODate("2025-08-30T11:59:46Z"), @@ -551,15 +551,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84afa2c10aa6c55", name: "inspiring_bardeen", - status: { connected: false, last_message: "2025-08-15T18:29:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "6dU2H5RdD5VaZyXi5ZWaj5rkydlEV5f2PS1l9+fvL+c=", created_at: ISODate("2025-08-07T16:21:46Z"), }, { _id: "trk-18608d12c84b11c367999861", name: "objective_glashow", - status: { connected: true, last_message: "2025-08-26T14:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "uW2kufM6EeR+tpJHDYRmd/5CPGLMn84jMBsu6mhWtRU=", created_at: ISODate("2025-08-19T03:27:46Z"), @@ -567,15 +567,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b2feca3792861", name: "modest_dyson", - status: { connected: false, last_message: "2025-08-25T10:25:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "5XYG/w/clJolar2hAO3n4D9+fJdckv7i0ZO0S2m2KRo=", created_at: ISODate("2025-08-02T00:09:46Z"), }, { _id: "trk-18608d12c84b469627a640d9", name: "boring_heisenberg", - status: { connected: false, last_message: "2025-08-25T03:19:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "EHPvNh8KnmQNbD8Kef2UZh+07Y6DiHKpNwTuuIcyemo=", created_at: ISODate("2025-08-10T16:52:46Z"), @@ -583,7 +583,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b5cae4cff3133", name: "strange_ampere", - status: { connected: true, last_message: "2025-08-28T04:31:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "fUqc7PB2fR3likQIUigerur96bUzz7dQZE/lMUiMJHY=", created_at: ISODate("2025-08-08T07:02:46Z"), @@ -591,7 +591,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b74702fe3e85c", name: "faithful_hardy", - status: { connected: false, last_message: "2025-08-27T20:37:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "etflzQEpsxaJqAlO2o/G45T+s80cWWaSV0KPobuWWFE=", created_at: ISODate("2025-08-02T15:50:46Z"), @@ -599,7 +599,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84b8b83746dbc8b", name: "eloquent_leibniz", - status: { connected: true, last_message: "2025-08-14T14:03:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "nwdfO4d3o5nmJsO/i8xQrgcjsa2KNSQQ9PQzgMNbEc8=", created_at: ISODate("2025-08-13T23:17:46Z"), @@ -607,23 +607,23 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ba2911c4a675e", name: "cool_ohm", - status: { connected: false, last_message: "2025-08-12T06:22:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "7TXa0utsnnJXx7tHWYt5KC8nwv1xThn8iy12KI2zOvE=", created_at: ISODate("2025-08-12T01:49:46Z"), }, { _id: "trk-18608d12c84bde9e8b791f18", name: "fervent_abel", - status: { connected: true, last_message: "2025-08-03T23:36:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "1doRBvIJsrXQIG1Ui9albOMUHxMjtrjU6VvNLZTZQak=", created_at: ISODate("2025-08-29T22:05:46Z"), }, { _id: "trk-18608d12c84bf63dcc1fb3e7", name: "groovy_shannon", - status: { connected: true, last_message: "2025-08-10T16:58:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "4NnTHCSG64xQf807OjIJlzc+MdgrOv9/a83gDgRzUwQ=", created_at: ISODate("2025-08-16T23:38:46Z"), @@ -631,7 +631,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c23101fa0a8b6", name: "grieving_wiener", - status: { connected: true, last_message: "2025-08-14T09:38:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "BpChhURWcEyErTfXhnx6J9gg4L+Qy4FyWzdeL6e27u4=", created_at: ISODate("2025-08-14T21:20:46Z"), @@ -639,15 +639,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c3ae0fd8aad7e", name: "optimized_higgs", - status: { connected: false, last_message: "2025-08-21T14:05:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "csXqVuqw2AXm2A46aCt97ytsUzs5pA6t5Jnit25JFkI=", created_at: ISODate("2025-08-15T19:27:46Z"), }, { _id: "trk-18608d12c84c5151bb792931", name: "boring_wozniak", - status: { connected: true, last_message: "2025-08-08T02:27:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "X3CHEXehWzXvRfQAJ6GGCoeEbVPcdX95JCzT3uEBYSs=", created_at: ISODate("2025-08-02T10:22:46Z"), @@ -655,15 +655,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c678173241105", name: "curious_feynman", - status: { connected: false, last_message: "2025-08-21T18:29:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "XxN0HpCbfzUE2AMRPcteDPSRLT5YgnAy4ILOxwIzm/g=", created_at: ISODate("2025-08-26T19:07:46Z"), }, { _id: "trk-18608d12c84c7e6385d059d6", name: "noble_weinberg", - status: { connected: true, last_message: "2025-08-07T01:06:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "SZzpdjeGSaaWEBBa8apWtQJdwscbjyJ4jC/nXgnxJu4=", created_at: ISODate("2025-08-21T16:34:46Z"), @@ -671,7 +671,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84c9470140d32f6", name: "upbeat_ritchie", - status: { connected: false, last_message: "2025-08-10T09:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "65mhzIpZvOx51yE6nPxQgjgy3rwlv13lMSM5X7vEFeQ=", created_at: ISODate("2025-08-01T12:55:46Z"), @@ -679,7 +679,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84caba1375500b4", name: "quizzical_doppler", - status: { connected: true, last_message: "2025-08-11T14:42:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "t/9N9FYcN12suJ/nZl5q6jC7OAWv0h2Pow6vJBSr4dA=", created_at: ISODate("2025-08-29T00:09:46Z"), @@ -687,7 +687,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84cc3de436f7bde", name: "silly_westinghouse", - status: { connected: true, last_message: "2025-08-28T04:22:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "Ba4MVIb1YQL6dm2eYRiJOX0CljrdeuFtAI2HZ3nBtsE=", created_at: ISODate("2025-08-02T07:07:46Z"), @@ -695,15 +695,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84cdbbc4c9327d2", name: "dreamy_tesla", - status: { connected: true, last_message: "2025-08-12T22:50:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "/kDru3xkiT7oAfKFD4Mp6nVl5IIFEMXponchC0ojKlo=", created_at: ISODate("2025-08-01T00:21:46Z"), }, { _id: "trk-18608d12c84cf3df4741680f", name: "jovial_mendeleev", - status: { connected: true, last_message: "2025-08-06T08:10:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "voqhD27X8L+VW4e/Op93AyazBBx/a7mqhTbAsvn6tnc=", created_at: ISODate("2025-08-26T23:38:46Z"), @@ -711,7 +711,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d0b5fc332f041", name: "great_kolmogorov", - status: { connected: false, last_message: "2025-08-14T09:59:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "TH/tO2mmlgP3dnH6h+ickwu8IaEajnm+DpvDLcAZDC8=", created_at: ISODate("2025-08-29T11:27:46Z"), @@ -719,15 +719,15 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d2247a477b6ff", name: "interesting_watson", - status: { connected: true, last_message: "2025-08-20T09:29:46Z" }, - device_type: "other", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "ZBNn287pTaxPwGShBOOICVcZDmQbcdr2vtmLYbelgoU=", created_at: ISODate("2025-08-19T19:46:46Z"), }, { _id: "trk-18608d12c84d385e1d5c7bbc", name: "zealous_berners", - status: { connected: true, last_message: "2025-08-04T11:23:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "5Wx/7819jHalM4yOsTrhtswnTS8ItR5/n0BEaYFotQM=", created_at: ISODate("2025-08-18T18:58:46Z"), @@ -735,7 +735,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d62f4adb3c923", name: "condescending_volta", - status: { connected: true, last_message: "2025-08-15T00:32:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "CKwOOv0LRsGpDURy9iXE9tnUFdjtI6ZqtAFoJkal360=", created_at: ISODate("2025-08-26T02:07:46Z"), @@ -743,7 +743,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84d9ce75be19956", name: "adoring_morse", - status: { connected: false, last_message: "2025-08-28T04:09:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "iL0T7kaXpwPrO6Z0mA7Kk2Gs+tHEBl6fNSbFV3rSgVM=", created_at: ISODate("2025-08-04T14:20:46Z"), @@ -751,23 +751,23 @@ db.devices.insertMany([ { _id: "trk-18608d12c84db5aa45ada38e", name: "elastic_fourier", - status: { connected: true, last_message: "2025-08-22T06:00:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "8loHVYewF31YJGSlkcOrMZpfc5eQ2DFhuvpne8M68Xc=", created_at: ISODate("2025-08-09T06:44:46Z"), }, { _id: "trk-18608d12c84dccaa3ac843bd", name: "thrilled_gauss", - status: { connected: false, last_message: "2025-08-14T07:07:46Z" }, - device_type: "public_transport", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", private_key: "jhESuO3AwXCtX0TcH5E9l91DDRUUx3SIErqpjt+SpUk=", created_at: ISODate("2025-08-25T19:51:46Z"), }, { _id: "trk-18608d12c84de3a8df5efbcc", name: "goofy_markov", - status: { connected: false, last_message: "2025-08-20T02:12:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "1bmj0Qvq4D9EsyHZJpITpEqEBBrOcD9KsiUhDX4X7R4=", created_at: ISODate("2025-08-27T19:33:46Z"), @@ -775,7 +775,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84dfa6590d8fbda", name: "relaxed_turing", - status: { connected: true, last_message: "2025-08-29T02:47:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "bMvQyF6Lu/h5zYPEn4TBAOAxjVj67QTXl3ifZYf3S7s=", created_at: ISODate("2025-08-09T23:16:46Z"), @@ -783,7 +783,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e12b161fb52f1", name: "exciting_godel", - status: { connected: true, last_message: "2025-08-19T23:11:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "L+ZecOMBWt0L7Ix8sPW8MZpDskk30MZydLOV1av++fk=", created_at: ISODate("2025-08-30T12:59:46Z"), @@ -791,7 +791,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e2a4a52da79d9", name: "sweet_galvani", - status: { connected: false, last_message: "2025-08-29T21:21:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "dDVOk5b+i7+npM3o7icJocZSNFaed64RKX6HuxK5TTk=", created_at: ISODate("2025-08-22T08:08:46Z"), @@ -799,7 +799,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e473a6d897698", name: "fabulous_ramanujan", - status: { connected: false, last_message: "2025-08-18T06:15:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "n/7uqrA1Xh7uzYaX4BouodyLFlU/HWXMF3Gjd5gRN6g=", created_at: ISODate("2025-08-19T16:05:46Z"), @@ -807,7 +807,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e67b817e2a8fd", name: "trusting_knuth", - status: { connected: true, last_message: "2025-08-28T09:04:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "private_transport", private_key: "frrz+vzXLVQV4HHhZf7MFMPo4iGIJQWsgSPIkHKJ8OE=", created_at: ISODate("2025-08-01T15:20:46Z"), @@ -815,7 +815,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e7f725b9dd529", name: "quirky_shannon", - status: { connected: false, last_message: "2025-08-16T17:49:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "443NJMtr5p+i0QIA8th2B9QTsh3aKhI1p51G0uwN1iQ=", created_at: ISODate("2025-08-09T21:36:46Z"), @@ -823,7 +823,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84e95effa9dc925", name: "nervous_hawking", - status: { connected: true, last_message: "2025-08-15T23:40:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "food", private_key: "XTAux5J/tThXxN+5AoXR61Pxm0nfvJ92SOpCVo9HVpk=", created_at: ISODate("2025-08-18T09:24:46Z"), @@ -831,7 +831,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84eaef489342f7e", name: "exotic_turing", - status: { connected: true, last_message: "2025-08-03T15:10:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "public_transport", private_key: "rrNb0cdFEBeDwGfwZXv8U1VqHz4QfxT6Re+o+HR8vGA=", created_at: ISODate("2025-08-16T03:12:46Z"), @@ -839,7 +839,7 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ec5eea3a8dae1", name: "gifted_kleene", - status: { connected: true, last_message: "2025-08-12T00:58:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "EaBPc3d6Edkfi3YYxsqc+sAMILGwn2RbsIkB4zmnSU0=", created_at: ISODate("2025-08-24T06:35:46Z"), @@ -847,9 +847,1610 @@ db.devices.insertMany([ { _id: "trk-18608d12c84ee5a2e49c6f83", name: "lucid_heisenberg", - status: { connected: true, last_message: "2025-08-08T11:32:46Z" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "valuable", private_key: "Rb/XilBAyjcoGfgM4JkwZbZNEZsdf5pNWUXtF9/NqR8=", created_at: ISODate("2025-08-19T05:39:46Z"), }, + + { + _id: "trk-186e120746e18c690d2440c6", + name: "busy_torvalds", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "zlaKfT1zfiGUFjuxVy2BuM9SqDVikurwVXUEmjbeuX0=", + created_at: ISODate("2025-09-26T01:56:26Z"), + }, + { + _id: "trk-186e120746e5100364a0d88c", + name: "kind_galvani", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "HaEBIVX8wM6DzAJe5H/efqhCLc+y/4Z0tk3qgN5G/KE=", + created_at: ISODate("2025-10-04T02:03:26Z"), + }, + { + _id: "trk-186e120746e53a7f8b9db50a", + name: "vibrant_abel", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "U9zhxIXPRmXASGaAW2rJ/MHIDQAhlc8/kO4bS/9V8wU=", + created_at: ISODate("2025-09-17T17:58:26Z"), + }, + { + _id: "trk-186e120746e5561da2248999", + name: "beautiful_lee", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "PSGrRaqjl2pJybbEnj17F31N/AncTgx7n1A+32GGzgU=", + created_at: ISODate("2025-10-10T15:37:26Z"), + }, + { + _id: "trk-186e120746e571646ff6b00c", + name: "agitated_penrose", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "/jxYt11fVWtx9+OLZcmO0664Lshxu+L9+Er2/DB5j8I=", + created_at: ISODate("2025-09-15T22:14:26Z"), + }, + { + _id: "trk-186e120746e592b6ef437676", + name: "serene_knuth", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "TBkHjWmYDA0m2prmSZqqk7wKO3FMAjNfNcwLbamTl14=", + created_at: ISODate("2025-09-18T02:02:26Z"), + }, + { + _id: "trk-186e120746e5b57fe57e549f", + name: "boring_crick", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "r5PJU83UZ0IsuJuMgFEZJy41dymcmoOCTVTGNe+fYp4=", + created_at: ISODate("2025-09-16T16:44:26Z"), + }, + { + _id: "trk-186e120746e5d2cfd3ad47f8", + name: "proud_curie", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "rwrEdWc3CovyYsAYXvhIojm2vM7RQf7R4waNRm7rZDQ=", + created_at: ISODate("2025-09-18T01:04:26Z"), + }, + { + _id: "trk-186e120746e5f3b0e0bd40a6", + name: "epic_hopper", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "NH0AzwhXWUdFM8m6r6FVT+2C2f9KsD++CHWAqRsrCIg=", + created_at: ISODate("2025-09-25T17:10:26Z"), + }, + { + _id: "trk-186e120746e61249f492cc92", + name: "fervent_ritchie", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "IpaYNNClPfrs776+wldgliZ/UenF/fN5qlY6R+R99kA=", + created_at: ISODate("2025-09-26T00:33:26Z"), + }, + { + _id: "trk-186e120746e62e8b1e50c009", + name: "optimistic_noether", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "dPgcLmsJh0nU5IyAjyhyCfHij0NkswJC7aLqhGnKdE4=", + created_at: ISODate("2025-10-01T22:39:26Z"), + }, + { + _id: "trk-186e120746e6498c6e55e12f", + name: "keen_ohm", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "6sBH2HqlO06ZS6qpAZ75i/0LOb5FEWXK+bbBfyTDZrU=", + created_at: ISODate("2025-09-23T09:49:26Z"), + }, + { + _id: "trk-186e120746e6a53392e5a1d6", + name: "dreamy_morse", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "AdaUNQ0ddcyYZlMHzoFPFDiM+lQS9akdrF086C5uvnU=", + created_at: ISODate("2025-09-19T01:06:26Z"), + }, + { + _id: "trk-186e120746e6c07ef43e3d13", + name: "outstanding_stallman", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "yVvpMOeozwtyH1gS4y8hrDO9hftYbwpzjZeVPwwYwZs=", + created_at: ISODate("2025-09-21T03:27:26Z"), + }, + { + _id: "trk-186e120746e70513852edb0f", + name: "faithful_knuth", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "8GbhtHVXcN9Yyi41F2wh/aR5Wdarq9uvYZLMoOyD8NM=", + created_at: ISODate("2025-10-09T08:08:26Z"), + }, + { + _id: "trk-186e120746e7235b73eb8923", + name: "modest_gauss", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "NmFP6t/trYYOSAQLQgdDJhheahC7kO3kDwuwEFLqeCE=", + created_at: ISODate("2025-10-10T04:12:26Z"), + }, + { + _id: "trk-186e120746e74151fb9b3cf6", + name: "clever_volta", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "K4eXVoUnDViQyeemkL77tJ7PLSTA5LB+dLqNtd8OfH4=", + created_at: ISODate("2025-09-17T02:43:26Z"), + }, + { + _id: "trk-186e120746e75e602ef60dd6", + name: "determined_jacobi", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "T6f5G5JQUuSrCJ9DnXquzN4IXarA+Og67HsRUQU1eHY=", + created_at: ISODate("2025-10-13T13:50:26Z"), + }, + { + _id: "trk-186e120746e77af4a17ef004", + name: "fervent_whitehead", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "+klSDF5Jvl3Cjkjr/GwKxBwusArwojYmcgURQQT6DPs=", + created_at: ISODate("2025-09-23T21:27:26Z"), + }, + { + _id: "trk-186e120746e798d285808a70", + name: "bold_fourier", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "4zwMIKqs8mtNveECyMvcFt+K7OedZ+MnOS6o42EUEWU=", + created_at: ISODate("2025-09-17T06:50:26Z"), + }, + { + _id: "trk-186e120746e7b4c678103ccf", + name: "boring_hardy", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "9cgnzOXmKaVWCBpvyfEC1rIo2coYrpOPrW2ZcvmHO9E=", + created_at: ISODate("2025-09-24T18:31:26Z"), + }, + { + _id: "trk-186e120746e7d271ec706172", + name: "sleepy_pauling", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "2+j+X2Im01XBzllw/xpq1fq8M2TxlOos/ClmmD0Gzaw=", + created_at: ISODate("2025-10-10T06:17:26Z"), + }, + { + _id: "trk-186e120746e7ef79f6ee0e1b", + name: "exotic_ritchie", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "E+6m5pEZbh0VyR2hmRRe4xJ4HzGBUn3qAVxCu0jeydY=", + created_at: ISODate("2025-10-12T18:18:26Z"), + }, + { + _id: "trk-186e120746e80afd0d8d37e1", + name: "beautiful_coulomb", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "SMPBDRGLD9aVElnzLvrsHyRgrM4ywaxwQa+ybapdStM=", + created_at: ISODate("2025-09-24T17:20:26Z"), + }, + { + _id: "trk-186e120746e833b3dfa6e5ff", + name: "jovial_kolmogorov", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "X9ZHSPIBXi5gsoXM0LO7Lsuv8LbVA8EMv/y/J8jXKMc=", + created_at: ISODate("2025-10-01T03:02:26Z"), + }, + { + _id: "trk-186e120746e851854660544e", + name: "serene_coulomb", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "L82m2B58IsoddlmF+eoMnwMpmn3VEBQiAjxGlx854CA=", + created_at: ISODate("2025-09-17T21:44:26Z"), + }, + { + _id: "trk-186e120746e86e09d1953e23", + name: "blissful_leibniz", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "uW6S9QZqdTN9n8nROsXWugdetpdTwdD/NAgsHfVlKVQ=", + created_at: ISODate("2025-10-11T13:42:26Z"), + }, + { + _id: "trk-186e120746e88add15d6df47", + name: "clever_berners", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "zioq96s+Os5u0wkjaOfUO9dZId7OHr8otFLIdl+4qfc=", + created_at: ISODate("2025-09-17T09:35:26Z"), + }, + { + _id: "trk-186e120746e8b7c338ebf0df", + name: "sweet_babbage", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "fDrzR+XXddUMT7sjv1lTdCqCrMxpIuoFhJF/FjORBTM=", + created_at: ISODate("2025-10-13T14:47:26Z"), + }, + { + _id: "trk-186e120746e8ea3b02c4a2e1", + name: "frosty_feynman", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "jzW8kxhoYXRGZAEfemost+RLvfyaXeAqCdA6Tvbrx4k=", + created_at: ISODate("2025-09-24T13:14:26Z"), + }, + { + _id: "trk-186e120746e9075df66cb3b3", + name: "strange_wiener", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "uv7ZnF4gR7DRC3GsRjup4ai5zG3Lm/V7rmeUW+MN1jg=", + created_at: ISODate("2025-10-05T16:02:26Z"), + }, + { + _id: "trk-186e120746e924c657e20745", + name: "noble_gates", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "HTPI0EkycKDjtalFGyzwQhWo64m8tWlgruPc6ytZnuU=", + created_at: ISODate("2025-09-26T02:08:26Z"), + }, + { + _id: "trk-186e120746e941381aa9726f", + name: "hyper_cauchy", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "BymguFW23joJGerJ6vLEL9MqDeSUD+Gu6nIAY2uLEJw=", + created_at: ISODate("2025-09-17T02:25:26Z"), + }, + { + _id: "trk-186e120746e95d892be20fae", + name: "beautiful_whitehead", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "CWlkHtuXinZ28j9pb59SPhjAJZtMTwnRtrXWe0ztijA=", + created_at: ISODate("2025-10-06T15:58:26Z"), + }, + { + _id: "trk-186e120746e97c1f615d85ec", + name: "original_kleene", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "QJ+QtIFC9ydN3CZEHrhY+TYoM95H9JZ9y9fAwNcMCpw=", + created_at: ISODate("2025-09-14T21:36:26Z"), + }, + { + _id: "trk-186e120746e9f277df403d3d", + name: "jolly_salam", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "HQDyTFoUqsHhVWbmVD148+qm4lQOKBp5bMdtXRwbM1s=", + created_at: ISODate("2025-09-28T18:07:26Z"), + }, + { + _id: "trk-186e120746ea1f2ea4d89687", + name: "exotic_berners", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "Y9z4ZdSWN8SPus1VpxPdFwSalBOr0xD60zRJdhCitbg=", + created_at: ISODate("2025-09-21T05:39:26Z"), + }, + { + _id: "trk-186e120746ea41e63919641b", + name: "funny_cooper", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "TYzBRY/HUoh5LpTEAeDcfshVDp7XIxVmsLOmAsFCSU4=", + created_at: ISODate("2025-09-25T17:16:26Z"), + }, + { + _id: "trk-186e120746ea5c4249128d8b", + name: "jaunty_carmack", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "Ln6LyXPBc9OF5bxhYJIoBUZ/r9gMHJYJO9b0fyopzaQ=", + created_at: ISODate("2025-09-30T18:22:26Z"), + }, + { + _id: "trk-186e120746ea750cf69b169b", + name: "thirsty_stallman", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "QAONLFQmus2CGZZzlKfGjAADNanq00bK8vaVcBhJo7o=", + created_at: ISODate("2025-10-07T16:31:26Z"), + }, + { + _id: "trk-186e120746ea8e7196b3093b", + name: "adoring_newton", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "aCIzO3EwhWTHXQpWQP7BYEvr6W5HybnsMSUEX13mL30=", + created_at: ISODate("2025-09-18T22:57:26Z"), + }, + { + _id: "trk-186e120746eaa755561196c8", + name: "hungry_planck", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "VKE7UxvJ2bltuQiytSDm2Nv31Iofy06JUwxP8jagn1M=", + created_at: ISODate("2025-10-12T15:43:26Z"), + }, + { + _id: "trk-186e120746eac0684a411127", + name: "energetic_carmack", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "jBN447T6Cs/2lwJOZsyHmrcYfrsWCMa6L9gGT3sblJA=", + created_at: ISODate("2025-09-21T13:50:26Z"), + }, + { + _id: "trk-186e120746eb21c56b310cda", + name: "admiring_darwin", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "s5ryhLp1YeNYA5ac6plvh1XbGAbrSinlGEAfFX+XB/w=", + created_at: ISODate("2025-10-02T21:14:26Z"), + }, + { + _id: "trk-186e120746eb4d6d70cf6733", + name: "pedantic_franklin", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "1YZoUDeyundPrkyl0bFKSQYfSloEJtJCJAcxri42jsc=", + created_at: ISODate("2025-09-24T04:37:26Z"), + }, + { + _id: "trk-186e120746eb649a77fcbe02", + name: "laughing_born", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "augBE5IFFPq2uubp+cOpB9XSWe6FNzkXRkLZkubX3Es=", + created_at: ISODate("2025-09-24T20:06:26Z"), + }, + { + _id: "trk-186e120746eba54428a46768", + name: "groovy_bohr", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "IkcQJDdUzKZ2WMpIIQwpknP998gOt0F7MP+EQo/T1Lc=", + created_at: ISODate("2025-09-17T20:37:26Z"), + }, + { + _id: "trk-186e120746ebbc9238d17b60", + name: "gracious_weierstrass", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "40PvNz4whHc4hLO/7Q9lMe6YGJO6ZkMvjN1dA30zAA4=", + created_at: ISODate("2025-10-04T07:08:26Z"), + }, + { + _id: "trk-186e120746ebd0fa0f990530", + name: "thoughtful_doppler", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "CQqhJ/Qj/x1s9H3Y4QuIo5FSGoMjA+ojGYiXskb0RRw=", + created_at: ISODate("2025-09-17T00:05:26Z"), + }, + { + _id: "trk-186e120746ebe5ac2d343d5f", + name: "zealous_laplace", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "y0VUGEVC3cOc4sX1nj7u10m75QasW8LIdwWVs1rf2TY=", + created_at: ISODate("2025-10-04T13:25:26Z"), + }, + { + _id: "trk-186e120746ebf958faa8280e", + name: "busy_jobs", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "OENsQJipdvtlRI+5C8F/phEfSjTHS2F9CWXt+cXGHcI=", + created_at: ISODate("2025-10-01T16:44:26Z"), + }, + { + _id: "trk-186e120746ec4fb0b411045b", + name: "adoring_hilbert", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "SAd8SIi1BnSt/9YexlOGPNZ7PJJkigxkvdnqZnwftwo=", + created_at: ISODate("2025-09-25T07:06:26Z"), + }, + { + _id: "trk-186e120746ec648c04ccbdc8", + name: "furious_euler", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "dSLgwgAmj27cw9ET0w8acChymnMLe24CxGCKwdLswbo=", + created_at: ISODate("2025-10-05T02:30:26Z"), + }, + { + _id: "trk-186e120746ecbbbe654ce1d4", + name: "fascinated_morse", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "bRVy3298YRSHvb3LE1Z6qj07TqEtH0oooPp5HMcGTJc=", + created_at: ISODate("2025-09-19T14:47:26Z"), + }, + { + _id: "trk-186e120746eccff893f335f9", + name: "curious_wozniak", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "pK3c6YRMn7EgtpNs+blSRqgdQL+Ic8drkEx9tX2Dy6c=", + created_at: ISODate("2025-10-11T13:06:26Z"), + }, + { + _id: "trk-186e120746ece530a32af864", + name: "dreamy_jacobi", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "Ubp1OPiqYtvjmJCrmp9JrPfCFjtLhiUoB+3oC4BTPFs=", + created_at: ISODate("2025-10-04T04:32:26Z"), + }, + { + _id: "trk-186e120746ed08a67703eabb", + name: "exotic_shockley", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "d2vpnUwGH7ALIWjUKILDnDshEqdD+Roko77fxsHUUqE=", + created_at: ISODate("2025-10-01T23:44:26Z"), + }, + { + _id: "trk-186e120746ed58f183b2985f", + name: "outstanding_weinberg", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "awREvy0yYRVf2XsYf/tNNIdBL55RXR96sGy7jTw5kl8=", + created_at: ISODate("2025-10-03T13:15:26Z"), + }, + { + _id: "trk-186e120746ed868a048f5dab", + name: "dreamy_hilbert", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "/t1Coa70kTMW55Qyu4sFA7+LLafl+vt8LxAoAQ0nDLY=", + created_at: ISODate("2025-10-07T20:10:26Z"), + }, + { + _id: "trk-186e120746edbab38815cdf3", + name: "focused_peano", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "WfNSRmk1sgJsXH/JVHELwriSEAoB5yD4CCyMuGRD/fs=", + created_at: ISODate("2025-09-24T00:54:26Z"), + }, + { + _id: "trk-186e120746edd0917f22f06e", + name: "motivated_compton", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "t4kvEtHD4zNZl6BciX0xcyU8mWDU8YMWEftEQE3eoRY=", + created_at: ISODate("2025-09-29T00:50:26Z"), + }, + { + _id: "trk-186e120746ee2283b18d3081", + name: "groovy_fermi", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "tlIl/BgRMxvE3+uojFxFVzBsUpgqO0DDieAllWAzULA=", + created_at: ISODate("2025-09-14T17:54:26Z"), + }, + { + _id: "trk-186e120746ee4e09aceffd05", + name: "xenodochial_wirth", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "SR7J6zXtzZ+MzTCt+PULdrBEVstzj3j8FWbY8lIEn90=", + created_at: ISODate("2025-09-14T14:54:26Z"), + }, + { + _id: "trk-186e120746ee68da26273cf5", + name: "lucid_nyquist", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "iakRwwyTjiD+zM7uC7h/UQm6gz6QIVntEhsL5kChETk=", + created_at: ISODate("2025-09-21T11:01:26Z"), + }, + { + _id: "trk-186e120746ee83e08e9af2ff", + name: "serene_lagrange", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "sZ9jRJt4nahWRjnYbEr8pROMxPW+rMqaEpwcxSbI25k=", + created_at: ISODate("2025-09-24T14:39:26Z"), + }, + { + _id: "trk-186e120746ee9c5f3732c18d", + name: "cranky_glashow", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "0x5c9KZ5LXKSmZZqvoYwIRmfmcI/QdC9pzGSw0iUnhI=", + created_at: ISODate("2025-09-30T09:44:26Z"), + }, + { + _id: "trk-186e120746eebc6662643d28", + name: "nervous_maxwell", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "R5tvEmp6htqC7RHz/hRTUwhZW6v3lVh3qJ+Pttgw9S8=", + created_at: ISODate("2025-09-30T08:21:26Z"), + }, + { + _id: "trk-186e120746eedcee1f256e01", + name: "amazing_westinghouse", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "UX2Qmw3cmXuF/IcgT0gIzavalplncAuB9c1hCDebu0k=", + created_at: ISODate("2025-09-22T15:06:26Z"), + }, + { + _id: "trk-186e120746eef951821b96c3", + name: "bold_born", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "YXD9WH6vDeIHC5YQZqEz7WvkHrcKgIAnfFAGl59htyE=", + created_at: ISODate("2025-09-22T18:36:26Z"), + }, + { + _id: "trk-186e120746ef14ada5d82853", + name: "ecstatic_bohr", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "EyfR76x+gHf56NtxCJrPP2yEbKNAWAytY7GnJ5iD6Dc=", + created_at: ISODate("2025-09-22T14:45:26Z"), + }, + { + _id: "trk-186e120746ef30a453a2681f", + name: "xenodochial_yang", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "KA7GeAJz7HYtBr+quD2ByRsih8xN94nkw82YHCa+ekE=", + created_at: ISODate("2025-09-23T11:28:26Z"), + }, + { + _id: "trk-186e120746ef5962725241a1", + name: "great_pascal", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "wTy5ke2Gu8rGjh90ryR4OO3dvnaveRb2lNO2J+DXM+k=", + created_at: ISODate("2025-09-29T17:03:26Z"), + }, + { + _id: "trk-186e120746ef7561a82b805e", + name: "puzzled_wirth", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "U3e0bbtRB/4oFdwAyqaUWdiWc3KBJxCdwL4GnrajrvE=", + created_at: ISODate("2025-09-16T18:55:26Z"), + }, + { + _id: "trk-186e120746ef911e84e6d8e2", + name: "flamboyant_wiener", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "ce/RLrLRYmFt61vSvrKtl+OayIQpjaFJRWJVABDKEWA=", + created_at: ISODate("2025-09-18T14:06:26Z"), + }, + { + _id: "trk-186e120746efabd4ebae5c25", + name: "modest_pascal", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "Pz4Pu+YceuyuUAEswXesnWSPbZL5PPPXefSAO4ueB3M=", + created_at: ISODate("2025-10-02T22:08:26Z"), + }, + { + _id: "trk-186e120746efd20be6218945", + name: "curious_turing", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "O2DSUjc6HIFMf8/a/mm+ONOVsXvJ9UmYdvhIirxW5LQ=", + created_at: ISODate("2025-10-10T00:12:26Z"), + }, + { + _id: "trk-186e120746efef261a621259", + name: "heuristic_galois", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "7xF3Ws3DC15XXi+wN0TLJ/xfOYsl1wthIcg1WDJSPGE=", + created_at: ISODate("2025-09-29T07:18:26Z"), + }, + { + _id: "trk-186e120746f00b07b9f277f3", + name: "charming_wozniak", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "CkNmnFv3gp6MKDfY7BFED1nGJLC5Gvho5M135S/bcSI=", + created_at: ISODate("2025-10-08T19:56:26Z"), + }, + { + _id: "trk-186e120746f0271720162899", + name: "cool_dijkstra", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "EXIB21hXaXxf6awOBsQvJF4YnOGec0JcliipsryVoJM=", + created_at: ISODate("2025-09-23T20:59:26Z"), + }, + { + _id: "trk-186e120746f04a21e1fbceb4", + name: "fabulous_henry", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "pA1fAK3JAJfuPhNF0pJ/AXM2x80lCClPT3YBaFm0aWU=", + created_at: ISODate("2025-09-24T14:03:26Z"), + }, + { + _id: "trk-186e120746f082919f1fb039", + name: "calm_glashow", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "cP03j+3EWXeGlrsFWTKdx+/BUsX+yfBpyqfRsHvNtNo=", + created_at: ISODate("2025-10-02T00:58:26Z"), + }, + { + _id: "trk-186e120746f09d390df56184", + name: "gifted_crick", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "Cd8phmxxYiKZykG4dz12tWjBqldOWLlXS4eHAt+QXuE=", + created_at: ISODate("2025-09-26T09:54:26Z"), + }, + { + _id: "trk-186e120746f0b6c58812e233", + name: "merry_torvalds", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "SfPGmp/POZ0OJ46PfaCF4YWga1fkw9hxx4Uu2Oqc12U=", + created_at: ISODate("2025-10-02T12:53:26Z"), + }, + { + _id: "trk-186e120746f0d2223b340dbc", + name: "calm_cantor", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "6y8OGDDKUegpudM8cpIEJOHa0XFm1kiYGq14k5lGqn8=", + created_at: ISODate("2025-09-25T08:49:26Z"), + }, + { + _id: "trk-186e120746f0f719e93740d8", + name: "gentle_kleene", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "kTOu2VN72K+KIS6dmhjw6gn7EJv8Sgy7Ftf0EVmyUAM=", + created_at: ISODate("2025-09-17T00:20:26Z"), + }, + { + _id: "trk-186e120746f12bb3b3afc7ca", + name: "angry_wirth", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "EaVi9jGo670sloZat37TqG9X6MSt9eYttd+OL9xtNmA=", + created_at: ISODate("2025-09-28T01:00:26Z"), + }, + { + _id: "trk-186e120746f146b5f322b565", + name: "furious_planck", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "YsXCrd24M5R0/N8Zo8NptQSAgfuOKlb/6Mquo9+5f6w=", + created_at: ISODate("2025-10-10T19:53:26Z"), + }, + { + _id: "trk-186e120746f15f4e4ac77e69", + name: "flamboyant_hertz", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "GwTCImAqU45wG0/+jxkHCuhKym5mzA7ga9AgmiPxXt8=", + created_at: ISODate("2025-10-04T23:43:26Z"), + }, + { + _id: "trk-186e120746f179039c3e5b94", + name: "thirsty_laplace", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "pFQF+nyzLCcElM/v9stFnzBiFOZmnAif8VZEf442V+U=", + created_at: ISODate("2025-10-09T19:46:26Z"), + }, + { + _id: "trk-186e120746f191c526e07e26", + name: "cranky_weber", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "GsdkxPFdv3TahWmqM9VkaazJQNxCzhS7PENt8gEoT0s=", + created_at: ISODate("2025-09-24T21:13:26Z"), + }, + { + _id: "trk-186e120746f1b4d87f6a4ab5", + name: "iron_galvani", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "W3QvXGjvUvqvTKippTBM1SFuOpMKW8QWnoD0Mme6OYE=", + created_at: ISODate("2025-09-26T05:09:26Z"), + }, + { + _id: "trk-186e120746f1ce4eea4b728b", + name: "relaxed_feynman", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "LZR+p/doe7p09bMaY/q8YakuBQGrsGyX74cbpYztN0I=", + created_at: ISODate("2025-09-28T06:39:26Z"), + }, + { + _id: "trk-186e120746f1e7f64d2a0414", + name: "merry_coulomb", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "pW2aKuxC2muX5hOWr+bk1aySwlsEof16dOwGGVbv0hE=", + created_at: ISODate("2025-10-06T09:51:26Z"), + }, + { + _id: "trk-186e120746f201234290fc20", + name: "youthful_edison", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "p46hK5+FShBIgUZvbfw6Wq3dfrtRYlX9RDMDKAUoE7k=", + created_at: ISODate("2025-09-25T12:29:26Z"), + }, + { + _id: "trk-186e120746f21be0e93185c3", + name: "focused_godel", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "UcYyvLXTyMi7vygZia8WHT6jtc+F7Ee2C01o5xS1Nr4=", + created_at: ISODate("2025-09-26T23:16:26Z"), + }, + { + _id: "trk-186e120746f234b4f6c9f2c7", + name: "dazzling_babbage", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "6u6OQXrxzetXHuITD4hRFsWMbJ7MMgaJAGicOFOX19Q=", + created_at: ISODate("2025-09-20T10:30:26Z"), + }, + { + _id: "trk-186e120746f24e76ff131fe3", + name: "upbeat_ohm", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "luzQWGdL+UzgoVO3aaIeSFYdqpx0MOd//U49+sKZBR4=", + created_at: ISODate("2025-09-24T03:21:26Z"), + }, + { + _id: "trk-186e120746f267cbf0b2059a", + name: "furious_hertz", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "nsgQCOmszyc8AS0cr/jasr6AqOfBCjGWWqBOEH3JYAQ=", + created_at: ISODate("2025-09-17T10:52:26Z"), + }, + { + _id: "trk-186e120746f2814d7e0754b6", + name: "affectionate_poincare", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "itBD6WM9tftIGfAJilP9de2TJa8Vog2wDz0ipw43Lgc=", + created_at: ISODate("2025-10-11T07:02:26Z"), + }, + { + _id: "trk-186e120746f2a3d87f0eeca4", + name: "frosty_kolmogorov", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "HR8uYTr8E1/UdfaOLnfZ3CP0JwPdg4LIkoPy4Yjg0O0=", + created_at: ISODate("2025-09-13T16:47:26Z"), + }, + { + _id: "trk-186e1309839bbd54be8f785e", + name: "gifted_hawking", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "ZCw5gk1MaebMg2IsOivMuSZgCAJQxYPZ8o3KMTn0oSY=", + created_at: ISODate("2025-10-02T18:57:55Z"), + }, + { + _id: "trk-186e1309839e1ffe329ebf18", + name: "zealous_penrose", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "LOjfvsoJj7zNBoByyrJqgnfbVuvg2oqSWSo23k5szrw=", + created_at: ISODate("2025-10-05T07:08:55Z"), + }, + { + _id: "trk-186e1309839e3cc066512dbd", + name: "elastic_morley", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "k7fQ1A277mZYtLp4yZwgQv5SAHg+CQykl65IYmBOcLw=", + created_at: ISODate("2025-10-13T05:51:55Z"), + }, + { + _id: "trk-186e1309839e5c57c7e9af1b", + name: "energetic_lovelace", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "cTlwhna05v7wqUSh4A6t7V59z5nfZPwlmu5phCNjhzs=", + created_at: ISODate("2025-09-15T01:26:55Z"), + }, + { + _id: "trk-186e1309839e7274fbb77443", + name: "noble_schwinger", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "zWDBZnVqBcN60DwbxryQ+eLp/b+dr/xlpafTcWfFs1c=", + created_at: ISODate("2025-09-23T19:53:55Z"), + }, + { + _id: "trk-186e1309839e85fe0ba301ac", + name: "puzzled_heisenberg", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "EE/TZ0jS3mEwbZx/YqElSKS2qxTLiADL3wTZc7UtTR4=", + created_at: ISODate("2025-09-15T11:31:55Z"), + }, + { + _id: "trk-186e1309839e99c2655eb597", + name: "magical_laplace", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "zaDTN2SiXFRcxdsnSyeZST0eJO2ymNrnWVh9KSiBjmQ=", + created_at: ISODate("2025-09-19T07:21:55Z"), + }, + { + _id: "trk-186e1309839eae938abbb06e", + name: "polite_franklin", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "3E5qvFq6m7TZW5scw/ZYc39zAnhourZwtQbte1KRFcg=", + created_at: ISODate("2025-09-22T23:00:55Z"), + }, + { + _id: "trk-186e1309839ec6a5f176f269", + name: "suspicious_weber", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "vPKKppUdZeMiXY5HYgFgHETpYx3jtWRHuMeNPNX+gIo=", + created_at: ISODate("2025-09-24T20:49:55Z"), + }, + { + _id: "trk-186e1309839edcbc0e9ccdee", + name: "hyper_schrodinger", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "m6ltZvHHeKGnbg/7OCLoDwz5JuXP2lr1pcY4Vj+iY6Y=", + created_at: ISODate("2025-09-15T11:08:55Z"), + }, + { + _id: "trk-186e1309839ef37e250b2ac1", + name: "flamboyant_hilbert", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "IZrX9Tvi8DNKS7uFTtf/HLwYxivfO6gnriULc081BW4=", + created_at: ISODate("2025-09-27T03:18:55Z"), + }, + { + _id: "trk-186e1309839f7ba1a81d392e", + name: "laughing_oppenheimer", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "sTqUXGZmpiGZHGxXEFElISIe3iJz9VW5h7LrKbdbT5Q=", + created_at: ISODate("2025-09-21T19:38:55Z"), + }, + { + _id: "trk-186e1309839fda077e230f18", + name: "sleepy_noether", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "K/fnViP3HJeORW02MDQiODA2KJIIOklTQ95dp8KR1Aw=", + created_at: ISODate("2025-10-08T09:06:55Z"), + }, + { + _id: "trk-186e1309839fef6604700630", + name: "cranky_volta", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "LhU6aqY/E9DHusg8AbbavTuikkHRVaOKAceEX4erXb8=", + created_at: ISODate("2025-10-13T10:22:55Z"), + }, + { + _id: "trk-186e130983a01ae99c930d78", + name: "brave_ritchie", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "DdkJUpGmJY7ZMiMKeyTIUh+NOId0DzLi7dp6alJhQ88=", + created_at: ISODate("2025-09-18T08:13:55Z"), + }, + { + _id: "trk-186e130983a034f970090b84", + name: "keen_carmack", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "bb/3zD4PlygRSNYJ2JfXWtQOGXRlw4iqjv0nESpWnnw=", + created_at: ISODate("2025-10-12T15:58:55Z"), + }, + { + _id: "trk-186e130983a059f6f1438b3e", + name: "awesome_fermi", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "KGd+GfOfvoG61VkVQFjTJR0Yq/znautGUH+9WAnDLoQ=", + created_at: ISODate("2025-09-24T09:31:55Z"), + }, + { + _id: "trk-186e130983a07ae5a7263ec1", + name: "crazy_thompson", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "hbTWXcPaFt/C79Q3ljLio7j7GkYT4DT6UKavFR/7tOg=", + created_at: ISODate("2025-10-04T02:54:55Z"), + }, + { + _id: "trk-186e130983a09704293604ee", + name: "calm_berners", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "OU1L1O+EUhwPhwmKJ8OtajzN5xPrV4wyWeLsTaoY1I8=", + created_at: ISODate("2025-10-01T22:19:55Z"), + }, + { + _id: "trk-186e130983a0b6016eeb4331", + name: "proud_jobs", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "Je7MWPdNzzyVBGdsKH0ZrdkDCkkk52S0sa9OOaloRQM=", + created_at: ISODate("2025-09-24T22:52:55Z"), + }, + { + _id: "trk-186e130983a0db3e0cc11f28", + name: "furious_chebyshev", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "rINraJ2KTESEfAph/IFXaUIsFf2DwvcV46lED/a+XZQ=", + created_at: ISODate("2025-09-30T07:18:55Z"), + }, + { + _id: "trk-186e130983a14583c71a2172", + name: "faithful_lagrange", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "oaNmemljb/RTXF5lwD+aYiyJduErvUitnjU9P6O3Qcc=", + created_at: ISODate("2025-10-06T15:35:55Z"), + }, + { + _id: "trk-186e130983a159e2bbb9f5f8", + name: "upbeat_babbage", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "FlfO21D9qS/voZQhw7ALRXcQpl+4P0+bcsAVx+HqTys=", + created_at: ISODate("2025-10-10T03:06:55Z"), + }, + { + _id: "trk-186e130983a1c1a1f3cb2f24", + name: "hungry_gauss", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "MP/K9fmbN8gjGL2dlUgPwvfkmH1AUPDCjdAiUqu944g=", + created_at: ISODate("2025-10-11T22:35:55Z"), + }, + { + _id: "trk-186e130983a1d5c90eeb7be5", + name: "motivated_jacobi", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "yKTjLolSSeYIykBPgT5XtvoUQdKALNFkkNu44kNoGP4=", + created_at: ISODate("2025-10-05T00:01:55Z"), + }, + { + _id: "trk-186e130983a1ea63e8dbee65", + name: "laughing_nyquist", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "gqr+1LMynMxT9/LCZi34YT7w7kNetDPAIM5vgxuJ62E=", + created_at: ISODate("2025-09-26T16:11:55Z"), + }, + { + _id: "trk-186e130983a1fd7e0e4791cc", + name: "blissful_schrodinger", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "46eu29Cek7uXQbQIikPXWXXtb5Mj4uF8qiNAjlAhHAg=", + created_at: ISODate("2025-09-20T07:34:55Z"), + }, + { + _id: "trk-186e130983a2109dd0df3463", + name: "modest_lagrange", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "wDLjDpT2iWWujj+LVSJmdJiiJvgBj4cZ+79LZdPwQ9s=", + created_at: ISODate("2025-09-23T15:00:55Z"), + }, + { + _id: "trk-186e130983a234a9618f898e", + name: "confident_peano", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "v6fr9Q4/AQTAwGGrEszqok6AU/RlCO7EmsWoaHPR1Wg=", + created_at: ISODate("2025-09-27T05:29:55Z"), + }, + { + _id: "trk-186e130983a2560f98ea7e47", + name: "jovial_carmack", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "KcMeDL6RVjQrEE3xf+j2tcYleM26p60oLqaVvRhQFDM=", + created_at: ISODate("2025-09-25T14:14:55Z"), + }, + { + _id: "trk-186e130983a26a15f661928b", + name: "eloquent_dedekind", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "X8atklQ+PoJ0GuKFPwfSs49w1C1tjiwstgnvu+3Uaq8=", + created_at: ISODate("2025-09-29T19:35:55Z"), + }, + { + _id: "trk-186e130983a27e5bc05251b9", + name: "frosty_mendeleev", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "1kASDQP73a3z2SN8IkuNtI3cYmX3jjvmO+/bU1sQwHY=", + created_at: ISODate("2025-09-21T07:50:55Z"), + }, + { + _id: "trk-186e130983a291f1e443cd5a", + name: "proud_markov", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "UzmsA89/TC3Y9HXzAj/FFajvNUheAN9dGb/0axlXtb8=", + created_at: ISODate("2025-10-01T10:25:55Z"), + }, + { + _id: "trk-186e130983a2a576491ef9af", + name: "angry_hilbert", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "sKpBt81A5fAZFsm4npMR5KEv3eJY9csJ0lU2dV09slw=", + created_at: ISODate("2025-10-08T07:07:55Z"), + }, + { + _id: "trk-186e130983a2d186e5d486fb", + name: "hungry_volta", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "fH0z5owaDwYFtDAHOTndNz6bZboOOrfJS7G0FcLWwSc=", + created_at: ISODate("2025-09-26T13:34:55Z"), + }, + { + _id: "trk-186e130983a2e58a9f17fdc3", + name: "distracted_mendeleev", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "T9pkDWyxASF5yTI/tPUa0oYGkbS7kEJw658mWEYmXuw=", + created_at: ISODate("2025-09-15T03:42:55Z"), + }, + { + _id: "trk-186e130983a2fe87872f4f49", + name: "thoughtful_compton", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "hcUkyXC/+F/nxBV8iOeTaXlbPlVbefYzFfrbXkGV8Sg=", + created_at: ISODate("2025-10-05T01:51:55Z"), + }, + { + _id: "trk-186e130983a322c165ec9526", + name: "sad_stallman", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "vNNucwhglkB7Y1aVZrx8AmdKQH0T8NQ/nDA6WvAmHYA=", + created_at: ISODate("2025-09-28T01:45:55Z"), + }, + { + _id: "trk-186e130983a340ddf0bf05e4", + name: "vibrant_erdos", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "JD9CEeoyzLC/HXULUtZRds3Zt3W73rlEK9SaMNABxX4=", + created_at: ISODate("2025-10-13T14:53:55Z"), + }, + { + _id: "trk-186e130983a35bf2a8e986cb", + name: "original_doppler", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "B7fsNi61OSIwh7sOPijfiaXcO35j4b8D4RzaOO9AI9o=", + created_at: ISODate("2025-09-13T22:37:55Z"), + }, + { + _id: "trk-186e130983a37e04ea2b2ff2", + name: "fabulous_schrodinger", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "BgA+RZUSOglE9OOaC64uOPYBH2Q3FXKTh92P7kevJmo=", + created_at: ISODate("2025-09-26T15:11:55Z"), + }, + { + _id: "trk-186e130983a39b9455bdeedf", + name: "awesome_dijkstra", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "ttzL9AQTJ9RKMTlAF7il5xpv84u4wPLSCxzJ7QgAK2o=", + created_at: ISODate("2025-10-07T23:48:55Z"), + }, + { + _id: "trk-186e130983a3c77bb7a4f5d7", + name: "heartwarming_michelson", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "HXOc5oU5YLvbceVhUGdpPTmDfF3FROJAPjZ2XkjjTvo=", + created_at: ISODate("2025-10-02T15:09:55Z"), + }, + { + _id: "trk-186e130983a42d5fb53a30e9", + name: "sweet_morse", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "Cogel1ZaSvbfbwjKN9kzoFdMkNmRVyjLhdVO1TIXRX8=", + created_at: ISODate("2025-09-21T15:57:55Z"), + }, + { + _id: "trk-186e130983a45e7421d17024", + name: "heuristic_wirth", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "XO/6YycdUYkH63d460y/NTW0dxLBPiAygeMjHuRmag8=", + created_at: ISODate("2025-10-05T02:06:55Z"), + }, + { + _id: "trk-186e130983a47ab26312a621", + name: "serene_leibniz", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "ZEey/z8mahhYZ6P2370knAdpoBRxrl+rUc8IRGe7X+s=", + created_at: ISODate("2025-10-11T19:15:55Z"), + }, + { + _id: "trk-186e130983a49cc1a4ead31d", + name: "admiring_markov", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "z1UanROYzQtod+7GLLbw9qYdIvet+pQQEc85sdmE1tI=", + created_at: ISODate("2025-10-10T21:17:55Z"), + }, + { + _id: "trk-186e130983a4af6de88c809f", + name: "practical_cantor", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "n7DXivw4BcVLW2WE3NGNZGADmQgpJuAqcDxd/3ys6oo=", + created_at: ISODate("2025-09-14T14:27:55Z"), + }, + { + _id: "trk-186e130983a4c1a2c1717cf8", + name: "keen_berners", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "QzRG4fj6Ieu/CEDn4y6+Bu1gBFqY4ISF+tHNhDtlNOU=", + created_at: ISODate("2025-10-11T10:09:55Z"), + }, + { + _id: "trk-186e130983a4d36ac7ba8b7a", + name: "proud_tesla", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "EX3KV2PKMP/B3ZQ2W/lWny+J+Ph56ZZvPU5Ee4JyKN4=", + created_at: ISODate("2025-09-25T21:44:55Z"), + }, + { + _id: "trk-186e130983a4e47156945059", + name: "goofy_crick", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "vazs6yunKp5x6D8iixRFQWIPPtuhKeHi8k+cnv/oP10=", + created_at: ISODate("2025-09-19T10:10:55Z"), + }, + { + _id: "trk-186e130983a4f6d211f892ab", + name: "phenomenal_chebyshev", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "F5ynV08uXxR+VAwDWviUzRt5DJSK4tZkuw76KO8gICI=", + created_at: ISODate("2025-09-25T04:49:55Z"), + }, + { + _id: "trk-186e130983a508c15816b9c0", + name: "nervous_shannon", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "wcyoP8iDI3UU5H1r/qqVYLR1tsMuB0fwqilajOO4+vc=", + created_at: ISODate("2025-09-19T21:06:55Z"), + }, + { + _id: "trk-186e130983a519b7a38da4ea", + name: "polite_coulomb", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "1tk9YjVrNKneUDY1Osjo/Dwi4i10Gso/pN46e3GvYYY=", + created_at: ISODate("2025-09-14T23:19:55Z"), + }, + { + _id: "trk-186e130983a52afe9fa16029", + name: "proud_weinberg", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "1f2wPh9Ox2inN18mT1YuK4jRHKvVGoKG8SpgAzQkHSY=", + created_at: ISODate("2025-09-23T19:32:55Z"), + }, + { + _id: "trk-186e130983a53d672348d95f", + name: "wonderful_morse", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "wTtqmRD/kVPjzkiGiJwEi6Ki7iSk/zTlmXS+hp4yNSo=", + created_at: ISODate("2025-10-04T21:25:55Z"), + }, + { + _id: "trk-186e130983a565faaf6e26d7", + name: "enchanting_russell", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "ZVUDm/FzS/zzVL4cCFnt3GhgJWabExYb4opnpTH0EF0=", + created_at: ISODate("2025-09-19T07:10:55Z"), + }, + { + _id: "trk-186e130983a5847b2c0e34c5", + name: "compassionate_pauli", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "fc20Hdx9vXJ178vB52/ZIhUUQdM5xEC06cYBsFH6qFQ=", + created_at: ISODate("2025-10-05T12:11:55Z"), + }, + { + _id: "trk-186e130983a5965d6dada7f4", + name: "quirky_nyquist", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "baCodUXLbJPBkTX0SYlNZoiS+wCQNxSCOQVl11U/nlw=", + created_at: ISODate("2025-09-26T23:13:55Z"), + }, + { + _id: "trk-186e130983a5c49aeba044f5", + name: "original_maxwell", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "8Fc0JUIo5wNsW9YpeJgMyIWoMB6efwN8TNO1wREwMn4=", + created_at: ISODate("2025-09-16T16:39:55Z"), + }, + { + _id: "trk-186e130983a5d7349ac354d7", + name: "epic_michelson", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "1Dw3ZR9Thr0Hcy4/nSnpDXiBfPsctFt9l+vlsNJ0IPk=", + created_at: ISODate("2025-10-05T23:44:55Z"), + }, + { + _id: "trk-186e130983a5e91f31ee5429", + name: "clever_planck", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "T1XjWjDNQy1lBxDSvVgKpThI1K4POhliXJOFvtZFo/k=", + created_at: ISODate("2025-10-09T21:17:55Z"), + }, + { + _id: "trk-186e130983a5fa39a2a976c0", + name: "gifted_whitehead", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "fr61QFVGUvqpBS8uWwQ49iudcMYGxgi9PW8RZcVg6so=", + created_at: ISODate("2025-09-19T08:34:55Z"), + }, + { + _id: "trk-186e130983a61fe1c2dd259e", + name: "jaunty_penrose", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "DpeFAN8wyPtjf1rGLO7JbAwB0VqowdXasLZOs8d0VXw=", + created_at: ISODate("2025-09-30T06:29:55Z"), + }, + { + _id: "trk-186e130983a6336076acfe23", + name: "nostalgic_rutherford", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "+8M4KNmUM9/n0mPsZJZdTKUCXZILPHMNfmPPNNkcTXc=", + created_at: ISODate("2025-09-19T23:53:55Z"), + }, + { + _id: "trk-186e130983a644b5f71646b8", + name: "fancy_shannon", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "Pf5p7rwp5Kz6sm9i5tkjoCze/i2Fg5YuTzJHBLSpNPs=", + created_at: ISODate("2025-09-27T10:30:55Z"), + }, + { + _id: "trk-186e130983a655ddcb922063", + name: "great_henry", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "ZhajzgyxcVnZi0u9Hm6C58119AdBDZyXhtMSF76y/G4=", + created_at: ISODate("2025-09-30T21:42:55Z"), + }, + { + _id: "trk-186e130983a667a828ff844f", + name: "grieving_shannon", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "iKFwj7MKj0zd5ON6UBQyqlhZlEUqHuSd6jmKr905gq4=", + created_at: ISODate("2025-10-04T07:32:55Z"), + }, + { + _id: "trk-186e130983a6794fd7167c67", + name: "dreamy_yang", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "RKml+qYAdgRUFEbo7NYC6v6L5x9iH1WAyZkg3ywu/Mc=", + created_at: ISODate("2025-09-29T12:13:55Z"), + }, + { + _id: "trk-186e130983a68a6c4e789068", + name: "xenodochial_pauli", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "5jEOTEisy9PAHGo2cUxTLjcS7U/j1z+iDVu4AmPAQB4=", + created_at: ISODate("2025-10-09T09:38:55Z"), + }, + { + _id: "trk-186e130983a6a42c3aa15795", + name: "eloquent_coulomb", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "fktsbKDjJeccJCECW3jEVln0OHXFMvTFa/JQb2T7zX4=", + created_at: ISODate("2025-09-27T11:41:55Z"), + }, + { + _id: "trk-186e130983a6b654add2ea90", + name: "optimized_feynman", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "kQeoHG8z2ONEFoa3e/6dviH2WAgRiAScU9R3czQHhiA=", + created_at: ISODate("2025-09-17T00:43:55Z"), + }, + { + _id: "trk-186e130983a6c82b57b03fe5", + name: "outstanding_ritchie", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "cmzsUIvVt8wibHWzZd9Ej5CaoTDDDN325i7lsSTNfnk=", + created_at: ISODate("2025-09-24T14:47:55Z"), + }, + { + _id: "trk-186e130983a6da34cfc550e0", + name: "quizzical_lovelace", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "pax2Gn8effRWcUhmxW/9Uc/KvWrfv6+IejTdzMlA7bk=", + created_at: ISODate("2025-09-15T21:11:55Z"), + }, + { + _id: "trk-186e130983a6ebaaea0d4795", + name: "competent_leibniz", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "P6dDQCF6Fgjub8xGk93uxOQKX/KYcyB6e5bVrDrmHZ0=", + created_at: ISODate("2025-09-25T07:45:55Z"), + }, + { + _id: "trk-186e130983a6fd0db9d5a841", + name: "keen_jacobi", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "wW1zdnRBu/V+TTrdSPjtT/enxkpbwHgcngL/n5oKQGI=", + created_at: ISODate("2025-09-22T07:56:55Z"), + }, + { + _id: "trk-186e130983a70f40a79fedae", + name: "fabulous_rutherford", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "2Rg2uMXrpuF9EtKsOXnqxokMWKCDy6Qtgy/92Vq6WHI=", + created_at: ISODate("2025-09-21T20:54:55Z"), + }, + { + _id: "trk-186e130983a720eff34ba9c8", + name: "interesting_thompson", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "38OdyJBYXtEDLQ1W6Ay98X7QXHbs/rvV1Slbn+DVDIQ=", + created_at: ISODate("2025-09-20T08:01:55Z"), + }, + { + _id: "trk-186e130983a739bb2f817dda", + name: "adoring_dyson", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "XNp46numS1q+uFS4mLsd3ixSmP98hQFwZpxJQvefmOA=", + created_at: ISODate("2025-09-27T23:47:55Z"), + }, + { + _id: "trk-186e130983a74c17ec7515dd", + name: "agitated_russell", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "egIN2RS4kylE+AN+PL4GUXYr1L1D23aOOWHaFMua8dA=", + created_at: ISODate("2025-10-11T05:41:55Z"), + }, + { + _id: "trk-186e130983a75eb0efc4d28f", + name: "merry_kleene", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "nELHiKhUE7Y7U8Zni+levsFKjHkUj8L7nlmXeKV5upA=", + created_at: ISODate("2025-09-14T02:49:55Z"), + }, + { + _id: "trk-186e130983a770bd6a437b45", + name: "inspiring_kolmogorov", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "GJRtvBmPmKbU2LIEG1J4qP5kjhin6FjzjpxcfdgmYHg=", + created_at: ISODate("2025-09-15T04:05:55Z"), + }, + { + _id: "trk-186e130983a782e44f0bccee", + name: "epic_wu", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "6WFt4fLWdRwbKb56kxs3UG7gbEmSRZJcWrs0Witu60I=", + created_at: ISODate("2025-09-25T11:46:55Z"), + }, + { + _id: "trk-186e130983a79453f87f853d", + name: "funny_hardy", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "DdlS8ZewVy8MnaPEyQ6ExhyVzZ+Ym6pmplr2OdFyVu8=", + created_at: ISODate("2025-10-09T09:21:55Z"), + }, + { + _id: "trk-186e130983a7abf10c38fab8", + name: "iron_hopper", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "jxb07WpM4h93Gdp+I+RcoQKmvhFapFRDedoY3KTRuAk=", + created_at: ISODate("2025-10-11T16:29:55Z"), + }, + { + _id: "trk-186e130983a7c5a7c28ded10", + name: "lucid_shockley", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "lQmhvYl/A6RCnqgQkuKmAgeI8w8+3XKoosilNbF3ls8=", + created_at: ISODate("2025-09-19T02:32:55Z"), + }, + { + _id: "trk-186e130983a7f22eb4ae7f9f", + name: "relaxed_hawking", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "KOf+6+kC7nFU7uRBYQG1Kn8fB0AWQnEs+JzKSVpmt60=", + created_at: ISODate("2025-09-17T22:44:55Z"), + }, + { + _id: "trk-186e130983a8041d25549f0e", + name: "pedantic_pauli", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "XjirKNQ0apJVVa03WWRHnhQU3/ucgTKHRo+ydtYvQf4=", + created_at: ISODate("2025-09-17T17:43:55Z"), + }, + { + _id: "trk-186e130983a8165b1981350b", + name: "nervous_michelson", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "u8kNHC3Cm0xRQYyNVAY4RCsBpacPE8gOG6yupuQehsY=", + created_at: ISODate("2025-10-01T09:36:55Z"), + }, + { + _id: "trk-186e130983a827f007a9b2b3", + name: "thoughtful_penrose", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "rWTp82vUW/6XjsvgtDfREFW5crHPAtI6fhHQMUalNTQ=", + created_at: ISODate("2025-09-16T19:10:55Z"), + }, + { + _id: "trk-186e130983a839a432f99c62", + name: "fearless_galvani", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "VPh59T5lqElB/rv7YVdSvF8jVC/MUKYPZe7AQrjP8Oo=", + created_at: ISODate("2025-09-28T04:08:55Z"), + }, + { + _id: "trk-186e130983a84c6c80d3a12d", + name: "merry_watson", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "ZdoTuYQHaObGbs+3cL1Snf2Z81VdKSm9+rpK88YgFHg=", + created_at: ISODate("2025-10-04T09:58:55Z"), + }, + { + _id: "trk-186e130983a85f66521883b1", + name: "awesome_russell", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "+mc3lyjvdmOs9fvgfehZmJD2ayxnCim9ejoV2M6+aYc=", + created_at: ISODate("2025-09-24T06:51:55Z"), + }, + { + _id: "trk-186e130983a870935e563e9f", + name: "sad_compton", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "food", + private_key: "70AYubA/PkCQBM54UEFGUK2FQyqpOYdonEt4/qVgEOE=", + created_at: ISODate("2025-10-02T14:01:55Z"), + }, + { + _id: "trk-186e130983a88346a0103419", + name: "trusting_whitehead", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "V2Gm/mIE2Uxwcrplrf/glr7A8e/VQFK/iBqjTdXarxw=", + created_at: ISODate("2025-09-22T03:30:55Z"), + }, + { + _id: "trk-186e130983a8948dd91d5777", + name: "enchanting_babbage", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "valuable", + private_key: "GpAgMGQ/TifcNTlfVC5jyOC4riwOaJZ7YGyiycZ3hfc=", + created_at: ISODate("2025-09-18T07:52:55Z"), + }, + { + _id: "trk-186e130983a8a62ec36b8234", + name: "noble_dijkstra", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "TWKFkqd+aAj+YNskjVxR6ftBy6AnldQ+UvzUXYr27kE=", + created_at: ISODate("2025-09-24T08:20:55Z"), + }, + { + _id: "trk-186e130983a8b81ed167fa68", + name: "relaxed_fourier", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "vShTmZ1PkdComu1SSjKsqbsJALZIcFpIQCP6e7m+mTE=", + created_at: ISODate("2025-09-15T07:10:55Z"), + }, + { + _id: "trk-186e130983a8cae50b739184", + name: "adoring_marconi", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "private_transport", + private_key: "tKns/mbBJ1Vj+M2XMvIg2t4a5clE8LKNcU4REn7kYb0=", + created_at: ISODate("2025-10-06T13:19:55Z"), + }, + { + _id: "trk-186e130983a8e3cfea83018f", + name: "blissful_carmack", + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, + device_type: "public_transport", + private_key: "gzu43V5D0h7gjzYWkhV8eGecgWY97ULsA+GAFp0votU=", + created_at: ISODate("2025-09-26T15:42:55Z"), + }, ]); diff --git a/services/playbooks/roles/nginx/defaults/main.yml b/services/playbooks/roles/nginx/defaults/main.yml new file mode 100644 index 00000000..9684eb0b --- /dev/null +++ b/services/playbooks/roles/nginx/defaults/main.yml @@ -0,0 +1,6 @@ +--- +nginx_namespace: nginx +nginx_app_name: nginx +nginx_image: nginx:1.29 +nginx_port: 80 +nginx_service_ip: 10.20.30.60 diff --git a/services/playbooks/roles/nginx/tasks/main.yml b/services/playbooks/roles/nginx/tasks/main.yml new file mode 100644 index 00000000..e8880d20 --- /dev/null +++ b/services/playbooks/roles/nginx/tasks/main.yml @@ -0,0 +1,18 @@ +- name: "Create nginx Kubernetes namespace" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + name: "{{ nginx_namespace }}" + api_version: v1 + kind: Namespace + state: present + +- name: "Deploy nginx Deployment and Service" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + state: present + namespace: "{{ nginx_namespace }}" + definition: "{{ lookup('template', item) | from_yaml }}" + loop: + - "01-configmap.yml.j2" + - "02-deployment.yml.j2" + - "03-service.yml.j2" diff --git a/services/playbooks/roles/nginx/templates/01-configmap.yml.j2 b/services/playbooks/roles/nginx/templates/01-configmap.yml.j2 new file mode 100644 index 00000000..edbef8fa --- /dev/null +++ b/services/playbooks/roles/nginx/templates/01-configmap.yml.j2 @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-conf +data: + nginx.conf: | + events {} + http { + server { + listen 80; + server_name _; + + location /dashboards/ { + proxy_pass http://grafana.grafana.svc.cluster.local:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + proxy_read_timeout 600s; + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + send_timeout 600s; + proxy_buffering off; + } + } + } diff --git a/services/playbooks/roles/nginx/templates/02-deployment.yml.j2 b/services/playbooks/roles/nginx/templates/02-deployment.yml.j2 new file mode 100644 index 00000000..f9f76a77 --- /dev/null +++ b/services/playbooks/roles/nginx/templates/02-deployment.yml.j2 @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ nginx_app_name }} + labels: + app: {{ nginx_app_name }} +spec: + nodeSelector: + workload-type: apps + replicas: 1 + selector: + matchLabels: + app: {{ nginx_app_name }} + template: + metadata: + labels: + app: {{ nginx_app_name }} + spec: + nodeSelector: + workload-type: apps + containers: + - name: {{ nginx_app_name }} + image: {{ nginx_image }} + imagePullPolicy: IfNotPresent + ports: + - containerPort: {{ nginx_port }} # Nginx listens on port 80 by default + volumeMounts: + - name: nginx-config-volume + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + volumes: + - name: nginx-config-volume + configMap: + name: nginx-conf diff --git a/services/playbooks/roles/nginx/templates/03-service.yml.j2 b/services/playbooks/roles/nginx/templates/03-service.yml.j2 new file mode 100644 index 00000000..55343ad1 --- /dev/null +++ b/services/playbooks/roles/nginx/templates/03-service.yml.j2 @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ nginx_app_name }} +spec: + selector: + app: {{ nginx_app_name }} + ports: + - name: http + port: {{ nginx_port }} + targetPort: {{ nginx_port }} + type: LoadBalancer + loadBalancerIP: {{ nginx_service_ip }} diff --git a/services/playbooks/roles/prometheus/defaults/main.yml b/services/playbooks/roles/prometheus/defaults/main.yml new file mode 100644 index 00000000..57ae6263 --- /dev/null +++ b/services/playbooks/roles/prometheus/defaults/main.yml @@ -0,0 +1,8 @@ +prometheus_namespace: monitoring +prometheus_app_name: prometheus +prometheus_replicas: 1 +prometheus_image: prom/prometheus:v3.0.0 +prometheus_ip: 10.20.30.130 +prometheus_port: 9090 +prometheus_storage_size: 5Gi +prometheus_scrape_interval: 15s diff --git a/services/playbooks/roles/prometheus/tasks/main.yml b/services/playbooks/roles/prometheus/tasks/main.yml new file mode 100644 index 00000000..cbd761b3 --- /dev/null +++ b/services/playbooks/roles/prometheus/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: "Create Prometheus Kubernetes namespace" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + name: "{{ prometheus_namespace }}" + api_version: v1 + kind: Namespace + state: present + +- name: "Deploy Prometheus components" + kubernetes.core.k8s: + kubeconfig: /etc/rancher/k3s/k3s.yaml + state: present + namespace: "{{ prometheus_namespace }}" + definition: "{{ lookup('template', item) | from_yaml }}" + loop: + - "01-configmap.yml.j2" + - "02-sa.yml.j2" + - "03-role.yml.j2" + - "04-rolebinding.yml.j2" + - "05-deployment.yml.j2" + - "06-service.yml.j2" diff --git a/services/playbooks/roles/prometheus/templates/01-configmap.yml.j2 b/services/playbooks/roles/prometheus/templates/01-configmap.yml.j2 new file mode 100644 index 00000000..428afc47 --- /dev/null +++ b/services/playbooks/roles/prometheus/templates/01-configmap.yml.j2 @@ -0,0 +1,76 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ prometheus_app_name }}-config" + labels: + app: {{ prometheus_app_name }} +data: + prometheus.yml: | + global: + scrape_interval: {{ prometheus_scrape_interval }} + scrape_configs: + # Scrape API servers + - job_name: 'kubernetes-apiservers' + kubernetes_sd_configs: + - role: endpoints + scheme: https + tls_config: + insecure_skip_verify: true + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + relabel_configs: + - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] + action: keep + regex: default;kubernetes;https + + + # Scrape kubelet metrics (node metrics) + - job_name: 'kubernetes-kubelet' + kubernetes_sd_configs: + - role: node + scheme: https # <-- CHANGED from http + metrics_path: /metrics + tls_config: + insecure_skip_verify: true + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + relabel_configs: + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + + # Scrape cAdvisor (container metrics) + - job_name: 'kubernetes-cadvisor' + kubernetes_sd_configs: + - role: node + scheme: https + metrics_path: /metrics/cadvisor + tls_config: + insecure_skip_verify: true + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + relabel_configs: + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + + # Scrape pods that expose /metrics + - job_name: 'kubernetes-pods' + kubernetes_sd_configs: + - role: pod + relabel_configs: + - action: keep + source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] + regex: true + - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] + action: replace + target_label: __metrics_path__ + regex: (.+) + - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] + action: replace + regex: ([^:]+)(?::\d+)?;(\d+) + replacement: ${1}:${2} + target_label: __address__ + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + - source_labels: [__meta_kubernetes_namespace] + action: replace + target_label: namespace + - source_labels: [__meta_kubernetes_pod_name] + action: replace + target_label: pod diff --git a/services/playbooks/roles/prometheus/templates/02-sa.yml.j2 b/services/playbooks/roles/prometheus/templates/02-sa.yml.j2 new file mode 100644 index 00000000..865e4ae3 --- /dev/null +++ b/services/playbooks/roles/prometheus/templates/02-sa.yml.j2 @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ prometheus_app_name }} + labels: + app: {{ prometheus_app_name }} diff --git a/services/playbooks/roles/prometheus/templates/03-role.yml.j2 b/services/playbooks/roles/prometheus/templates/03-role.yml.j2 new file mode 100644 index 00000000..e695656f --- /dev/null +++ b/services/playbooks/roles/prometheus/templates/03-role.yml.j2 @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ prometheus_app_name }} +rules: + - apiGroups: [""] + resources: + - nodes + - nodes/metrics + - services + - endpoints + - pods + - configmaps + verbs: ["get", "list", "watch"] + + - apiGroups: ["extensions", "apps"] + resources: + - deployments + - replicasets + verbs: ["get", "list", "watch"] + + - apiGroups: [""] + resources: + - namespaces + verbs: ["get", "list", "watch"] + + - nonResourceURLs: ["/metrics"] + verbs: ["get"] diff --git a/services/playbooks/roles/prometheus/templates/04-rolebinding.yml.j2 b/services/playbooks/roles/prometheus/templates/04-rolebinding.yml.j2 new file mode 100644 index 00000000..48b52b66 --- /dev/null +++ b/services/playbooks/roles/prometheus/templates/04-rolebinding.yml.j2 @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ prometheus_app_name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ prometheus_app_name }} +subjects: + - kind: ServiceAccount + name: {{ prometheus_app_name }} + namespace: {{ prometheus_namespace }} diff --git a/services/playbooks/roles/prometheus/templates/05-deployment.yml.j2 b/services/playbooks/roles/prometheus/templates/05-deployment.yml.j2 new file mode 100644 index 00000000..dea30e71 --- /dev/null +++ b/services/playbooks/roles/prometheus/templates/05-deployment.yml.j2 @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ prometheus_app_name }} + labels: + app: {{ prometheus_app_name }} +spec: + replicas: {{ prometheus_replicas }} + selector: + matchLabels: + app: {{ prometheus_app_name }} + template: + metadata: + labels: + app: {{ prometheus_app_name }} + spec: + serviceAccountName: {{ prometheus_app_name }} + containers: + - name: prometheus + image: {{ prometheus_image }} + args: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + ports: + - containerPort: {{ prometheus_port }} + volumeMounts: + - name: config + mountPath: /etc/prometheus/ + - name: data + mountPath: /prometheus + volumes: + - name: config + configMap: + name: "{{ prometheus_app_name }}-config" + - name: data + emptyDir: {} diff --git a/services/playbooks/roles/prometheus/templates/06-service.yml.j2 b/services/playbooks/roles/prometheus/templates/06-service.yml.j2 new file mode 100644 index 00000000..fdaa6bc7 --- /dev/null +++ b/services/playbooks/roles/prometheus/templates/06-service.yml.j2 @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ prometheus_app_name }} + labels: + app: {{ prometheus_app_name }} +spec: + type: LoadBalancer + loadBalancerIP: {{ prometheus_ip }} + ports: + - port: {{ prometheus_port }} + targetPort: {{ prometheus_port }} + protocol: TCP + name: http + selector: + app: {{ prometheus_app_name }} diff --git a/services/playbooks/roles/rabbitmq/defaults/main.yml b/services/playbooks/roles/rabbitmq/defaults/main.yml index 6dd538ef..23857f5c 100644 --- a/services/playbooks/roles/rabbitmq/defaults/main.yml +++ b/services/playbooks/roles/rabbitmq/defaults/main.yml @@ -4,6 +4,8 @@ rabbitmq_namespace: "trackeroo" rabbitmq_app_name: "rabbitmq" rabbitmq_replicas: 3 rabbitmq_erlang_cookie: "{{ rabbitmq_erlang_cookie_secret }}" +rabbitmq_admin_user: "{{ rabbitmq_admin_user_secret }}" +rabbitmq_admin_password: "{{ rabbitmq_admin_password_secret }}" # Docker image settings rabbitmq_image: "rabbitmq:3-management" @@ -16,6 +18,8 @@ rabbitmq_pvc_storage_class: "longhorn" rabbitmq_pvc_storage_size: "5Gi" # HA settings -rabbitmq_ha_policy_name: "ha-all" +rabbitmq_ha_policy_name: "quorum" +# rabbitmq_ha_policy_name: "ha-all" rabbitmq_ha_policy_pattern: ".*" -rabbitmq_ha_policy_definition: '{"ha-mode":"all", "ha-sync-mode":"automatic"}' +rabbitmq_ha_policy_definition: '{"queue-type":"quorum","queue-leader-locator":"balanced"}' +# rabbitmq_ha_policy_definition: '{"ha-mode":"all", "ha-sync-mode":"automatic"}' diff --git a/services/playbooks/roles/rabbitmq/tasks/main.yml b/services/playbooks/roles/rabbitmq/tasks/main.yml index 077d8364..26ee389b 100644 --- a/services/playbooks/roles/rabbitmq/tasks/main.yml +++ b/services/playbooks/roles/rabbitmq/tasks/main.yml @@ -7,14 +7,7 @@ kind: Namespace state: present -- name: "Create RabbitMQ ConfigMap" - kubernetes.core.k8s: - kubeconfig: /etc/rancher/k3s/k3s.yaml - state: present - namespace: "{{ rabbitmq_namespace }}" - definition: "{{ lookup('template', '01-configmap.yml.j2') | from_yaml }}" - -- name: "Create RabbitMQ Secrets (Erlang Cookie)" +- name: "Create RabbitMQ Secrets (Erlang Cookie and admin credentials)" kubernetes.core.k8s: kubeconfig: /etc/rancher/k3s/k3s.yaml state: present @@ -26,6 +19,8 @@ name: "{{ rabbitmq_app_name }}-secrets" stringData: rabbitmq_erlang_cookie: "{{ rabbitmq_erlang_cookie }}" + rabbitmq_admin_user: "{{ rabbitmq_admin_user }}" + rabbitmq_admin_password: "{{ rabbitmq_admin_password }}" - name: "Create RabbitMQ TLS certificates Secret" kubernetes.core.k8s: @@ -45,39 +40,32 @@ server_certificate.pem: "{{ server_certificate }}" server_key.pem: "{{ server_key }}" -- name: "Create RabbitMQ Headless Service for clustering" +- name: "Deploy RabbitMQ" kubernetes.core.k8s: kubeconfig: /etc/rancher/k3s/k3s.yaml state: present namespace: "{{ rabbitmq_namespace }}" - definition: "{{ lookup('template', '03-headless-service.yml.j2') | from_yaml }}" + definition: "{{ lookup('template', item) | from_yaml }}" + loop: + - "01-configmap.yml.j2" + - "02-sa.yml.j2" + - "03-role.yml.j2" + - "04-rolebinding.yml.j2" + - "05-statefulset.yml.j2" + - "06-headless-service.yml.j2" + - "07-service.yml.j2" -- name: "Create RabbitMQ StatefulSet" - kubernetes.core.k8s: - kubeconfig: /etc/rancher/k3s/k3s.yaml - state: present - namespace: "{{ rabbitmq_namespace }}" - definition: "{{ lookup('template', '02-statefulset.yml.j2') | from_yaml }}" - -- name: "Create RabbitMQ client-facing Service" - kubernetes.core.k8s: +- name: "Wait for RabbitMQ cluster formation" + kubernetes.core.k8s_info: kubeconfig: /etc/rancher/k3s/k3s.yaml - state: present - namespace: "{{ rabbitmq_namespace }}" - definition: "{{ lookup('template', '04-service.yml.j2') | from_yaml }}" - -- name: "Ensure previous HA Policy Job is removed" - kubernetes.core.k8s: - kubeconfig: /etc/rancher/k3s/k3s.yaml - state: absent - namespace: "{{ rabbitmq_namespace }}" - kind: Job - # The name must match the metadata.name in your J2 template - name: "{{ rabbitmq_app_name }}-set-ha-policy" - -- name: "Create Job to apply RabbitMQ HA Policy" - kubernetes.core.k8s: - kubeconfig: /etc/rancher/k3s/k3s.yaml - state: present namespace: "{{ rabbitmq_namespace }}" - definition: "{{ lookup('template', '05-policy-job.yml.j2') | from_yaml }}" + api_version: apps/v1 + kind: StatefulSet + name: "{{ rabbitmq_app_name }}" + register: rabbitmq_statefulset + until: > + rabbitmq_statefulset.resources | length > 0 and + rabbitmq_statefulset.resources[0].status.readyReplicas is defined and + rabbitmq_statefulset.resources[0].status.readyReplicas | int == rabbitmq_replicas | int + retries: 60 # 60 retries + delay: 10 # 10 seconds between retries = 600 seconds total diff --git a/services/playbooks/roles/rabbitmq/templates/01-configmap.yml.j2 b/services/playbooks/roles/rabbitmq/templates/01-configmap.yml.j2 index 4e10d4cd..4aad2b50 100644 --- a/services/playbooks/roles/rabbitmq/templates/01-configmap.yml.j2 +++ b/services/playbooks/roles/rabbitmq/templates/01-configmap.yml.j2 @@ -12,6 +12,8 @@ data: # --- Clustering Configuration --- cluster_formation.peer_discovery_backend = k8s cluster_formation.k8s.host = kubernetes.default.svc.cluster.local + cluster_formation.k8s.hostname_suffix = .rabbitmq-headless.trackeroo.svc.cluster.local + cluster_formation.k8s.address_type = hostname cluster_formation.k8s.service_name = {{ rabbitmq_app_name }}-headless cluster_formation.node_cleanup.interval = 60 cluster_formation.node_cleanup.only_log_warning = true diff --git a/services/playbooks/roles/rabbitmq/templates/02-sa.yml.j2 b/services/playbooks/roles/rabbitmq/templates/02-sa.yml.j2 new file mode 100644 index 00000000..53e4fbe1 --- /dev/null +++ b/services/playbooks/roles/rabbitmq/templates/02-sa.yml.j2 @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: "{{ rabbitmq_app_name }}" + namespace: "{{ rabbitmq_namespace }}" diff --git a/services/playbooks/roles/rabbitmq/templates/03-role.yml.j2 b/services/playbooks/roles/rabbitmq/templates/03-role.yml.j2 new file mode 100644 index 00000000..129e7ea3 --- /dev/null +++ b/services/playbooks/roles/rabbitmq/templates/03-role.yml.j2 @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: "{{ rabbitmq_app_name }}" + namespace: "{{ rabbitmq_namespace }}" +rules: +- apiGroups: [""] + resources: ["endpoints"] + verbs: ["get", "list", "watch"] # These are the required permissions +- apiGroups: [""] + resources: ["events"] + verbs: ["create"] diff --git a/services/playbooks/roles/rabbitmq/templates/04-rolebinding.yml.j2 b/services/playbooks/roles/rabbitmq/templates/04-rolebinding.yml.j2 new file mode 100644 index 00000000..85c971d4 --- /dev/null +++ b/services/playbooks/roles/rabbitmq/templates/04-rolebinding.yml.j2 @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "{{ rabbitmq_app_name }}" + namespace: "{{ rabbitmq_namespace }}" +subjects: +- kind: ServiceAccount + name: "{{ rabbitmq_app_name }}" +roleRef: + kind: Role + name: "{{ rabbitmq_app_name }}" + apiGroup: rbac.authorization.k8s.io diff --git a/services/playbooks/roles/rabbitmq/templates/05-policy-job.yml.j2 b/services/playbooks/roles/rabbitmq/templates/05-policy-job.yml.j2 deleted file mode 100644 index bcaca787..00000000 --- a/services/playbooks/roles/rabbitmq/templates/05-policy-job.yml.j2 +++ /dev/null @@ -1,28 +0,0 @@ ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: "{{ rabbitmq_app_name }}-policy-setter" - namespace: "{{ rabbitmq_namespace }}" -spec: - template: - spec: - restartPolicy: OnFailure - containers: - - name: rabbitmq-policy-setter - image: "{{ rabbitmq_image }}" - command: - - /bin/sh - - -c - - | - echo "Waiting for RabbitMQ service to be ready..." - while ! rabbitmqctl -q -n rabbit@rabbitmq-0.rabbitmq-headless.{{ rabbitmq_namespace }}.svc.cluster.local ping; do - sleep 5 - done - echo "RabbitMQ is up. Applying HA policy..." - rabbitmqadmin \ - -H rabbitmq-service.{{ rabbitmq_namespace }}.svc.cluster.local \ - declare policy \ - name={{ rabbitmq_ha_policy_name }} \ - pattern="{{ rabbitmq_ha_policy_pattern }}" \ - definition='{{ rabbitmq_ha_policy_definition }}' diff --git a/services/playbooks/roles/rabbitmq/templates/02-statefulset.yml.j2 b/services/playbooks/roles/rabbitmq/templates/05-statefulset.yml.j2 similarity index 63% rename from services/playbooks/roles/rabbitmq/templates/02-statefulset.yml.j2 rename to services/playbooks/roles/rabbitmq/templates/05-statefulset.yml.j2 index d0d1a685..231a6b6a 100644 --- a/services/playbooks/roles/rabbitmq/templates/02-statefulset.yml.j2 +++ b/services/playbooks/roles/rabbitmq/templates/05-statefulset.yml.j2 @@ -15,6 +15,7 @@ spec: labels: app: "{{ rabbitmq_app_name }}" spec: + serviceAccountName: "{{ rabbitmq_app_name }}" nodeSelector: workload-type: apps containers: @@ -25,16 +26,40 @@ spec: containerPort: 5672 - name: management containerPort: 15672 + - name: epmd + containerPort: 4369 + - name: dist + containerPort: 25672 - name: mqtt containerPort: 1883 - name: mqtts containerPort: 8883 env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: RABBITMQ_USE_LONGNAME + value: "true" + - name: RABBITMQ_NODENAME + value: "rabbit@$(POD_NAME).rabbitmq-headless.$(POD_NAMESPACE).svc.cluster.local" + - name: K8S_SERVICE_NAME + value: "rabbitmq-headless" + - name: K8S_HOSTNAME_SUFFIX + value: ".rabbitmq-headless.$(POD_NAMESPACE).svc.cluster.local" - name: RABBITMQ_ERLANG_COOKIE valueFrom: secretKeyRef: name: "{{ rabbitmq_app_name }}-secrets" key: rabbitmq_erlang_cookie + - name: RABBITMQ_USE_LONGNAME + value: "true" + - name: K8S_SERVICE_NAME + value: "{{ rabbitmq_app_name }}-headless" volumeMounts: - name: config-volume mountPath: /etc/rabbitmq/ diff --git a/services/playbooks/roles/rabbitmq/templates/03-headless-service.yml.j2 b/services/playbooks/roles/rabbitmq/templates/06-headless-service.yml.j2 similarity index 73% rename from services/playbooks/roles/rabbitmq/templates/03-headless-service.yml.j2 rename to services/playbooks/roles/rabbitmq/templates/06-headless-service.yml.j2 index 93b9947e..0562f818 100644 --- a/services/playbooks/roles/rabbitmq/templates/03-headless-service.yml.j2 +++ b/services/playbooks/roles/rabbitmq/templates/06-headless-service.yml.j2 @@ -15,3 +15,9 @@ spec: - name: management protocol: TCP port: 15672 + - name: epmd + port: 4369 + targetPort: 4369 + - name: dist + port: 25672 + targetPort: 25672 diff --git a/services/playbooks/roles/rabbitmq/templates/04-service.yml.j2 b/services/playbooks/roles/rabbitmq/templates/07-service.yml.j2 similarity index 100% rename from services/playbooks/roles/rabbitmq/templates/04-service.yml.j2 rename to services/playbooks/roles/rabbitmq/templates/07-service.yml.j2 diff --git a/services/playbooks/roles/rabbitmq/templates/08-policy-job.yml.j2 b/services/playbooks/roles/rabbitmq/templates/08-policy-job.yml.j2 new file mode 100644 index 00000000..f9fdbc3a --- /dev/null +++ b/services/playbooks/roles/rabbitmq/templates/08-policy-job.yml.j2 @@ -0,0 +1,55 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ rabbitmq_app_name }}-policy-setter" + namespace: "{{ rabbitmq_namespace }}" +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: rabbitmq-policy-setter + image: "{{ rabbitmq_image }}" + env: + - name: RABBITMQ_ERLANG_COOKIE + valueFrom: + secretKeyRef: + name: "{{ rabbitmq_app_name }}-secrets" + key: rabbitmq_erlang_cookie + - name: RABBITMQ_USER + valueFrom: + secretKeyRef: + name: "{{ rabbitmq_app_name }}-secrets" + key: rabbitmq_admin_user + - name: RABBITMQ_PASSWORD + valueFrom: + secretKeyRef: + name: "{{ rabbitmq_app_name }}-secrets" + key: rabbitmq_admin_password + command: + - /bin/sh + - -c + - | + echo "Waiting for RabbitMQ service to be ready..." + while ! rabbitmqctl -q --longnames -n rabbit@rabbitmq-0.rabbitmq-headless.{{ rabbitmq_namespace }}.svc.cluster.local ping; do + sleep 5 + done + echo "RabbitMQ is up. Applying quorum queue policy..." + rabbitmqadmin \ + -H rabbitmq.{{ rabbitmq_namespace }}.svc.cluster.local \ + -P 15672 \ + -u "$RABBITMQ_USER" \ + -p "$RABBITMQ_PASSWORD" \ + declare policy \ + name={{ rabbitmq_ha_policy_name }} \ + pattern="{{ rabbitmq_ha_policy_pattern }}" \ + definition='{{ rabbitmq_ha_policy_definition }}' + + if [ $? -eq 0 ]; then + echo "Policy applied successfully!" + else + echo "Error applying policies!" + fi + rabbitmqctl --longnames -n rabbit@rabbitmq-0.rabbitmq-headless.{{ rabbitmq_namespace }}.svc.cluster.local \ + list_policies diff --git a/services/playbooks/roles/trackeroo/defaults/main.yml b/services/playbooks/roles/trackeroo/defaults/main.yml index 9967edaf..e05711e4 100644 --- a/services/playbooks/roles/trackeroo/defaults/main.yml +++ b/services/playbooks/roles/trackeroo/defaults/main.yml @@ -3,7 +3,7 @@ trackeroo_namespace: "trackeroo" trackeroo_app_name: "trackeroo-backend" trackeroo_image: "ghcr.io/skiby7/trackeroo-backend:latest" -trackeroo_replicas: 2 +trackeroo_replicas: 1 trackeroo_users_key: "{{ trackeroo_users_key_secret }}" trackeroo_service_ip: "10.20.30.55" diff --git a/services/playbooks/roles/trackeroo/tasks/main.yml b/services/playbooks/roles/trackeroo/tasks/main.yml index 93c96e38..8006cf4f 100644 --- a/services/playbooks/roles/trackeroo/tasks/main.yml +++ b/services/playbooks/roles/trackeroo/tasks/main.yml @@ -33,3 +33,4 @@ loop: - "01-deployment.yml.j2" - "02-service.yml.j2" + - "03-hpa.yml.j2" diff --git a/services/playbooks/roles/trackeroo/templates/01-deployment.yml.j2 b/services/playbooks/roles/trackeroo/templates/01-deployment.yml.j2 index f40cc672..29a6cdb2 100644 --- a/services/playbooks/roles/trackeroo/templates/01-deployment.yml.j2 +++ b/services/playbooks/roles/trackeroo/templates/01-deployment.yml.j2 @@ -27,6 +27,13 @@ spec: ports: - containerPort: 8080 name: http + resources: + requests: + cpu: "512m" + memory: "256Mi" + limits: + cpu: "1" + memory: "512Mi" env: - name: POD_NAME valueFrom: diff --git a/services/playbooks/roles/trackeroo/templates/03-hpa.yml.j2 b/services/playbooks/roles/trackeroo/templates/03-hpa.yml.j2 new file mode 100644 index 00000000..9cdaf570 --- /dev/null +++ b/services/playbooks/roles/trackeroo/templates/03-hpa.yml.j2 @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ trackeroo_app_name }}-hpa + labels: + app: {{ trackeroo_app_name }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ trackeroo_app_name }} + minReplicas: 3 + maxReplicas: 6 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 diff --git a/services/playbooks/roles/tsdb/defaults/main.yml b/services/playbooks/roles/tsdb/defaults/main.yml index d2ca51c5..2bbde7e7 100644 --- a/services/playbooks/roles/tsdb/defaults/main.yml +++ b/services/playbooks/roles/tsdb/defaults/main.yml @@ -5,7 +5,7 @@ tsdb_namespace: "data" cluster_name: tsdb instances: 3 image: ghcr.io/skiby7/tsdb:17 -storage_size: 12Gi +storage_size: 20Gi storage_class: longhorn database_name: tracker_db database_user: postgres diff --git a/services/playbooks/roles/tsdb/files/02-schema.sql b/services/playbooks/roles/tsdb/files/02-schema.sql index 82869cd1..bbc48d67 100644 --- a/services/playbooks/roles/tsdb/files/02-schema.sql +++ b/services/playbooks/roles/tsdb/files/02-schema.sql @@ -22,18 +22,35 @@ CREATE TABLE IF NOT EXISTS trackeroo.aggregated ( PRIMARY KEY (ts, route_hash, dev_id) ); -SELECT create_hypertable('trackeroo.data', 'ts', 'dev_id', 16); -SELECT create_hypertable('trackeroo.aggregated', 'ts', 'dev_id', 16); - +SELECT create_hypertable('trackeroo.data', 'ts', 'dev_id', 4); +SELECT create_hypertable('trackeroo.aggregated', 'ts', 'dev_id', 4); +SELECT set_chunk_time_interval('trackeroo.data', INTERVAL '12 hours'); +SELECT set_chunk_time_interval('trackeroo.aggregated', INTERVAL '12 hours'); CREATE INDEX IF NOT EXISTS idx_trackeroo_data_dev_id ON trackeroo.data(dev_id); CREATE INDEX IF NOT EXISTS idx_trackeroo_data_tag ON trackeroo.data(tag); CREATE INDEX IF NOT EXISTS idx_trackeroo_data_ts ON trackeroo.data(ts); +-- Added to speedup the last position query +CREATE INDEX IF NOT EXISTS idx_trackeroo_data_dev_id_ts_desc ON trackeroo.data (dev_id, ts DESC); +CREATE INDEX IF NOT EXISTS idx_data_device_type ON trackeroo.data ((payload->>'device_type')); +ALTER TABLE trackeroo.data SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'dev_id,tag', + timescaledb.compress_orderby = 'ts DESC' +); +SELECT add_compression_policy('trackeroo.data', INTERVAL '6 hours'); + CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_dev_id ON trackeroo.aggregated(dev_id); CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_route_hash ON trackeroo.aggregated(route_hash); CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_tag ON trackeroo.aggregated(tag); CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_ts ON trackeroo.aggregated(ts); +ALTER TABLE trackeroo.aggregated SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'dev_id,route_hash,tag', + timescaledb.compress_orderby = 'ts DESC' +); +SELECT add_compression_policy('trackeroo.aggregated', INTERVAL '6 hours'); SELECT add_retention_policy('trackeroo.data', INTERVAL '3 days'); SELECT add_retention_policy('trackeroo.aggregated', INTERVAL '3 days'); @@ -52,3 +69,46 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA trackeroo ALTER DEFAULT PRIVILEGES IN SCHEMA trackeroo GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO apps; + +--- Latest positions continuous aggregate (replaces materialized view) +CREATE MATERIALIZED VIEW trackeroo.latest_positions +WITH (timescaledb.continuous) AS +SELECT + dev_id, + time_bucket('30 seconds', ts) AS bucket, + last(ts, ts) AS ts, + last((payload->'position'->>'lat')::double precision, ts) AS lat, + last((payload->'position'->>'lon')::double precision, ts) AS lon, + last(payload->>'device_name', ts) AS device_name, + last(payload->>'device_type', ts) AS device_type +FROM trackeroo.data +WHERE (payload->'position'->>'lat') IS NOT NULL + AND (payload->'position'->>'lon') IS NOT NULL +GROUP BY dev_id, bucket +WITH NO DATA; + +-- Add automatic refresh policy (refreshes every 2 minutes) +SELECT add_continuous_aggregate_policy('trackeroo.latest_positions', + start_offset => INTERVAL '10 minutes', + end_offset => INTERVAL '30 seconds', + schedule_interval => INTERVAL '2 minutes'); + +-- Create index on the continuous aggregate +CREATE INDEX idx_latest_positions_dev_bucket ON trackeroo.latest_positions (dev_id, bucket DESC); + +-- Create a simple view to get the current position for each device +-- This view queries the continuous aggregate and returns only the latest position per device +CREATE VIEW trackeroo.current_positions AS +SELECT DISTINCT ON (dev_id) + dev_id, + ts, + lat, + lon, + device_name, + device_type +FROM trackeroo.latest_positions +ORDER BY dev_id, bucket DESC; + +-- Grant permissions on the new views +GRANT SELECT ON trackeroo.latest_positions TO apps; +GRANT SELECT ON trackeroo.current_positions TO apps; diff --git a/services/playbooks/roles/tsdb/templates/tsdb-cluster.yml.j2 b/services/playbooks/roles/tsdb/templates/tsdb-cluster.yml.j2 index a96c9f95..61be4322 100644 --- a/services/playbooks/roles/tsdb/templates/tsdb-cluster.yml.j2 +++ b/services/playbooks/roles/tsdb/templates/tsdb-cluster.yml.j2 @@ -12,6 +12,13 @@ spec: instances: {{ instances }} imageName: {{ image }} imagePullPolicy: Always + resources: + requests: + memory: "1Gi" + cpu: "1" + limits: + memory: "3Gi" + cpu: "2" postgresql: shared_preload_libraries: - "timescaledb" diff --git a/services/playbooks/secrets.yml b/services/playbooks/secrets.yml index 0f48f4ba..027e780a 100644 --- a/services/playbooks/secrets.yml +++ b/services/playbooks/secrets.yml @@ -1,302 +1,318 @@ $ANSIBLE_VAULT;1.1;AES256 -33323239366637313934376464393335376563373063653666333739633366373335353664306535 -6565316230316662623861326433383933623831393831310a353139316562396539386464336130 -66333038653062616664613330366138616361383465313537316231633763373432353534363565 -3633363336343638330a303166356136653337626366353464346531393165383164353464363231 -34323039366162383861303337333337366636336565373765396662343935336238303063366436 -30383231383737373566376365343931336634613534633031386234396435656365363434383439 -38626665343836323562633339663664393161623861343236313834613136386165336364623034 -31323534643330323235653336633439623764383365303664653435393434373234313866623266 -64633435343234616135623734373435383932393935356235613163633265313339313238336534 -30663232636338303230626462636330306631633837333263636530303762303932373230313762 -63303936313432653432653032363836316264396634306536376635353965656532616138313538 -64666134313064636434386335333162666266383562366561393135393032623436373434373231 -34646266373066616436303761306638653236303938323664333765393462333161633537613736 -34626231343663313131363339643939313861323063643538616364373363323465346638633838 -30336134643362313536346138386464653232343534386335313238656630626134653639623064 -33666262393361393065313330656363633665383039303033303331326663356464316336666266 -33633562323061633436373836613338323363373361373562633065313734663937323538376434 -66636238646262626339666430323434313932343335333363306531633636326337393364333735 -30616436313835323434666431646636333230376165616332383366303664336130653966393162 -33323339313838333761613966323034653665373466396137633032313031616230373065353035 -36393665373430646365636535343163336539393736366661646661643739386636363464616137 -63316434626639383232636466646330653462643333666131643764383230306332393533383865 -38623964623831386639333630333062333061653035396466326461633432616364306335316633 -66326138333638306131663566313737316164663831316238656330646165313465616233326537 -35373462333038373662326638363634323634643034636665356661356264303466306237643336 -39623665613333383765626265653735313163343437616430613036306436613563363339623065 -33623861353462323230373064623630626261356262666466393063633564393335646264303038 -64383837313237376664346631656463393636393937303165373138386165616438663634306634 -64393035326331376135316565313133393337326230333661643533656231386461326632336234 -32636230396437366165333939313365373266326433663030323362373866383433323035316364 -65376366356564303530623936663963633337633063643733643534653164346531326163333433 -33636633303336616233663065643334303431303730383938663136363161333534363036353562 -37383663383335326636313636323266313961343632613263616339656333653931316563393935 -65656461363363663365613834326531633764333465663764306530333739303733303361333033 -62366233646562313464646262613936653137663535333736633035396163366132343637323833 -37363132386436656131303633313239386236346239666261643732663366316233353836613966 -65383138616437373330333766306266363564336338636538326361643734636561303435366561 -31653830656631346636346332353430353264613134636535383330626237353434396433356462 -31653365613831656135343462646538343235396264623837376334616632383730636639636166 -36303436363634373430373763333166326561643163366262666435363338343362316161656630 -64633233346565666161343539336231396334336465643263383866326639646435313731336336 -37666138333564663161396431356134663365616263306662663763336161663436376565323563 -39363261356162663763393864613062373833303832613631393735393865336162366430623766 -64376434356664356662316134373766643631646163376430643532393835663533616332663235 -36333661656434316633376561633763653163663131353339353164373361326463666330343836 -61363663346265336361663864303235303636333662366164353663336363356633333432656438 -30613138313962356136323839323639303266636133633366633531326234643364653730626564 -61613064363864326436636464393138376366336366656237356232316439316330383163323166 -34346138636233336363363834623761323635373938323166313962396233313562353232343330 -66343261343833643963396134346130613062633436633332633936323336653331616637393562 -32636566333564336538363865303239393032633139633766356535326336623333336234376165 -38663835636466313234663934366164396363626261336166373165313739313064396662336564 -36323631623734663837646437636264353232393366666532386665393563396336346666623739 -30623965663532633766636132623632343230336235633761323937373861363262653332643832 -30313966333031356165646333316331613137346334626335663831383863646563346464623063 -62643836353332383130666138383364653332623661633061343461646639393439646435613362 -30336532663563656334613337326361613437383331393739323966633635663937663233343431 -32323435306133363835333031343763303832353432313364646439626265353430666537613066 -31393134653638386166336139393034313863356231333131656437656233666339626334623136 -66623762663433303864616430393962643732393631373230616562653833373563306265356237 -39343734336130663734636431373539393731363835626434633961393439663534613931313463 -62633436303336316131313531643634613465343931356634653163303538333837643932336339 -38313563343639356538626639623033633735636135326434313839616164643562333262383431 -30633135306233623534333131376563346135623066353962333663623137643565333364396164 -63356331353332316165306431356462313833636462373661333436363638666133316166656537 -39323663626564303634333264346530373339616136333836653037393930316666343739663533 -31363637343437383030323833376361373465373539363061326164336638613364663966313439 -62663862663635333737393365346438363832353436323132636536353330353430303333343533 -62333039383366366633653263663365343933633034326132363730313134353836373936343532 -30343966353964643134613264313064613130666464393837323363653336393135363034643231 -66366665663964623665353039643230303362323666616534363561653837623935386331636461 -62306365656139386630303662323938323662633134386266383537376134343035376534613435 -61356536313933346537333261343334633161336262313634613463326437633538656632396630 -38346434646233663131373162353462313336366636653734373466323565363430306463313431 -64633761343131383762356535353037333666633337393835363463333161313534643335366664 -35373966653137393262383366663863366531333332623166386666653263646632616461633138 -62333039633734396339643362643931386434613662303263333838623039636663646464326134 -36303261353637653033633035653464363364366438353561306664646338363463356266663562 -64363932363065613130323561366432643734356538366538303566633862303161363332666261 -64373065663330653463646431353834643762373430626636366536613634303830656531663763 -30313565636662303735343635646135393866626535336439643366306263336334333066313335 -61333265343635623066363531363637613665613035396135393435363831313433383638643337 -33323530646139353339356335396162383530613062363833373731343036356234666332343534 -63633330613331323363643862656438613564613330363864616262313936656461356239636136 -38323865383563616665383737343832313630383432643839313666333538376531626236313863 -32323566343764386534323664393561323761626139393633646133393964373861666139336434 -31326135396663313132633135353633613161643962643765363839313932376232343864613334 -33316230636266373635366665646433303138333436396333363939643335643230366232376563 -36623466393564663562376366643864363661613964663539623366386261653339653933326165 -38663334363361323231393138383466333863336438643561656432303739623437323562333863 -30373666316334626433643931303535333161343064656264646536663766386332643263643731 -37333131306233356538613035336338356662383532343562666439346438393534633361303939 -37653239353233663430373163666663373164663138356164396534313835333534636131636266 -62306235363332626136623832316365626562383465396235376161383564666338633632396532 -66326536663931656164383635613035613731373830333166636330636637643461306338383139 -31326435356534373866376131343031373533653735326637636334343566336137663165313030 -30323833376562326639343234666362393466623464396563393838306561373939313237356166 -38386633393137336135356662366436633265653530666138373935323665383334306633346533 -32376566616164646363623131346662393265653661353139343437373838666230323364346464 -36373036373631326632376631383362356366376634623063396461306162393165663937323830 -39623738313764343938323664353262616261333362323934343864366232336563383265303238 -61336437326136656531383030633764343331653463363832663833663765646462313564303038 -65316530616263636231656334623039633839333230303538383637626362643636306162643464 -30666633336237386231613037313064306163316139663038383935346235306536373262373064 -66303636383733626331373435613632666438636366396266613532373066613164653261396565 -35623737356266643731623262333332666331383536356634363230636430323761613962306138 -66373765356231353333666133653263616338373431636336366462613762343533333065303835 -61326637613731386333356563643737656135663438343639653231323636363764316439353763 -35663632636535303338636532353835623661396338366534333630616538393231633862323831 -35343832303866353361306233643762653831373239643565386132653539633837396131373562 -31383135626131326133323466356465323431376264613462376133383464666237336438623138 -63313862643865356331633532646638326637333035646361343263333538356331356338666435 -33656638653763396535323832333765646531616366323361373436383261646536376338386233 -66663039653566666133613535383132376537333962303665633136613161303062376138316133 -30613134383332626538626265656461386633396430646132613430666165306637323433653634 -36333639356334636562353739383566386365356664663034326637613362356532643066326535 -38643136303066623130326137373331626438353366616264326339656636616232336130663463 -30666461313038333337613635656166353530323866353030363736336365333062376139366632 -66656333363664326637376132663933343133343462356137363639303363633534643131383561 -32663834393363366232336432353761353538373734643730336433333239363965656262303737 -63346137636438316139623732306236316465613863653532663535363139613566323838303336 -61336138343365393439313536616234313733346531313237373563613862613431373731643465 -38643663303762353364313238613336383139646335623365616463393033363834373865353931 -65353433306338333561333961646662356431376131626638303265636136363665333863383639 -32363730613761626633613136636139323130303434643439336134633136646630333266643432 -33346430633435313365306331393133646164336166393233663339633465616532663035383261 -65663065643637643963373934613865643832316434363131363431396564356462663866663437 -31653166386561363932346531633666326136363732326233356665383636623334306431326532 -37653534383533346432663636373730646164623763363565663832366530636163306365303161 -37393534373831623931633938326639333861346233333833363938346565616436333734323835 -61356566396439636130663037383733313234643665666538643863613165366431333964373963 -65383336366535373663356137366234623466333937303230343637396639666238393136303431 -34313038663939656232313862623263663734613334363430663063376136336333316164653931 -65636639343232656332623835663234336362333131316531356534383135363238366431653237 -66666261353137623362613966613762343633396636623163336635323039303132613165353337 -36306238636635353431363634336562613239613162323764383838663361366132383937666535 -36323637376539393030613437386166396161396532633865366663613263396366303735646233 -32646338393136323361363332323030656162346338663436613365666362323733633862383231 -36383733616138613932386236353264303532386566326662646335383838376430363164646164 -31306338356133363434613334373339343839336161313330323431386330666330386361353832 -61313636653837656630353035306336646434616265383635333130383939613939303965316366 -62656632316536613964343864636430306132636137386663386431346139656635323238313433 -32623761363433333265333538643533353862633534306435663132626431386461303964333337 -62633132636431326331653336373936346533393964663737643733656635353161633463336432 -31313331613334346562633264353035643661366362316235653539306433623039663932633533 -63383639653864336134323331343037373362363632663566363063373763356636373033623661 -37353530613462303936343435336130306465656330353430336664376166323464303734316538 -39666262363232353832396663376139386335653734653739333539376237656365393033656634 -37363462356465646432326666313438316638646437363439303862666137356465303237666332 -34313263356165333065643533306139386634333432333930623230633433373261343032326265 -39323138356239383530373934633034656430383631323238366635353939626135373737376530 -62643835663539303433613931613838636537336437396131393465633631663637316532346462 -37613862313534333066333563343333613931613433373362326335373734316638653933363765 -34626638373232656231396165613364333061373830303132383439303330643764396564656131 -63653766356663333138656161313831373537646166326232636338363564366438643764343339 -66386335313237616266343033663332383432386132336566663430393636653239636563373934 -38636564643261646235666133383263323838326635366164353531613238666363363633346463 -65343231656231646431643666376639666638653836396234393034656666346163616464663136 -31313234383363383437396333623338666163326336366663643265656631396138636662303562 -36623434613666613663386366383335383237626433663164643564323064346436666131363936 -33303637663035353632393633303635333764633837313432323863333937336435653630636331 -30303664386666346665623265343434366364326636623762363936316330316132666231303230 -30643461653938396230653566336132613832656237643535306335366435383930306430306233 -63656465666333363430336664376133316135353431623739393261366139376636663866626266 -38666665303538616437613232626134303764643163653132393036373033343239316665616331 -62313261373137346531386434626535373665393432343763663562353232383534613266346166 -38386231666239326464323636393338303563626135353864663236313938316663613136656132 -33366331646563333862616438613137396662643838303034356430326631646430303633653033 -35613833336639363532353865303063356465383735633365636465653138656561626434363565 -31663139623663663634643266316633346237623961666534303338323839303432316333313333 -34313533373162373164396635363230356661376237643838353733336630323833353066646234 -64353938323437306264653063346465653562326262393330343661373533323033363861646538 -30323365616533633330356532666631656433313066613632316162386561333234666366613632 -30326431363932643238353263303532303635653365353633623532313938326436373762646138 -66313032303937383065343336633130646136363933386437343734396664663265323030376431 -66666630326262396332386565323235316335386462323961376362613162336333313838656533 -32643137616531613433643764376232616166353130326463636233333262636136336436393134 -64656465303139643738376266366165666661636631386362646338383836363563373633313362 -32653539613139653532376462303538663134313639366663643531653639393633396462633230 -61353533396337396534623139346638376237653235626564643231366136663330363061396231 -36613532373833313136336630613038626163336330633930313434333137343932646133633635 -33663834643866333562643263653532333931383162353531383432313332643363646438633338 -33663336363031333166346436613939346466343236326336613837313737656265383136613262 -39616430383232643635313061333861343034363261373431653038636432373233333539343530 -66633333623563336134396335393638396539306466313161663562636531396365636266666433 -35323435623536316463326538376136333334353163626164353232343364616139663764646266 -36323139363232326434323764616333653962373562613336383161633439316164393462393834 -31346330396638303938636536333036306636363233656539613438623331343939313863613566 -33346262333361376535303031356337383235643262616137393236363764623462373464393363 -64626334356236323336633965326437326465393265613233663033346666346561373231633062 -64636563656539393730626239313261313136353734656432336332386532363338626137616562 -36353937343239666266363162393766663435393631356166396566626463343563363964346561 -65633962616362643734373237633934383261623063396662343664313737616239353034346462 -34346334633830636330666366663638663335376639323539646339323166386535636361346236 -37393638316132643131396237616433346236373036393530316133326639623638313966666335 -38653336396233653834363562373663316535396539356365633430633966613938373738303330 -63666637393164373063653663363665393232313532346162356666623839396534383638393564 -32393162646430346364313131656237353934646135346363386265626433633834333537366466 -34646136323964366665353033656663383632636633376432383765623730643933623839623335 -62633036643232643830626138303737343464303464313066383431616265633466393364346561 -34653534626365333533653434643733383861393464353635313738393735303163666536613930 -32376266613563363931373262343363633433303534353966336632616261643263396239316238 -32616336373034313339336133656462636637326238643733343831393962383066373764373732 -36376665306236663732656533613363336162373363616262373962643866323132653938386139 -38626461376261633637613334333337643565376636323737313232323365393864303137323730 -35333566393835386564363438653531343730623964306435663232383164346238663038366365 -30636263343533353138656339383633633338663630636461656130616335623836346337333535 -36343964653664393833353833373864613364353432333933623139323639353432343630313764 -39663835343338333130643365613134356361663631633138623263363735303764636265643665 -31313663646536663533303033633832373934323131333832303336376237623137646531333231 -34326632303134653033663931633664376537303366613630643963326136623461636166663464 -35393334666631643431656339313334363831366431623937303131626238633732383539373631 -63623033396461333632623463633738386639323930643366663530623961623465633466353331 -30373231333632356464623737393234623666363466313032653639396662646436623933646530 -34383938656534646261666333643864616138393533393266616161356633356332643431353637 -62366335663332353066303761643061306435663437656162316665366233336639356530653931 -66393566613765303937613939646361343163663066313631366133653739333436333033306337 -31313862636239306664393538333361303833646630383331623765306137313236373132326466 -31363162393232656538303966343038313631323765636332316339346436626136386336376366 -30353063303433353665613861386265333765336463643064333339626566666363633864616265 -39393836303530663064663562356537643765346233623038333432333162393539303934376530 -39636161643431653163663662373032363265373861323730373530666334626433346235363439 -36633663336239623566393232363761363237666263616531666438396461326536613036363366 -38613265653137306237396134633238633138336463383132333162393830303866323365626330 -62316633316262613638623935346232313530656132326133653133613662336137323463366630 -35313766653962653062646134313937646533326436366364376132326661613131353033323662 -37386365323834323366373263393636383566303261653138323833313331643035623234656462 -64363236373831636434313631663739396363393433336230626635396633346532643461326461 -35306439666131383762616463653765373438613636653934656630623133333961653565306465 -33666139336464336535363835643664303862393161386466373235346237636134386463323232 -33643830643133303732613738666635393864323936653739376233393831326136623764306335 -63386562656332353333616239333831383335616439396266326139646361643262313736626237 -61366439616264353266313462636539323764626362346337343435646462306564646463346438 -37313133373430616635356334623136373639656331306561376131383036346231623730663465 -38396666356665633039636334613632653231393765643639626437393337613237636361393563 -63346234383033353861383134366462643461393535616465316434346664306134366635623162 -64306436636631306136653134326237323236656364386362346632383730663032343035363532 -63353531303061353532386639646164313465633866383839626438333866323337336165353766 -33376530656133333261633737383731333031396365356530343433663863383736643733376464 -31653164663537393934656432393830623563623163326334333664363838326566616265373266 -31626237343537383930613563363832383139336130393434376632643762376433336633356337 -37333665333435623030306435656339636234616331633961313739316238323563383065623734 -64373635383137626565346432383633376337653134646232616435663439343439666363646431 -63623536383065396165613839613631363635666535386336396433623261303234393666653064 -36326637373563323932636231656466306663313934613238333162636639643337313662313139 -35623735646532306331373664396133666262326333613635313739653363376464616139643061 -39643434343163666130393633666637356232626131623332626661323036616338633237393239 -62353033333637323336663264373138313931323531623632333136353564623239653533356235 -62663366333036316165373831636637636166663062383738333164396465323336323938303733 -38373732303736663365633065636564323732623732346665643430616532653438383363653436 -36393637323263313233616536646662613861306135376130396534363538643361336662343262 -38616536666436663635373661653362343562633164313662373666616161393266316436646664 -31373762343062636461636163633266353564313362646539303365303630333035323136393062 -31373738613536383661313133643065373138623166373032313765336138353233613236366630 -37366630666430313464623234303135356536393438636138653731663938666532663834373665 -37666464386136323838623263633335636338353433643834323533316636303833306539373965 -30316462643264656433323431643263373934363062326662306164633238333933663934383362 -34343730353836323834376438333431366536376539613235373634316261356665366235643163 -34383239643936383238646430313237376363386639373035376165646164316332373233363235 -30356630393330303764613736333235646266396537653330396131303434643138616665616166 -63316138633534633961336437353539613664613931663234623839653963343539646462373731 -34353238623232346161376232333434663033303433353164356637303531663832663563646536 -34333336316665656233303438383836313637623866336639323539323962316434666235616264 -34306165653938626461326233343338613465666339326336333838346438653433363930616438 -64316437333337626431326562643734623834393263396139653362313937353737383033376638 -66336664303238323566356565333565343736366137633236666230313965383062326661376232 -34323264316663333332316433313835383765376438623436393161623465363863346537336636 -38613638343934336562303431386135643934656336383633386665616534323630346331393565 -64643336346335656330366566656231353862626364386638303137633535663339396262623339 -64643964313561323632613631393534653833343638646630333832303436336536363833386362 -38333836646336346536333632653762316463383032633566376436313866396361396262623331 -64646137383635636336643136373436636166376136643236633431343530663439323434663164 -66323566613664653630616630626334326438313132653535616165383931653262386539626361 -37346364326538643737396439643435316130666164363835666137653861633139383235316334 -61653632343363613832316461323838366466303837353762333034663335306561366338626131 -63376533303236316163303063316362643939353037376434353366323461396561393666613461 -30303264666365373265393033653536313630326539633563393239663264356232386437363131 -32376130616162393261363136666130303235393664343466343235343065313432386631383265 -63343563383234353332636436633066306431333465336136663036356465396461393064316663 -36353632343064343361343261643832376431333837343038653837346437383339356662333234 -32343763633862623434336566633332643365353939373039326139643239353133376133313630 -35666531626364353537346633356536303433356639306536633264346562646665346363633531 -32396130613762623336396563303762643937353032313836366331623237323234646333353333 -62623365366236366164343038336661343466373664616366663037323339373635656664663236 -33343037376365323638653561356261376635376438616437386365626236353232646335383530 -62353936666263346630623037636164353438643030316432386139356136313133666638306438 -63613864316135373733343832346531656362636265663462366133636563356566386662343036 -33653462326633613861346431623033343432623139386165363961376164616534303536623631 -35303536643336613265386637613866373732383132613931613039656139633365373434613363 -65373132653661376333373138303766343838373833623131366330663734393762656138333832 -32376436373433306562316333663035343262363538303834333138306436653430633035633866 -64353531383461633962336132326566356662386361346134346532333238643432653034326465 -62396134326332636534666433626336653663313963313034356332656265653731326566393463 -32353062303438643563363466373836643835616263393964323631333564396138336637636637 -62333933333538656130646362386366656233633039613132633466363239613933343639333465 -63313338306662313866346263363034666534616236613866663935316238356235643938393364 -32663664366333356463313232376631303035653638616533633063336131363933616261633836 -35313966336434373430393832313136393835383236643430646533363533613734343464323530 -31666466653531333465383864643138643265663033333230306566303964643463316463373532 -33353138396164306334373435306636623138653532313437613461303839623131623130633065 -66646365306635306665376238353663383839643434643566653237373032656361 +35326333313331623365633462323932656139656133666465656537366566663033306632346139 +3363653733306261316131326664396137613866646134650a373638303861623834633432383164 +33623861656234323436663137636561343062633734653661356461626335313535623637396239 +3633656539333131320a656439383032636665616539383666386662373166323730666364396363 +63333363333830613433383564363039313864326235303262333662326233623365343830333666 +35663363656336646435653535333466393262353732386230626263363035333765323930343366 +31346333336637313232303035313238616566323239616539333264393262396332303035646561 +31383630323961343730656533616639353337383963646138623263333838333961326330393064 +31396135343564383166353232306130653131623262366465363065316566326361336237646565 +34383563343435613839623831356535303231633665313539623834636165353736353137613666 +37636632393933376438376533326432643937623331393535663433393636633031356163333833 +35333762636365333136653239643866386633396466393731383330363336333739643636633266 +33306662623363396663303933366530373231366331333330303339623336373165313131383234 +65663639616531336562313730336439353132316238376638353661313266656335386534303332 +32643839376536333162326538366230386139336339636566383166363666613965636566336537 +61323830623733306232316331376537643433326239303566343366396565396439383063343138 +30383339646332613437353730386237653063333130633463633334326530633336303236396538 +32323735353962373635306466616161323964623763656231363533366639383832623666666164 +39383933626336383636363463373063336166316566363964396430363036646134396334383062 +38633063303466323234303164366661306136366235633738343832386364653830663162316461 +32646464653664646332323438373532323061396630616462383037353337393833356563646433 +32623933326162653662353336666465373362323436333432316163666366303466313535633433 +31383561646339326631623034626463323432616435303563383339376231353161393862353137 +38353734326364366132343733646231316337643266383135383939333666663439363432373664 +35353363326339623634326131663964643939663536343134393133643833316465333332343337 +39653461653235333936376134393238653164633032393665383335313237623833613437346638 +35623833646363396233393563306336333338313165393066313064316139366532306261643838 +36376133356633623364666334653530393565623138616233366330303366663333333837393037 +33373466643736333534383862333730363661636264663161343666323965613238326361653434 +31393632343136633636626430623164393832383661343531316232326264396135303735333761 +36643265373365663166333438613061343138653064633432303833313265356265663066666236 +66383966323539376234353633393736396231346361623362663439393262316164666632303434 +64386434653334313530653332393136396230656233326432643466353038363961656431616234 +34396233663531613763306162303063333339333361303861343138633761313539643162313463 +63613661323765306639316532333165306562386434383863643464663562353461363839633139 +30656366313830376531306264626532393066396236376334396230626264306235303161623632 +65323136633531313233316136383965376538343739643466326233363564363964316533356465 +38383566626431653061326330623962376338646461306238323030633735313661306239346461 +37303637383561313835323763383230376233306631343738653462353433303961386133313036 +35633264383362626465356436636162366232363935363936626432663933373336393238303662 +39666335346230303365333738326131383065393631656236616365313837663933386663646236 +32313130626562353065663030636563303637656564303933663862666164356164316433313239 +36623366326562346661616465656133303662643832353633626339353366623939363738346464 +38363434363666316238313436313562636437616161343031333066373566326363313461386163 +33313238356463336565383838346531386339346633343436346563373362336666303165666437 +66356231336534313939313364656538366331313166626633616138373134336661363838616163 +65393836356134396265326237323338306163626639366661346437326133356262336361353538 +39643762306437643261303465313232303262343964663539646361313336613965333163653936 +34373364313134376635373734346234633033356163306535333363623366346338643531383366 +35386532613064323637303637393039343865303137363965363439366535636666373366366362 +37396230363362333632393439333631306266656232386636626630326664353863373434393631 +36623435336263663033376164323537343832393465396435336530633531346434386563623130 +37383935616431353537346333323165386531376238656266613132623065663039313339643039 +36616237326532653932636136653766343462656439353337383537623231366563356165363034 +36343236333231656230666131363133313931623435376134356530356435306439383737336463 +30366431623436666138616366633630663761366638326337386436613962646534643637336539 +63343431356538343533626238336563613839353730363332643533396364623834363833306535 +38386266383961663464396330613838373438653237626336396630316165376166353031393962 +66303833363435643433613432323638663038343738623165373965353336313437636465376539 +39323633303238393766323730613937653634323034636565636562343066393233326261333062 +32353636623138313030643937333161653564343365613265363064636435663933643436306533 +30343537393133626266386231613632303834353030623834373833363932323332353563383763 +31373961363961373165613434393039363462373837613064613237666337323935636665353561 +64393831646438323339396163666533623336336532396433373336663066343432646164653635 +66356537363833616638383463646162333137336436633638616238363139396261666233633431 +31653036613862376166313865666639373566613735396131313133336537643136343661666436 +32326331386464666137356166343038376163383838633535656262383934613766653335646461 +37646362663564616230396465313166643632326232626631306161623934656165323335343764 +39323162626230353862643237646265333862363164636334323033313665613366336365386231 +37636139373439326462363131613061326235666231316133333166303035623431393764636633 +38613530306339323839666539653937653635393035323366666363666439643964636634363436 +33303465393632393139643762333062306538643264336237393039383062393039663332663037 +34353339656562616538346532353432356566373831343862343437343838343130613863616138 +63643265623762366561313631643765353638653834643331343465653662633764336361353838 +66346665653265663932366438623836343061363939633734313237343866333936646134613462 +65643235323466656239393434393530326332346161613435306361643430653863663635626562 +37643037373930623639356332393837643539636535386430633239366162383834363431666262 +34343866653463376533326135393533323637636366323333613939306261613838633261383333 +36333362366532343162303964636632316337343665326331323430373065363032373265343861 +38663661356236376563373130323662313563613532333735373964646435313039626264633537 +34343134346434396163393635633131623365333739336435666265396266626664376435333631 +62336166613431383932303234343164363534616465643033626662313630636666396461336533 +62663165386335643130356337373937363666353061373563366337316236626638313862323235 +61346166326237383332303637373139306231633631353964313165383338373832356633393461 +34636332326430333334353066383035376465663162333334653663313261636535356562663862 +34353834616531373738313732646664626461383562396661356134373439393134353765373231 +32363434333136613534353937646365613739643264663861363263613865396138633966643937 +31356339636530313836643938633937636533366435323737626561396165643464643831643330 +36303264633434376538363839336162643436653866363831646132323363646235363561366135 +32363462646333633534363666623333636137666432303132303033653939326239383861383363 +66313166656136653433626561633561376431313237613531343232656135393263636336323936 +65303339396364373063623939636661353738663039646463363262653030636234653136613530 +34653531373736303839323236656237353036383537303064303138616630386634643833613033 +62396561326334613963393666643335646636323539386331343536353634646664336234643131 +33653436353132396230653166363364373736613430643135373930373239636337383831363838 +31336266666631633364633333613333333964303138613037303965646166363434636231306230 +39303136656164346664643036356534383433653036326530326234656261633566346134613763 +35636136623965313031666531393038633165643532343062363863623934303736396631633161 +33363330376135656365643433623866323434333939666132636232616238616131303431353331 +39326237653831343434626630646639363538323332306434316539623233333837356531633331 +33646237613164623037306130633563343266343164653566336431653862303734633134623933 +66363337303836643166383532363538373566666461623666363836653964653039313362623236 +37356437343737653630363766613137363632373631316137333331336361393238363237323862 +66313366613931316132613939643537656236383730623832613761373066646261343631626166 +39633730323539663231613038623739383132623730643866663565346330333332666263363732 +66633939303136303861333837643432336139646538316438383466633961383337633661323764 +31633337376432663461323961653931363961343837366263323732633434663364343537623238 +30386136343438376434626231623132623538346633616432346532653761323035386435613332 +64373936326362653334343037363336396539656564643830313339363733653964383732663334 +32356331636632366266386632303962303834396136643233613737633332373533383433313334 +39316262376163666162313534633636613764386633363230393437663238616166316633633638 +31313733313234316361646364313134363431316564613137303031346362393566366634323237 +38323439306165363265666662663230376334663664393661326263663161373033356539363730 +65373039343762393231633635353161316332643130643438613065313763346137353734636531 +32326438336232363139303665616234366435326334373264643539346531316565343364393533 +62613462346566616135666432323665383363643538393566393137336334623638383636393363 +39666666303635323034356466613263326532383262666134323438656462306666373937333765 +65616131346432313861393966313331333663633838366339313537643336646566633363323764 +34343863666363653163383330326463356234323132363338663966366233396131393365303965 +39623866646464613438366461633935303964393637313036383838656430623236653934393836 +30343436386165373032303261313931343134323035633464613532613833643039343533306337 +32663736653865633538353433396666313264396632333966393366386133666562383962613966 +61303034336630613264393438356662656535646433646235613661313534623662653665666437 +63663834326663346530623864643164353533616239303133373065643938323334356234666138 +31373634323834386664633235383362633339633766313832353130393633653033373062323331 +30393837656463633332363035303532303735623039356666363237633830393639336638623563 +66366338396236386665666535363938646161313031313966666134333534636538663532346663 +30643837373062653763343131636463623966383331313163336364616430356331616333663765 +33396562376261393236326262356639306532333465633930333638386264316630313162316162 +37363262393033306562383032383634666464396234396630353735383962323939323965653839 +34666461306265373561323730366431343066313335633164303734326563643061313366643062 +34323165653739656235333563643164373238666664356133373865613862633730313131316536 +30613833363162646138323732353739326366653366353734653464663138626263666639313135 +36633530323361666139363264353930613939306631313932316339643135343633383730326234 +61616263663136383033356436313364636530353030383130666563323766616562346135396637 +34633563633061663461616164316365633262373939636666303161663062313836333637613865 +61636535373832353564613237343331633632353665343831386266663130313761353865366639 +61633462666666393137396431626634633961376665666562313433386530343363623235653130 +36653034346364633865663134306638333165323266396635343962313563323935336233356133 +34633234663966303139623066333261636232666436386334353561623765623965626430656362 +66353938626238393365363132396438613232316566373362386165393363363165653030663037 +63373633323132323764613231396135336333323033316631323965346165616261393366323639 +61336332313561623333313762393032326236623166616265613062656331363738366165353233 +64376635306233326463633535306138303135656531313739636266356262303262376161376535 +38653333346235383030383139643735376430326633623266613861643437393566356433393062 +32623539343366303738373362316536656338373338323530313463363037313634383264343837 +64336165643437633033646466643431663339613761373437616339633861336263663037396563 +33616439316539653931376635396463646331636664363239353930353062663231323665346364 +30343536343261373733363733653861616439316134326538666362353432623536366662633463 +34313464663635633331383232333936306233383562356363373866396538363064633939346563 +33383639626664653637323534396633393366313361623161643735336533316139303763376366 +64396139353962326465626537646462363664313130383237663439316537636234613930386436 +36396336613833356334346333636465613031613932336234373836336166313634386366623461 +30383833316438396334633335616639623863336333623039343836613537656438623566323231 +31353562313166336431653430663266373162316534366234383536306166633561346230373337 +62306239376530343036643264373432336437346631666138326237616435646538333732396437 +34346535626330376136396464653233326233636163343566396665343338643837396663333134 +34313935383330633039313739663564626638626430333034303631303966663236653763643730 +39313965316233346564326464636363643431663235323535386132653063386139373237303339 +30636531613334316432646331613131313663626332323461613735646337656461383031336438 +39376466363735643865353765356564656238306134326439363261363332623561336330646637 +39643835356231366163323739343166323932653961306464333938343463386233393831313966 +61366334306134373938663130616163363039663536386263656662663536303062623961396434 +31343435313166333931316364626163616563306466633532656535336437626632663162633761 +63373831656132316636323130376434383462343635666566393966393564343638653937346463 +30646139396531373034633738373736636163326533656637393335393431323064346666623563 +37353862373736666236353663303263343437663038373534633961396130396430353939393331 +39313633626436313338623837613836353433383535316132613138653563336139613137386161 +34353834343834646438346466353739663964636463373432653564373465643532396231643033 +39623039633765653133653666313439383665366365303630613036386538346435356338383465 +65663766363564393732623635313932323436303134643135613634613333643539336438653030 +64353661363366653661333262306431653537316332623237393534303430313231326565346535 +39333936333432386536373531613963396130643234393533383637623665663635343438303466 +35393965363433333631663831643437653362363534303133633232613464343762666464373936 +37666561363631363638633434626365363336393230313063623531333665613738343162323133 +61313736633335363337383361353738346561383437646338363636623430386236373236616336 +35323634386538656663623735333261383537623563343139313261613036323461306462356636 +62303536663534373038336363323662633562346638336136613133383133356130353166363039 +65633861386365646532666139386333303630613961663038376633656137323932356331623865 +32396234366430333433343862366666396438313033393833663938623330646436333733303266 +32313539623435303938653432393164653633663065373230333561336533343333333739396235 +61346661363163356232646462313238326661356439373561646636616463623937373265633530 +35316131316438363030326130303334343438393833386232313032666662313133633734303332 +66353133343835323834623339633663303937613564303539666431663030646233373461623836 +36663563346166663530353231636632653332303739363139396366343432323232623964383163 +66643639333166336138346531653664643330336166663564623233396261663531393365393637 +33613332303530343839656237366665633434653265383661356531383631323733333931643834 +62613832636265303834613365373533363033353534633461623736633665653436353361343131 +30363933363265623539633131326535643662383833623531383135366438663139303630316664 +39366634626637616638666439356266656231396331356630643461366233633663336336643761 +61393838653235383662346364356261303033353932333833313564313238656532333231343935 +34303737323539636363363432656438386361306361346366373562633132373839646563666530 +65363761326263373233343263613039346434376434616236616631663433646538323065343266 +32656530333163373433613535633133366566636461383633396264326566366531653663623366 +32366266316261303262363233353665343931386663643736333130373863313133383964643035 +64366562393331643561316334646639616439653937623534343563633065383162623665333234 +37616531623338653666383939306665373235633565616232353733363166326162613634663130 +36306464313333333361633835383434363264343934666337363934393831646432646465366462 +36653261636366626437343231613366303338666134346531363739363662396638363333386662 +65303036326438656531663434646664353337636532353832323331353636393637666666346164 +33343964386438356434663164396535666261393031646133663036306262313935373065633336 +66646538383338343063363236383437653063613631666432613064363833353666393437616533 +30353337643338653133356564663231663661306162386432333031376165323138643961386237 +64643762386335313834396532633838306331646666386465386230326230613934323139643832 +38316666343930646439666233626662633762336162616664373236373661356562343361623863 +63636639633966663034333837363664323266623964343733306530643738636230386631353333 +30623365373533616164666462636432646661643737633031333865356363363231643461373165 +65353663343561643533623963373732303530333963353233316135656330386666613238643631 +30663237353565333431333930643233333062313232316532386531313333653233643838663562 +32333631643965633565343066656133663639626366326561623734356233336134363465653132 +62303836313838636436373730336333383830363239633766323461666135393064643131626234 +39333664653935326462343836616632303261383363346437643534643530373763613864353137 +35333163323135323639323564353637653739303630386466393931363738373361303334396537 +64616466353337666233326639343662346465346532343235303936653836333537353738396365 +33363830633761623666393039393961396333393335303664613032653530363432633330626536 +39343165303433626166396134336437316437656661323466333436333638613361643834336436 +30666135353165626161656335613335363630313063643237393964633339306264363737616533 +35316533333939643565393265643165643264316636376466623039343264303862313162666361 +30646264306363636539353063353035366138356561626565303736343238306238363161626132 +34333531386263346335636262643862646661353262336662646538623430653961393938323762 +63353461653964646537353332333138313231396465353638633731336665336264303061306633 +63353066623163333838313235633430653232393031613038626434343337383039646363313366 +31666233646134356662363833626566393564343837396261653137656337376630323364653035 +38396164373434323930623733303562366235666165383663383534366364386136343861363461 +31393331393363653662623737373336316363356539626638623064636633363562343530663734 +38383532393261633631383330653332393230373734656633343431643032303266366266663535 +30306232356433643161653661393639653935323938663230363336616465356636383933363332 +33356466666566636137346363386364303237323761616135353264613338613036623538646330 +31333566353464383139383030653435653130633638633238656535343361316134623863336162 +33656234373939316364643931313664333561366161316230633737373233653139363230613161 +35353935333831313566626138343031313038396439373032383761653637646231636634313734 +34383163316530366266636537363733626465373961626338666462373935393839373966323163 +35663632633339303361653265373239666431653061383131343631313934663761376665323164 +34353836636362666665343538613238376331336564626531393634616132346236373133386364 +32643031383836616133373238633764623031646537623935323539663964336238306162663939 +33633535613734393430323336323064663836666233373265333331346333653430666564396262 +65666666353738316463323132363936323334303634646436393161313666356265666630643732 +62333862626362646365303761346363393633363838376438333139383366306137343162356565 +32653462663336666131383766393736306131333533373033323665303665393761643933353664 +34633264313030363832643364303832333336306235653533366432396261383632303637306130 +63666238326636373831353137343833353730343264623764346563333662613731376366663861 +66663361353533316235616165623938376562633662626537633730336163303862356335363137 +34633038643333643562323065313366613135393036303164363033653165316539323363653636 +64323863623438353833623235623565316235333230653464353932633262373135643832326236 +61323831323265663833666333316238373731383061353862316133333136623362653863363732 +32336664316461613763383962663632313762333962656634636163323632613136633139646530 +62343432316334626161383335366466303963373664376633306132303232346162313030626464 +36366162613233633532643162656161346234656437396630383631646537373339643136373232 +66346531616334313761336564383636346566656634356433316636643038306632353337303065 +65363264363363303437313066616265343137333836366266363137663032336637316533396663 +31356464376661616439663836323338303739383066373636383430303439346261393863393064 +33343135306131336365663066323139613730336533626163613232313434386231656564356636 +32643937666238303464333939393633353937323339653331373233623433653631336431623738 +65336564626638396462313538333239366538663961313036643732316464353834623463336664 +33653261333134633237643938656337306463343563306539343239636564386463396530313435 +63613866326234623630663438353062323236353966316165333763346166353932363162393564 +62653430333938353466643764643233363365393637376436636631646230636565623866323633 +38646262643138343762313033313335643337363163353161666238663561626131636661353732 +32303131636638303630316131646530313235663234393139383934343334343736663635343763 +63373563373835623238313162333438346637653736643831646636366635376561336437313566 +33343532613636623638633161633639306539303664366661316435303864333534666261633638 +39613439383763333465336332326463613231323737376363326162346164313139333364313233 +31303731633566653539643465353962363238373865323162383237643361303439616662393839 +63383932356537353063636438656133633334633963393438653932396234356163346138353863 +66343037626463623730373035306136653738306632613638353030303866316332303939636661 +39626466323462633235383439346163653766396362363234353531346439366636393131323561 +35653762623763396235326339633138633038343730663232316662393333343133613639323761 +34616132326366353137323365313039383939356130353463336362323235663636626236383863 +66643932393663346132663539653661626562643863616535616138386530613339636437366535 +66353939353533373336373465643936646463363962323132303932326137336666306235393432 +33666335333466633632323065643933626164623661363362383535383330613562643963643439 +39613465633635353339633466343538303962373663323234343461633839643837373566376232 +66356430376432346562343461346438656432633336633763336439356632373731396363346533 +36303839616635643632636530343266306263313537613262343563623165616661323133633334 +35333766393938653531356264326164306265326431373763373563616439306265656333346266 +30636438663236303331666662646330643935386466666530373831653066353438373864316463 +39323463336161376562653535373836666166373233363463646637323234356636303666353337 +36653639653732643330623634396334333031386433656538373133323864653238643566636639 +39623338616363326139353265373361613934653832303639613962306332306366366161616338 +66616162376632356136313434333939616431643036613263663262326133366331303361656434 +64393261646565626336613231336136363437656135373037643436336163313534373063316461 +66376334633362643532336561613633626234653636373331333531363334373365643138666438 +33333933613038666265346538313034393066386634366631366131626532663831353330663631 +32393933646164663539363264393135653834653735363364623663666438303063613366396261 +32623739316161626266336436393534336661393033653038353239336232656234666139376431 +32363964616161626532326261366238646638386664363836643035303235393535333738393732 +66633335383637323264336637363136363762346561646630663438643364316533336531623564 +66396463313964643737616632333161343664323564653433356431326137303438653233396439 +63326533313039303462376433333763343161616166366566616135396365306565353563633565 +65663336633531643335363534353033633764656463386438363664653537373763393961396332 +63623837616634623665343765653935303631613436333232306331656433666633333764626665 +30386432623563323865393166343662373636303063623739626634626163323464373861306432 +32373766383130376339306165356338383561616433383066306564343664626632626135623730 +35663234353466613039323866316364323262623433323336386265306537626532646165383161 +36313061386363396266363364333837316534346439396636643939643633396432313966393735 +65346136303966363438646639366563306564633464656135656361623734656535366331666462 +32306630663838363566303133646634346235313933616332356537643564373731386335643836 +36386330313365303634336162643933353530346135313263326461336634616335393963653465 +35646230626537363935333162653966393031616234393563656435393664393839386263353864 +64313932373737306136663436636530323534343166626161653535333137316437303363326637 +66326136336665373330646336646438663561383133313532373237386230333864343863636462 +31373036326365393862343035356433313231663963393136653234343835323866666131366362 +65646334623066353064663431393830643963353065653631636164316339313963393039626634 +32346431616137656262633362333230616633316239643237396663336562313139313930653930 +36323836316630373961396565383166633733613861333463393036366538323662613462383465 +30356264643161613339396661336462653337633434333030313264613437626533626638346438 +34336566636135633736346537333833376664623139376430326666633666656664306439666164 +34366530313137343439303330303666396534326134636237373534636561653361656436363437 +63626637633335333336306437346162396532363634313932626565313361303532623030343937 +63396332356566313736366162666165333438343862386366316463656336363239376665626563 +33313034353062623432366663663562633230663764643861346433333436646637363462643539 +35333331666566326334643937336635306334633231666632313462363038366435353933646130 +30306663386166626261623466353465616435633439373363333235303237343836386537643634 +33326464663662613566396635386332653938303039373961386165353530313134633861376262 +30636462353766616537393837383537353064336630346661633638626333356635393561653038 +61653761613638656632333133316264386439323236643463363332333538323562633236336230 +6262 diff --git a/services/playbooks/site.yml b/services/playbooks/site.yml index 4591949c..ceb04cc4 100644 --- a/services/playbooks/site.yml +++ b/services/playbooks/site.yml @@ -1,13 +1,45 @@ - name: Deploy tsdb - hosts: servers + hosts: kubernetes tags: - tsdb - data roles: - tsdb +- name: Deploy mongo + hosts: kubernetes + tags: + - mongo + - data + roles: + - mongo + +- name: Deploy redis + hosts: kubernetes + tags: + - redis + - data + roles: + - redis + +- name: Deploy Kafka and Zookeeper + hosts: kubernetes + tags: + - kafka + - data + roles: + - kafka + +- name: Deploy Flink + hosts: kubernetes + tags: + - flink + - data + roles: + - flink + - name: Deploy pgAdmin - hosts: servers + hosts: kubernetes tags: - pgadmin - management @@ -15,15 +47,23 @@ - pgadmin - name: Deploy Mongo Express - hosts: servers + hosts: kubernetes tags: - mongo-express - management roles: - mongo-express +- name: Deploy keda + hosts: kubernetes + tags: + - trackeroo + - keda + roles: + - keda + - name: Deploy rabbitmq - hosts: servers + hosts: kubernetes tags: - rabbitmq - trackeroo @@ -32,57 +72,44 @@ roles: - rabbitmq -- name: Deploy mongo - hosts: servers - tags: - - mongo - - data - roles: - - mongo - -- name: Deploy redis - hosts: servers - tags: - - redis - - data - roles: - - redis - -- name: Deploy trackeroo - hosts: servers +- name: Deploy trackeroo-backend + hosts: kubernetes tags: - trackeroo + - trackeroo-backend vars_files: - secrets.yml roles: - trackeroo -- name: Deploy Flink - hosts: servers - tags: - - flink - - data - roles: - - flink - - name: Deploy Brokeroo - hosts: servers + hosts: kubernetes tags: - brokeroo - trackeroo + vars_files: + - secrets.yml roles: - brokeroo - name: Deploy Grafana - hosts: servers + hosts: kubernetes tags: grafana roles: - grafana -- name: Deploy Kafka and Zookeeper - hosts: servers +- name: Deploy Nginx + hosts: kubernetes tags: - - kafka - - data + - grafana + - nginx roles: - - kafka + - nginx + +- name: Deploy Prometheus + hosts: kubernetes + tags: + - grafana + - prometheus + roles: + - prometheus diff --git a/trackeroo-backend/src/internal/handler/mqttauth.go b/trackeroo-backend/src/internal/handler/mqttauth.go index fdf7bdd3..e43c104f 100644 --- a/trackeroo-backend/src/internal/handler/mqttauth.go +++ b/trackeroo-backend/src/internal/handler/mqttauth.go @@ -3,6 +3,7 @@ package handler import ( "fmt" "net/http" + "slices" "strings" "trackeroo-backend/internal/logger" "trackeroo-backend/internal/model" @@ -40,7 +41,7 @@ func UserAuth(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "deny") return } - fmt.Fprint(w, "allow administrator") + fmt.Fprint(w, "allow administrator monitoring") return } key, err := service.GetDeviceKey(ctx, form.Username) @@ -65,6 +66,7 @@ func UserAuth(w http.ResponseWriter, r *http.Request) { func TopicAuth(w http.ResponseWriter, r *http.Request) { /* Every response should be 200 */ + allowedTopics := []string{"data", "up", "dn"} ctx := r.Context() w.WriteHeader(http.StatusOK) if err := r.ParseForm(); err != nil { @@ -104,7 +106,22 @@ func TopicAuth(w http.ResponseWriter, r *http.Request) { return } logger.Debug("Authenticating device %s for topic %s vhost %s resource %s permission %s", form.Username, form.Topic, form.Vhost, form.Resource, form.Permission) - if strings.Contains(form.Topic, form.Username) { + parts := strings.Split(form.Topic, ".") + if len(parts) != 4 { + logger.Debug("Device %s not authenticated for topic %s", form.Username, form.Topic) + fmt.Fprint(w, "deny") + return + } + + topic := parts[1] + if !slices.Contains(allowedTopics, topic) { + logger.Debug("Device %s not authenticated for topic %s", form.Username, form.Topic) + fmt.Fprint(w, "deny") + return + } + + devID := parts[2] + if devID == form.Username { logger.Debug("Device %s authenticated for topic %s", form.Username, form.Topic) fmt.Fprint(w, "allow") return diff --git a/trackeroo-backend/src/internal/service/rabbit.go b/trackeroo-backend/src/internal/service/rabbit.go index 7b271c39..48a2513c 100644 --- a/trackeroo-backend/src/internal/service/rabbit.go +++ b/trackeroo-backend/src/internal/service/rabbit.go @@ -63,13 +63,16 @@ func runRabbitWatcher(ctx context.Context) { } // Declare queue + args := amqp.Table{ + "x-queue-type": "quorum", + } q, err := ch.QueueDeclare( "device_conn_events", true, // durable false, // auto-delete false, // exclusive false, // no-wait - nil, + args, ) if err != nil { logger.Error("Queue declare failed: %v", err) diff --git a/tracky/.gitignore b/tracky/.gitignore index 94149f83..ce111733 100644 --- a/tracky/.gitignore +++ b/tracky/.gitignore @@ -1,3 +1,3 @@ trk-* -docker-compose.yml +./docker-compose.yml geo-services/*data diff --git a/tracky/build.sh b/tracky/build.sh new file mode 100755 index 00000000..91200e08 --- /dev/null +++ b/tracky/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker build -t tracky:latest src diff --git a/tracky/create_local_network.sh b/tracky/create_local_network.sh new file mode 100755 index 00000000..c4235d12 --- /dev/null +++ b/tracky/create_local_network.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +docker network rm tracky + +docker network create tracky diff --git a/tracky/create_swarm_network.sh b/tracky/create_swarm_network.sh new file mode 100755 index 00000000..af0a333a --- /dev/null +++ b/tracky/create_swarm_network.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +docker network rm tracky + +docker network create \ + --driver overlay \ + --attachable \ + tracky diff --git a/tracky/geo-services/docker-compose-local.yml b/tracky/geo-services/docker-compose-local.yml new file mode 100644 index 00000000..66cd7377 --- /dev/null +++ b/tracky/geo-services/docker-compose-local.yml @@ -0,0 +1,43 @@ +services: + overpass: + image: wiktorn/overpass-api + container_name: overpass-italy + ports: + - "12345:80" + environment: + - OVERPASS_META=yes + - OVERPASS_MODE=init + - OVERPASS_PLANET_URL=http://download.geofabrik.de/europe/italy/centro-latest.osm.bz2-disabled + - OVERPASS_RATE_LIMIT=0 + - OVERPASS_RATE_SPACE=1073741824 + - OVERPASS_ALLOW_DUPLICATE_QUERIES=yes + volumes: + - ./overpass-data:/db + nominatim: + image: mediagis/nominatim:4.4 + container_name: nominatim + ports: + - "8082:8080" + environment: + - PBF_URL=https://download.geofabrik.de/europe/italy/centro-latest.osm.pbf + - REPLICATION_URL=https://download.geofabrik.de/europe/italy/centro-updates + - NOMINATIM_PASSWORD=password + volumes: + - ./nominatim-data:/var/lib/postgresql/14/main + + osrm: + image: osrm/osrm-backend + container_name: osrm + ports: + - "5000:5000" + volumes: + - ./osrm-data:/data + command: > + sh -c " + if [ ! -f /data/centro-latest.osrm ]; then + osrm-extract -p /opt/car.lua /data/centro-latest.osm.pbf && + osrm-partition /data/centro-latest.osrm && + osrm-customize /data/centro-latest.osrm; + fi && + osrm-routed --algorithm mld /data/centro-latest.osrm + " diff --git a/tracky/geo-services/docker-compose.yml b/tracky/geo-services/docker-compose.yml index a8972f59..cea0128e 100644 --- a/tracky/geo-services/docker-compose.yml +++ b/tracky/geo-services/docker-compose.yml @@ -1,29 +1,80 @@ services: - nominatim: - image: mediagis/nominatim:4.4 - container_name: nominatim - ports: - - "8082:8080" + overpass: + image: wiktorn/overpass-api environment: - - PBF_URL=https://download.geofabrik.de/europe/italy/centro-latest.osm.pbf - - REPLICATION_URL=https://download.geofabrik.de/europe/italy/centro-updates - - NOMINATIM_PASSWORD=password + OVERPASS_META: "yes" + OVERPASS_MODE: "init" + OVERPASS_PLANET_URL: "http://download.geofabrik.de/europe/italy-latest.osm.bz2-disabled" + OVERPASS_RATE_LIMIT: "0" + OVERPASS_RATE_SPACE: "1073741824" + OVERPASS_ALLOW_DUPLICATE_QUERIES: "yes" + volumes: + - ./overpass-data:/db + networks: + - tracky + deploy: + placement: + constraints: [node.role == manager] + restart_policy: + condition: on-failure + + nginx: + image: nginx:alpine + ports: + - "12345:80" # external port for Overpass volumes: - - ./nominatim-data:/var/lib/postgresql/14/main + - ./nginx:/etc/nginx:ro + depends_on: + - overpass + networks: + - tracky + deploy: + placement: + constraints: [node.role == manager] + restart_policy: + condition: on-failure osrm: image: osrm/osrm-backend - container_name: osrm - ports: - - "5000:5000" volumes: - ./osrm-data:/data command: > sh -c " - if [ ! -f /data/centro-latest.osrm ]; then - osrm-extract -p /opt/car.lua /data/centro-latest.osm.pbf && - osrm-partition /data/centro-latest.osrm && - osrm-customize /data/centro-latest.osrm; + if [ ! -f /data/italy-latest.osrm ]; then + osrm-extract -p /opt/car.lua /data/italy-latest.osm.pbf && + osrm-partition /data/italy-latest.osrm && + osrm-customize /data/italy-latest.osrm; fi && - osrm-routed --algorithm mld /data/centro-latest.osrm + osrm-routed --algorithm mld /data/italy-latest.osrm " + ports: + - "5000:5000" + networks: + - tracky + deploy: + placement: + constraints: [node.role == manager] + restart_policy: + condition: on-failure + + redis: + image: redis:8.2.1-alpine + volumes: + - ./redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + networks: + - tracky + deploy: + placement: + constraints: [node.role == manager] + restart_policy: + condition: on-failure + +networks: + tracky: + external: true diff --git a/tracky/geo-services/nginx/nginx.conf b/tracky/geo-services/nginx/nginx.conf new file mode 100644 index 00000000..68b78d93 --- /dev/null +++ b/tracky/geo-services/nginx/nginx.conf @@ -0,0 +1,97 @@ +events { + worker_connections 1024; +} + +http { + # Define cache path and settings + proxy_cache_path /var/cache/nginx/overpass + levels=1:2 + keys_zone=overpass_cache:100m + max_size=10g + inactive=7d + use_temp_path=off; + + upstream overpass_backend { + least_conn; + server overpass:80 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + server { + listen 80; + server_name _; + + # Enable request body caching + client_body_buffer_size 1M; + client_max_body_size 10M; + + # Increase timeouts for long-running Overpass queries + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + send_timeout 300s; + + # Increase buffer sizes for large responses + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + + location / { + proxy_pass http://overpass_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # HTTP/1.1 for keepalive + proxy_http_version 1.1; + proxy_set_header Connection ""; + + # Cache configuration + proxy_cache overpass_cache; + + # CRITICAL: Cache key must include request body for POST requests + proxy_cache_key "$request_method$request_uri$request_body"; + + # Cache GET and POST requests + proxy_cache_methods GET HEAD POST; + + proxy_cache_valid 200 1h; # Cache successful responses for 1 hour + proxy_cache_valid 404 10m; # Cache 404s for 10 minutes + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; + proxy_cache_background_update on; + proxy_cache_lock on; # Prevent cache stampede + + # Add cache status header for debugging + add_header X-Cache-Status $upstream_cache_status; + + # Ignore cache control from backend + proxy_ignore_headers Cache-Control Expires; + } + + # Health check endpoint + location /nginx-health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Cache stats endpoint (optional) + location /cache-stats { + allow 127.0.0.0/8; + allow 172.0.0.0/8; + deny all; + + stub_status; + } + } + + # Logging with cache status + log_format cache_log '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + 'Cache: $upstream_cache_status'; + + access_log /var/log/nginx/access.log cache_log; + error_log /var/log/nginx/error.log warn; +} diff --git a/tracky/make_compose.py b/tracky/make_compose.py index 3af0fa77..457eebe7 100644 --- a/tracky/make_compose.py +++ b/tracky/make_compose.py @@ -4,103 +4,97 @@ from typing import Dict, List, Optional, Union from pydantic import BaseModel import json - import requests import yaml -class VolumeConfig(BaseModel): - driver: Optional[str] = None - driver_opts: Optional[Dict[str, str]] = None - external: Optional[Union[bool, Dict[str, str]]] = None + +class SecretConfig(BaseModel): + file: str class NetworkConfig(BaseModel): driver: Optional[str] = None - driver_opts: Optional[Dict[str, str]] = None - external: Optional[Union[bool, Dict[str, str]]] = None + attachable: Optional[bool] = None + external: Optional[bool] = None + name: Optional[str] = None + + + +class ServiceSecret(BaseModel): + source: str + target: str class ServiceConfig(BaseModel): image: Optional[str] = None - build: Optional[Union[str, Dict[str, Union[str, Dict[str, str]]]]] = None - command: Optional[Union[str, List[str]]] = None - ports: Optional[List[str]] = None environment: Optional[Dict[str, Union[str, int]]] = None - volumes: Optional[List[str]] = None - depends_on: Optional[List[str]] = None networks: Optional[List[str]] = None - network_mode: Optional[str] = None - restart: Optional[str] = None - extra_hosts: Optional[List[str]] = None + secrets: Optional[List[ServiceSecret]] = None + deploy: Optional[Dict] = None class DockerCompose(BaseModel): services: Dict[str, ServiceConfig] - volumes: Optional[Dict[str, VolumeConfig]] = None - networks: Optional[Dict[str, NetworkConfig]] = None - include: Optional[List[str]] = None + secrets: Dict[str, SecretConfig] + networks: Dict[str, NetworkConfig] -def load_credentials(path: str): - with open(path, 'r') as file: - return json.load(file) def create_compose(credentials: List[Dict]): - """ - Create a DockerCompose object from a dictionary of credentials. - An example item of the credentials list is: - { - "id": "trk-5dac1f15577caca3", - "name": "fancy_shockley", - "device_type": "valuable", - "private_key": "2mtRLJjIO0yB1UDtOzEVOpiBnAxebQagR7LvaM9MMLM=", - "mqtt_host": "localhost", - "mqtt_port": 1883, - "mqtt_mode": "insecure", - "ca_cert": "" - } - Args: - credentials (Dict[str, Dict]): A dictionary of credentials. - - Returns: - DockerCompose: A DockerCompose object. - """ - compose = DockerCompose(services={} )#, include=["geo-services/docker-compose.yml"]) + compose = DockerCompose( + services={}, + secrets={}, + # networks={"trackynet": NetworkConfig(driver="overlay", attachable=True)}, + networks={"tracky": NetworkConfig(external=True)}, + ) print("SELECT * FROM ( VALUES ") for i, cred in enumerate(credentials): - # print(f"Processing device {cred}") - # continue - compose.services[cred["id"]] = ServiceConfig( - build={ - "context": "src", - "dockerfile": "Dockerfile" - }, + regional = "true" if random.randint(1, 100) > 10 else "false" + urban = "true" if random.randint(1, 100) > 50 and regional == "true" else "false" + + service_name = cred["id"] + + os.makedirs(service_name, exist_ok=True) + with open(f"{service_name}/tdevice.json", "w") as f: + json.dump(cred, f, indent=2) + + tdevice_secret = f"{service_name}_tdevice" + compose.secrets[tdevice_secret] = SecretConfig(file=f"./{service_name}/tdevice.json") + + compose.services[service_name] = ServiceConfig( + image="ghcr.io/skiby7/tracky:latest", environment={ - "GEOCODING_SERVICE_URL": "http://localhost:8082", - "ROUTING_SERVICE_URL": "http://localhost:5000", - "PUBLISH_PERIOD": "500", - "PIRATE": "true" if random.randint(1, 100) < 10 else "false" + "ROUTING_SERVICE_URL": "http://osrm:5000", + "OVERPASS_URL": "http://nginx:80/api/interpreter", + "REDIS_URI": "redis://redis:6379", + "REGIONAL": regional, + "URBAN": urban, + "PUBLISH_PERIOD": "10000", + "PIRATE": "true" if random.randint(1, 100) < 10 else "false", + }, + networks=["tracky"], + secrets=[ + ServiceSecret( + source=tdevice_secret, + target="/app/credentials/tdevice.json", + ) + ], + deploy={ + "replicas": 1, + "restart_policy": {"condition": "any"}, }, - # depends_on=["nominatim", "osrm"], - network_mode="host", - volumes=[f"./{cred["id"]}:/app/credentials"], - restart="no", ) - # if compose.services[cred["id"]].environment["PIRATE"] == "true": - # print(f"{cred['id']} is a pirate 🏴‍☠️") - os.makedirs(f"{cred['id']}", exist_ok=True) - with open(f"{cred['id']}/tdevice.json", "w") as f: - json.dump(cred, f, indent=2) if i == len(credentials) - 1: print(f"('{cred['name']}', '{cred['id']}')") else: print(f"('{cred['name']}', '{cred['id']}'),") print(") AS t (__text, __value)") + # Dump YAML correctly with open("docker-compose.yml", "w") as f: compose_dict = compose.model_dump(exclude_none=True) - yaml_str = yaml.safe_dump(compose_dict, sort_keys=False) f.write(yaml_str) + def token(): response = requests.post(f"http://{sys.argv[1]}/login/", json={"username": "leonardo", "password": "subemelaradio"}) return response.json()["token"] diff --git a/tracky/src-redis-primer/Dockerfile b/tracky/src-redis-primer/Dockerfile new file mode 100644 index 00000000..f8c31e28 --- /dev/null +++ b/tracky/src-redis-primer/Dockerfile @@ -0,0 +1,18 @@ +# -------- Build Stage -------- +FROM golang:1.24-alpine AS builder + +ENV CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH=amd64 +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o tracky tracky.go firmware.go + +FROM alpine:latest +WORKDIR /app +RUN mkdir -p /data +COPY --from=builder /app/tracky . + +ENTRYPOINT ["./tracky"] diff --git a/tracky/src-redis-primer/docker-compose.yml b/tracky/src-redis-primer/docker-compose.yml new file mode 100644 index 00000000..3fc59109 --- /dev/null +++ b/tracky/src-redis-primer/docker-compose.yml @@ -0,0 +1,16 @@ +services: + redis-primer: + build: + context: . + dockerfile: Dockerfile + environment: + ROUTING_SERVICE_URL: http://osrm:5000 + OVERPASS_URL: http://nginx:80/api/interpreter + REDIS_URI: redis://redis:6379 + networks: + - tracky + restart: "no" + +networks: + tracky: + external: true diff --git a/tracky/src-redis-primer/firmware.go b/tracky/src-redis-primer/firmware.go new file mode 100644 index 00000000..6e2c7aff --- /dev/null +++ b/tracky/src-redis-primer/firmware.go @@ -0,0 +1,155 @@ +package main + +import ( + "math/rand" + "os" + "sync" + "time" + "tracky/trackeroo" +) + +type ValuableSensors struct { + Alarm bool `json:"alarm"` + Vibration float64 `json:"vibration"` + RearHatchOpen bool `json:"rear_hatch_open"` + FrontHatchOpen bool `json:"front_hatch_open"` + Collision bool `json:"collision"` +} + +type FoodSensors struct { + RearHatchOpen bool `json:"rear_hatch_open"` + FrontHatchOpen bool `json:"front_hatch_open"` + Temperature float64 `json:"temperature"` + Humidity float64 `json:"humidity"` + Pressure float64 `json:"pressure"` +} + +type Payload struct { + TS int64 `json:"ts"` + Speed float64 `json:"speed"` + SpeedLimit float64 `json:"speed_limit"` + SpeedStats map[string]any `json:"speed_stats"` + Position trackeroo.Coordinate `json:"position"` + DeviceName string `json:"device_name"` + DeviceType string `json:"device_type"` + DeviceID string `json:"device_id"` + Status string `json:"status"` + Sensors any `json:"sensors,omitempty"` + Start trackeroo.Coordinate `json:"start"` + End trackeroo.Coordinate `json:"end"` + InstantConsumption float64 `json:"instant_consumption"` + ConsumptionStats map[string]any `json:"consumption_stats"` + DeltaDistance float64 `json:"delta_distance"` +} + +func normalValuablesData(position trackeroo.DrivePosition) ValuableSensors { + return ValuableSensors{ + Alarm: false, + Vibration: rand.Float64() * 100, + RearHatchOpen: position.Status == trackeroo.ARRIVED, + FrontHatchOpen: position.Status == trackeroo.ARRIVED, + Collision: false, + } +} + +func robberyValuablesData() ValuableSensors { + return ValuableSensors{ + Alarm: true, + Vibration: 100 + rand.Float64()*300, + RearHatchOpen: true, + FrontHatchOpen: rand.Float32() < 0.5, + Collision: true, + } +} + +func normalFoodData(position trackeroo.DrivePosition) FoodSensors { + minTemperature := 2.0 + minHumidity := 60.0 + minPressure := 1013.25 // Millibars + return FoodSensors{ + Temperature: minTemperature + rand.Float64()*2, + Humidity: minHumidity + rand.Float64()*10, + Pressure: minPressure + rand.Float64()*10, + RearHatchOpen: position.Status == trackeroo.ARRIVED, + FrontHatchOpen: position.Status == trackeroo.ARRIVED, + } +} + +func alteredFoodData(position trackeroo.DrivePosition) FoodSensors { + minTemperature := 4.0 + minHumidity := 70.0 + minPressure := 1013.25 // Millibars + return FoodSensors{ + Temperature: minTemperature + rand.Float64()*7, + Humidity: minHumidity + rand.Float64()*30, + Pressure: minPressure + rand.Float64()*100, + RearHatchOpen: rand.Float64() < 0.05, + FrontHatchOpen: position.Status == trackeroo.ARRIVED, + } +} + +func Init() { + debugLevel := os.Getenv("DEBUG_LEVEL") + level := trackeroo.INFO + switch debugLevel { + case "DEBUG": + level = trackeroo.DEBUG + case "INFO": + level = trackeroo.INFO + case "WARN": + case "WARNING": + level = trackeroo.WARNING + case "ERROR": + level = trackeroo.ERROR + default: + level = trackeroo.INFO + } + trackeroo.InitLogger(level, "") +} + +func Terminate() { +} + +func Loop() { + rand.Seed(time.Now().UnixNano()) + trackeroo.InitRedisClient() + overpassClient := trackeroo.NewOverpassClient(os.Getenv("OVERPASS_URL")) + + regions, err := overpassClient.GetRegions() + if err != nil { + trackeroo.Error("Cannot retrieve regions: %+v", err) + } + cities := make([]string, 0) + for _, region := range regions { + c, _ := overpassClient.GetCities(region) + cities = append(cities, c...) + } + wg := sync.WaitGroup{} + parallelism := 8 + i := 0 + for i = 0; i < len(cities); i += parallelism { + tmp := cities[i : i+parallelism] + for j := range parallelism { + if j > len(tmp) { + break + } + wg.Add(1) + go func() { + overpassClient.GetStreets(tmp[j], 100, 2000) + wg.Done() + }() + } + wg.Wait() + } + tmp := cities[i-parallelism:] + if len(tmp) > 0 { + for j := range len(cities) { + wg.Add(1) + go func() { + overpassClient.GetStreets(tmp[j], 100, 2000) + wg.Done() + }() + } + wg.Wait() + } +} diff --git a/tracky/src-redis-primer/go.mod b/tracky/src-redis-primer/go.mod new file mode 100755 index 00000000..fcdbcd21 --- /dev/null +++ b/tracky/src-redis-primer/go.mod @@ -0,0 +1,30 @@ +module tracky + +go 1.22.6 + +toolchain go1.23.3 + +require ( + github.com/eclipse/paho.mqtt.golang v1.4.3 + github.com/golang-jwt/jwt/v5 v5.2.1 + modernc.org/sqlite v1.36.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/redis/go-redis/v9 v9.14.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.30.0 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect +) diff --git a/tracky/src-redis-primer/go.sum b/tracky/src-redis-primer/go.sum new file mode 100755 index 00000000..31e3d6d4 --- /dev/null +++ b/tracky/src-redis-primer/go.sum @@ -0,0 +1,61 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= +github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= +github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8= +modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/tracky/src-redis-primer/trackeroo/art.go b/tracky/src-redis-primer/trackeroo/art.go new file mode 100644 index 00000000..3c6a5011 --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/art.go @@ -0,0 +1,30 @@ +package trackeroo + +const ART = ` + + ⢠⣿⣦⡀ + ⢾⣿⣿⣿⣦⡀ + ⢀⣴⣿⣿⣿⣿⣿⣦⡀ + ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀ + ⣀⣀⣀⣀⡀ ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⠟⢿⡿⠿⠟ + ⠈⠻⣿⣿⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁ + ⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁ + ⠈⠻⣿⣿⣿⣿⣿⣿⣿⡟⠁ + ⣠⣦⡈⠻⣿⣿⣿⣿⣿⣧ + ⢀⣴⣿⣿⡿⠂⠈⠻⣿⣿⣿⣿⡆ + ⣠⣾⣿⠟⠁ ⠈⠻⣿⣿⡇ + ⢀⣼⠿⠋ ⠈⠻⠃ + ⡠⠛⠁ + ⠈ + /$$$$$$$$ /$$ + |__ $$__/ | $$ + | $$ /$$$$$$ /$$$$$$ /$$$$$$$| $$ /$$ /$$ /$$ + | $$ /$$__ $$|____ $$ /$$_____/| $$ /$$/| $$ | $$ + | $$| $$ \__/ /$$$$$$$| $$ | $$$$$$/ | $$ | $$ + | $$| $$ /$$__ $$| $$ | $$_ $$ | $$ | $$ + | $$| $$ | $$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$ + |__/|__/ \_______/ \_______/|__/ \__/ \____ $$ + /$$ | $$ + | $$$$$$/ + \______/ + ` diff --git a/tracky/src-redis-primer/trackeroo/checkpoint.go b/tracky/src-redis-primer/trackeroo/checkpoint.go new file mode 100644 index 00000000..f84d42f8 --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/checkpoint.go @@ -0,0 +1,80 @@ +package trackeroo + +import ( + "encoding/json" + "fmt" + "os" +) + +func ExistsCheckpoint(filename string) bool { + if redisClient != nil { + exists, err := redisClient.Exists(ctx, filename).Result() + return err == nil && exists > 0 + } + + _, err := os.Stat(filename) + return !os.IsNotExist(err) +} + +func SaveCheckpoint(checkpoint *Checkpoint, filename string) error { + if checkpoint == nil { + return fmt.Errorf("cannot save nil checkpoint") + } + + jsonData, err := json.Marshal(checkpoint) + if err != nil { + return err + } + + if redisClient != nil { + return redisClient.Set(ctx, filename, jsonData, 0).Err() + } + + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + fmt.Fprintf(file, "%s\n", jsonData) + return nil +} + +func LoadCheckpoint(filename string) (*Checkpoint, error) { + var jsonData []byte + var err error + + if redisClient != nil { + jsonData, err = redisClient.Get(ctx, filename).Bytes() + if err != nil { + return nil, err + } + } else { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var checkpoint Checkpoint + err = json.NewDecoder(file).Decode(&checkpoint) + if err != nil { + return nil, err + } + return &checkpoint, nil + } + + var checkpoint Checkpoint + err = json.Unmarshal(jsonData, &checkpoint) + if err != nil { + return nil, err + } + return &checkpoint, nil +} + +func DeleteCheckpoint(filename string) error { + if redisClient != nil { + return redisClient.Del(ctx, filename).Err() + } + return os.Remove(filename) +} diff --git a/tracky/src-redis-primer/trackeroo/driving.go b/tracky/src-redis-primer/trackeroo/driving.go new file mode 100644 index 00000000..a2677783 --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/driving.go @@ -0,0 +1,392 @@ +package trackeroo + +import ( + "encoding/json" + "fmt" + "io" + "math" + "math/rand" + "net/http" + "time" +) + +const ( + ARRIVED = "arrived" + DRIVING = "driving" + STOPPED = "stopped" + SLOWING = "slowing" + ACCELERATING = "accelerating" +) + +var consumptionMultipliers = map[string]float32{ + "valuable": 1.3, // heavier, armored vans/trucks + "food": 1.1, // refrigerated transport adds load + "private_transport": 1.0, // baseline + "public_transport": 1.8, // buses have much higher consumption +} + +const MAX_CONSUMPTION = 30.0 + +type Coordinate struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lon"` +} + +type RouteSegment struct { + Coordinates []Coordinate + SpeedKmh float64 + ShouldStop bool +} + +type OSRMResponse struct { + Code string `json:"code"` + Routes []struct { + Geometry struct { + Coordinates [][]float64 `json:"coordinates"` + } `json:"geometry"` + Distance float64 `json:"distance"` + Duration float64 `json:"duration"` + Legs []struct { + Steps []struct { + Geometry struct { + Coordinates [][]float64 `json:"coordinates"` + } `json:"geometry"` + Distance float64 `json:"distance"` + Duration float64 `json:"duration"` + Name string `json:"name"` + Maneuver struct { + Type string `json:"type"` + Modifier string `json:"modifier"` + Location []float64 `json:"location"` + } `json:"maneuver"` + } `json:"steps"` + } `json:"legs"` + } `json:"routes"` +} + +type DrivePosition struct { + Coordinate + Timestamp time.Time + Speed float64 // km/h + SpeedLimit float64 + Status string // "driving", "stopped", "slowing", "accelerating" + Start Coordinate + End Coordinate + Consumption float64 + Distance float64 +} + +type RoutingService struct { + NominatimURL string + OSRMURL string + httpClient *http.Client +} + +func NewRoutingService(osrmURL string) *RoutingService { + return &RoutingService{ + OSRMURL: osrmURL, + httpClient: &http.Client{Timeout: 120 * time.Second}, + } +} + +// GetRoute gets routing directions from point A to B using OSRM +func (rs *RoutingService) GetRoute(from, to Coordinate) ([]RouteSegment, error) { + requestURL := fmt.Sprintf( + "%s/route/v1/driving/%f,%f;%f,%f?geometries=geojson&overview=full&steps=true", + rs.OSRMURL, from.Lng, from.Lat, to.Lng, to.Lat) + + resp, err := rs.httpClient.Get(requestURL) + if err != nil { + return nil, fmt.Errorf("failed to get route: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var osrmResp OSRMResponse + if err := json.Unmarshal(body, &osrmResp); err != nil { + return nil, fmt.Errorf("failed to parse OSRM response: %w", err) + } + + if osrmResp.Code != "Ok" || len(osrmResp.Routes) == 0 { + return nil, fmt.Errorf("no routes found") + } + + var segments []RouteSegment + + for _, leg := range osrmResp.Routes[0].Legs { + for _, step := range leg.Steps { + speed := 0.0 + if step.Duration > 0 { + speed = (step.Distance / step.Duration) * 3.6 + } + + coords := make([]Coordinate, len(step.Geometry.Coordinates)) + for i, c := range step.Geometry.Coordinates { + coords[i] = Coordinate{Lat: c[1], Lng: c[0]} + } + shouldStop := false + switch step.Maneuver.Type { + case "turn", "roundabout", "end_of_road": + shouldStop = true + } + segments = append(segments, RouteSegment{ + Coordinates: coords, + SpeedKmh: speed, + ShouldStop: shouldStop, + }) + } + } + + return segments, nil +} + +func getConsumption(speed float64, devType string) float64 { + if speed == 0 { + return 0.0 + } + threshold := 80.0 + consumption := 5 + 60.7/speed + if speed >= threshold { + consumption += 5 * math.Log(speed/threshold) + } + return max(consumption*float64(consumptionMultipliers[devType]), MAX_CONSUMPTION) +} + +// DrivingSimulator simulates realistic car driving +type DrivingSimulator struct { + Route []RouteSegment + Start Coordinate + End Coordinate + DefaultAverageSpeed float64 // km/h + SpeedVariation float64 // percentage (0.0-1.0) + StopProbability float64 // probability of stopping per segment (0.0-1.0) + DevType string + StopDuration struct { + Min time.Duration + Max time.Duration + } + UpdateInterval time.Duration + IsPirate bool + rand *rand.Rand + Distance float64 +} + +func NewDrivingSimulator(routingService *RoutingService, checkpoint *Checkpoint, avgSpeed float64, updateIntervalMs int, isPirate bool, devType string) (*DrivingSimulator, error) { + route := make([]RouteSegment, 0) + for i := 0; i < len(checkpoint.Poles)-1; i++ { + start := checkpoint.Poles[i].Coordinate + end := checkpoint.Poles[i+1].Coordinate + r, err := routingService.GetRoute(start, end) + if err != nil { + return nil, err + } + checkpointRouteIndex := -1 + checkpointCoordinatesIndex := -1 + + for i, pos := range r { + if checkpoint == nil || checkpoint.LastPosition.Lat == 0.0 || checkpoint.LastPosition.Lng == 0.0 { + Warning("Invalid checkpoit +%v, starting from first waypoint", checkpoint) + break + } + for j, c := range pos.Coordinates { + // 50 meters + if haversineDistance(checkpoint.LastPosition, c) < 0.05 { + checkpointRouteIndex = i + checkpointCoordinatesIndex = j + Info("Found checkpoint %+v at index (%d, %d)", checkpoint, checkpointRouteIndex, checkpointCoordinatesIndex) + goto found + } + } + } + found: + if checkpointRouteIndex >= 0 { + r = r[checkpointRouteIndex:] + r[0].Coordinates = r[0].Coordinates[checkpointCoordinatesIndex:] + } + route = append(route, r...) + } + + return &DrivingSimulator{ + Route: route, + DefaultAverageSpeed: avgSpeed, + Start: checkpoint.Poles[0].Coordinate, + End: checkpoint.Poles[1].Coordinate, + SpeedVariation: 0.03, + StopProbability: 0.05, + StopDuration: struct { + Min time.Duration + Max time.Duration + }{ + Min: 2 * time.Second, + Max: 5 * time.Second, + }, + UpdateInterval: time.Duration(updateIntervalMs) * time.Millisecond, + rand: rand.New(rand.NewSource(time.Now().UnixNano())), + IsPirate: isPirate, + DevType: devType, + }, nil +} + +// SimulateDrive simulates driving along a route and sends positions through a channel +func (ds *DrivingSimulator) SimulateDrive() <-chan DrivePosition { + positionChan := make(chan DrivePosition, 100) + lastCoord := ds.Route[len(ds.Route)-1].Coordinates[len(ds.Route[len(ds.Route)-1].Coordinates)-1] + lastSpeed := 0.0 + go func() { + defer close(positionChan) + + if len(ds.Route) < 2 { + return + } + + currentTime := time.Now() + stopCompensation := 1 + consumptionCompensation := 1.0 + speedLimit := 0.0 + for _, segment := range ds.Route { + for i := 0; i < len(segment.Coordinates)-1; i++ { + currentPos := segment.Coordinates[i] + nextPos := segment.Coordinates[i+1] + + // Calculate distance between points + distance := haversineDistance(currentPos, nextPos) + + // Check if we should stop + if i == 0 && segment.ShouldStop { + // Send stopped position + positionChan <- DrivePosition{ + Coordinate: currentPos, + Timestamp: currentTime, + Speed: 0, + SpeedLimit: speedLimit, + Status: STOPPED, + Start: ds.Start, + End: ds.End, + Consumption: 0, + Distance: 0, + } + + // Random stop duration + stopTime := ds.StopDuration.Min + time.Duration( + ds.rand.Float64()*float64(ds.StopDuration.Max-ds.StopDuration.Min)) + time.Sleep(stopTime) + currentTime = time.Now() + stopCompensation = 3 + lastSpeed = 0 + } + + // Calculate current speed with variation + baseSpeed := ds.DefaultAverageSpeed + if segment.SpeedKmh > 0 { + baseSpeed = segment.SpeedKmh + } + if stopCompensation > 1 { + baseSpeed /= float64(stopCompensation) + stopCompensation -= 1 + } + speedLimit = baseSpeed + speedVariation := 1.0 + (ds.rand.Float64()-0.5)*2*ds.SpeedVariation + currentSpeed := baseSpeed * speedVariation + if ds.IsPirate { + currentSpeed *= 1.40 + } + + // Calculate time to travel this segment + travelTimeHours := distance / currentSpeed + segmentDuration := time.Duration(travelTimeHours * float64(time.Hour)) + + // Interpolate positions along the segment + steps := max(int(segmentDuration/ds.UpdateInterval), 1) + + status := DRIVING + consumptionCompensation = 1 + speedDifference := math.Abs(currentSpeed - lastSpeed) + tolerance := currentSpeed * 0.05 // 5% tolerance + + if speedDifference > tolerance { + if lastSpeed < currentSpeed { + status = ACCELERATING + consumptionCompensation = 1.3 + } else { + status = SLOWING + consumptionCompensation = 0.5 + } + } + lastSpeed = currentSpeed + + for step := 0; step <= steps; step++ { + progress := float64(step) / float64(steps) + + interpolatedPos := ds.interpolatePosition(currentPos, nextPos, progress) + + positionChan <- DrivePosition{ + Coordinate: interpolatedPos, + Timestamp: currentTime, + Speed: currentSpeed, + SpeedLimit: speedLimit, + Status: status, + Start: ds.Start, + End: ds.End, + Consumption: getConsumption(currentSpeed, ds.DevType) * consumptionCompensation, + Distance: distance, + } + time.Sleep(ds.UpdateInterval) + currentTime = time.Now() + } + } + } + + // Send final position + positionChan <- DrivePosition{ + Coordinate: lastCoord, + Timestamp: currentTime, + Speed: 0, + SpeedLimit: speedLimit, + Status: ARRIVED, + Start: ds.Start, + End: ds.End, + Consumption: 0, + Distance: 0, + } + }() + Info("Simulation started") + + return positionChan +} + +// haversineDistance calculates the great circle distance between two points +func haversineDistance(pos1, pos2 Coordinate) float64 { + const R = 6371 // Earth's radius in kilometers + + lat1Rad := pos1.Lat * math.Pi / 180 + lat2Rad := pos2.Lat * math.Pi / 180 + deltaLat := (pos2.Lat - pos1.Lat) * math.Pi / 180 + deltaLng := (pos2.Lng - pos1.Lng) * math.Pi / 180 + + a := math.Sin(deltaLat/2)*math.Sin(deltaLat/2) + + math.Cos(lat1Rad)*math.Cos(lat2Rad)* + math.Sin(deltaLng/2)*math.Sin(deltaLng/2) + + c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) + + return R * c +} + +// interpolatePosition interpolates between two positions +func (ds *DrivingSimulator) interpolatePosition(pos1, pos2 Coordinate, progress float64) Coordinate { + return Coordinate{ + Lat: pos1.Lat + (pos2.Lat-pos1.Lat)*progress, + Lng: pos1.Lng + (pos2.Lng-pos1.Lng)*progress, + } +} + +// Helper function to parse float from string +func parseFloat(s string) (float64, error) { + var f float64 + _, err := fmt.Sscanf(s, "%f", &f) + return f, err +} diff --git a/tracky/src-redis-primer/trackeroo/logger.go b/tracky/src-redis-primer/trackeroo/logger.go new file mode 100755 index 00000000..5cacf795 --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/logger.go @@ -0,0 +1,74 @@ +package trackeroo + +import ( + "fmt" + "path/filepath" + "runtime" + "time" +) + +type Logger struct { + logLevel int + logFile string +} + +const ( + DEBUG = iota + INFO = iota + WARNING = iota + ERROR = iota + DISABLED = iota +) + +var logger *Logger + +func newLogger(logLevel int, logFile string) *Logger { + return &Logger{ + logLevel: logLevel, + logFile: logFile, + } +} + +func InitLogger(logLevel int, logFile string) { + logger = newLogger(logLevel, logFile) +} + +func GetLogPrefixString(level int) string { + _, file, no, _ := runtime.Caller(2) + file = filepath.Base(file) + switch level { + case DEBUG: + return fmt.Sprintf("%v :: %s:%d :: [ \x1b[36mDEBUG\x1b[0m ] - ", time.Now().Format(time.UnixDate), file, no) + case INFO: + return fmt.Sprintf("%v :: [ \x1b[32mINFO\x1b[0m ] - ", time.Now().Format(time.UnixDate)) + case WARNING: + return fmt.Sprintf("%v :: %s:%d :: [ \x1b[33mWARNING\x1b[0m ] - ", time.Now().Format(time.UnixDate), file, no) + case ERROR: + return fmt.Sprintf("%v :: %s:%d :: [ \x1b[31mERROR\x1b[0m ] - ", time.Now().Format(time.UnixDate), file, no) + } + return "" +} + +func Debug(fmtStr string, args ...any) { + if logger.logLevel <= DEBUG { + fmt.Printf(GetLogPrefixString(DEBUG)+fmtStr+"\n", args...) + } +} + +func Info(fmtStr string, args ...any) { + if logger.logLevel <= INFO { + fmt.Printf(GetLogPrefixString(INFO)+fmtStr+"\n", args...) + } +} + +func Warning(fmtStr string, args ...any) { + if logger.logLevel <= WARNING { + fmt.Printf(GetLogPrefixString(WARNING)+fmtStr+"\n", args...) + } +} + +func Error(fmtStr string, args ...any) { + if logger.logLevel <= ERROR { + fmt.Printf(GetLogPrefixString(ERROR)+fmtStr+"\n", args...) + } +} diff --git a/tracky/src-redis-primer/trackeroo/overpass.go b/tracky/src-redis-primer/trackeroo/overpass.go new file mode 100644 index 00000000..c4575f14 --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/overpass.go @@ -0,0 +1,249 @@ +package trackeroo + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "strings" + "time" + + "github.com/redis/go-redis/v9" +) + +type OverpassElement struct { + Type string `json:"type"` + ID int64 `json:"id"` + Tags map[string]string `json:"tags"` + Geometry []Coordinate `json:"geometry"` +} + +type OverpassResponse struct { + Elements []OverpassElement `json:"elements"` +} + +type OverpassClient struct { + BaseURL string + Client *http.Client +} + +func NewOverpassClient(baseURL string) *OverpassClient { + return &OverpassClient{ + BaseURL: baseURL, + Client: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +func (c *OverpassClient) runQuery(query string) (OverpassResponse, error) { + const maxRetries = 10 + + form := url.Values{} + form.Set("data", query) + + var lastErr error + var overpassResp OverpassResponse + + for attempt := 1; attempt <= maxRetries; attempt++ { + resp, err := c.Client.Post( + c.BaseURL, + "application/x-www-form-urlencoded", + strings.NewReader(form.Encode()), + ) + if err != nil { + lastErr = fmt.Errorf("failed to query Overpass API: %w", err) + time.Sleep(time.Duration(attempt) * time.Second) + continue + } + + bodyBytes, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + lastErr = fmt.Errorf("Overpass API returned status %d: %s", resp.StatusCode, string(bodyBytes)) + + if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 { + time.Sleep(time.Duration(attempt) * time.Second) + continue + } + break + } + + contentType := resp.Header.Get("Content-Type") + if strings.Contains(contentType, "text/html") || bytes.HasPrefix(bodyBytes, []byte(".italy; +node["name"="%s"]["place"~"city|town"](area.italy)->.citynode; +( + way(around.citynode:%d)["highway"~"primary|secondary|tertiary|residential|unclassified"]["name"]["highway"!~"motorway|trunk|motorway_link|trunk_link"](area.italy); +); +out tags geom %d; +`, city, radiusMeters, limit) + + overpassResp, err := c.runQuery(query) + if err != nil { + return nil, err + } + + if len(overpassResp.Elements) == 0 { + return nil, fmt.Errorf("no streets found for city: %s", city) + } + + if redisClient != nil { + data, err := json.Marshal(overpassResp.Elements) + if err == nil { + if err := redisClient.Set(ctx, cityKey, data, 0).Err(); err != nil { + Warning("Failed to cache regions in Redis: %v", err) + } else { + Info("Cached %d streets (city: %s, limit: %d, radius: %d) in Redis", len(overpassResp.Elements), city, limit, radiusMeters) + } + } + } + return overpassResp.Elements, nil +} + +func (c *OverpassClient) GetRandomStreet(city string, radiusMeters int) (Street, error) { + streets, err := c.GetStreets(city, 100, radiusMeters) + if err != nil { + return Street{}, err + } + randStreet := streets[rand.Intn(len(streets))] + return stringToStreet(city, randStreet), nil +} + +func (c *OverpassClient) GetRegions() ([]string, error) { + if redisClient != nil { + cached, err := redisClient.Get(ctx, "tracky::regions").Result() + if err == nil { + var regions []string + if err := json.Unmarshal([]byte(cached), ®ions); err == nil { + Info("Retrieved %d regions from Redis cache", len(regions)) + return regions, nil + } + Warning("Failed to unmarshal cached regions: %v", err) + } else if err != redis.Nil { + Warning("Redis get error: %v", err) + } + } + query := `[out:json][timeout:120]; +area["ISO3166-1"="IT"][admin_level=2]->.italy; +relation["boundary"="administrative"]["admin_level"=4]["ISO3166-2"~"^IT-"](area.italy); +out tags;` + + overpassResp, err := c.runQuery(query) + if err != nil { + return nil, err + } + + if len(overpassResp.Elements) == 0 { + return nil, fmt.Errorf("no regions found") + } + regions := make([]string, 0) + for _, element := range overpassResp.Elements { + if element.Type == "relation" { + regions = append(regions, element.Tags["name"]) + } + } + if redisClient != nil { + data, err := json.Marshal(regions) + if err == nil { + if err := redisClient.Set(ctx, "tracky::regions", data, 0).Err(); err != nil { + Warning("Failed to cache regions in Redis: %v", err) + } else { + Info("Cached %d regions in Redis", len(regions)) + } + } + } + return regions, nil +} + +func (c *OverpassClient) GetCities(region string) ([]string, error) { + regionKey := fmt.Sprintf("tracky::regions::%s", region) + if redisClient != nil { + cached, err := redisClient.Get(ctx, regionKey).Result() + if err == nil { + var cities []string + if err := json.Unmarshal([]byte(cached), &cities); err == nil { + Info("Retrieved %d cities (%s) from Redis cache", len(cities), region) + return cities, nil + } + Warning("Failed to unmarshal cached cities: %v", err) + } else if err != redis.Nil { + Warning("Redis get error: %v", err) + } + } + query := fmt.Sprintf(`[out:json][timeout:120]; +relation["boundary"="administrative"]["name"="%s"]["admin_level"=4]->.reg; +.reg map_to_area->.region; +node["place"~"city|town"](area.region); +out tags;`, region) + + overpassResp, err := c.runQuery(query) + if err != nil { + return nil, err + } + + if len(overpassResp.Elements) == 0 { + return nil, fmt.Errorf("no cities found") + } + cities := make([]string, 0) + for _, element := range overpassResp.Elements { + if element.Type == "node" { + cities = append(cities, element.Tags["name"]) + } + } + + if redisClient != nil { + data, err := json.Marshal(cities) + if err == nil { + if err := redisClient.Set(ctx, regionKey, data, 0).Err(); err != nil { + Warning("Failed to cache regions in Redis: %v", err) + } else { + Info("Cached %d cities in Redis", len(cities)) + } + } + } + return cities, nil +} diff --git a/tracky/src-redis-primer/trackeroo/redis.go b/tracky/src-redis-primer/trackeroo/redis.go new file mode 100644 index 00000000..ae18d9b4 --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/redis.go @@ -0,0 +1,33 @@ +package trackeroo + +import ( + "context" + "os" + + "github.com/redis/go-redis/v9" +) + +var ( + redisClient *redis.Client + ctx = context.Background() +) + +func InitRedisClient() { + redisURI := os.Getenv("REDIS_URI") + if redisURI != "" { + opt, err := redis.ParseURL(redisURI) + if err != nil { + Error("Failed to parse REDIS_URI: %v\n", err) + return + } + redisClient = redis.NewClient(opt) + + if err := redisClient.Ping(ctx).Err(); err != nil { + Error("Failed to connect to Redis: %v\n", err) + redisClient = nil + } + } + if redisClient != nil { + Info("Saving checkpoint to %s", redisURI) + } +} diff --git a/tracky/src-redis-primer/trackeroo/routes.go b/tracky/src-redis-primer/trackeroo/routes.go new file mode 100644 index 00000000..096cbc2a --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/routes.go @@ -0,0 +1,80 @@ +package trackeroo + +import ( + "math/rand" +) + +type Street struct { + Coordinate Coordinate `json:"coordinate"` + StreetName string `json:"street_name"` + City string `json:"city"` +} + +type Checkpoint struct { + Poles []Street `json:"poles"` + LastPosition Coordinate `json:"last_position"` +} + +func stringToStreet(city string, street OverpassElement) Street { + return Street{ + Coordinate: street.Geometry[rand.Intn(len(street.Geometry))], + StreetName: street.Tags["name"], + City: city, + } +} + +type CachedStreetProvider struct { + client *OverpassClient + cache map[string][]OverpassElement +} + +func NewCachedStreetProvider(overpassClient *OverpassClient) *CachedStreetProvider { + return &CachedStreetProvider{ + client: overpassClient, + cache: make(map[string][]OverpassElement), + } +} + +func (p *CachedStreetProvider) GetRandomStreet(city string, isUrban bool) (Street, error) { + Info("Getting random street from %s", city) + if streets, ok := p.cache[city]; ok && len(streets) > 0 { + Info("Cache hit for %s", city) + return stringToStreet(city, streets[rand.Intn(len(streets))]), nil + } + radiusMeters := 2000 + if isUrban { + radiusMeters = 10000 + } + streets, err := p.client.GetStreets(city, 100, radiusMeters) + if err != nil { + return Street{}, err + } + + p.cache[city] = streets + + return stringToStreet(city, streets[rand.Intn(len(streets))]), nil +} + +func GetRoute(provider *CachedStreetProvider, cities []string, lastEnd Street, isUrban bool) ([]Street, error, string) { + var route []Street + + if lastEnd.StreetName != "" { + route = append(route, lastEnd) + } else { + city := cities[rand.Intn(len(cities))] + randStreet, err := provider.GetRandomStreet(city, isUrban) + if err != nil { + return nil, err, city + } + route = append(route, randStreet) + } + + city := cities[rand.Intn(len(cities))] + randStreet, err := provider.GetRandomStreet(city, isUrban) + if err != nil { + return nil, err, city + } + route = append(route, randStreet) + + return route, nil, "" +} diff --git a/tracky/src-redis-primer/trackeroo/utils.go b/tracky/src-redis-primer/trackeroo/utils.go new file mode 100755 index 00000000..ee205674 --- /dev/null +++ b/tracky/src-redis-primer/trackeroo/utils.go @@ -0,0 +1,79 @@ +package trackeroo + +import ( + "encoding/json" + "time" +) + +type Number interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | + ~float32 | ~float64 +} + +type StatVar[T Number] struct { + Avg float64 `json:"avg"` + Min T `json:"min"` + Max T `json:"max"` + Count int `json:"count"` +} + +func NewStatVar[T Number]() StatVar[T] { + return StatVar[T]{} +} + +func (sv *StatVar[T]) Add(value T) { + if sv.Count == 0 { + sv.Avg = 0.0 + sv.Min = value + sv.Max = value + } else { + sv.Avg += (float64(value) - sv.Avg) / float64(sv.Count+1) + if value < sv.Min { + sv.Min = value + } + if value > sv.Max { + sv.Max = value + } + } + sv.Count++ +} + +func (sv *StatVar[T]) Get() map[string]any { + res := map[string]any{ + "avg": sv.Avg, + "min": sv.Min, + "max": sv.Max, + "count": sv.Count, + } + sv.Avg = 0.0 + sv.Min = 0 + sv.Max = 0 + sv.Count = 0 + return res +} + +func UnixTime() uint64 { + return uint64(time.Now().Unix()) +} + +func Millisleep(millis int) { + time.Sleep(time.Duration(millis) * time.Millisecond) +} + +func StructToMap[T any](s T) (map[string]any, error) { + // Marshal struct to JSON + jsonData, err := json.Marshal(s) + if err != nil { + return nil, err + } + + // Unmarshal JSON to map + var result map[string]any + err = json.Unmarshal(jsonData, &result) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/tracky/src-redis-primer/tracky.go b/tracky/src-redis-primer/tracky.go new file mode 100755 index 00000000..3054af68 --- /dev/null +++ b/tracky/src-redis-primer/tracky.go @@ -0,0 +1,12 @@ +package main + +import ( + "fmt" + "tracky/trackeroo" +) + +func main() { + fmt.Println(trackeroo.ART) + Init() + Loop() +} diff --git a/tracky/src/Dockerfile b/tracky/src/Dockerfile index 8aea8981..f8c31e28 100644 --- a/tracky/src/Dockerfile +++ b/tracky/src/Dockerfile @@ -12,7 +12,7 @@ RUN go build -o tracky tracky.go firmware.go FROM alpine:latest WORKDIR /app - +RUN mkdir -p /data COPY --from=builder /app/tracky . ENTRYPOINT ["./tracky"] diff --git a/tracky/src/firmware.go b/tracky/src/firmware.go index 60e92b39..6ce8b7e2 100644 --- a/tracky/src/firmware.go +++ b/tracky/src/firmware.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "math/rand" "os" "strconv" @@ -91,7 +92,22 @@ func alteredFoodData(position trackeroo.DrivePosition) FoodSensors { var taskManager *trackeroo.TaskManager func Init() { - trackeroo.InitLogger(trackeroo.INFO, "") + debugLevel := os.Getenv("DEBUG_LEVEL") + level := trackeroo.INFO + switch debugLevel { + case "DEBUG": + level = trackeroo.DEBUG + case "INFO": + level = trackeroo.INFO + case "WARN": + case "WARNING": + level = trackeroo.WARNING + case "ERROR": + level = trackeroo.ERROR + default: + level = trackeroo.INFO + } + trackeroo.InitLogger(level, "") trackeroo.InitQueue("data") taskManager = trackeroo.NewTaskManager() go taskManager.Start() @@ -102,10 +118,14 @@ func Terminate() { } func Loop() { + rand.Seed(time.Now().UnixNano()) creds, _ := trackeroo.GetCredentials() + checkpointFile := fmt.Sprintf("/data/%s_checkpoint.json", creds.ID) + trackeroo.InitRedisClient() deviceType := creds.DeviceType + overpassClient := trackeroo.NewOverpassClient(os.Getenv("OVERPASS_URL")) + streetProvider := trackeroo.NewCachedStreetProvider(overpassClient) routingService := trackeroo.NewRoutingService( - os.Getenv("GEOCODING_SERVICE_URL"), os.Getenv("ROUTING_SERVICE_URL"), ) pubPeriod := time.Millisecond * 2000 @@ -116,34 +136,118 @@ func Loop() { } } lastPublish := time.Now() + lastCheckpoint := time.Now() isPirate := os.Getenv("PIRATE") == "true" || os.Getenv("PIRATE") == "1" + isRegional := os.Getenv("REGIONAL") == "true" + isUrban := os.Getenv("URBAN") == "true" + if isUrban { + isRegional = true + } + var cities []string + if isRegional { + regions, err := overpassClient.GetRegions() + if err != nil || len(regions) == 0 { + trackeroo.Error("Error getting regions, falling back to Toscana: %v", err) + regions = []string{"Toscana"} + trackeroo.DeleteCheckpoint(checkpointFile) + } + region := regions[rand.Intn(len(regions))] + cities, err = overpassClient.GetCities(region) + if err != nil || len(cities) == 0 { + trackeroo.Error("Error getting cities for %s, falling back to default cities: %v", region, err) + cities = []string{"Firenze", "Pisa", "Siena", "Lucca", "Vinci", "Prato", "Montecatini", "Arezzo", "Grosseto", "Massa"} + trackeroo.DeleteCheckpoint(checkpointFile) + } + + if isUrban { + city := cities[rand.Intn(len(cities))] + cities = []string{city} + } + } else { + regions, err := overpassClient.GetRegions() + if err != nil || len(regions) == 0 { + trackeroo.Error("Error getting regions, falling back to Toscana: %v", err) + regions = []string{"Toscana"} + trackeroo.DeleteCheckpoint(checkpointFile) + } + cities = make([]string, 0) + for _, region := range regions { + c, _ := overpassClient.GetCities(region) + cities = append(cities, c...) + } + } trackeroo.Info("Is pirate: %t", isPirate) lastStatus := "" - lastEnd := "" + lastEnd := trackeroo.Street{} normalRun := true deltaDistance := 0.0 speedVar := trackeroo.NewStatVar[float64]() consumptionVar := trackeroo.NewStatVar[float64]() for { - trackeroo.Info("Getting route from %s", lastEnd) - route := trackeroo.GetRoute(lastEnd) - trackeroo.Info("Route: %+v", route) - drivingSimulator, err := trackeroo.NewDrivingSimulator(routingService, route, 60, 100, isPirate, creds.DeviceType) + var checkpoint *trackeroo.Checkpoint + var err error + if trackeroo.ExistsCheckpoint(checkpointFile) { + checkpoint, err = trackeroo.LoadCheckpoint(checkpointFile) + if err != nil { + trackeroo.Error("Error loading route %v, discarding file", err) + trackeroo.DeleteCheckpoint(checkpointFile) + continue + } + lastEnd = checkpoint.Poles[1] + trackeroo.Info("Route loaded successfully") + } else { + checkpoint = &trackeroo.Checkpoint{} + trackeroo.Info("Getting route from %+v - REGIONAL: %t - URBAN: %t", lastEnd, isRegional, isUrban) + route, err, cityErr := trackeroo.GetRoute(streetProvider, cities, lastEnd, isUrban) + checkpoint.Poles = route + if err != nil { + trackeroo.Error("Error getting route from %s %v", cityErr, err) + trackeroo.Warning("Removing %s", cityErr) + for i, city := range cities { + if city == cityErr { + cities = append(cities[:i], cities[i+1:]...) + } + } + trackeroo.DeleteCheckpoint(checkpointFile) + continue + } + } + trackeroo.Info("Route: %+v -> %+v @ %+v", checkpoint.Poles[0], checkpoint.Poles[1], checkpoint.LastPosition) + err = trackeroo.SaveCheckpoint(checkpoint, checkpointFile) + if err != nil { + trackeroo.Error("Error saving route %v", err) + } else { + trackeroo.Info("Route saved to %s", checkpointFile) + } + + drivingSimulator, err := trackeroo.NewDrivingSimulator(routingService, checkpoint, 60, 100, isPirate, creds.DeviceType) if err != nil { trackeroo.Error("Error initializing driving simulator %v", err) continue } - lastEnd = route[len(route)-1] + lastEnd = checkpoint.Poles[len(checkpoint.Poles)-1] positionChan := drivingSimulator.SimulateDrive() if rand.Float64() < 0.05 || os.Getenv("NORMAL_RUN") == "false" { normalRun = false + } else { + normalRun = true } for position := range positionChan { deltaDistance += position.Distance speedVar.Add(position.Speed) consumptionVar.Add(position.Consumption) if time.Since(lastPublish) > pubPeriod || lastStatus != position.Status { + if time.Since(lastCheckpoint) > 60*time.Second { + err := trackeroo.SaveCheckpoint(&trackeroo.Checkpoint{ + Poles: checkpoint.Poles, + LastPosition: position.Coordinate, + }, checkpointFile) + if err != nil { + trackeroo.Warning("Cannot save checkpoint: %+v", err) + } + lastCheckpoint = time.Now() + } lastStatus = position.Status payload := Payload{ TS: position.Timestamp.Unix(), @@ -191,6 +295,7 @@ func Loop() { trackeroo.Millisleep(100) } /* Routing terminated, waiting before next route */ + trackeroo.DeleteCheckpoint(checkpointFile) time.Sleep(time.Second * 120) if !normalRun { /* Wait some more */ diff --git a/tracky/src/go.mod b/tracky/src/go.mod index d86d95e7..fcdbcd21 100755 --- a/tracky/src/go.mod +++ b/tracky/src/go.mod @@ -11,11 +11,14 @@ require ( ) require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/redis/go-redis/v9 v9.14.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect golang.org/x/net v0.22.0 // indirect diff --git a/tracky/src/go.sum b/tracky/src/go.sum index ad9d40cc..31e3d6d4 100755 --- a/tracky/src/go.sum +++ b/tracky/src/go.sum @@ -1,3 +1,7 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= @@ -14,6 +18,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= +github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= diff --git a/tracky/src/trackeroo/checkpoint.go b/tracky/src/trackeroo/checkpoint.go new file mode 100644 index 00000000..f84d42f8 --- /dev/null +++ b/tracky/src/trackeroo/checkpoint.go @@ -0,0 +1,80 @@ +package trackeroo + +import ( + "encoding/json" + "fmt" + "os" +) + +func ExistsCheckpoint(filename string) bool { + if redisClient != nil { + exists, err := redisClient.Exists(ctx, filename).Result() + return err == nil && exists > 0 + } + + _, err := os.Stat(filename) + return !os.IsNotExist(err) +} + +func SaveCheckpoint(checkpoint *Checkpoint, filename string) error { + if checkpoint == nil { + return fmt.Errorf("cannot save nil checkpoint") + } + + jsonData, err := json.Marshal(checkpoint) + if err != nil { + return err + } + + if redisClient != nil { + return redisClient.Set(ctx, filename, jsonData, 0).Err() + } + + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + fmt.Fprintf(file, "%s\n", jsonData) + return nil +} + +func LoadCheckpoint(filename string) (*Checkpoint, error) { + var jsonData []byte + var err error + + if redisClient != nil { + jsonData, err = redisClient.Get(ctx, filename).Bytes() + if err != nil { + return nil, err + } + } else { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var checkpoint Checkpoint + err = json.NewDecoder(file).Decode(&checkpoint) + if err != nil { + return nil, err + } + return &checkpoint, nil + } + + var checkpoint Checkpoint + err = json.Unmarshal(jsonData, &checkpoint) + if err != nil { + return nil, err + } + return &checkpoint, nil +} + +func DeleteCheckpoint(filename string) error { + if redisClient != nil { + return redisClient.Del(ctx, filename).Err() + } + return os.Remove(filename) +} diff --git a/tracky/src/trackeroo/driving.go b/tracky/src/trackeroo/driving.go index 30c977f0..7fc0bc60 100644 --- a/tracky/src/trackeroo/driving.go +++ b/tracky/src/trackeroo/driving.go @@ -7,7 +7,6 @@ import ( "math" "math/rand" "net/http" - "net/url" "time" ) @@ -26,6 +25,8 @@ var consumptionMultipliers = map[string]float32{ "public_transport": 1.8, // buses have much higher consumption } +const MAX_CONSUMPTION = 30.0 + type Coordinate struct { Lat float64 `json:"lat"` Lng float64 `json:"lon"` @@ -37,12 +38,6 @@ type RouteSegment struct { ShouldStop bool } -type NominatimResponse []struct { - Lat string `json:"lat"` - Lon string `json:"lon"` - DisplayName string `json:"display_name"` -} - type OSRMResponse struct { Code string `json:"code"` Routes []struct { @@ -87,58 +82,15 @@ type RoutingService struct { httpClient *http.Client } -func NewRoutingService(nominatimURL, osrmURL string) *RoutingService { +func NewRoutingService(osrmURL string) *RoutingService { return &RoutingService{ - NominatimURL: nominatimURL, - OSRMURL: osrmURL, - httpClient: &http.Client{Timeout: 30 * time.Second}, - } -} - -func (rs *RoutingService) Geocode(address string) (*Coordinate, error) { - encodedAddress := url.QueryEscape(address) - requestURL := fmt.Sprintf("%s/search?q=%s&format=json&limit=1", rs.NominatimURL, encodedAddress) - - resp, err := rs.httpClient.Get(requestURL) - if err != nil { - return nil, fmt.Errorf("failed to geocode address: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("nominatim API returned status %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - var nominatimResp NominatimResponse - if err := json.Unmarshal(body, &nominatimResp); err != nil { - return nil, fmt.Errorf("failed to parse nominatim response: %w", err) + OSRMURL: osrmURL, + httpClient: &http.Client{Timeout: 120 * time.Second}, } - - if len(nominatimResp) == 0 { - return nil, fmt.Errorf("no results found for address: %s", address) - } - - // Parse coordinates - lat, err := parseFloat(nominatimResp[0].Lat) - if err != nil { - return nil, fmt.Errorf("invalid latitude: %w", err) - } - - lng, err := parseFloat(nominatimResp[0].Lon) - if err != nil { - return nil, fmt.Errorf("invalid longitude: %w", err) - } - - return &Coordinate{Lat: lat, Lng: lng}, nil } // GetRoute gets routing directions from point A to B using OSRM -func (rs *RoutingService) GetRoute(from, to *Coordinate) ([]RouteSegment, error) { +func (rs *RoutingService) GetRoute(from, to Coordinate) ([]RouteSegment, error) { requestURL := fmt.Sprintf( "%s/route/v1/driving/%f,%f;%f,%f?geometries=geojson&overview=full&steps=true", rs.OSRMURL, from.Lng, from.Lat, to.Lng, to.Lat) @@ -193,7 +145,7 @@ func (rs *RoutingService) GetRoute(from, to *Coordinate) ([]RouteSegment, error) } func getConsumption(speed float64, devType string) float64 { - if speed == 0 { + if speed <= 5.0 { return 0.0 } threshold := 80.0 @@ -201,12 +153,14 @@ func getConsumption(speed float64, devType string) float64 { if speed >= threshold { consumption += 5 * math.Log(speed/threshold) } - return consumption * float64(consumptionMultipliers[devType]) + return min(consumption*float64(consumptionMultipliers[devType]), MAX_CONSUMPTION) } // DrivingSimulator simulates realistic car driving type DrivingSimulator struct { Route []RouteSegment + Start Coordinate + End Coordinate DefaultAverageSpeed float64 // km/h SpeedVariation float64 // percentage (0.0-1.0) StopProbability float64 // probability of stopping per segment (0.0-1.0) @@ -221,29 +175,48 @@ type DrivingSimulator struct { Distance float64 } -func NewDrivingSimulator(routingService *RoutingService, waypoints []string, avgSpeed float64, updateIntervalMs int, isPirate bool, devType string) (*DrivingSimulator, error) { +func NewDrivingSimulator(routingService *RoutingService, checkpoint *Checkpoint, avgSpeed float64, updateIntervalMs int, isPirate bool, devType string) (*DrivingSimulator, error) { route := make([]RouteSegment, 0) - for i := 0; i < len(waypoints)-1; i++ { - start, err := routingService.Geocode(waypoints[i]) - if err != nil { - return nil, fmt.Errorf("Error geocoding address %s: %v", waypoints[i], err) - } - end, err := routingService.Geocode(waypoints[i+1]) - if err != nil { - return nil, fmt.Errorf("Error geocoding address %s: %v", waypoints[i], err) - } + for i := 0; i < len(checkpoint.Poles)-1; i++ { + start := checkpoint.Poles[i].Coordinate + end := checkpoint.Poles[i+1].Coordinate r, err := routingService.GetRoute(start, end) if err != nil { return nil, err } + checkpointRouteIndex := -1 + checkpointCoordinatesIndex := -1 + + for i, pos := range r { + if checkpoint == nil || checkpoint.LastPosition.Lat == 0.0 || checkpoint.LastPosition.Lng == 0.0 { + Warning("Invalid checkpoit +%v, starting from first waypoint", checkpoint) + break + } + for j, c := range pos.Coordinates { + // 50 meters + if haversineDistance(checkpoint.LastPosition, c) < 0.05 { + checkpointRouteIndex = i + checkpointCoordinatesIndex = j + Info("Found checkpoint %+v at index (%d, %d)", checkpoint, checkpointRouteIndex, checkpointCoordinatesIndex) + goto found + } + } + } + found: + if checkpointRouteIndex >= 0 { + r = r[checkpointRouteIndex:] + r[0].Coordinates = r[0].Coordinates[checkpointCoordinatesIndex:] + } route = append(route, r...) } return &DrivingSimulator{ Route: route, DefaultAverageSpeed: avgSpeed, - SpeedVariation: 0.03, // 3% speed variation - StopProbability: 0.05, // 5% chance of stopping per segment + Start: checkpoint.Poles[0].Coordinate, + End: checkpoint.Poles[1].Coordinate, + SpeedVariation: 0.03, + StopProbability: 0.05, StopDuration: struct { Min time.Duration Max time.Duration @@ -280,7 +253,7 @@ func (ds *DrivingSimulator) SimulateDrive() <-chan DrivePosition { nextPos := segment.Coordinates[i+1] // Calculate distance between points - distance := ds.haversineDistance(currentPos, nextPos) + distance := haversineDistance(currentPos, nextPos) // Check if we should stop if i == 0 && segment.ShouldStop { @@ -291,8 +264,8 @@ func (ds *DrivingSimulator) SimulateDrive() <-chan DrivePosition { Speed: 0, SpeedLimit: speedLimit, Status: STOPPED, - Start: ds.Route[0].Coordinates[0], - End: lastCoord, + Start: ds.Start, + End: ds.End, Consumption: 0, Distance: 0, } @@ -356,8 +329,8 @@ func (ds *DrivingSimulator) SimulateDrive() <-chan DrivePosition { Speed: currentSpeed, SpeedLimit: speedLimit, Status: status, - Start: ds.Route[0].Coordinates[0], - End: lastCoord, + Start: ds.Start, + End: ds.End, Consumption: getConsumption(currentSpeed, ds.DevType) * consumptionCompensation, Distance: distance, } @@ -374,18 +347,19 @@ func (ds *DrivingSimulator) SimulateDrive() <-chan DrivePosition { Speed: 0, SpeedLimit: speedLimit, Status: ARRIVED, - Start: ds.Route[0].Coordinates[0], - End: lastCoord, + Start: ds.Start, + End: ds.End, Consumption: 0, Distance: 0, } }() + Info("Simulation started") return positionChan } // haversineDistance calculates the great circle distance between two points -func (ds *DrivingSimulator) haversineDistance(pos1, pos2 Coordinate) float64 { +func haversineDistance(pos1, pos2 Coordinate) float64 { const R = 6371 // Earth's radius in kilometers lat1Rad := pos1.Lat * math.Pi / 180 diff --git a/tracky/src/trackeroo/mqtt.go b/tracky/src/trackeroo/mqtt.go index 275f4830..396d7d15 100755 --- a/tracky/src/trackeroo/mqtt.go +++ b/tracky/src/trackeroo/mqtt.go @@ -128,15 +128,13 @@ func (t *TdmClient) handleDnMsg(client pahoMqtt.Client, msg pahoMqtt.Message) { func (t *TdmClient) run() { t.initClient() t.running = true - // fmt.Printf("%+v", z) + for t.connect() != nil { + Info("Trying to connect to the tdm...") + } for t.running { for !t.client.IsConnected() { - t.initClient() - err := t.connect() - if err != nil { - Error("Cannot connect, %v", err) - Millisleep(2000) - } + Info("Not connected...") + Millisleep(2000) } t.Kick() Millisleep(1000) @@ -145,8 +143,7 @@ func (t *TdmClient) run() { func (t *TdmClient) connect() error { token := t.client.Connect() - for !token.WaitTimeout(3 * time.Second) { - } + token.WaitTimeout(3 * time.Second) return token.Error() } @@ -171,6 +168,16 @@ func (t *TdmClient) initClient() { opts.SetKeepAlive(time.Duration(t.heartbeat) * time.Second) opts.SetPingTimeout(time.Duration(t.heartbeat) * time.Second) opts.SetAutoReconnect(true) + opts.SetMaxReconnectInterval(5 * time.Second) + opts.SetReconnectingHandler(func(c pahoMqtt.Client, op *pahoMqtt.ClientOptions) { + token, err := GetToken(t.creds.PrivateKey, 200, 200, t.creds.ID) + // fmt.Println(token) + if err != nil { + Error("Cannot create token, %v", err) + } + op.SetPassword(token) + Info("Trying to reconnect with new token...") + }) opts.SetConnectionLostHandler(func(c pahoMqtt.Client, err error) { Error("MQTT Connection lost:", err) }) diff --git a/tracky/src/trackeroo/overpass.go b/tracky/src/trackeroo/overpass.go new file mode 100644 index 00000000..c4575f14 --- /dev/null +++ b/tracky/src/trackeroo/overpass.go @@ -0,0 +1,249 @@ +package trackeroo + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "strings" + "time" + + "github.com/redis/go-redis/v9" +) + +type OverpassElement struct { + Type string `json:"type"` + ID int64 `json:"id"` + Tags map[string]string `json:"tags"` + Geometry []Coordinate `json:"geometry"` +} + +type OverpassResponse struct { + Elements []OverpassElement `json:"elements"` +} + +type OverpassClient struct { + BaseURL string + Client *http.Client +} + +func NewOverpassClient(baseURL string) *OverpassClient { + return &OverpassClient{ + BaseURL: baseURL, + Client: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +func (c *OverpassClient) runQuery(query string) (OverpassResponse, error) { + const maxRetries = 10 + + form := url.Values{} + form.Set("data", query) + + var lastErr error + var overpassResp OverpassResponse + + for attempt := 1; attempt <= maxRetries; attempt++ { + resp, err := c.Client.Post( + c.BaseURL, + "application/x-www-form-urlencoded", + strings.NewReader(form.Encode()), + ) + if err != nil { + lastErr = fmt.Errorf("failed to query Overpass API: %w", err) + time.Sleep(time.Duration(attempt) * time.Second) + continue + } + + bodyBytes, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + lastErr = fmt.Errorf("Overpass API returned status %d: %s", resp.StatusCode, string(bodyBytes)) + + if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 { + time.Sleep(time.Duration(attempt) * time.Second) + continue + } + break + } + + contentType := resp.Header.Get("Content-Type") + if strings.Contains(contentType, "text/html") || bytes.HasPrefix(bodyBytes, []byte(".italy; +node["name"="%s"]["place"~"city|town"](area.italy)->.citynode; +( + way(around.citynode:%d)["highway"~"primary|secondary|tertiary|residential|unclassified"]["name"]["highway"!~"motorway|trunk|motorway_link|trunk_link"](area.italy); +); +out tags geom %d; +`, city, radiusMeters, limit) + + overpassResp, err := c.runQuery(query) + if err != nil { + return nil, err + } + + if len(overpassResp.Elements) == 0 { + return nil, fmt.Errorf("no streets found for city: %s", city) + } + + if redisClient != nil { + data, err := json.Marshal(overpassResp.Elements) + if err == nil { + if err := redisClient.Set(ctx, cityKey, data, 0).Err(); err != nil { + Warning("Failed to cache regions in Redis: %v", err) + } else { + Info("Cached %d streets (city: %s, limit: %d, radius: %d) in Redis", len(overpassResp.Elements), city, limit, radiusMeters) + } + } + } + return overpassResp.Elements, nil +} + +func (c *OverpassClient) GetRandomStreet(city string, radiusMeters int) (Street, error) { + streets, err := c.GetStreets(city, 100, radiusMeters) + if err != nil { + return Street{}, err + } + randStreet := streets[rand.Intn(len(streets))] + return stringToStreet(city, randStreet), nil +} + +func (c *OverpassClient) GetRegions() ([]string, error) { + if redisClient != nil { + cached, err := redisClient.Get(ctx, "tracky::regions").Result() + if err == nil { + var regions []string + if err := json.Unmarshal([]byte(cached), ®ions); err == nil { + Info("Retrieved %d regions from Redis cache", len(regions)) + return regions, nil + } + Warning("Failed to unmarshal cached regions: %v", err) + } else if err != redis.Nil { + Warning("Redis get error: %v", err) + } + } + query := `[out:json][timeout:120]; +area["ISO3166-1"="IT"][admin_level=2]->.italy; +relation["boundary"="administrative"]["admin_level"=4]["ISO3166-2"~"^IT-"](area.italy); +out tags;` + + overpassResp, err := c.runQuery(query) + if err != nil { + return nil, err + } + + if len(overpassResp.Elements) == 0 { + return nil, fmt.Errorf("no regions found") + } + regions := make([]string, 0) + for _, element := range overpassResp.Elements { + if element.Type == "relation" { + regions = append(regions, element.Tags["name"]) + } + } + if redisClient != nil { + data, err := json.Marshal(regions) + if err == nil { + if err := redisClient.Set(ctx, "tracky::regions", data, 0).Err(); err != nil { + Warning("Failed to cache regions in Redis: %v", err) + } else { + Info("Cached %d regions in Redis", len(regions)) + } + } + } + return regions, nil +} + +func (c *OverpassClient) GetCities(region string) ([]string, error) { + regionKey := fmt.Sprintf("tracky::regions::%s", region) + if redisClient != nil { + cached, err := redisClient.Get(ctx, regionKey).Result() + if err == nil { + var cities []string + if err := json.Unmarshal([]byte(cached), &cities); err == nil { + Info("Retrieved %d cities (%s) from Redis cache", len(cities), region) + return cities, nil + } + Warning("Failed to unmarshal cached cities: %v", err) + } else if err != redis.Nil { + Warning("Redis get error: %v", err) + } + } + query := fmt.Sprintf(`[out:json][timeout:120]; +relation["boundary"="administrative"]["name"="%s"]["admin_level"=4]->.reg; +.reg map_to_area->.region; +node["place"~"city|town"](area.region); +out tags;`, region) + + overpassResp, err := c.runQuery(query) + if err != nil { + return nil, err + } + + if len(overpassResp.Elements) == 0 { + return nil, fmt.Errorf("no cities found") + } + cities := make([]string, 0) + for _, element := range overpassResp.Elements { + if element.Type == "node" { + cities = append(cities, element.Tags["name"]) + } + } + + if redisClient != nil { + data, err := json.Marshal(cities) + if err == nil { + if err := redisClient.Set(ctx, regionKey, data, 0).Err(); err != nil { + Warning("Failed to cache regions in Redis: %v", err) + } else { + Info("Cached %d cities in Redis", len(cities)) + } + } + } + return cities, nil +} diff --git a/tracky/src/trackeroo/redis.go b/tracky/src/trackeroo/redis.go new file mode 100644 index 00000000..ae18d9b4 --- /dev/null +++ b/tracky/src/trackeroo/redis.go @@ -0,0 +1,33 @@ +package trackeroo + +import ( + "context" + "os" + + "github.com/redis/go-redis/v9" +) + +var ( + redisClient *redis.Client + ctx = context.Background() +) + +func InitRedisClient() { + redisURI := os.Getenv("REDIS_URI") + if redisURI != "" { + opt, err := redis.ParseURL(redisURI) + if err != nil { + Error("Failed to parse REDIS_URI: %v\n", err) + return + } + redisClient = redis.NewClient(opt) + + if err := redisClient.Ping(ctx).Err(); err != nil { + Error("Failed to connect to Redis: %v\n", err) + redisClient = nil + } + } + if redisClient != nil { + Info("Saving checkpoint to %s", redisURI) + } +} diff --git a/tracky/src/trackeroo/routes.go b/tracky/src/trackeroo/routes.go index 325d8377..096cbc2a 100644 --- a/tracky/src/trackeroo/routes.go +++ b/tracky/src/trackeroo/routes.go @@ -1,126 +1,80 @@ package trackeroo -import "math/rand" +import ( + "math/rand" +) -var streets = []string{ - // Pisa (56127) - "Piazza dei Cavalieri, Pisa, Italy, 56126", - "Borgo Stretto, Pisa, Italy, 56127", - "Via San Frediano, Pisa, Italy, 56126", - "Lungarno Mediceo, Pisa, Italy, 56127", - "Via San Martino, Pisa, Italy, 56125", - "Via di Gargalone, Pisa, Italy, 56127", - "Via Asmara, Pisa, Italy, 56127", - "Via Putignano, Pisa, Italy, 56127", - "Via Fagiana, Pisa, Italy, 56127", - "Via Santa Bona, Pisa, Italy, 56127", - - // Lucca (55100) - "Via Fillungo, Lucca, Italy, 55100", - "Piazza Napoleone, Lucca, Italy, 55100", - "Via Santa Croce, Lucca, Italy, 55100", - "Via San Paolino, Lucca, Italy, 55100", - "Via Guinigi, Lucca, Italy, 55100", - "Via del Cimitero, Lucca, Italy, 55100", - "Via di Vicopelago, Lucca, Italy, 55100", - "Via dei Bollori, Lucca, Italy, 55100", - "Via delle Fornacette, Lucca, Italy, 55100", - "Via Stefano Tofanelli, Lucca, Italy, 55100", +type Street struct { + Coordinate Coordinate `json:"coordinate"` + StreetName string `json:"street_name"` + City string `json:"city"` +} - // Firenze (50123) - "Via dei Calzaiuoli, Firenze, Italy, 50123", - "Via Tornabuoni, Firenze, Italy, 50123", - "Borgo San Lorenzo, Firenze, Italy, 50123", - "Via della Vigna Nuova, Firenze, Italy, 50123", - "Via Ghibellina, Firenze, Italy, 50122", - "Via del Gelsomino, Firenze, Italy, 50125", - "Via Livorno, Firenze, Italy, 50142", - "Via Gherardo Starnina, Firenze, Italy, 50142", - "Via della Casella, Firenze, Italy, 50142", - "Via Antonio del Pollaiolo, Firenze, Italy, 50142", +type Checkpoint struct { + Poles []Street `json:"poles"` + LastPosition Coordinate `json:"last_position"` +} - // Livorno (57123) - "Via Grande, Livorno, Italy, 57123", - "Piazza della Repubblica, Livorno, Italy, 57123", - "Via Magenta, Livorno, Italy, 57123", - "Scali delle Cantine, Livorno, Italy, 57123", - "Via Ricasoli, Livorno, Italy, 57123", - "Via di Quercianella, Livorno, Italy, 57128", - "Via del Littorale, Livorno, Italy, 57128", - "Viale di Antignano, Livorno, Italy, 57128", - "Via del Pastore, Livorno, Italy, 57128", - "Via Uberto Mondolfi, Livorno, Italy, 57128", +func stringToStreet(city string, street OverpassElement) Street { + return Street{ + Coordinate: street.Geometry[rand.Intn(len(street.Geometry))], + StreetName: street.Tags["name"], + City: city, + } +} - // Pontedera (56025) - "Corso Matteotti, Pontedera, Italy, 56025", - "Piazza Curtatone, Pontedera, Italy, 56025", - "Via Roma, Pontedera, Italy, 56025", - "Via Dante Alighieri, Pontedera, Italy, 56025", - "Via Verdi, Pontedera, Italy, 56025", - "Via di Gello, Pontedera, Italy, 56025", - "Via di Lavaiano, Pontedera, Italy, 56025", - "Via dell’Industria, Pontedera, Italy, 56025", - "Via delle Colombaie, Pontedera, Italy, 56025", - "Via della Fornace, Pontedera, Italy, 56025", +type CachedStreetProvider struct { + client *OverpassClient + cache map[string][]OverpassElement +} - // Poggibonsi (53036) - "Via della Repubblica, Poggibonsi, Italy, 53036", - "Via Trento, Poggibonsi, Italy, 53036", - "Via San Gimignano, Poggibonsi, Italy, 53036", - "Via Borgaccio, Poggibonsi, Italy, 53036", - "Via Sardegna, Poggibonsi, Italy, 53036", - "Via dell’Ospedale, Poggibonsi, Italy, 53036", - "Via Abruzzo, Poggibonsi, Italy, 53036", - "Via Lazio, Poggibonsi, Italy, 53036", - "Via Sicilia, Poggibonsi, Italy, 53036", - "Via Calabria, Poggibonsi, Italy, 53036", +func NewCachedStreetProvider(overpassClient *OverpassClient) *CachedStreetProvider { + return &CachedStreetProvider{ + client: overpassClient, + cache: make(map[string][]OverpassElement), + } +} - // Viareggio (55049) - "Viale Giosuè Carducci, Viareggio, Italy, 55049", - "Piazza Mazzini, Viareggio, Italy, 55049", - "Via Cesare Battisti, Viareggio, Italy, 55049", - "Via Santa Maria Goretti, Viareggio, Italy, 55049", - "Via Coppino, Viareggio, Italy, 55049", - "Via delle Cavalle, Viareggio, Italy, 55049", - "Via Marina di Levante, Viareggio, Italy, 55049", - "Via dei Partigiani, Viareggio, Italy, 55049", - "Via Santa Gemma Galgani, Viareggio, Italy, 55049", - "Via della Ferrovia, Viareggio, Italy, 55049", +func (p *CachedStreetProvider) GetRandomStreet(city string, isUrban bool) (Street, error) { + Info("Getting random street from %s", city) + if streets, ok := p.cache[city]; ok && len(streets) > 0 { + Info("Cache hit for %s", city) + return stringToStreet(city, streets[rand.Intn(len(streets))]), nil + } + radiusMeters := 2000 + if isUrban { + radiusMeters = 10000 + } + streets, err := p.client.GetStreets(city, 100, radiusMeters) + if err != nil { + return Street{}, err + } - // Camaiore (55041) - "Via Vittorio Emanuele, Camaiore, Italy, 55041", - "Via XX Settembre, Camaiore, Italy, 55041", - "Via Roma, Camaiore, Italy, 55041", - "Via del Secco, Camaiore, Italy, 55041", - "Via dei Ghivizzani, Camaiore, Italy, 55041", - "Via Mentana, Camaiore, Italy, 55041", - "Via Montecassino, Camaiore, Italy, 55041", - "Via Alessandro Volta, Camaiore, Italy, 55041", - "Via dei Papaveri, Camaiore, Italy, 55041", - "Via Montemagno, Camaiore, Italy, 55041", + p.cache[city] = streets - // Siena (53100) - "Via di Città, Siena, Italy, 53100", - "Banchi di Sopra, Siena, Italy, 53100", - "Via dei Rossi, Siena, Italy, 53100", - "Via Pantaneto, Siena, Italy, 53100", - "Via della Sapienza, Siena, Italy, 53100", - "Strada Grossetana, Siena, Italy, 53100", - "Strada Massetana Romana, Siena, Italy, 53100", - "Via Paolo Mascagni, Siena, Italy, 53100", - "Strada del Ruffolo, Siena, Italy, 53100", - "Via di Fiera Vecchia, Siena, Italy, 53100", + return stringToStreet(city, streets[rand.Intn(len(streets))]), nil } -func GetRoute(lastEnd string) []string { - var route []string - if lastEnd != "" { +func GetRoute(provider *CachedStreetProvider, cities []string, lastEnd Street, isUrban bool) ([]Street, error, string) { + var route []Street + + if lastEnd.StreetName != "" { route = append(route, lastEnd) } else { - randStreet := streets[rand.Intn(len(streets))] + city := cities[rand.Intn(len(cities))] + randStreet, err := provider.GetRandomStreet(city, isUrban) + if err != nil { + return nil, err, city + } route = append(route, randStreet) } - randStreet := streets[rand.Intn(len(streets))] + + city := cities[rand.Intn(len(cities))] + randStreet, err := provider.GetRandomStreet(city, isUrban) + if err != nil { + return nil, err, city + } route = append(route, randStreet) - return route + + return route, nil, "" } diff --git a/tsdb/docker-entrypoint-initdb.d/02-schema.sql b/tsdb/docker-entrypoint-initdb.d/02-schema.sql index 3c5f91d4..af84a730 100644 --- a/tsdb/docker-entrypoint-initdb.d/02-schema.sql +++ b/tsdb/docker-entrypoint-initdb.d/02-schema.sql @@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS trackeroo.data ( PRIMARY KEY (ts, dev_id) ); - CREATE TABLE IF NOT EXISTS trackeroo.aggregated ( ts_unix BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, @@ -23,18 +22,35 @@ CREATE TABLE IF NOT EXISTS trackeroo.aggregated ( PRIMARY KEY (ts, route_hash, dev_id) ); -SELECT create_hypertable('trackeroo.data', 'ts', 'dev_id', 16); -SELECT create_hypertable('trackeroo.aggregated', 'ts', 'dev_id', 16); - +SELECT create_hypertable('trackeroo.data', 'ts', 'dev_id', 4); +SELECT create_hypertable('trackeroo.aggregated', 'ts', 'dev_id', 4); +SELECT set_chunk_time_interval('trackeroo.data', INTERVAL '12 hours'); +SELECT set_chunk_time_interval('trackeroo.aggregated', INTERVAL '12 hours'); CREATE INDEX IF NOT EXISTS idx_trackeroo_data_dev_id ON trackeroo.data(dev_id); CREATE INDEX IF NOT EXISTS idx_trackeroo_data_tag ON trackeroo.data(tag); CREATE INDEX IF NOT EXISTS idx_trackeroo_data_ts ON trackeroo.data(ts); +-- Added to speedup the last position query +CREATE INDEX IF NOT EXISTS idx_trackeroo_data_dev_id_ts_desc ON trackeroo.data (dev_id, ts DESC); +CREATE INDEX IF NOT EXISTS idx_data_device_type ON trackeroo.data ((payload->>'device_type')); +ALTER TABLE trackeroo.data SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'dev_id,tag', + timescaledb.compress_orderby = 'ts DESC' +); +SELECT add_compression_policy('trackeroo.data', INTERVAL '6 hours'); + CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_dev_id ON trackeroo.aggregated(dev_id); CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_route_hash ON trackeroo.aggregated(route_hash); CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_tag ON trackeroo.aggregated(tag); CREATE INDEX IF NOT EXISTS idx_trackeroo_aggregated_ts ON trackeroo.aggregated(ts); +ALTER TABLE trackeroo.aggregated SET ( + timescaledb.compress, + timescaledb.compress_segmentby = 'dev_id,route_hash,tag', + timescaledb.compress_orderby = 'ts DESC' +); +SELECT add_compression_policy('trackeroo.aggregated', INTERVAL '6 hours'); SELECT add_retention_policy('trackeroo.data', INTERVAL '3 days'); SELECT add_retention_policy('trackeroo.aggregated', INTERVAL '3 days'); @@ -53,3 +69,46 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA trackeroo ALTER DEFAULT PRIVILEGES IN SCHEMA trackeroo GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO apps; + +-- Latest positions continuous aggregate (replaces materialized view) +CREATE MATERIALIZED VIEW trackeroo.latest_positions +WITH (timescaledb.continuous) AS +SELECT + dev_id, + time_bucket('30 seconds', ts) AS bucket, + last(ts, ts) AS ts, + last((payload->'position'->>'lat')::double precision, ts) AS lat, + last((payload->'position'->>'lon')::double precision, ts) AS lon, + last(payload->>'device_name', ts) AS device_name, + last(payload->>'device_type', ts) AS device_type +FROM trackeroo.data +WHERE (payload->'position'->>'lat') IS NOT NULL + AND (payload->'position'->>'lon') IS NOT NULL +GROUP BY dev_id, bucket +WITH NO DATA; + +-- Add automatic refresh policy (refreshes every 2 minutes) +SELECT add_continuous_aggregate_policy('trackeroo.latest_positions', + start_offset => INTERVAL '1 hour', + end_offset => INTERVAL '30 seconds', + schedule_interval => INTERVAL '2 minutes'); + +-- Create unique index on the continuous aggregate +CREATE UNIQUE INDEX idx_latest_positions_dev_bucket ON trackeroo.latest_positions (dev_id, bucket); + +-- Create a simple view to get the current position for each device +-- This view queries the continuous aggregate and returns only the latest position per device +CREATE VIEW trackeroo.current_positions AS +SELECT DISTINCT ON (dev_id) + dev_id, + ts, + lat, + lon, + device_name, + device_type +FROM trackeroo.latest_positions +ORDER BY dev_id, bucket DESC; + +-- Grant permissions on the new views +GRANT SELECT ON trackeroo.latest_positions TO apps; +GRANT SELECT ON trackeroo.current_positions TO apps; diff --git a/utils/gen_mongo_dev/main.go b/utils/gen_mongo_dev/main.go index 9813d73d..c85271f0 100644 --- a/utils/gen_mongo_dev/main.go +++ b/utils/gen_mongo_dev/main.go @@ -14,47 +14,74 @@ const ( FOOD = "food" PRIVATE_TRANSPORT = "private_transport" PUBLIC_TRANSPORT = "public_transport" - OTHER = "other" + // OTHER = "other" ) -// Container-style names for randomization -var containerNames = []string{ - "amazing_turing", "boring_wozniak", "clever_newton", "dreamy_tesla", - "eager_darwin", "friendly_curie", "gracious_hawking", "happy_einstein", - "intelligent_jobs", "jolly_gates", "kind_torvalds", "loving_lovelace", - "mystifying_feynman", "naughty_dijkstra", "optimistic_babbage", "peaceful_pascal", - "quirky_shannon", "relaxed_turing", "serene_hopper", "trusting_knuth", - "upbeat_ritchie", "vibrant_thompson", "wonderful_wirth", "xenodochial_carmack", - "youthful_stallman", "zealous_berners", "admiring_bohr", "adoring_morse", - "affectionate_bell", "agitated_planck", "amazing_galileo", "angry_maxwell", - "boring_heisenberg", "brave_schrodinger", "busy_pauli", "charming_dirac", - "clever_fermi", "compassionate_oppenheimer", "competent_rutherford", "condescending_volta", - "confident_faraday", "cool_ohm", "cranky_ampere", "crazy_coulomb", - "curious_feynman", "dazzling_maxwell", "determined_kelvin", "distracted_planck", - "dreamy_euler", "eager_gauss", "ecstatic_riemann", "elastic_fourier", - "elegant_lagrange", "elated_laplace", "eloquent_leibniz", "enchanting_poincare", - "energetic_hilbert", "epic_cantor", "exciting_godel", "exotic_turing", - "fabulous_ramanujan", "faithful_hardy", "fancy_erdos", "fascinated_noether", - "fearless_galois", "fervent_abel", "flamboyant_jacobi", "focused_cauchy", - "friendly_weierstrass", "frosty_dedekind", "funny_peano", "furious_russell", - "gallant_whitehead", "gentle_church", "gifted_kleene", "goofy_markov", - "graceful_chebyshev", "great_kolmogorov", "grieving_wiener", "groovy_shannon", - "happy_nyquist", "hardcore_bell", "heartwarming_bose", "heuristic_fermi", - "hopeful_bardeen", "hungry_cooper", "hyper_shockley", "inspiring_bardeen", - "interesting_watson", "inventive_crick", "iron_franklin", "jaunty_pauling", - "jovial_mendeleev", "keen_bohr", "laughing_rutherford", "lucid_heisenberg", - "magical_dirac", "magnificent_feynman", "merry_schwinger", "modest_dyson", - "motivated_penrose", "nervous_hawking", "noble_weinberg", "nostalgic_salam", - "objective_glashow", "optimized_higgs", "original_yang", "outstanding_lee", - "patient_wu", "pedantic_pauli", "phenomenal_born", "pious_planck", - "playful_compton", "polite_millikan", "practical_michelson", "proud_morley", - "puzzled_fizeau", "quizzical_doppler", "romantic_hertz", "sad_marconi", - "serene_tesla", "sharp_edison", "silly_westinghouse", "sleepy_siemens", - "stoic_ohm", "strange_ampere", "suspicious_volta", "sweet_galvani", - "tender_faraday", "thirsty_henry", "thoughtful_weber", "thrilled_gauss", +var adjectives = []string{ + "admiring", "adoring", "affectionate", "agitated", "amazing", + "angry", "awesome", "beautiful", "blissful", "bold", + "boring", "brave", "busy", "calm", "charming", + "clever", "cool", "compassionate", "competent", "condescending", + "confident", "cranky", "crazy", "curious", "dazzling", + "determined", "distracted", "dreamy", "eager", "ecstatic", + "elastic", "elated", "elegant", "eloquent", "enchanting", + "energetic", "epic", "exciting", "exotic", "fabulous", + "faithful", "fancy", "fascinated", "fearless", "fervent", + "flamboyant", "focused", "friendly", "frosty", "funny", + "furious", "gallant", "gentle", "gifted", "goofy", + "graceful", "gracious", "great", "grieving", "groovy", + "happy", "hardcore", "heartwarming", "heuristic", "hopeful", + "hungry", "hyper", "inspiring", "intelligent", "interesting", + "inventive", "iron", "jaunty", "jolly", "jovial", + "keen", "kind", "laughing", "loving", "lucid", + "magical", "magnificent", "merry", "modest", "motivated", + "mystifying", "naughty", "nervous", "noble", "nostalgic", + "objective", "optimistic", "optimized", "original", "outstanding", + "patient", "peaceful", "pedantic", "phenomenal", "pious", + "playful", "polite", "practical", "proud", "puzzled", + "quirky", "quizzical", "relaxed", "romantic", "sad", + "serene", "sharp", "silly", "sleepy", "stoic", + "strange", "suspicious", "sweet", "tender", "thirsty", + "thoughtful", "thrilled", "trusting", "upbeat", "vibrant", + "wonderful", "xenodochial", "youthful", "zealous", } -var deviceTypes = []string{VALUABLES, FOOD, PRIVATE_TRANSPORT, PUBLIC_TRANSPORT, OTHER} +var names = []string{ + "abel", "ampere", "babbage", "bardeen", "bell", + "berners", "bohr", "born", "bose", "cantor", + "carmack", "cauchy", "chebyshev", "church", "compton", + "cooper", "coulomb", "crick", "curie", "darwin", + "dedekind", "dijkstra", "dirac", "doppler", "dyson", + "edison", "einstein", "erdos", "euler", "faraday", + "fermi", "feynman", "fizeau", "fourier", "franklin", + "galileo", "galois", "galvani", "gates", "gauss", + "glashow", "godel", "hardy", "hawking", "heisenberg", + "henry", "hertz", "higgs", "hilbert", "hopper", + "jacobi", "jobs", "kelvin", "kleene", "knuth", + "kolmogorov", "lagrange", "laplace", "lee", "leibniz", + "lovelace", "marconi", "markov", "maxwell", "mendeleev", + "michelson", "millikan", "morley", "morse", "newton", + "noether", "nyquist", "ohm", "oppenheimer", "pascal", + "pauli", "pauling", "peano", "penrose", "planck", + "poincare", "ramanujan", "riemann", "ritchie", "russell", + "rutherford", "salam", "schrodinger", "schwinger", "shannon", + "shockley", "siemens", "stallman", "tesla", "thompson", + "torvalds", "turing", "volta", "watson", "weber", + "weinberg", "weierstrass", "westinghouse", "whitehead", "wiener", + "wirth", "wozniak", "wu", "yang", +} + +func generateRandomName() string { + // Seed the random number generator (do this once in your main function, not every time) + rand.Seed(time.Now().UnixNano()) + + adjective := adjectives[rand.Intn(len(adjectives))] + name := names[rand.Intn(len(names))] + + return fmt.Sprintf("%s_%s", adjective, name) +} + +var deviceTypes = []string{VALUABLES, FOOD, PRIVATE_TRANSPORT, PUBLIC_TRANSPORT} func GenPrivateKey() (string, error) { privateKey := make([]byte, 32) @@ -91,7 +118,7 @@ func generateJSObject() (string, error) { return "", err } - name := containerNames[mrand.Intn(len(containerNames))] + name := generateRandomName() deviceType := deviceTypes[mrand.Intn(len(deviceTypes))] id := generateRandomID() connected := mrand.Intn(2) == 1 // Random boolean @@ -120,7 +147,7 @@ func generateJSObjectList(count int) error { // Ensure unique name for { - name = containerNames[mrand.Intn(len(containerNames))] + name = generateRandomName() if !usedNames[name] { usedNames[name] = true break @@ -142,18 +169,16 @@ func generateJSObjectList(count int) error { } deviceType := deviceTypes[mrand.Intn(len(deviceTypes))] - connected := mrand.Intn(2) == 1 // Random boolean - lastMessage := generateRandomTime() createdAt := generateRandomTime() jsObject := fmt.Sprintf(`{ _id: "%s", name: "%s", - status: { connected: %t, last_message: "%s" }, + status: { connected: false, last_message: "1970-01-01T00:00:00Z" }, device_type: "%s", private_key: "%s", created_at: ISODate("%s"), -}`, id, name, connected, lastMessage, deviceType, privateKey, createdAt) +}`, id, name, deviceType, privateKey, createdAt) fmt.Print(jsObject) if i < count-1 { @@ -169,15 +194,7 @@ func generateJSObjectList(count int) error { func main() { rand.Seed(time.Now().UnixNano()) - // Generate 5 objects by default (max 50 due to name uniqueness) count := 100 - - if count > len(containerNames) { - fmt.Printf("Warning: Requested %d objects but only %d unique names available. Using %d objects.\n", - count, len(containerNames), len(containerNames)) - count = len(containerNames) - } - fmt.Printf("// Generated %d JS objects with no duplicates:\n", count) err := generateJSObjectList(count) if err != nil {