Phase 7: daily summary and read-only Hub API (AUD-73)
- internal/hub: project side of the frozen ask-contract. GET /capabilities, read-only POST /ask answerer (no LLM, <3s), and the outbound POST /notify client. - internal/apprise: daily-summary delivery through the house Apprise API. - internal/summary: deterministic plain-text daily summary builder. - internal/store: CountByCategory, MessagesByCategory, and summary CRUD. - internal/server: /capabilities and /ask (bearer-token protected), plus the Reports screen (/reports, build, send, delete). - main.go: -summary, -summary-send, and -notify CLI entry points for n8n. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
104
internal/hub/notify.go
Normal file
104
internal/hub/notify.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Outbound notify path (ask-contract.md, "Notify (project to Hub)").
|
||||
//
|
||||
// A project may tell the Hub something so the Hub can speak or notify. This is
|
||||
// the one call this service makes into the Hub. It is best-effort: the Hub's
|
||||
// /notify receiver is not part of its frozen v1 routes (docs/hub-api.md §12),
|
||||
// so a failure is logged and never blocks the local work.
|
||||
package hub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notifier posts project notifications to the Hub. The nil zero value is not
|
||||
// usable; build one with NewNotifier.
|
||||
type Notifier struct {
|
||||
BaseURL string
|
||||
Token string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
// NewNotifier builds a notifier for the Hub at baseURL. timeout defaults to
|
||||
// five seconds when it is zero or negative.
|
||||
func NewNotifier(baseURL, token string, timeout time.Duration, httpClient *http.Client) *Notifier {
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{Timeout: timeout}
|
||||
}
|
||||
return &Notifier{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
Token: token,
|
||||
HTTP: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
// Configured reports whether the outbound notify path can be attempted.
|
||||
func (n *Notifier) Configured() bool {
|
||||
return n != nil && strings.TrimSpace(n.BaseURL) != ""
|
||||
}
|
||||
|
||||
// Notify posts one notification. A nil notifier is a no-op so callers do not
|
||||
// need to branch on configuration.
|
||||
func (n *Notifier) Notify(ctx context.Context, req NotifyRequest) error {
|
||||
if !n.Configured() {
|
||||
return fmt.Errorf("hub notify is not configured")
|
||||
}
|
||||
if strings.TrimSpace(req.From) == "" {
|
||||
req.From = ProjectName
|
||||
}
|
||||
body, err := jsonBody(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode notify: %w", err)
|
||||
}
|
||||
url := n.BaseURL + "/notify"
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build notify request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
if n.Token != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+n.Token)
|
||||
}
|
||||
|
||||
resp, err := n.HTTP.Do(httpReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("post notify: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("post notify: status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifySummaryReady is the convenience call used after a daily summary is
|
||||
// built.
|
||||
func (n *Notifier) NotifySummaryReady(ctx context.Context, message string) error {
|
||||
return n.Notify(ctx, NotifyRequest{
|
||||
From: ProjectName,
|
||||
Level: "info",
|
||||
Message: message,
|
||||
Person: "sam",
|
||||
})
|
||||
}
|
||||
|
||||
// NotifyImportantCount tells the Hub how many messages need attention. It is
|
||||
// used by the daily summary job when the count is non-zero.
|
||||
func (n *Notifier) NotifyImportantCount(ctx context.Context, n2 int) error {
|
||||
if n2 <= 0 {
|
||||
return nil
|
||||
}
|
||||
return n.Notify(ctx, NotifyRequest{
|
||||
From: ProjectName,
|
||||
Level: "important",
|
||||
Message: fmt.Sprintf("%d emails need a decision.", n2),
|
||||
Person: "sam",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user