package queue import ( "fmt" "strconv" "strings" "time" ) // Erlaubte Werte für quiet_mode (öffentlicher Config-Wert). const ( QuietModeDiscard = "discard" QuietModeToast = "toast" ) // clockTime ist eine Tageszeit in Minuten-Auflösung (lokale Zeit, 24 h). type clockTime struct{ hour, minute int } // minutes liefert Minuten seit Mitternacht. func (c clockTime) minutes() int { return c.hour*60 + c.minute } // String rendert die Uhrzeit als "HH:MM". func (c clockTime) String() string { return fmt.Sprintf("%02d:%02d", c.hour, c.minute) } // QuietHours ist ein Ruhezeit-Fenster in lokaler Zeit, z. B. "18:00-08:00" // (überspannt Mitternacht). Der Nullwert (Start == End) ist deaktiviert — // rund-um-die-Uhr-Stumm ist mute, nicht quiet_hours. type QuietHours struct { Start, End clockTime } // String rendert das Fenster als "HH:MM-HH:MM" (Nullwert: ""). func (q QuietHours) String() string { if q.Start == q.End { return "" } return q.Start.String() + "-" + q.End.String() } // ParseQuietHours parst ein Ruhezeit-Fenster "HH:MM-HH:MM" (lokale Zeit, // 24 h; Stunden ein- oder zweistellig). Fenster über Mitternacht // (Start > End) sind erlaubt; Start == End deaktiviert das Fenster. Der // Leerstring ist das deaktivierte Fenster (kein Fehler). func ParseQuietHours(s string) (QuietHours, error) { if s == "" { return QuietHours{}, nil } startS, endS, ok := strings.Cut(s, "-") if !ok { return QuietHours{}, fmt.Errorf("quiet_hours: %q ist nicht HH:MM-HH:MM", s) } start, err := parseClock(startS) if err != nil { return QuietHours{}, fmt.Errorf("quiet_hours: %v", err) } end, err := parseClock(endS) if err != nil { return QuietHours{}, fmt.Errorf("quiet_hours: %v", err) } return QuietHours{Start: start, End: end}, nil } // parseClock parst "H[H]:MM" in Uhrzeit-Bestandteile. func parseClock(s string) (clockTime, error) { hourS, minuteS, ok := strings.Cut(s, ":") if !ok || len(hourS) < 1 || len(hourS) > 2 || len(minuteS) != 2 { return clockTime{}, fmt.Errorf("%q ist nicht HH:MM", s) } hour, err := strconv.Atoi(hourS) if err != nil { return clockTime{}, fmt.Errorf("%q ist nicht HH:MM", s) } minute, err := strconv.Atoi(minuteS) if err != nil { return clockTime{}, fmt.Errorf("%q ist nicht HH:MM", s) } if hour < 0 || hour > 23 || minute < 0 || minute > 59 { return clockTime{}, fmt.Errorf("%q ist keine gültige Uhrzeit (24 h)", s) } return clockTime{hour: hour, minute: minute}, nil } // Active meldet, ob der lokale Zeitpunkt t im Ruhefenster liegt. // Tagfenster (Start < End) gelten ab Start bis ausschließlich End; // Nachtfenster (Start > End) überspannen Mitternacht. func (q QuietHours) Active(t time.Time) bool { if q.Start == q.End { return false } m := t.Hour()*60 + t.Minute() s, e := q.Start.minutes(), q.End.minutes() if s < e { return m >= s && m < e } return m >= s || m < e } // Until liefert das lokale Ende des gerade aktiven Fensters — Nullzeit, // wenn gerade kein Ruhefenster aktiv ist. Basis für GET /status. func (q QuietHours) Until(t time.Time) time.Time { if !q.Active(t) { return time.Time{} } end := time.Date(t.Year(), t.Month(), t.Day(), q.End.hour, q.End.minute, 0, 0, t.Location()) if q.Start.minutes() > q.End.minutes() && t.Hour()*60+t.Minute() >= q.Start.minutes() { end = end.AddDate(0, 0, 1) // Nachtfenster: das Ende ist am Folgetag } return end }