package controllers import ( "context" "strconv" "sync/atomic" "time" "doormile/db" "doormile/internal/ai/telemetry" "doormile/models" "doormile/utils" "github.com/gofiber/fiber/v2" ) // GET /admin/ai/status — what is actually wired, for the banner on Settings → // Skills & Tools. Replaces a fixed "Half wired" note that stayed on screen // after everything was deployed, because nothing on the page checked. // // "AI_engine reads these settings" is observed, not assumed: every poll of // GET /internal/ai/registry (a 200 or a 304) stamps the time in Redis, shared by // every backend replica. The engine polls about every 30 seconds, so a stamp // younger than engineReadFreshFor means it is following the registry now. const ( registryReadKey = "ai:registry:lastread" engineReadFreshFor = 5 * time.Minute ) // lastRegistryRead is the in-process copy, used when Redis is unavailable. var lastRegistryRead atomic.Int64 // markRegistryRead records an AI_engine registry poll. Best effort: a Redis // failure must never fail the poll itself. func markRegistryRead() { now := time.Now().Unix() lastRegistryRead.Store(now) if db.Rdb == nil { return } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _ = db.Rdb.Set(ctx, registryReadKey, now, 30*time.Minute).Err() } // registryLastRead returns the latest poll time seen by any replica, or nil. func registryLastRead() *time.Time { secs := lastRegistryRead.Load() if db.Rdb != nil { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if v, err := db.Rdb.Get(ctx, registryReadKey).Result(); err == nil { if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > secs { secs = n } } } if secs == 0 { return nil } t := time.Unix(secs, 0) return &t } type aiStatus struct { Engine struct { ReadingSettings bool `json:"readingsettings"` LastReadAt *time.Time `json:"lastreadat"` Telemetry bool `json:"telemetry"` LiveAgents int `json:"liveagents"` } `json:"engine"` Playground struct { Configured bool `json:"configured"` Model string `json:"model,omitempty"` } `json:"playground"` } // GetAIStatus — GET /admin/ai/status func GetAIStatus(c *fiber.Ctx) error { var s aiStatus s.Engine.LastReadAt = registryLastRead() s.Engine.ReadingSettings = s.Engine.LastReadAt != nil && time.Since(*s.Engine.LastReadAt) < engineReadFreshFor s.Engine.Telemetry = telemetry.Receiving.Load() var engineAgents []string if db.DB != nil { db.DB.Model(&models.AIAgent{}).Where("runtime = ?", "engine").Pluck("agentid", &engineAgents) } s.Engine.LiveAgents = len(telemetry.LiveStates(db.Rdb, engineAgents)) if PlaygroundModel != nil { s.Playground.Configured = true if named, ok := PlaygroundModel.(interface{ ModelName() string }); ok { s.Playground.Model = named.ModelName() } } return utils.OK(c, s) }