package app import ( "context" "fmt" "log" "os" "sync" "time" "git.az-it.net/az/morz-infoboard/player/agent/internal/config" "git.az-it.net/az/morz-infoboard/player/agent/internal/statusreporter" ) type Status string type Connectivity string const ( StatusStarting Status = "starting" StatusRunning Status = "running" StatusStopped Status = "stopped" ConnectivityUnknown Connectivity = "unknown" ConnectivityOnline Connectivity = "online" ConnectivityDegraded Connectivity = "degraded" ConnectivityOffline Connectivity = "offline" ) const offlineFailureThreshold = 3 type HealthSnapshot struct { Status Status ServerConnectivity Connectivity 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 reporter statusSender mu sync.RWMutex status Status serverConnectivity Connectivity consecutiveReportFailures int startedAt time.Time lastHeartbeatAt time.Time } type statusSender interface { Send(ctx context.Context, snapshot statusreporter.Snapshot) error } 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, statusreporter.New(cfg.ServerBaseURL, nil, time.Now)), nil } func newApp(cfg config.Config, logger *log.Logger, now func() time.Time, reporter statusSender) *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, reporter: reporter, status: StatusStarting, serverConnectivity: ConnectivityUnknown, } } func (a *App) Snapshot() HealthSnapshot { a.mu.RLock() defer a.mu.RUnlock() return HealthSnapshot{ Status: a.status, ServerConnectivity: a.serverConnectivity, 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") } select { case <-ctx.Done(): a.mu.Lock() a.status = StatusStopped a.mu.Unlock() return nil default: } a.mu.Lock() 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() a.mu.Lock() a.status = StatusRunning a.mu.Unlock() ticker := time.NewTicker(time.Duration(a.Config.HeartbeatEvery) * time.Second) defer ticker.Stop() reportTicker := time.NewTicker(time.Duration(a.Config.StatusReportEvery) * time.Second) defer reportTicker.Stop() a.reportStatus(ctx) 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() case <-reportTicker.C: a.reportStatus(ctx) } } } 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) } func (a *App) reportStatus(ctx context.Context) { if a.reporter == nil { return } snapshot := a.Snapshot() err := a.reporter.Send(ctx, statusreporter.Snapshot{ Status: string(snapshot.Status), ScreenID: snapshot.ScreenID, ServerBaseURL: snapshot.ServerBaseURL, MQTTBroker: snapshot.MQTTBroker, HeartbeatEverySeconds: snapshot.HeartbeatEvery, StartedAt: snapshot.StartedAt, LastHeartbeatAt: snapshot.LastHeartbeatAt, }) if err != nil { a.mu.Lock() a.consecutiveReportFailures++ a.serverConnectivity = ConnectivityDegraded if a.consecutiveReportFailures >= offlineFailureThreshold { a.serverConnectivity = ConnectivityOffline } a.mu.Unlock() a.logger.Printf("event=status_report_failed screen_id=%s error=%v", a.Config.ScreenID, err) return } a.mu.Lock() a.consecutiveReportFailures = 0 a.serverConnectivity = ConnectivityOnline a.mu.Unlock() a.logger.Printf("event=status_report_sent screen_id=%s", a.Config.ScreenID) }