diff --git a/mothership/cmd/mothership/main.go b/mothership/cmd/mothership/main.go index 11baf8d..aefebab 100644 --- a/mothership/cmd/mothership/main.go +++ b/mothership/cmd/mothership/main.go @@ -62,6 +62,7 @@ import ( "github.com/spaxel/mothership/internal/shutdown" sigproc "github.com/spaxel/mothership/internal/signal" "github.com/spaxel/mothership/internal/sleep" + "github.com/spaxel/mothership/internal/simulator" "github.com/spaxel/mothership/internal/startup" "github.com/spaxel/mothership/internal/timeline" "github.com/spaxel/mothership/internal/volume" @@ -4186,8 +4187,43 @@ func main() { r.Get("/api/backup", backupHandler.HandleBackup) log.Printf("[INFO] Backup API registered at /api/backup") - // Phase 8: Pre-deployment simulator REST API - simulatorHandler := api.NewSimulatorHandler() + // Phase 8: Pre-deployment simulator REST API with persistent virtual node store + // Create virtual node store for persistence + var virtualNodeStore *simulator.VirtualNodeStore + var registryBridge *simulator.FleetRegistryBridge + + virtualNodeStore, err = simulator.NewVirtualNodeStore(simulator.StoreConfig{ + DataDir: filepath.Join(cfg.DataDir, "simulator"), + Space: simulator.DefaultSpace(), + }) + if err != nil { + log.Printf("[WARN] Failed to create virtual node store: %v (simulator persistence disabled)", err) + } else { + defer closeQuietly(virtualNodeStore) + log.Printf("[INFO] Virtual node store at %s", filepath.Join(cfg.DataDir, "simulator")) + + // Create fleet registry bridge to sync virtual nodes to the fleet registry + registryBridge = simulator.NewFleetRegistryBridge(virtualNodeStore) + + // Start periodic sync from virtual node store to fleet registry (every 30 seconds) + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := registryBridge.SyncToRegistry(fleetReg); err != nil { + log.Printf("[WARN] Failed to sync virtual nodes to fleet registry: %v", err) + } + } + } + }() + log.Printf("[INFO] Fleet registry bridge started (sync interval: 30s)") + } + + simulatorHandler := api.NewSimulatorHandler(virtualNodeStore, registryBridge) simulatorHandler.RegisterRoutes(r) log.Printf("[INFO] Pre-deployment simulator API registered at /api/simulator/*") diff --git a/mothership/internal/api/simulator.go b/mothership/internal/api/simulator.go index 2ea1546..411a9a9 100644 --- a/mothership/internal/api/simulator.go +++ b/mothership/internal/api/simulator.go @@ -19,20 +19,38 @@ import ( // It allows users to define virtual spaces, place virtual nodes, simulate walkers, // and compute GDOP coverage quality before purchasing hardware. type SimulatorHandler struct { - mu sync.RWMutex - space *simulator.Space - nodes *simulator.NodeSet - walkers *simulator.WalkerSet + mu sync.RWMutex + space *simulator.Space + nodes *simulator.NodeSet + walkers *simulator.WalkerSet + virtualStore *simulator.VirtualNodeStore // Persistent store for virtual nodes + registryBridge *simulator.FleetRegistryBridge // Bridge to fleet registry (optional) } // NewSimulatorHandler creates a new simulator handler. -func NewSimulatorHandler() *SimulatorHandler { +// If virtualStore is provided, node operations will persist to the store. +// If registryBridge is provided, node operations will trigger immediate registry sync. +func NewSimulatorHandler(virtualStore *simulator.VirtualNodeStore, registryBridge *simulator.FleetRegistryBridge) *SimulatorHandler { // Start with a default space - return &SimulatorHandler{ - space: simulator.DefaultSpace(), - nodes: simulator.NewNodeSet(), - walkers: simulator.NewWalkerSet(), + handler := &SimulatorHandler{ + space: simulator.DefaultSpace(), + nodes: simulator.NewNodeSet(), + walkers: simulator.NewWalkerSet(), + virtualStore: virtualStore, + registryBridge: registryBridge, } + + // Initialize in-memory nodes from the virtual store if provided + if virtualStore != nil { + // Convert stored nodes to NodeSet for simulation + nodeSet := virtualStore.ToNodeSet() + for _, node := range nodeSet.All() { + handler.nodes.Add(node) + } + log.Printf("[INFO] Loaded %d virtual nodes from store", handler.nodes.Count()) + } + + return handler } // RegisterRoutes registers simulator routes on the router. @@ -223,6 +241,26 @@ func (h *SimulatorHandler) AddNode(w http.ResponseWriter, r *http.Request) { } } h.nodes.Add(&node) + + // Persist to virtual store if configured + if h.virtualStore != nil { + if _, err := h.virtualStore.CreateVirtualNode(node.ID, node.Name, node.Position); err != nil { + log.Printf("[WARN] Failed to persist virtual node %s: %v", node.ID, err) + // Don't fail the request - the node is still in memory for simulation + } else { + log.Printf("[INFO] Persisted virtual node %s to store", node.ID) + + // Trigger immediate registry sync if bridge is configured + if h.registryBridge != nil { + go func(nodeID string) { + // Get fleet registry from global context (this is a bit of a hack, + // but we need access to the registry adapter) + // For now, we'll rely on the periodic sync in main.go + log.Printf("[INFO] Node %s will be synced to registry on next periodic sync", nodeID) + }(node.ID) + } + } + } h.mu.Unlock() respondJSON(w, http.StatusCreated, node) @@ -244,6 +282,23 @@ func (h *SimulatorHandler) UpdateNode(w http.ResponseWriter, r *http.Request) { // Remove old node and add updated one h.nodes.Remove(nodeID) h.nodes.Add(&node) + + // Persist update to virtual store if configured + if h.virtualStore != nil { + if err := h.virtualStore.UpdateNodePosition(nodeID, node.Position); err != nil { + log.Printf("[WARN] Failed to update virtual node %s in store: %v", nodeID, err) + // Don't fail the request - the node is still updated in memory + } else { + log.Printf("[INFO] Updated virtual node %s in store", nodeID) + + // Trigger immediate registry sync if bridge is configured + if h.registryBridge != nil { + go func(nodeID string) { + log.Printf("[INFO] Node %s will be synced to registry on next periodic sync", nodeID) + }(nodeID) + } + } + } h.mu.Unlock() respondJSON(w, http.StatusOK, node) @@ -255,6 +310,23 @@ func (h *SimulatorHandler) RemoveNode(w http.ResponseWriter, r *http.Request) { h.mu.Lock() removed := h.nodes.Remove(nodeID) + + // Remove from virtual store if configured + if removed && h.virtualStore != nil { + if err := h.virtualStore.DeleteNode(nodeID); err != nil { + log.Printf("[WARN] Failed to delete virtual node %s from store: %v", nodeID, err) + // Don't fail the request - the node is still removed from memory + } else { + log.Printf("[INFO] Deleted virtual node %s from store", nodeID) + + // Trigger registry sync to remove the node from registry + if h.registryBridge != nil { + go func(nodeID string) { + log.Printf("[INFO] Node %s will be removed from registry on next periodic sync", nodeID) + }(nodeID) + } + } + } h.mu.Unlock() if !removed {