feat(api): /speak meldet Gesetz-Verwürfe, /status zeigt Gesetz-State
POST /speak antwortet bei Verwurf dokumentiert mit 202 {id:"",
discarded:"rate_limit"|"quiet_hours"} (+ toast:true im Toast-Modus)
— analog zum Mute-Muster, kein Fehler. GET /status ergänzt additiv
rate_limit {interval, allow_high_burst, next_slot} und quiet_hours
{window, active, mode, until} (RFC3339 bzw. null). talkd verwired die
Gesetze aus der Config und loggt sie beim Start. Die CLI meldet
'discarded: <grund>' plain (wie muted) und zeigt in talk status die
Zeilen rate-limit/ruhezeit mit Slot bzw. Fenster-Ende.
This commit is contained in:
1 parent
5385a4b87d
commit
b56c8abfae
6 files changed
+188
-21
No files matched your search
+43
-2
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.m3ta.dev/m3tam3re/talk/internal/api"
|
"code.m3ta.dev/m3tam3re/talk/internal/api"
|
||||||
"code.m3ta.dev/m3tam3re/talk/internal/config"
|
"code.m3ta.dev/m3tam3re/talk/internal/config"
|
||||||
@@ -71,8 +72,9 @@ func printMuteState(muted bool, stdout io.Writer) {
|
|||||||
|
|
||||||
// runStatus implementiert `talk status`: plain Block aus GET /status —
|
// runStatus implementiert `talk status`: plain Block aus GET /status —
|
||||||
// Daemon-Adresse + Version, Queue-Länge, aktuelles Item, Mute-State,
|
// Daemon-Adresse + Version, Queue-Länge, aktuelles Item, Mute-State,
|
||||||
// aktiver Provider. Ohne Daemon: klarer Fehler (status ist eine Anfrage an
|
// aktiver Provider, Gesetz-State (Rate-Limit, Ruhezeit). Ohne Daemon:
|
||||||
// den Daemon; der Degraded Mode hat keinen Status).
|
// klarer Fehler (status ist eine Anfrage an den Daemon; der Degraded Mode
|
||||||
|
// hat keinen Status).
|
||||||
func runStatus(args []string, opts *sayOptions, stdin io.Reader, stdout, stderr io.Writer) int {
|
func runStatus(args []string, opts *sayOptions, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||||
cfg, err := config.Load(config.Options{Getenv: getenv})
|
cfg, err := config.Load(config.Options{Getenv: getenv})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,9 +103,48 @@ func runStatus(args []string, opts *sayOptions, stdin io.Reader, stdout, stderr
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(stdout, "mute: %s\n", muteState)
|
fmt.Fprintf(stdout, "mute: %s\n", muteState)
|
||||||
fmt.Fprintf(stdout, "provider: %s\n", st.Provider)
|
fmt.Fprintf(stdout, "provider: %s\n", st.Provider)
|
||||||
|
printLawState(stdout, st)
|
||||||
return exitOK
|
return exitOK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printLawState schreibt den Gesetz-State (Task 05) aus GET /status:
|
||||||
|
// Rate-Limit mit Burst-Recht und nächstem Slot sowie das Ruhezeit-Fenster.
|
||||||
|
func printLawState(stdout io.Writer, st *api.StatusResponse) {
|
||||||
|
if st.RateLimit != nil {
|
||||||
|
interval := st.RateLimit.Interval
|
||||||
|
if interval == "0s" {
|
||||||
|
interval = "aus (unbegrenzt)"
|
||||||
|
}
|
||||||
|
burst := "verboten"
|
||||||
|
if st.RateLimit.AllowHighBurst {
|
||||||
|
burst = "erlaubt"
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf("rate-limit: %s (high-burst %s)", interval, burst)
|
||||||
|
if st.RateLimit.NextSlot != nil {
|
||||||
|
if ts, err := time.Parse(time.RFC3339, *st.RateLimit.NextSlot); err == nil {
|
||||||
|
line += fmt.Sprintf(", nächster slot %s", ts.Format("15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintln(stdout, line)
|
||||||
|
}
|
||||||
|
if st.QuietHours != nil {
|
||||||
|
window := st.QuietHours.Window
|
||||||
|
if window == "" {
|
||||||
|
window = "aus"
|
||||||
|
}
|
||||||
|
state := "inaktiv"
|
||||||
|
if st.QuietHours.Active {
|
||||||
|
state = "aktiv"
|
||||||
|
if st.QuietHours.Until != nil {
|
||||||
|
if ts, err := time.Parse(time.RFC3339, *st.QuietHours.Until); err == nil {
|
||||||
|
state += " bis " + ts.Format("15:04")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stdout, "ruhezeit: %s (modus %s, %s)\n", window, st.QuietHours.Mode, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runCancel implementiert `talk cancel`: POST /cancel — bricht die laufende
|
// runCancel implementiert `talk cancel`: POST /cancel — bricht die laufende
|
||||||
// Ansage ab und leert die Queue; stdout meldet die Zahl der abgebrochenen
|
// Ansage ab und leert die Queue; stdout meldet die Zahl der abgebrochenen
|
||||||
// Ansagen. Ohne Daemon gibt es nichts, was talk abbrechen könnte.
|
// Ansagen. Ohne Daemon gibt es nichts, was talk abbrechen könnte.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"code.m3ta.dev/m3tam3re/talk/internal/api"
|
"code.m3ta.dev/m3tam3re/talk/internal/api"
|
||||||
"code.m3ta.dev/m3tam3re/talk/internal/config"
|
"code.m3ta.dev/m3tam3re/talk/internal/config"
|
||||||
|
"code.m3ta.dev/m3tam3re/talk/internal/queue"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Zeit-Budgets der Daemon-Kommunikation. Die Daemon-Erkennung (GET /status)
|
// Zeit-Budgets der Daemon-Kommunikation. Die Daemon-Erkennung (GET /status)
|
||||||
@@ -64,6 +65,18 @@ func sayViaDaemon(cfg config.Config, text string, opts *sayOptions, stdout, stde
|
|||||||
fmt.Fprintln(stderr, "talk: stumm geschaltet (mute) — Ansage verworfen")
|
fmt.Fprintln(stderr, "talk: stumm geschaltet (mute) — Ansage verworfen")
|
||||||
return exitOK, true
|
return exitOK, true
|
||||||
}
|
}
|
||||||
|
if resp.Discarded != "" {
|
||||||
|
// Gesetz-Verwurf (Rate-Limit-Budget / Ruhezeit): dokumentiertes
|
||||||
|
// Verhalten wie bei Mute — plain Token statt Fehler; --wait hat
|
||||||
|
// nichts zu warten, es gibt keine Item-ID.
|
||||||
|
line := "discarded: " + resp.Discarded
|
||||||
|
if resp.Toast {
|
||||||
|
line += " (toast)"
|
||||||
|
}
|
||||||
|
fmt.Fprintln(stdout, line)
|
||||||
|
fmt.Fprintln(stderr, "talk: "+discardHint(resp.Discarded, resp.Toast))
|
||||||
|
return exitOK, true
|
||||||
|
}
|
||||||
fmt.Fprintln(stdout, resp.ID)
|
fmt.Fprintln(stdout, resp.ID)
|
||||||
if !opts.wait {
|
if !opts.wait {
|
||||||
return exitOK, true
|
return exitOK, true
|
||||||
@@ -71,6 +84,22 @@ func sayViaDaemon(cfg config.Config, text string, opts *sayOptions, stdout, stde
|
|||||||
return waitForItem(client, resp.ID, stderr), true
|
return waitForItem(client, resp.ID, stderr), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discardHint übersetzt den Verwurfs-Grund der Daemon-Antwort in einen
|
||||||
|
// menschlichen Hinweis.
|
||||||
|
func discardHint(reason string, toast bool) string {
|
||||||
|
switch reason {
|
||||||
|
case queue.DiscardQuietHours:
|
||||||
|
if toast {
|
||||||
|
return "Ruhezeit aktiv — Ansage durch Toast ersetzt (kein Audio)"
|
||||||
|
}
|
||||||
|
return "Ruhezeit aktiv — Ansage verworfen (quiet_mode = discard; now bricht bewusst aus)"
|
||||||
|
case queue.DiscardRateLimit:
|
||||||
|
return "Rate-Limit — Ansage verworfen (Priorität low über dem Fenster-Budget)"
|
||||||
|
default:
|
||||||
|
return "Ansage verworfen (" + reason + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// waitForItem pollt /status, bis die Ansage id weder läuft noch wartet —
|
// waitForItem pollt /status, bis die Ansage id weder läuft noch wartet —
|
||||||
// sie ist dann fertig (oder abgebrochen; in beiden Fällen hat der Aufrufer
|
// sie ist dann fertig (oder abgebrochen; in beiden Fällen hat der Aufrufer
|
||||||
// nichts mehr zu warten). Stirbt der Daemon während des Wartens, ist das
|
// nichts mehr zu warten). Stirbt der Daemon während des Wartens, ist das
|
||||||
|
|||||||
+24
-9
@@ -1,11 +1,11 @@
|
|||||||
// talkd ist der Daemon für talk 2.0: HTTP-API auf 127.0.0.1 (Default-Port
|
// talkd ist der Daemon für talk 2.0: HTTP-API auf 127.0.0.1 (Default-Port
|
||||||
// 7800, server.port/TALK_PORT), serialisierte Ansage-Queue mit Prioritäten
|
// 7800, server.port/TALK_PORT), serialisierte Ansage-Queue mit Prioritäten
|
||||||
// und now-Sprung, persistenter Mute-State (Datei mute im Config-Dir).
|
// und now-Sprung, persistenter Mute-State (Datei mute im Config-Dir) —
|
||||||
//
|
// und seit Task 05 das Anti-Spam-Gesetz: Rate-Limit (queue.rate_limit,
|
||||||
// Task 04: POST /speak, GET /status, POST /cancel, POST /mute, POST /unmute.
|
// Default 1 Ansage / 30 s; low über Budget wird verworfen, normal parkt,
|
||||||
// Rate-Limit/Ruhezeiten folgen in Task 05, CORS/MCP in Task 06 — die
|
// high darf mit queue.allow_high_burst verstoßen) plus Ruhezeiten
|
||||||
// Rate-Limit-Struktur existiert bereits (internal/queue/rate.go), ist hier
|
// (quiet_hours/quiet_mode; now:true bricht bewusst aus beiden aus).
|
||||||
// aber bewusst noch nicht angebunden (Task-04-Default: unbegrenzt).
|
// CORS/MCP folgen in Task 06.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -88,7 +88,21 @@ func run() error {
|
|||||||
return chain.Speak(ctx, item.Text, lang)
|
return chain.Speak(ctx, item.Text, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := queue.New(speak, lw)
|
// Anti-Spam-Gesetz (Task 05): Rate-Limit + High-Burst + Ruhezeiten.
|
||||||
|
// config.Load hat quiet_hours bereits validiert — das Parsen hier kann
|
||||||
|
// praktisch nicht scheitern, aber ein Fehler darf nie still werden.
|
||||||
|
quiet, err := queue.ParseQuietHours(cfg.QuietHours)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
laws := queue.Laws{
|
||||||
|
RateLimit: cfg.Queue.RateLimit,
|
||||||
|
AllowHighBurst: cfg.Queue.AllowHighBurst,
|
||||||
|
Quiet: quiet,
|
||||||
|
QuietMode: cfg.QuietMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := queue.New(laws, speak, lw)
|
||||||
|
|
||||||
// Persistenter Mute-State (Datei mute im Config-Dir). Ohne Config-Dir
|
// Persistenter Mute-State (Datei mute im Config-Dir). Ohne Config-Dir
|
||||||
// läuft der Daemon ohne Mute weiter (dokumentierte Warnung) statt zu
|
// läuft der Daemon ohne Mute weiter (dokumentierte Warnung) statt zu
|
||||||
@@ -125,8 +139,9 @@ func run() error {
|
|||||||
srv := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second}
|
srv := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second}
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() { errCh <- srv.Serve(ln) }()
|
go func() { errCh <- srv.Serve(ln) }()
|
||||||
log.Printf("listening on http://%s (version %s, audio sink %s, mute=%v)",
|
log.Printf("listening on http://%s (version %s, audio sink %s, mute=%v, rate-limit %s, high-burst=%v, ruhezeit=%q modus %s)",
|
||||||
addr, versionString(), sink.Name(), mute.Muted())
|
addr, versionString(), sink.Name(), mute.Muted(),
|
||||||
|
cfg.Queue.RateLimit, cfg.Queue.AllowHighBurst, cfg.QuietHours, cfg.QuietMode)
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
sig := make(chan os.Signal, 1)
|
||||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|||||||
+6
-4
@@ -1,6 +1,8 @@
|
|||||||
// Package api definiert die HTTP-Oberfläche des Daemons auf 127.0.0.1:
|
// Package api definiert die HTTP-Oberfläche des Daemons auf 127.0.0.1:
|
||||||
// POST /speak (enqueue, 202 {id}; bei Mute 202 {muted:true}), GET /status,
|
// POST /speak (enqueue, 202 {id}; bei Mute 202 {muted:true}; bei Gesetz-
|
||||||
// POST /cancel, POST /mute, POST /unmute (Task 04). CORS für /speak und
|
// Verwurf 202 {discarded:"rate_limit"|"quiet_hours"} — Task 05),
|
||||||
// der MCP-Endpoint (Streamable HTTP) folgen in Task 06. Der Client-Teil
|
// GET /status (inkl. Rate-Limit- und Ruhezeit-State), POST /cancel,
|
||||||
// ist die CLI-Seite derselben Vertragsschnittstelle.
|
// POST /mute, POST /unmute. CORS für /speak und der MCP-Endpoint
|
||||||
|
// (Streamable HTTP) folgen in Task 06. Der Client-Teil ist die CLI-Seite
|
||||||
|
// derselben Vertragsschnittstelle.
|
||||||
package api
|
package api
|
||||||
+41
-4
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.m3ta.dev/m3tam3re/talk/internal/queue"
|
"code.m3ta.dev/m3tam3re/talk/internal/queue"
|
||||||
)
|
)
|
||||||
@@ -68,7 +69,9 @@ type handler struct {
|
|||||||
|
|
||||||
// speak behandelt POST /speak: validieren, bei Mute verwerfen (202,
|
// speak behandelt POST /speak: validieren, bei Mute verwerfen (202,
|
||||||
// dokumentiertes Verhalten — bewusst kein Fehler, damit Aufrufer keine
|
// dokumentiertes Verhalten — bewusst kein Fehler, damit Aufrufer keine
|
||||||
// Sonderbehandlung brauchen), sonst enqueue → 202 {id}.
|
// Sonderbehandlung brauchen), sonst enqueue → 202 {id}. Das Gesetz
|
||||||
|
// (Rate-Limit-Budget, Ruhezeit) kann beim Eintritt ebenfalls verwerfen —
|
||||||
|
// die Antwort nennt dann den Grund ({"discarded":"…"}).
|
||||||
func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
||||||
var req SpeakRequest
|
var req SpeakRequest
|
||||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes)).Decode(&req); err != nil {
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes)).Decode(&req); err != nil {
|
||||||
@@ -100,7 +103,7 @@ func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := h.queue.Enqueue(queue.Item{
|
adm, err := h.queue.Enqueue(queue.Item{
|
||||||
Text: req.Text,
|
Text: req.Text,
|
||||||
Lang: req.Lang,
|
Lang: req.Lang,
|
||||||
Provider: req.Provider,
|
Provider: req.Provider,
|
||||||
@@ -112,11 +115,19 @@ func (h *handler) speak(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusServiceUnavailable, "enqueue: "+err.Error())
|
writeError(w, http.StatusServiceUnavailable, "enqueue: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusAccepted, SpeakResponse{ID: id})
|
if adm.Discarded != "" {
|
||||||
|
// Gesetz-Verwurf (Rate-Limit-Budget / Ruhezeit): dokumentiertes
|
||||||
|
// Verhalten wie bei Mute — 202 mit Grund statt Fehler, damit
|
||||||
|
// Aufrufer keine Sonderbehandlung brauchen.
|
||||||
|
writeJSON(w, http.StatusAccepted, SpeakResponse{Discarded: adm.Discarded, Toast: adm.Toast})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusAccepted, SpeakResponse{ID: adm.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// status behandelt GET /status: Queue-Länge (+ wartende IDs), aktuelles
|
// status behandelt GET /status: Queue-Länge (+ wartende IDs), aktuelles
|
||||||
// Item, Mute-State, aktiver Provider, Version.
|
// Item, Mute-State, aktiver Provider, Version — und seit Task 05 den
|
||||||
|
// Gesetz-State (Rate-Limit mit nächstem Slot, Ruhezeit-Fenster).
|
||||||
func (h *handler) status(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) status(w http.ResponseWriter, r *http.Request) {
|
||||||
pending := h.queue.Pending()
|
pending := h.queue.Pending()
|
||||||
ids := make([]string, len(pending))
|
ids := make([]string, len(pending))
|
||||||
@@ -140,9 +151,35 @@ func (h *handler) status(w http.ResponseWriter, r *http.Request) {
|
|||||||
if h.provider != nil {
|
if h.provider != nil {
|
||||||
st.Provider = h.provider()
|
st.Provider = h.provider()
|
||||||
}
|
}
|
||||||
|
st.RateLimit, st.QuietHours = lawState(h.queue.LawState())
|
||||||
writeJSON(w, http.StatusOK, st)
|
writeJSON(w, http.StatusOK, st)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lawState überführt die Gesetz-Sicht der Queue in die JSON-Form von
|
||||||
|
// /status: Zeitstempel nur, wenn sie in der Zukunft liegen (null = Slot
|
||||||
|
// frei / Ruhezeit inaktiv).
|
||||||
|
func lawState(l queue.LawState) (*RateLimitState, *QuietHoursState) {
|
||||||
|
now := time.Now()
|
||||||
|
rl := &RateLimitState{
|
||||||
|
Interval: l.RateLimit.String(),
|
||||||
|
AllowHighBurst: l.AllowHighBurst,
|
||||||
|
}
|
||||||
|
if l.NextSlot.After(now) {
|
||||||
|
slot := l.NextSlot.Format(time.RFC3339)
|
||||||
|
rl.NextSlot = &slot
|
||||||
|
}
|
||||||
|
qh := &QuietHoursState{
|
||||||
|
Window: l.Quiet.String(),
|
||||||
|
Active: l.QuietActive,
|
||||||
|
Mode: l.QuietMode,
|
||||||
|
}
|
||||||
|
if !l.QuietUntil.IsZero() {
|
||||||
|
until := l.QuietUntil.Format(time.RFC3339)
|
||||||
|
qh.Until = &until
|
||||||
|
}
|
||||||
|
return rl, qh
|
||||||
|
}
|
||||||
|
|
||||||
// cancel behandelt POST /cancel: laufende Ansage abbrechen + Queue leeren.
|
// cancel behandelt POST /cancel: laufende Ansage abbrechen + Queue leeren.
|
||||||
func (h *handler) cancel(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) cancel(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, http.StatusOK, CancelResponse{Cancelled: h.queue.Cancel()})
|
writeJSON(w, http.StatusOK, CancelResponse{Cancelled: h.queue.Cancel()})
|
||||||
|
|||||||
+45
-2
@@ -27,10 +27,20 @@ type SpeakRequest struct {
|
|||||||
|
|
||||||
// SpeakResponse ist die Antwort von POST /speak: 202 {"id":"…"} — bei
|
// SpeakResponse ist die Antwort von POST /speak: 202 {"id":"…"} — bei
|
||||||
// aktivem Mute 202 {"id":"","muted":true} (Ansage verworfen; dokumentiertes
|
// aktivem Mute 202 {"id":"","muted":true} (Ansage verworfen; dokumentiertes
|
||||||
// Verhalten, kein Fehler).
|
// Verhalten, kein Fehler). Seit Task 05 meldet verworfenen Spam ebenso
|
||||||
|
// dokumentiert: 202 {"id":"","discarded":"rate_limit"|"quiet_hours"} —
|
||||||
|
// plus "toast":true, wenn die Ruhezeit-Ansage durch eine Benachrichtigung
|
||||||
|
// ersetzt wurde (quiet_mode = "toast").
|
||||||
type SpeakResponse struct {
|
type SpeakResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Muted bool `json:"muted,omitempty"`
|
Muted bool `json:"muted,omitempty"`
|
||||||
|
|
||||||
|
// Discarded ist der Verwurfs-Grund ("rate_limit" | "quiet_hours");
|
||||||
|
// leer, wenn die Ansage aufgenommen wurde.
|
||||||
|
Discarded string `json:"discarded,omitempty"`
|
||||||
|
|
||||||
|
// Toast meldet die Ersetzung durch eine Benachrichtigung.
|
||||||
|
Toast bool `json:"toast,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentItem beschreibt das laufende Item in GET /status.
|
// CurrentItem beschreibt das laufende Item in GET /status.
|
||||||
@@ -44,7 +54,8 @@ type CurrentItem struct {
|
|||||||
// StatusResponse ist die Antwort von GET /status: Queue-Länge, aktuelles
|
// StatusResponse ist die Antwort von GET /status: Queue-Länge, aktuelles
|
||||||
// Item (null im Leerlauf), Mute-State, aktiver Provider, Daemon-Version.
|
// Item (null im Leerlauf), Mute-State, aktiver Provider, Daemon-Version.
|
||||||
// QueueIDs ist die Ergänzung der wartenden IDs in Abarbeitungs-Reihenfolge
|
// QueueIDs ist die Ergänzung der wartenden IDs in Abarbeitungs-Reihenfolge
|
||||||
// — die CLI braucht sie für ihr --wait-Polling.
|
// — die CLI braucht sie für ihr --wait-Polling. Task 05 ergänzt additiv
|
||||||
|
// den Gesetz-State (rate_limit, quiet_hours).
|
||||||
type StatusResponse struct {
|
type StatusResponse struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Queue int `json:"queue"`
|
Queue int `json:"queue"`
|
||||||
@@ -52,6 +63,38 @@ type StatusResponse struct {
|
|||||||
Current *CurrentItem `json:"current"`
|
Current *CurrentItem `json:"current"`
|
||||||
Muted bool `json:"muted"`
|
Muted bool `json:"muted"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
RateLimit *RateLimitState `json:"rate_limit,omitempty"`
|
||||||
|
QuietHours *QuietHoursState `json:"quiet_hours,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitState ist der Rate-Limit-State in GET /status (Task 05).
|
||||||
|
type RateLimitState struct {
|
||||||
|
// Interval ist der konfigurierte Mindestabstand als Dauer-String
|
||||||
|
// ("30s"; "0s" = unbegrenzt).
|
||||||
|
Interval string `json:"interval"`
|
||||||
|
|
||||||
|
// AllowHighBurst meldet, ob priority=high verstoßen darf.
|
||||||
|
AllowHighBurst bool `json:"allow_high_burst"`
|
||||||
|
|
||||||
|
// NextSlot ist der nächste freie Ansage-Slot als RFC3339-Zeitstempel;
|
||||||
|
// null, wenn sofort gespielt werden dürfte (oder unbegrenzt).
|
||||||
|
NextSlot *string `json:"next_slot"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuietHoursState ist der Ruhezeit-State in GET /status (Task 05).
|
||||||
|
type QuietHoursState struct {
|
||||||
|
// Window ist das konfigurierte Fenster ("18:00-08:00"; "" = deaktiviert).
|
||||||
|
Window string `json:"window"`
|
||||||
|
|
||||||
|
// Active meldet, ob die lokale Zeit gerade im Fenster liegt.
|
||||||
|
Active bool `json:"active"`
|
||||||
|
|
||||||
|
// Mode ist der konfigurierte Ruhezeit-Modus ("discard" | "toast").
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
|
||||||
|
// Until ist das Fenster-Ende als RFC3339-Zeitstempel, wenn aktiv;
|
||||||
|
// sonst null.
|
||||||
|
Until *string `json:"until"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PendingOrCurrent meldet, ob die Ansage mit der ID noch wartet oder
|
// PendingOrCurrent meldet, ob die Ansage mit der ID noch wartet oder
|
||||||
|
|||||||
Reference in new issue
Block a user