diff --git a/cmd/bot/webhook.go b/cmd/bot/webhook.go index 4d790bf..b992e3a 100644 --- a/cmd/bot/webhook.go +++ b/cmd/bot/webhook.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/http" "os" + "time" tgbot "github.com/go-telegram/bot" @@ -26,10 +27,25 @@ func startWebhook(ctx context.Context, b *tgbot.Bot, h *handler.Handler, webhook tlsCert := os.Getenv("BOT_TLS_CERT") tlsKey := os.Getenv("BOT_TLS_KEY") + server := &http.Server{ + Addr: listenAddr, + Handler: b.WebhookHandler(), + } + + go func() { + <-ctx.Done() + slog.Info("shutting down webhook server") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + slog.Error("webhook server shutdown", "error", err) + } + }() + if tlsCert != "" && tlsKey != "" { slog.Info("starting HTTPS webhook", "addr", listenAddr) go func() { - if err := http.ListenAndServeTLS(listenAddr, tlsCert, tlsKey, b.WebhookHandler()); err != nil { + if err := server.ListenAndServeTLS(tlsCert, tlsKey); err != nil && err != http.ErrServerClosed { slog.Error("HTTPS server", "error", err) os.Exit(1) } @@ -37,7 +53,7 @@ func startWebhook(ctx context.Context, b *tgbot.Bot, h *handler.Handler, webhook } else { slog.Info("starting HTTP webhook (TLS by reverse proxy)", "addr", listenAddr) go func() { - if err := http.ListenAndServe(listenAddr, b.WebhookHandler()); err != nil { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("HTTP server", "error", err) os.Exit(1) } diff --git a/internal/domain/totals.go b/internal/domain/totals.go index b5ca6b7..7448194 100644 --- a/internal/domain/totals.go +++ b/internal/domain/totals.go @@ -18,7 +18,7 @@ func ComputeDailyTotals(events []Event, minBreakThreshold int64, dayStart, dayEn state := StateIdle var workStart, breakStart int64 - if dayStart > 0 { + if dayStart > 0 && sorted[0].EventType == "out" { state = StateWorking workStart = dayStart } else if sorted[0].EventType == "out" { diff --git a/internal/domain/totals_test.go b/internal/domain/totals_test.go index 0c4696d..1c8412d 100644 --- a/internal/domain/totals_test.go +++ b/internal/domain/totals_test.go @@ -36,7 +36,7 @@ func TestComputeDailyTotals_SingleInOut(t *testing.T) { } } -func TestComputeDailyTotals_MissingFirstIn(t *testing.T) { +func TestComputeDailyTotals_DayStartWithOutFirst(t *testing.T) { events := []Event{ mkEvent("out", 200), mkEvent("in", 300), @@ -44,7 +44,39 @@ func TestComputeDailyTotals_MissingFirstIn(t *testing.T) { } got := ComputeDailyTotals(events, 0, 100, 500) if got.TotalSeconds != 200 { - t.Errorf("missing first in: TotalSeconds=%d, want 200", got.TotalSeconds) + t.Errorf("dayStart with out first: TotalSeconds=%d, want 200", got.TotalSeconds) + } +} + +func TestComputeDailyTotals_DayStartWithInFirst(t *testing.T) { + events := []Event{ + mkEvent("in", 100), + mkEvent("out", 200), + mkEvent("in", 300), + mkEvent("out", 400), + } + got := ComputeDailyTotals(events, 0, 50, 500) + if got.TotalSeconds != 200 { + t.Errorf("dayStart with in first: TotalSeconds=%d, want 200", got.TotalSeconds) + } + if got.BreakSeconds != 200 { + t.Errorf("dayStart with in first: BreakSeconds=%d, want 200", got.BreakSeconds) + } +} + +func TestComputeDailyTotals_DayStartInFirstRealistic(t *testing.T) { + events := []Event{ + mkEvent("in", 33060), + mkEvent("out", 47100), + mkEvent("in", 48780), + mkEvent("out", 74460), + } + got := ComputeDailyTotals(events, 0, 0, 86399) + if got.TotalSeconds != 39720 { + t.Errorf("realistic day: TotalSeconds=%d, want 39720 (11h 02m)", got.TotalSeconds) + } + if got.BreakSeconds != 13619 { + t.Errorf("realistic day: BreakSeconds=%d, want 13619 (3h 19m)", got.BreakSeconds) } } diff --git a/internal/handler/settings.go b/internal/handler/settings.go index c115abb..a9cd5f7 100644 --- a/internal/handler/settings.go +++ b/internal/handler/settings.go @@ -144,7 +144,8 @@ func (h *Handler) leagueToggleCallback(ctx context.Context, chatID int64, msgID return } if !st.RPGEnabled { - h.editText(ctx, chatID, msgID, "League requires the RPG system. Enable RPG first in Settings.", nil) + kb := settingsBackKeyboard() + h.editText(ctx, chatID, msgID, "League requires the RPG system. Enable RPG first in Settings.", &kb) return } text, kb := h.buildToggleKeyboard(user.ID, "league", "WorkTime League")