morz-infoboard/player/agent/internal/app/app.go
Jesko Anschütz 6623a313bb Melde Agent-Status periodisch an das Backend
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-22 17:43:01 +01:00

178 lines
3.8 KiB
Go

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
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
reporter statusSender
mu sync.RWMutex
status Status
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,
}
}
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")
}
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.logger.Printf("event=status_report_failed screen_id=%s error=%v", a.Config.ScreenID, err)
return
}
a.logger.Printf("event=status_report_sent screen_id=%s", a.Config.ScreenID)
}