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:
2026-10-11 22:38:49 +11:00
parent c4213ebed1
commit a349b42b1f
24 changed files with 2454 additions and 1 deletions

View File

@@ -17,13 +17,16 @@ import (
"net/http"
"time"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/apprise"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/classify"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/config"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/hub"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/mailfetch"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/mailops"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/reply"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/session"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/store"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/summary"
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/version"
"gitea.lab.audasmedia.com.au/sam/email_agent/web"
)
@@ -46,6 +49,12 @@ type Server struct {
actions MailActions
replies ReplyGenerator
sender MailSender
// Phase 7 (FR-10/FR-12): the read-only Hub answerer, the outbound
// notifier, the daily-summary builder, and the Apprise delivery client.
answerer *hub.Answerer
notifier *hub.Notifier
summaries *summary.Builder
apprise *apprise.Client
}
// New builds a Server. It does not start listening.
@@ -82,6 +91,10 @@ func New(cfg *config.Config, st *store.Store, logger *slog.Logger) (*Server, err
reply.NewClient(cfg.OmniRouteBaseURL, cfg.OmniRouteAPIKey, cfg.OmniRouteModel, logger),
logger,
),
answerer: hub.NewAnswerer(st),
notifier: hub.NewNotifier(cfg.HubBaseURL, cfg.HubToken, cfg.HubNotifyTimeout, nil),
summaries: summary.New(st),
apprise: apprise.New(cfg.AppriseURL, cfg.AppriseKey, cfg.AppriseURLs, cfg.AppriseToken, 0),
}
s.http = &http.Server{
Addr: cfg.Addr,
@@ -161,6 +174,16 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("POST /unsubscribe/unignore", s.requireAuth(s.handleUnsubscribeUnignore))
mux.HandleFunc("POST /unsubscribe/mark", s.requireAuth(s.handleUnsubscribeMark))
// Phase 7 (FR-12): the Hub contract. /ask is read-only and token-protected.
mux.HandleFunc("GET /capabilities", s.requireHub(s.handleCapabilities))
mux.HandleFunc("POST /ask", s.requireHub(s.handleAsk))
// Phase 7 (FR-10): daily summary and the Reports screen.
mux.HandleFunc("GET /reports", s.requireAuth(s.handleReports))
mux.HandleFunc("POST /reports/generate", s.requireAuth(s.handleSummaryGenerate))
mux.HandleFunc("POST /reports/send", s.requireAuth(s.handleSummarySend))
mux.HandleFunc("POST /reports/delete", s.requireAuth(s.handleSummaryDelete))
return s.recoverPanic(s.logRequests(mux))
}