package app import ( "context" "fmt" "log" "os" "sync" "time" "git.az-it.net/az/morz-infoboard/player/agent/internal/config" ) type Status string const ( StatusStarting Status = "starting" StatusRunning Status = "running" StatusStopped Status = "stopped" ) type HealthSnapshot struct { Status Status ScreenID string ServerBaseURL string MQTTBroker string HeartbeatEvery int StartedAt time.Time LastHeartbeatAt time.Time } type App struct { Config config.Config logger *log.Logger now func() time.Time mu sync.RWMutex status Status startedAt time.Time lastHeartbeatAt time.Time } func New() (*App, error) { cfg, err := config.Load() if err != nil { return nil, err } logger := log.New(os.Stdout, "agent ", log.LstdFlags|log.LUTC) return newApp(cfg, logger, time.Now), nil } func newApp(cfg config.Config, logger *log.Logger, now func() time.Time) *App { if logger == nil { logger = log.New(os.Stdout, "agent ", log.LstdFlags|log.LUTC) } if now == nil { now = time.Now } return &App{ Config: cfg, logger: logger, now: now, status: StatusStarting, } } func (a *App) Snapshot() HealthSnapshot { a.mu.RLock() defer a.mu.RUnlock() return HealthSnapshot{ Status: a.status, ScreenID: a.Config.ScreenID, ServerBaseURL: a.Config.ServerBaseURL, MQTTBroker: a.Config.MQTTBroker, HeartbeatEvery: a.Config.HeartbeatEvery, StartedAt: a.startedAt, LastHeartbeatAt: a.lastHeartbeatAt, } } func (a *App) Run(ctx context.Context) error { if a.Config.ScreenID == "" { return fmt.Errorf("screen id is required") } a.mu.Lock() a.status = StatusRunning a.startedAt = a.now() a.mu.Unlock() a.logger.Printf( "event=agent_configured screen_id=%s server_url=%s mqtt_broker=%s heartbeat_every_seconds=%d", a.Config.ScreenID, a.Config.ServerBaseURL, a.Config.MQTTBroker, a.Config.HeartbeatEvery, ) a.emitHeartbeat() ticker := time.NewTicker(time.Duration(a.Config.HeartbeatEvery) * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): a.mu.Lock() a.status = StatusStopped a.mu.Unlock() a.logger.Printf("event=agent_stopped screen_id=%s", a.Config.ScreenID) return nil case <-ticker.C: a.emitHeartbeat() } } } func (a *App) emitHeartbeat() { now := a.now() a.mu.Lock() a.lastHeartbeatAt = now a.mu.Unlock() a.logger.Printf("event=heartbeat_tick screen_id=%s", a.Config.ScreenID) }