Files
synapbus/internal/api/router.go
T
Algis DumbrisandClaude Opus 4.6 3b97429f20
Release / Build darwin/amd64 (push) Canceled after 0s
Release / Build linux/amd64 (push) Canceled after 0s
Release / Build darwin/arm64 (push) Canceled after 0s
Release / Build linux/arm64 (push) Canceled after 0s
Release / Generate Homebrew Formula (push) Canceled after 0s
Release / GitHub Release (push) Canceled after 0s
Release / Docker Image (push) Canceled after 0s
Release / Publish to MCP Registry (push) Canceled after 0s
fix: DM sidebar shows conversation partners, not owned agents
- New API: GET /api/dm/partners — returns DM conversation partners
  ordered by most recent message, with unread counts
- Sidebar DM section now shows actual conversation partners (agents
  you've exchanged messages with) instead of owned agents
- Each partner shows name, unread badge, clickable to /dm/{name}
- Fixes issue where all DMs were shown mixed in one view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:19:22 +02:00

314 lines
11 KiB
Go

package api
import (
"database/sql"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/synapbus/synapbus/internal/agents"
"github.com/synapbus/synapbus/internal/apikeys"
"github.com/synapbus/synapbus/internal/attachments"
"github.com/synapbus/synapbus/internal/channels"
"github.com/synapbus/synapbus/internal/k8s"
"github.com/synapbus/synapbus/internal/messaging"
"github.com/synapbus/synapbus/internal/reactor"
"github.com/synapbus/synapbus/internal/push"
"github.com/synapbus/synapbus/internal/reactions"
"github.com/synapbus/synapbus/internal/trace"
"github.com/synapbus/synapbus/internal/trust"
"github.com/synapbus/synapbus/internal/webhooks"
)
// RouterConfig holds optional services for the API router.
// Fields may be nil if the corresponding feature is not configured.
type RouterConfig struct {
TraceStore trace.TraceStore
Metrics *trace.Metrics
AttachmentService *attachments.Service
MsgService *messaging.MessagingService
AgentService *agents.AgentService
ChannelService *channels.Service
APIKeyService *apikeys.Service
DeadLetterStore *messaging.DeadLetterStore
WebhookService *webhooks.WebhookService
WebhookStore webhooks.WebhookStore
K8sService *k8s.K8sService
K8sStore k8s.K8sStore
ReactionService *reactions.Service
PushService *push.Service
TrustService *trust.Service
ReactorStore *reactor.Store
ReactorEngine *reactor.Reactor
SSEHub *SSEHub
Broadcaster *SSEBroadcaster
SessionMiddleware func(http.Handler) http.Handler
DB *sql.DB
Version string
BaseURL string
}
// NewRouter creates a chi router with all API routes configured.
// metricsInstance may be nil if metrics are disabled.
// attachmentService may be nil if attachments are not configured.
func NewRouter(traceStore trace.TraceStore, metricsInstance *trace.Metrics, attachmentService *attachments.Service) chi.Router {
return NewRouterWithConfig(RouterConfig{
TraceStore: traceStore,
Metrics: metricsInstance,
AttachmentService: attachmentService,
})
}
// NewRouterWithConfig creates a chi router using the full configuration.
func NewRouterWithConfig(cfg RouterConfig) chi.Router {
r := chi.NewRouter()
// Global middleware
r.Use(RequestIDMiddleware)
r.Use(LoggingMiddleware)
tracesHandler := NewTracesHandler(cfg.TraceStore)
// Determine which auth middleware to use for API routes
authMiddleware := OwnerAuthMiddleware
if cfg.SessionMiddleware != nil {
authMiddleware = cfg.SessionMiddleware
}
// Authenticated API routes (traces)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/traces", tracesHandler.ListTraces)
r.Get("/api/traces/export", tracesHandler.ExportTraces)
r.Get("/api/traces/stats", tracesHandler.TraceStats)
})
// Attachment API routes (for Web UI)
if cfg.AttachmentService != nil {
attachmentsHandler := NewAttachmentsHandler(cfg.AttachmentService)
r.Get("/api/attachments/{hash}", attachmentsHandler.Download)
r.Get("/api/attachments/{hash}/meta", attachmentsHandler.Metadata)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Post("/api/attachments", attachmentsHandler.Upload)
})
}
// Web UI API routes (messages, agents, channels, SSE)
if cfg.MsgService != nil && cfg.AgentService != nil {
messagesHandler := NewMessagesHandler(cfg.MsgService, cfg.AgentService)
agentsHandler := NewAgentsHandler(cfg.AgentService, cfg.TraceStore, cfg.ChannelService)
notificationsHandler := NewNotificationsHandler(cfg.MsgService, cfg.AgentService, cfg.ChannelService)
// Wire up SSE broadcaster for real-time events
if cfg.Broadcaster != nil {
messagesHandler.SetBroadcaster(cfg.Broadcaster)
} else if cfg.SSEHub != nil {
messagesHandler.SetBroadcaster(NewSSEBroadcaster(cfg.SSEHub, cfg.AgentService, cfg.ChannelService))
}
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
// Messages
r.Get("/api/messages", messagesHandler.ListMessages)
r.Get("/api/messages/search", messagesHandler.SearchMessages)
r.Get("/api/messages/{id}", messagesHandler.GetMessage)
r.Get("/api/messages/{id}/replies", messagesHandler.GetReplies)
r.Post("/api/messages", messagesHandler.SendMessage)
r.Post("/api/messages/{id}/done", messagesHandler.MarkDone)
// Conversations
r.Get("/api/conversations", messagesHandler.ListConversations)
r.Get("/api/conversations/{id}", messagesHandler.GetConversation)
// Agents
r.Get("/api/agents", agentsHandler.ListAgents)
r.Get("/api/agents/{name}", agentsHandler.GetAgent)
r.Post("/api/agents", agentsHandler.RegisterAgent)
r.Put("/api/agents/{name}", agentsHandler.UpdateAgent)
r.Delete("/api/agents/{name}", agentsHandler.DeleteAgent)
r.Post("/api/agents/{name}/revoke-key", agentsHandler.RevokeKey)
r.Get("/api/agents/{name}/messages", messagesHandler.DMMessages)
r.Get("/api/dm/partners", messagesHandler.DMPartners)
// Notifications
r.Get("/api/notifications/unread", notificationsHandler.UnreadCounts)
r.Post("/api/notifications/mark-read", notificationsHandler.MarkRead)
})
// API Keys
if cfg.APIKeyService != nil {
apiKeysHandler := NewAPIKeysHandler(cfg.APIKeyService)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/keys", apiKeysHandler.ListKeys)
r.Post("/api/keys", apiKeysHandler.CreateKey)
r.Get("/api/keys/{id}", apiKeysHandler.GetKey)
r.Delete("/api/keys/{id}", apiKeysHandler.RevokeKey)
})
}
// Reactions
if cfg.ReactionService != nil {
reactionsHandler := NewReactionsHandler(cfg.ReactionService, cfg.MsgService, cfg.AgentService)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Post("/api/messages/{id}/reactions", reactionsHandler.Toggle)
r.Get("/api/messages/{id}/reactions", reactionsHandler.GetReactions)
r.Delete("/api/messages/{id}/reactions/{reaction}", reactionsHandler.Remove)
})
}
// Channels
if cfg.ChannelService != nil {
channelsHandler := NewChannelsHandler(cfg.ChannelService, cfg.AgentService, cfg.MsgService)
if cfg.ReactionService != nil {
channelsHandler.SetReactionService(cfg.ReactionService)
}
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/channels", channelsHandler.ListChannels)
r.Get("/api/channels/{name}", channelsHandler.GetChannel)
r.Post("/api/channels", channelsHandler.CreateChannel)
r.Get("/api/channels/{name}/messages", channelsHandler.ChannelMessages)
r.Get("/api/channels/{name}/messages/by-state", channelsHandler.ListByState)
r.Put("/api/channels/{name}/settings", channelsHandler.UpdateSettings)
r.Post("/api/channels/{name}/join", channelsHandler.JoinChannel)
r.Post("/api/channels/{name}/leave", channelsHandler.LeaveChannel)
})
}
// SSE events
if cfg.SSEHub != nil {
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/events", cfg.SSEHub.HandleEvents)
})
}
// Dead Letters
if cfg.DeadLetterStore != nil {
deadLettersHandler := NewDeadLettersHandler(cfg.DeadLetterStore)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/dead-letters", deadLettersHandler.List)
r.Get("/api/dead-letters/count", deadLettersHandler.Count)
r.Post("/api/dead-letters/{id}/acknowledge", deadLettersHandler.Acknowledge)
})
}
// Webhooks
if cfg.WebhookService != nil && cfg.WebhookStore != nil {
webhooksHandler := NewWebhooksHandler(cfg.WebhookService, cfg.WebhookStore, cfg.AgentService)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/webhooks", webhooksHandler.ListWebhooks)
r.Get("/api/webhooks/{id}/deliveries", webhooksHandler.WebhookDeliveries)
r.Get("/api/deliveries/dead-letters", webhooksHandler.DeadLetters)
r.Post("/api/deliveries/{id}/retry", webhooksHandler.RetryDelivery)
})
}
// K8s Handlers
if cfg.K8sService != nil && cfg.K8sStore != nil {
k8sHandler := NewK8sHandler(cfg.K8sService, cfg.K8sStore, cfg.AgentService)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/k8s/handlers", k8sHandler.ListHandlers)
r.Get("/api/k8s/job-runs", k8sHandler.ListJobRuns)
r.Get("/api/k8s/job-runs/{id}/logs", k8sHandler.JobRunLogs)
})
}
// Push Notifications
if cfg.PushService != nil {
pushHandler := NewPushHandler(cfg.PushService)
// VAPID key endpoint is unauthenticated (needed before subscription)
r.Get("/api/push/vapid-key", pushHandler.VAPIDKey)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Post("/api/push/subscribe", pushHandler.Subscribe)
r.Delete("/api/push/subscribe", pushHandler.Unsubscribe)
})
}
}
// Reactive Runs
if cfg.ReactorStore != nil && cfg.ReactorEngine != nil && cfg.AgentService != nil {
runsHandler := NewRunsHandler(cfg.ReactorStore, cfg.ReactorEngine, agents.NewSQLiteAgentStore(cfg.DB))
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/runs", runsHandler.ListRuns)
r.Get("/api/runs/{id}", runsHandler.GetRun)
r.Post("/api/runs/{id}/retry", runsHandler.RetryRun)
r.Get("/api/agents/reactive", runsHandler.ReactiveAgents)
})
}
// Trust Scores
if cfg.TrustService != nil {
trustHandler := NewTrustHandler(cfg.TrustService)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/trust/{name}", trustHandler.GetScores)
})
}
// Onboarding (CLAUDE.md generator, MCP config, archetypes, skills)
if cfg.AgentService != nil {
onboardingHandler := NewOnboardingHandler(cfg.AgentService, cfg.ChannelService, cfg.BaseURL)
// Unauthenticated: archetypes list, skills list, skill content
r.Get("/api/archetypes", onboardingHandler.ListArchetypes)
r.Get("/api/skills", onboardingHandler.ListSkills)
r.Get("/api/skills/{name}", onboardingHandler.GetSkill)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/agents/{name}/claude-md", onboardingHandler.GetCLAUDEMD)
r.Get("/api/agents/{name}/mcp-config", onboardingHandler.GetMCPConfig)
})
}
// Analytics (authenticated, requires DB)
if cfg.DB != nil {
analyticsHandler := NewAnalyticsHandler(cfg.DB, cfg.AgentService, cfg.ChannelService)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/api/analytics/timeline", analyticsHandler.Timeline)
r.Get("/api/analytics/top-agents", analyticsHandler.TopAgents)
r.Get("/api/analytics/top-channels", analyticsHandler.TopChannels)
r.Get("/api/analytics/summary", analyticsHandler.Summary)
})
}
// Version (unauthenticated)
if cfg.Version != "" {
versionHandler := NewVersionHandler(cfg.Version)
r.Get("/api/version", versionHandler.GetVersion)
}
// Metrics endpoint (unauthenticated, only registered when enabled)
if cfg.Metrics != nil {
r.Get("/metrics", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
cfg.Metrics.WritePrometheus(w)
})
}
return r
}