feat(queue): serialisierte Ansage-Queue mit Prioritäten, now-Sprung, Mute-Store und Rate-Limit-Struktur

- Worker-Loop: eine Ansage gleichzeitig; now > high > normal > low, FIFO je Level
- Cancel bricht die laufende Ansage ab (ctx) und leert die Queue (Zahl als Rückgabe)
- Close verwirft wartende Items, bricht die laufende Ansage ab und wartet auf den Worker
- MuteStore: persistenter Mute-State als Datei mute im Config-Dir (atomar geschrieben)
- RateLimiter: Struktur + Semantik für Task 05, bewusst noch nicht angebunden
  (Task-04-Default unbegrenzt, dokumentierte Seam in rate.go)
This commit is contained in:
m3ta-chiron committed 2026-10-18 17:10:26 +02:00
1 parent 716f6b2fc9
commit e492baddac
5 files changed
+742 -3

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
package queue
import (
"errors"
"os"
"path/filepath"
"strings"
)
// MuteFileName ist der Name der Mute-Datei im Config-Verzeichnis (bindender
// Vertrag: „persistenten Mute-State toggeln (Datei mute im Config-Dir)").
const MuteFileName = "mute"
// ErrNoMutePath meldet einen Set-Versuch ohne konfigurierten Datei-Pfad
// (Config-Dir nicht bestimmbar — der Daemon deaktiviert dann Mute mit einer
// Start-Warnung statt zu scheitern).
var ErrNoMutePath = errors.New("mute: kein Datei-Pfad konfiguriert (Config-Dir nicht bestimmbar)")
// MuteStore verwaltet den persistenten Mute-State als Datei im
// Config-Verzeichnis: Inhalt „true" = stumm, alles andere (oder keine Datei)
// = nicht stumm.
//
// Der Daemon liest die Datei bei jeder /speak-Anfrage neu — dadurch wirkt
// auch ein im Degraded Mode (Daemon tot) von der CLI direkt geschriebener
// State sofort, und der Zustand überlebt Daemon-Restarts.
type MuteStore struct {
// Path ist der vollständige Pfad der Mute-Datei. Leer (kein Config-Dir
// bestimmbar): Muted() ist immer false, Set ein Fehler.
Path string
}
// Muted meldet den persistenten Mute-State. Eine fehlende oder unlesbare
// Datei heißt: nicht stumm (Fehlstart ohne Config-Dir wird nie „stumm aus
// Versehen").
func (s MuteStore) Muted() bool {
if s.Path == "" {
return false
}
b, err := os.ReadFile(s.Path)
if err != nil {
return false
}
return strings.TrimSpace(string(b)) == "true"
}
// Set schreibt den Mute-State atomar (temporäre Datei + Rename, damit
// Daemon und CLI nie eine halb geschriebene Datei lesen) und legt das
// Config-Verzeichnis bei Bedarf an.
func (s MuteStore) Set(muted bool) error {
if s.Path == "" {
return ErrNoMutePath
}
dir := filepath.Dir(s.Path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
content := "false\n"
if muted {
content = "true\n"
}
tmp, err := os.CreateTemp(dir, ".mute-*")
if err != nil {
return err
}
name := tmp.Name()
if _, err := tmp.WriteString(content); err != nil {
tmp.Close()
os.Remove(name)
return err
}
if err := tmp.Close(); err != nil {
os.Remove(name)
return err
}
if err := os.Chmod(name, 0o644); err != nil {
os.Remove(name)
return err
}
if err := os.Rename(name, s.Path); err != nil {
os.Remove(name)
return err
}
return nil
}