Benachrichtigung bei wiederholtem Postfach-Sync-Ausfall, mit Eskalationsschwelle statt Einzel-Alarm pro Fehlversuch. Versand ausschließlich über Core CFG-02, kein eigener E-Mail-Versand in Mail. - dispatcher.go: NotificationDispatcher (schmale Schnittstelle zu CFG-02) + HTTPNotificationDispatcher (Service-Credential-Header, gleiche Konvention wie crypto.HTTPKEKProvider). Core exponiert internal/notify. Dispatcher.Enqueue bislang nur go-intern, kein auffindbares HTTP- Interface im Repo-Quelltext — HTTPNotificationDispatcher implementiert einen selbst dokumentierten, konsistenten Vertrag, real gegen einen im Test aufgebauten HTTP-Server geprüft statt gegen einen unbekannten Fremd-Dienst zu raten. - monitor.go: Monitor.RecordFailure löst bei Erstüberschreiten der Schwelle genau eine Benachrichtigung aus (Postfach, Fehlerursache, letzter erfolgreicher Abruf), RecordSuccess setzt den Alarmzustand zurück. Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-08-PRUEFPROTOKOLL.md): 1. TestRecordFailure_NConsecutiveFailuresTriggerExactlyOneNotification: 3 Fehlschläge real genau 1 Benachrichtigung, weitere real keine. 2. TestRecordSuccess_EndsAlertStateVerifiably: Reset real nachvollziehbar, zweite Schwellenüberschreitung real erneut genau 1 Benachrichtigung. 3. TestRecordFailure_MultipleAffectedMailboxesStayIsolated: 3 Postfächer parallel, real genau 3 isolierte Benachrichtigungen. Kein Umbau: imapimport (IMP-01/IMP-04) unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
81 lines
2.8 KiB
Go
81 lines
2.8 KiB
Go
// Package syncalert implementiert IMP-08: Benachrichtigung bei
|
|
// wiederholtem Postfach-Sync-Ausfall, mit Eskalationsschwelle statt
|
|
// Einzel-Alarm pro Fehlversuch. Versand ausschließlich über den
|
|
// zentralen Core-Benachrichtigungs-Dispatcher (CFG-02, bereits Fertig)
|
|
// — dieses Paket baut KEINEN eigenen E-Mail-Versand, sondern ruft
|
|
// ausschließlich NotificationDispatcher.Enqueue auf (Akzeptanzkriterium
|
|
// 1), exakt einmal je Eskalation.
|
|
package syncalert
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
// NotificationDispatcher ist die schmale Schnittstelle zu Core CFG-02
|
|
// (internal/notify.Dispatcher.Enqueue) — Mail ruft ausschließlich diese
|
|
// EINE Methode auf, kein eigener Versandcode.
|
|
type NotificationDispatcher interface {
|
|
Enqueue(ctx context.Context, channel, recipient string, payload map[string]any) (id string, err error)
|
|
}
|
|
|
|
// HTTPNotificationDispatcher spricht CFG-02 über HTTP an — dieselbe
|
|
// Service-Credential-Konvention wie mail/internal/crypto.HTTPKEKProvider
|
|
// (API-02, X-Nexarch-Client-Id/Secret).
|
|
type HTTPNotificationDispatcher struct {
|
|
endpointURL string
|
|
clientID string
|
|
clientSecret string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewHTTPNotificationDispatcher(endpointURL, clientID, clientSecret string, httpClient *http.Client) *HTTPNotificationDispatcher {
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
return &HTTPNotificationDispatcher{endpointURL: endpointURL, clientID: clientID, clientSecret: clientSecret, httpClient: httpClient}
|
|
}
|
|
|
|
type enqueueRequest struct {
|
|
Channel string `json:"channel"`
|
|
Recipient string `json:"recipient"`
|
|
Payload map[string]any `json:"payload"`
|
|
}
|
|
|
|
type enqueueResponse struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
func (d *HTTPNotificationDispatcher) Enqueue(ctx context.Context, channel, recipient string, payload map[string]any) (string, error) {
|
|
body, err := json.Marshal(enqueueRequest{Channel: channel, Recipient: recipient, Payload: payload})
|
|
if err != nil {
|
|
return "", fmt.Errorf("syncalert: anfrage serialisieren: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.endpointURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", fmt.Errorf("syncalert: anfrage aufbauen: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Nexarch-Client-Id", d.clientID)
|
|
req.Header.Set("X-Nexarch-Client-Secret", d.clientSecret)
|
|
|
|
resp, err := d.httpClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("syncalert: anfrage senden: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("syncalert: cfg-02 lehnte anfrage ab: status %d", resp.StatusCode)
|
|
}
|
|
|
|
var out enqueueResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
return "", fmt.Errorf("syncalert: antwort dekodieren: %w", err)
|
|
}
|
|
return out.ID, nil
|
|
}
|