services

package
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 24, 2026 License: AGPL-3.0 Imports: 31 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrSlackNotConfigured = errors.New("slack not configured")

ErrSlackNotConfigured is returned by LazySlackService when no valid Slack config exists in the database. Callers that treat "not configured" as a no-op should check for this error with errors.Is.

Functions

func AcknowledgeAlertWithTimeline

func AcknowledgeAlertWithTimeline(
	alertID uuid.UUID,
	userName string,
	via models.AcknowledgmentVia,
	engine EscalationEngine,
	incidentRepo repository.IncidentRepository,
	timelineRepo repository.TimelineRepository,
) error

AcknowledgeAlertWithTimeline acknowledges an alert via the escalation engine and, if the alert is linked to an incident, appends a timeline entry recording who acknowledged it, when, and via which channel.

This is the canonical acknowledgment call used by the REST handler and Slack button callback — both paths converge here so timeline entries are always written.

func FetchTelegramChatID

func FetchTelegramChatID(ctx context.Context, botToken string) (string, string, error)

FetchTelegramChatID calls getUpdates to find the most recent group chat ID the bot has seen. Returns chat_id and chat title of the most recent group/supergroup.

func SetAIService

func SetAIService(svc IncidentService, ai AIService)

SetAIService wires the optional AI service into the incident service. Called by routes.go after construction to avoid changing the constructor signature.

func SetCommanderDeps

func SetCommanderDeps(svc IncidentService, userRepo repository.UserRepository, scheduleRepo repository.ScheduleRepository, evaluator ScheduleEvaluator)

SetCommanderDeps wires the optional user/schedule dependencies for commander auto-assignment. Called by routes.go after construction. Safe to skip in tests that don't test this feature.

func SetTeamsService

func SetTeamsService(svc IncidentService, teams *TeamsService)

SetTeamsService wires the optional Teams service into the incident service (v0.8+). Called by routes.go after construction when TEAMS_APP_ID is configured.

func SetTelegramService

func SetTelegramService(svc IncidentService, tg *TelegramService)

SetTelegramService wires the optional Telegram service into the incident service. Called by routes.go after construction when Telegram is configured in DB.

func SupportedCountryCodes

func SupportedCountryCodes() []string

SupportedCountryCodes returns the list of country codes supported for holiday sync.

func TestTeamsCredentials

func TestTeamsCredentials(ctx context.Context, appID, appPassword, tenantID, teamID string) (string, error)

TestTeamsCredentials validates Teams credentials by fetching team info from the Graph API. Returns the team display name on success. Intended for use by the setup wizard test endpoint.

func TestTelegramConnection

func TestTelegramConnection(ctx context.Context, botToken, chatID string) (string, error)

TestTelegramConnection verifies the bot token by calling getMe and sends a test message.

Types

type AIService

type AIService interface {
	// IsEnabled returns true if the AI service is properly configured.
	IsEnabled() bool

	// Reload reinitialises the LLM clients with the given provider and credentials.
	// Passing an empty apiKey (for cloud providers) or empty ollamaBaseURL disables AI features.
	Reload(provider, apiKey, model, ollamaBaseURL string)

	// GenerateSummary generates a concise incident summary using all available context.
	// slackMessages should be the plain-text messages from the incident Slack thread.
	// Pass an empty slice when Slack is not configured or the channel has no messages.
	GenerateSummary(ctx context.Context, incident *models.Incident, timeline []models.TimelineEntry, alerts []models.Alert, slackMessages []string) (string, error)

	// GenerateHandoffDigest generates a structured shift handoff document.
	// Suitable for posting to Slack or displaying in the UI at shift change.
	GenerateHandoffDigest(ctx context.Context, incident *models.Incident, timeline []models.TimelineEntry, alerts []models.Alert) (string, error)

	// GeneratePostMortem generates a full post-mortem document in Markdown.
	// sections is the ordered list of section names from the chosen template.
	// Uses a higher token budget than summary generation.
	GeneratePostMortem(ctx context.Context, incident *models.Incident, timeline []models.TimelineEntry, alerts []models.Alert, sections []string) (string, error)

	// EnhancePostMortem improves an existing post-mortem draft for clarity,
	// structure, and completeness while preserving all factual details.
	EnhancePostMortem(ctx context.Context, content string) (string, error)

	// EnhanceIncidentDraft converts a rough user brief into a polished incident
	// title and summary. Returns structured title + summary strings.
	EnhanceIncidentDraft(ctx context.Context, brief string) (title, summary string, err error)

	// AnswerQuestion answers a natural-language question asked in a Slack channel.
	// postMortems maps "INC-NNN" to the full post-mortem markdown for that incident.
	// Returns a Slack-formatted reply (mrkdwn).
	AnswerQuestion(ctx context.Context, question string, current *models.Incident, similar []models.Incident, postMortems map[string]string) (string, error)
}

AIService provides AI-powered features for incidents. When no provider is configured, the service is disabled; callers must check IsEnabled().

func NewAIService

func NewAIService(provider, apiKey, model string, maxTokens, pmMaxTokens int, ollamaBaseURL string) AIService

NewAIService creates a reloadable AIService backed by the given provider. If the provider cannot be initialised (e.g. empty key, missing Ollama URL) the service starts disabled; call Reload after credentials are saved in Settings.

type AlertLookupFunc

type AlertLookupFunc func(id uuid.UUID) (*models.Alert, error)

AlertLookupFunc retrieves an alert by ID; injected for testing.

type AlertService

type AlertService interface {
	// ProcessAlertmanagerPayload processes Prometheus Alertmanager webhooks (legacy method for v0.1 compatibility)
	ProcessAlertmanagerPayload(payload *webhooks.AlertmanagerPayload) (*ProcessingResult, error)

	// ProcessNormalizedAlerts processes alerts from any source after normalization (v0.3+)
	ProcessNormalizedAlerts(alerts []webhooks.NormalizedAlert) (*ProcessingResult, error)

	// SetGroupingEngine sets the grouping engine for alert deduplication and grouping (v0.3+)
	SetGroupingEngine(engine GroupingEngine)

	// SetRoutingEngine sets the routing engine for alert routing decisions (v0.3+)
	SetRoutingEngine(engine RoutingEngine)

	// SetEscalationEngine sets the escalation engine for alert escalation (v0.5+)
	SetEscalationEngine(engine EscalationEngine)

	// SetEscalationRepos provides repos for the severity-rule and global-fallback
	// steps in the escalation policy resolution chain.
	SetEscalationRepos(escalationRepo repository.EscalationPolicyRepository, systemSettingsRepo repository.SystemSettingsRepository)
}

AlertService defines the interface for alert processing operations

func NewAlertService

func NewAlertService(alertRepo repository.AlertRepository, incidentSvc IncidentService) AlertService

NewAlertService creates a new alert service

type AuthService

type AuthService interface {
	UpsertFromSAML(ctx context.Context, subject, issuer, email, name string) error
}

AuthService handles user provisioning from SAML assertions.

func NewAuthService

func NewAuthService(userRepo repository.UserRepository, ossUserLimit int) AuthService

type BotAccount

type BotAccount struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type BotActivity

type BotActivity struct {
	Type         string          `json:"type"`
	ID           string          `json:"id"`
	Text         string          `json:"text"`
	ChannelID    string          `json:"channelId"` // "msteams"
	Conversation BotConversation `json:"conversation"`
	From         BotAccount      `json:"from"`
	Recipient    BotAccount      `json:"recipient"`
	ServiceURL   string          `json:"serviceUrl"`
	ChannelData  json.RawMessage `json:"channelData"`
}

BotActivity is a Bot Framework Activity JSON object (simplified subset we use).

type BotConversation

type BotConversation struct {
	ID      string `json:"id"`
	IsGroup bool   `json:"isGroup"`
}

type BuiltInTemplateError

type BuiltInTemplateError struct{ Name string }

BuiltInTemplateError is returned when trying to modify or delete a built-in template.

func (*BuiltInTemplateError) Error

func (e *BuiltInTemplateError) Error() string

type Channel

type Channel struct {
	// ID is the platform-specific channel identifier
	// Examples: "C01234567" (Slack), "19:xxx@thread.tacv2" (Teams)
	ID string

	// Name is the human-readable channel name
	// Examples: "inc-042-api-gateway-errors"
	Name string

	// URL is the direct link to the channel in the platform's web/app UI
	URL string
}

Channel represents a chat channel with platform-specific details.

type ChatService

type ChatService interface {
	// CreateChannel creates a new channel with the given name and description.
	// The channel should be public by default.
	//
	// Parameters:
	//   - name: The channel name (will be sanitized by implementation)
	//   - description: Optional channel description/topic
	//
	// Returns:
	//   - Channel with ID, Name, and URL populated
	//   - Error if creation fails (auth, network, name collision, etc.)
	//
	// Thread-safe: Yes
	CreateChannel(name, description string) (*Channel, error)

	// PostMessage posts a message to the specified channel.
	//
	// Parameters:
	//   - channelID: The platform-specific channel identifier
	//   - message: The message to post (text + optional blocks)
	//
	// Returns:
	//   - Message timestamp/ID for future updates
	//   - Error if posting fails (invalid channel, permissions, rate limits, network, etc.)
	//
	// Thread-safe: Yes
	PostMessage(channelID string, message Message) (string, error)

	// UpdateMessage updates an existing message in a channel.
	//
	// Parameters:
	//   - channelID: The platform-specific channel identifier
	//   - messageTS: The message timestamp/ID returned from PostMessage
	//   - message: The updated message content
	//
	// Returns:
	//   - Error if update fails (message not found, invalid channel, permissions, rate limits, network, etc.)
	//
	// Thread-safe: Yes
	UpdateMessage(channelID, messageTS string, message Message) error

	// ArchiveChannel archives the specified channel.
	// Archived channels are read-only and hidden from active channel lists.
	//
	// Parameters:
	//   - channelID: The platform-specific channel identifier
	//
	// Returns:
	//   - Error if archiving fails (channel not found, permissions, already archived, network, etc.)
	//
	// Thread-safe: Yes
	ArchiveChannel(channelID string) error

	// InviteUsers invites users to a channel.
	// Used for auto-inviting specific users to incident channels.
	//
	// Parameters:
	//   - channelID: Platform-specific channel identifier
	//   - userIDs: List of platform-specific user identifiers
	//
	// Returns:
	//   - Error if invitation fails (permissions, invalid IDs, rate limits, network)
	//
	// Implementation notes:
	//   - Empty userIDs list should return nil (no-op)
	//   - Handle rate limits with exponential backoff
	//   - Partial failures should return descriptive error
	//
	// Thread-safe: Yes
	InviteUsers(channelID string, userIDs []string) error

	// SendDirectMessage sends a direct message to a user by their display name or email.
	// The implementation is responsible for resolving the username to a platform user ID
	// and opening a DM conversation.
	//
	// Parameters:
	//   - username: Display name or email of the recipient (as stored in ScheduleParticipant.UserName)
	//   - message: The message to send
	//
	// Returns:
	//   - Error if lookup, DM open, or send fails
	//   - Graceful no-op (returns nil) if Slack is not configured
	//
	// Thread-safe: Yes
	SendDirectMessage(username string, message Message) error

	// GetThreadMessages fetches the plain-text messages from a channel thread.
	// Used to provide Slack conversation context to the AI summarizer.
	//
	// Parameters:
	//   - channelID: The platform-specific channel identifier
	//   - threadTS: The parent message timestamp identifying the thread
	//
	// Returns:
	//   - Slice of plain-text message strings (author + text, one per message)
	//   - Empty slice (not error) if the thread is empty or has no text messages
	//   - Error only for auth/network failures
	//
	// Thread-safe: Yes
	GetThreadMessages(channelID, threadTS string) ([]string, error)
}

ChatService defines the interface for chat platform operations. This abstraction enables support for multiple chat platforms (Slack, Teams) while keeping business logic platform-agnostic.

Implementations should: - Handle authentication internally - Implement exponential backoff for rate limits - Return descriptive errors for debugging - Be safe for concurrent use

func NewLazySlackService

func NewLazySlackService(repo repository.SlackConfigRepository) ChatService

NewLazySlackService returns a ChatService backed by the given config repo. It is always non-nil; individual calls return ErrSlackNotConfigured when Slack has not been set up yet.

func NewMultiChatService

func NewMultiChatService(providers ...ChatService) ChatService

NewMultiChatService creates a ChatService that fans DMs out to all providers.

func NewSlackService

func NewSlackService(token string) (ChatService, error)

NewSlackService creates a new Slack implementation of ChatService. It validates the token on initialization by calling auth.test.

Parameters:

  • token: Slack bot token (xoxb-...)

Returns:

  • ChatService implementation
  • Error if token is invalid or auth fails

type CreateActionItemParams

type CreateActionItemParams struct {
	Title   string
	Owner   *string
	DueDate *time.Time
}

type CreateCommentParams

type CreateCommentParams struct {
	AuthorID   string
	AuthorName string
	Content    string
}

type CreateIncidentParams

type CreateIncidentParams struct {
	Title       string
	Severity    models.IncidentSeverity
	Description string
	CreatedBy   string // "user", "system", "api"
	AIEnabled   bool   // Controls whether AI agents process this incident. Defaults to true.
}

CreateIncidentParams holds parameters for creating a manual incident

type CreatePostMortemTemplateParams

type CreatePostMortemTemplateParams struct {
	Name        string
	Description string
	Sections    []string
}

type CreateTimelineEntryParams

type CreateTimelineEntryParams struct {
	IncidentID uuid.UUID
	Type       string
	Content    models.JSONB
	ActorType  string
	ActorID    string
}

CreateTimelineEntryParams holds parameters for creating a timeline entry

type EscalationEngine

type EscalationEngine interface {
	TriggerEscalation(alert *models.Alert) error
	TriggerIncidentEscalation(incidentID uuid.UUID, policyID uuid.UUID) error
	EvaluateEscalations() error
	AcknowledgeAlert(alertID uuid.UUID, by string, via models.AcknowledgmentVia) error
	MarkAlertCompleted(alertID uuid.UUID) error
}

EscalationEngine drives the escalation lifecycle for alerts.

Responsibilities:

  • TriggerEscalation: called by the alert processing pipeline when a new alert is linked to a policy.
  • EvaluateEscalations: called by the background worker every 30 s to send notifications and advance timed-out tiers.
  • AcknowledgeAlert: called by the acknowledgment API (Slack, REST, CLI).
  • MarkAlertCompleted: called when an alert resolves before being acknowledged.

func NewEscalationEngine

func NewEscalationEngine(
	repo repository.EscalationPolicyRepository,
	evaluator ScheduleEvaluator,
	notifier EscalationNotifier,
) EscalationEngine

NewEscalationEngine creates a new escalation engine. notifier may be nil when chat is not configured; notifications are skipped.

type EscalationNotifier

type EscalationNotifier interface {
	// SendEscalationDM sends a direct message to userID about the given alert
	// at the specified escalation tier.
	SendEscalationDM(userID string, alert *models.Alert, tierIndex int) error
}

EscalationNotifier sends notifications to on-call users during escalation. The worker implements this via the ChatService.

type GroupingAction

type GroupingAction string

GroupingAction defines the possible grouping actions

const (
	// GroupActionCreateNew means create a new incident for this alert
	GroupActionCreateNew GroupingAction = "create_new"

	// GroupActionLinkToExisting means link this alert to an existing incident
	GroupActionLinkToExisting GroupingAction = "link_to_existing"

	// GroupActionDefault means no rule matched, use default behavior (create new)
	GroupActionDefault GroupingAction = "default"
)

type GroupingDecision

type GroupingDecision struct {
	// Action specifies what to do with this alert
	Action GroupingAction

	// IncidentID is the existing incident to link to (only set if Action = GroupActionLinkToExisting)
	IncidentID *uuid.UUID

	// RuleName is the name of the rule that matched (empty for default action)
	RuleName string

	// GroupKey is the derived key for this alert group
	GroupKey string
}

GroupingDecision represents the result of evaluating an alert against grouping rules

type GroupingEngine

type GroupingEngine interface {
	// EvaluateAlert determines if an alert should create a new incident or link to existing
	EvaluateAlert(alert *models.Alert) (*GroupingDecision, error)

	// RefreshRules reloads grouping rules from the database (called on cache expiry)
	RefreshRules() error
}

GroupingEngine evaluates alerts against grouping rules to determine incident creation/linking

func NewGroupingEngine

func NewGroupingEngine(
	ruleRepo repository.GroupingRuleRepository,
	incidentRepo repository.IncidentRepository,
	db *gorm.DB,
) GroupingEngine

NewGroupingEngine creates a new grouping engine

type HolidayService

type HolidayService struct {
	// contains filtered or unexported fields
}

HolidayService fetches public holidays from ICS feeds and syncs them to the DB.

func NewHolidayService

func NewHolidayService(repo repository.ScheduleRepository) *HolidayService

NewHolidayService creates a new HolidayService.

func (*HolidayService) SyncAll

func (s *HolidayService) SyncAll()

SyncAll refreshes holidays for every schedule that has holiday countries configured.

func (*HolidayService) SyncSchedule

func (s *HolidayService) SyncSchedule(scheduleID uuid.UUID, countries []string) error

SyncSchedule fetches holidays for the given countries and upserts them for scheduleID. Countries not in supportedCountries are silently skipped.

type IncidentService

type IncidentService interface {
	// Alert-triggered incident creation
	CreateIncidentFromAlert(alert *models.Alert, aiEnabled bool) (*models.Incident, error)
	CreateIncidentFromAlertWithGrouping(alert *models.Alert, groupKey string, aiEnabled bool) (*models.Incident, error)
	LinkAlertToExistingIncident(alert *models.Alert, incidentID uuid.UUID) error
	ShouldCreateIncident(severity models.AlertSeverity) bool
	CreateSlackChannelForIncident(incident *models.Incident, alerts []models.Alert) error

	// API operations
	ListIncidents(filters repository.IncidentFilters, pagination repository.Pagination) ([]models.Incident, int64, error)
	GetIncident(id uuid.UUID, number int) (*models.Incident, error)
	GetIncidentBySlackChannelID(channelID string) (*models.Incident, error)
	GetIncidentBySlackMessageTS(messageTS string) (*models.Incident, error)
	CreateIncident(params *CreateIncidentParams) (*models.Incident, error)
	UpdateIncident(id uuid.UUID, params *UpdateIncidentParams) (*models.Incident, error)
	AcknowledgeIncident(id uuid.UUID, actorType, actorID string) error
	ResolveIncident(id uuid.UUID, actorType, actorID string) error
	GetIncidentAlerts(incidentID uuid.UUID) ([]models.Alert, error)
	GetIncidentTimeline(incidentID uuid.UUID, pagination repository.Pagination) ([]models.TimelineEntry, int64, error)
	CreateTimelineEntry(params *CreateTimelineEntryParams) (*models.TimelineEntry, error)

	// Generic status transition used by bots/integrations (e.g. Slack reactions).
	UpdateIncidentStatus(id uuid.UUID, status models.IncidentStatus, actorType, actorID string) error

	// Slack notifications
	PostStatusUpdateToSlack(incident *models.Incident, previousStatus, newStatus models.IncidentStatus) error

	// AI features (v0.6+)
	// GenerateAISummary generates an AI summary for an incident and persists it.
	GenerateAISummary(incident *models.Incident) (string, error)
	// GenerateHandoffDigest generates a shift handoff digest for an incident (not persisted).
	GenerateHandoffDigest(incident *models.Incident) (string, error)
}

IncidentService defines the interface for incident operations

func NewIncidentService

func NewIncidentService(
	incidentRepo repository.IncidentRepository,
	timelineRepo repository.TimelineRepository,
	alertRepo repository.AlertRepository,
	chatService ChatService,
	db *gorm.DB,
) IncidentService

NewIncidentService creates a new incident service

type LocalAuthService

type LocalAuthService interface {
	Login(email, password string) (*models.LocalSession, error)
	Logout(token string) error
	GetSessionUser(token string) (*models.User, error)
	CreateUser(email, name, password string, role models.UserRole) (*models.User, string, error)
	UpdateUser(id uuid.UUID, name string, role models.UserRole, newPassword string) error
	ResetPassword(id uuid.UUID) (string, error)
	DeactivateUser(id uuid.UUID) error
	GetUser(id uuid.UUID) (*models.User, error)
	ListUsers() ([]models.User, error)
	CountUsers() (int64, error)
	CountAdmins() (int64, error)
	// GetUserByEmail returns the user with the given email, regardless of auth source.
	// Used by middleware to resolve a SAML JWT session to a DB user record.
	GetUserByEmail(email string) (*models.User, error)
	// LoginByUserID creates a session for the given user ID without a password check.
	// Used by Slack OAuth login after the user's identity is verified by Slack.
	LoginByUserID(id uuid.UUID) (*models.LocalSession, error)
	// UpdateUserSlackID sets or clears the Slack member ID for a user.
	// Pass nil to clear, or a pointer to an ID string like "U0AJLLY3678".
	UpdateUserSlackID(id uuid.UUID, slackUserID *string) error
	// UpdateUserTeamsID sets or clears the Azure AD Object ID for a user.
	// Pass nil to clear, or a pointer to the AAD Object ID string.
	UpdateUserTeamsID(id uuid.UUID, teamsUserID *string) error
}

LocalAuthService handles local email/password authentication.

type Message

type Message struct {
	// Text is the plain text fallback content
	// Required for notifications and accessibility
	Text string

	// Blocks contains platform-specific rich content blocks
	// For Slack: []slack.Block
	// For Teams: []teams.MessageBlock
	// Stored as []interface{} to remain platform-agnostic
	Blocks []interface{}

	// ThreadTS is the parent message timestamp for threaded replies
	// Empty string for top-level messages
	ThreadTS string
}

Message represents a chat message with platform-agnostic content. Implementations should convert Blocks to their platform's format.

type PostMortemService

type PostMortemService interface {
	// Templates
	ListTemplates() ([]models.PostMortemTemplate, error)
	GetTemplate(id uuid.UUID) (*models.PostMortemTemplate, error)
	CreateTemplate(params *CreatePostMortemTemplateParams) (*models.PostMortemTemplate, error)
	UpdateTemplate(id uuid.UUID, params *UpdatePostMortemTemplateParams) (*models.PostMortemTemplate, error)
	DeleteTemplate(id uuid.UUID) error

	// Post-mortems
	GetPostMortem(incidentID uuid.UUID) (*models.PostMortem, error)
	GeneratePostMortem(incident *models.Incident, templateID *uuid.UUID, createdByID string) (*models.PostMortem, error)
	UpdatePostMortem(id uuid.UUID, params *UpdatePostMortemParams) (*models.PostMortem, error)

	// Post-mortem manual creation
	CreatePostMortem(incidentID uuid.UUID, createdByID string) (*models.PostMortem, error)

	// AI enhance
	EnhancePostMortem(pm *models.PostMortem, content string) (*models.PostMortem, error)

	// Comments
	ListComments(postMortemID uuid.UUID) ([]models.PostMortemComment, error)
	CreateComment(postMortemID uuid.UUID, params *CreateCommentParams) (*models.PostMortemComment, error)
	DeleteComment(id uuid.UUID) error

	// Action items
	CreateActionItem(postMortemID uuid.UUID, params *CreateActionItemParams) (*models.ActionItem, error)
	UpdateActionItem(id uuid.UUID, params *UpdateActionItemParams) (*models.ActionItem, error)
	DeleteActionItem(id uuid.UUID) error
}

PostMortemService manages post-mortem templates, documents, and action items.

type ProcessingResult

type ProcessingResult struct {
	Received         int // Total alerts received in payload
	Created          int // New alerts created
	Updated          int // Existing alerts updated
	IncidentsCreated int // New incidents created from alerts
}

ProcessingResult holds statistics about webhook processing

type RoutingDecision

type RoutingDecision struct {
	// Suppress means: store the alert but do not create an incident
	Suppress bool

	// SeverityOverride replaces the alert's severity for incident creation (empty = no override)
	SeverityOverride string

	// ChannelOverride replaces the auto-generated Slack channel name suffix (empty = no override)
	ChannelOverride string

	// RuleName is the name of the rule that matched (empty if no rule matched)
	RuleName string

	// EscalationPolicyID is the escalation policy to trigger for this alert (nil = no escalation).
	// Set from the "escalation_policy_id" key in the matching rule's actions JSONB.
	EscalationPolicyID *uuid.UUID

	// AIEnabled controls whether AI agents process the resulting incident.
	// Defaults to true; can be set false via the "ai_enabled" key in the rule's actions JSONB.
	AIEnabled bool
}

RoutingDecision represents the result of evaluating an alert against routing rules

type RoutingEngine

type RoutingEngine interface {
	// EvaluateAlert returns the routing decision for a given alert
	EvaluateAlert(alert *models.Alert) (*RoutingDecision, error)

	// RefreshRules reloads routing rules from the database
	RefreshRules() error
}

RoutingEngine evaluates alerts against routing rules to determine incident routing behavior

func NewRoutingEngine

func NewRoutingEngine(ruleRepo repository.RoutingRuleRepository) RoutingEngine

NewRoutingEngine creates a new routing engine

type ScheduleEvaluator

type ScheduleEvaluator interface {
	// WhoIsOnCall returns the username on call for the given schedule at the given time.
	// Returns ("", nil) if the schedule has no layers/participants configured.
	WhoIsOnCall(scheduleID uuid.UUID, at time.Time) (string, error)

	// GetTimeline returns the on-call schedule broken into contiguous segments
	// for the window [from, to). Defaults to next 7 days if from/to are zero.
	GetTimeline(scheduleID uuid.UUID, from, to time.Time) ([]TimelineSegment, error)

	// GetLayerTimelines returns per-layer timelines and the effective merged timeline.
	// layerTimelines is keyed by layer UUID. Each layer is computed independently
	// (no override application). effective is the fully-merged timeline (same as GetTimeline).
	GetLayerTimelines(scheduleID uuid.UUID, from, to time.Time) (layerTimelines map[uuid.UUID][]TimelineSegment, effective []TimelineSegment, err error)
}

ScheduleEvaluator computes who is on call at any given time.

func NewScheduleEvaluator

func NewScheduleEvaluator(repo repository.ScheduleRepository) ScheduleEvaluator

NewScheduleEvaluator creates a new schedule evaluator.

type SlackEventHandler

type SlackEventHandler struct {
	// contains filtered or unexported fields
}

SlackEventHandler listens for Slack events via Socket Mode (WebSocket) and dispatches them to the appropriate handlers. Socket Mode avoids needing a public URL or SSL certificate — it uses an outbound WebSocket connection.

func NewSlackEventHandler

func NewSlackEventHandler(
	appToken string,
	botToken string,
	incidentService IncidentService,
	chatService ChatService,
	userRepo repository.UserRepository,
	pmRepo repository.PostMortemRepository,
) (*SlackEventHandler, error)

NewSlackEventHandler creates a Socket Mode event handler. Requires both SLACK_APP_TOKEN (xapp-...) and SLACK_BOT_TOKEN (xoxb-...).

func (*SlackEventHandler) SetAIService

func (h *SlackEventHandler) SetAIService(ai AIService)

SetAIService wires an optional AI service into the handler for @mention responses.

func (*SlackEventHandler) Start

func (h *SlackEventHandler) Start()

Start begins the Socket Mode connection and event listener in background goroutines. It returns immediately; events are processed asynchronously.

type SlackMessageBuilder

type SlackMessageBuilder struct{}

SlackMessageBuilder constructs rich Slack messages using Block Kit.

func NewSlackMessageBuilder

func NewSlackMessageBuilder() *SlackMessageBuilder

NewSlackMessageBuilder creates a new SlackMessageBuilder instance.

func (*SlackMessageBuilder) BuildAlertLinkedMessage

func (b *SlackMessageBuilder) BuildAlertLinkedMessage(alert *models.Alert, incident *models.Incident) Message

BuildIncidentUpdatedMessage rebuilds the incident message with status-aware buttons. Used to update the original pinned message when status changes from any source.

BuildAlertLinkedMessage creates a message for when an alert is linked to an existing incident. This is used for grouped alerts (v0.3+) to notify that a new alert has been added to the incident.

func (*SlackMessageBuilder) BuildEscalationDMMessage

func (b *SlackMessageBuilder) BuildEscalationDMMessage(alert *models.Alert, tierIndex int) Message

BuildEscalationDMMessage creates the Slack DM sent to an on-call user when an alert escalates to their tier. Includes an "Acknowledge" interactive button whose action_id encodes the alert ID so the Slack event handler can route it to POST /api/v1/alerts/:id/acknowledge.

tierIndex is 0-based; the display shows "Tier N" where N = tierIndex+1.

func (*SlackMessageBuilder) BuildIncidentCreatedMessage

func (b *SlackMessageBuilder) BuildIncidentCreatedMessage(
	incident *models.Incident,
	alerts []models.Alert,
) Message

BuildIncidentCreatedMessage creates a rich Block Kit message for a new incident. The message includes: - Header with severity emoji and incident title - Incident details (severity, status, created time) - Linked alerts (if any) - Action buttons (Acknowledge, Resolve)

func (*SlackMessageBuilder) BuildIncidentUpdatedMessage

func (b *SlackMessageBuilder) BuildIncidentUpdatedMessage(incident *models.Incident) Message

Button rules:

  • triggered: Acknowledge + Resolve
  • acknowledged: Resolve only
  • resolved/canceled: no buttons

func (*SlackMessageBuilder) BuildShiftChannelNotification

func (b *SlackMessageBuilder) BuildShiftChannelNotification(
	scheduleName, layerName, outgoingUser, incomingUser string,
	shiftEnd time.Time,
) Message

BuildShiftChannelNotification creates a channel post announcing a shift handoff. Posted to schedule.NotificationChannel when configured.

func (*SlackMessageBuilder) BuildShiftHandoffIncomingMessage

func (b *SlackMessageBuilder) BuildShiftHandoffIncomingMessage(
	scheduleName, layerName string,
	shiftEnd time.Time,
) Message

BuildShiftHandoffIncomingMessage creates a DM for the user who is starting their on-call shift.

func (*SlackMessageBuilder) BuildShiftHandoffOutgoingMessage

func (b *SlackMessageBuilder) BuildShiftHandoffOutgoingMessage(
	scheduleName, layerName, incomingUser string,
) Message

BuildShiftHandoffOutgoingMessage creates a DM for the user whose on-call shift has ended.

func (*SlackMessageBuilder) BuildStatusUpdateMessage

func (b *SlackMessageBuilder) BuildStatusUpdateMessage(
	incident *models.Incident,
	previousStatus models.IncidentStatus,
	newStatus models.IncidentStatus,
) Message

BuildStatusUpdateMessage creates a message for incident status changes

type SlackValidator

type SlackValidator struct {
	// contains filtered or unexported fields
}

SlackValidator validates Slack configuration and permissions.

func NewSlackValidator

func NewSlackValidator(token string) *SlackValidator

NewSlackValidator creates a new Slack validator.

func (*SlackValidator) ValidateConfiguration

func (v *SlackValidator) ValidateConfiguration() error

ValidateConfiguration performs complete validation of Slack configuration. This is a convenience method that calls both ValidateToken and ValidateScopes.

func (*SlackValidator) ValidateScopes

func (v *SlackValidator) ValidateScopes() error

ValidateScopes validates that the bot has the required OAuth scopes. Since auth.test doesn't return scopes directly, we verify by attempting operations that require each scope where possible without side effects.

Required scopes: - channels:manage - Create and manage channels - channels:read - Read channel information - chat:write - Post messages

Note: channels:manage and chat:write cannot be validated without side effects (creating channels/posting messages). These scopes will be validated on first use, and the Slack API will return clear error messages if they are missing.

func (*SlackValidator) ValidateToken

func (v *SlackValidator) ValidateToken() error

ValidateToken validates the Slack bot token by calling auth.test. This verifies the token is valid and active.

type TeamMember

type TeamMember struct {
	UserID      string // AAD Object ID — stable identifier for proactive DMs (teams_user_id)
	DisplayName string
	Email       string
}

TeamMember is a member of the Teams team as returned by the Graph API.

func ListTeamMembers

func ListTeamMembers(ctx context.Context, appID, appPassword, tenantID, teamID string) ([]TeamMember, error)

ListTeamMembers fetches all members of the configured team from the Graph API. Intended for the "Import from Teams" UI flow; does not require a full TeamsService.

type TeamsEventHandler

type TeamsEventHandler struct {
	// contains filtered or unexported fields
}

TeamsEventHandler processes inbound Bot Framework activities from Teams. It is wired to POST /api/v1/webhooks/teams by the Gin router. JWT authentication is handled by middleware/teams_auth.go before this handler is called.

func NewTeamsEventHandler

func NewTeamsEventHandler(appID string, incidentSvc IncidentService, incidentRepo repository.IncidentRepository, timelineRepo repository.TimelineRepository, teamsSvc *TeamsService) *TeamsEventHandler

NewTeamsEventHandler creates a handler for inbound Teams Bot Framework activities.

func (*TeamsEventHandler) Handle

func (h *TeamsEventHandler) Handle(ctx context.Context, activity BotActivity)

Handle processes an inbound Bot Framework activity payload.

type TeamsService

type TeamsService struct {
	// contains filtered or unexported fields
}

TeamsService implements ChatService using Microsoft Graph API for channel management and Bot Framework REST API for posting messages to channels.

Why two clients?

Graph API (ChannelMessage.Send) only works with delegated (user) auth — app-only
tokens get a 403. Bot Framework Proactive Messaging uses a separate OAuth scope
(https://api.botframework.com/.default) that does work with client credentials,
giving us true Slack parity: same bot, same credentials, no per-channel setup.

Client split:

graphClient  — Graph API (create/archive channels, DMs, team info)
botfwClient  — Bot Framework REST API (post/update messages in channels)

func NewTeamsService

func NewTeamsService(ctx context.Context, appID, appPassword, tenantID, teamID, botUserID, serviceURL string) (*TeamsService, error)

NewTeamsService creates a TeamsService with two authenticated HTTP clients. serviceURL is the Bot Framework relay endpoint for the tenant. It can be found in any inbound Bot Framework activity's serviceUrl field. Defaults to the US region endpoint if empty; set TEAMS_SERVICE_URL for other regions (EU: smba.trafficmanager.net/emea/).

func (*TeamsService) ArchiveChannel

func (s *TeamsService) ArchiveChannel(channelID string) error

ArchiveChannel best-effort renames the channel to mark it as resolved. Graph API does not support archiving standard channels (only private channels), so this is a rename-only operation. Always returns nil — the error is logged internally. Callers should not check the return value; the incident is already resolved regardless of whether the rename succeeds.

func (*TeamsService) CreateChannel

func (s *TeamsService) CreateChannel(name, description string) (*Channel, error)

func (*TeamsService) GetThreadMessages

func (s *TeamsService) GetThreadMessages(channelID, threadTS string) ([]string, error)

func (*TeamsService) InviteUsers

func (s *TeamsService) InviteUsers(channelID string, userIDs []string) error

func (*TeamsService) PostMessage

func (s *TeamsService) PostMessage(channelID string, msg Message) (string, error)

PostMessage sends a message to a Teams channel via the ChatService interface. Internally this creates a new Bot Framework conversation in the channel and posts to it. Callers that need the conversationID for future updates should use PostToChannel instead.

func (*TeamsService) PostToChannel

func (s *TeamsService) PostToChannel(teamsChannelID string, msg Message) (conversationID, activityID string, err error)

PostToChannel creates a new Bot Framework conversation in the given Teams channel and posts a message to it in a single API call. Returns both the conversationID and activityID so callers can store both for future PostToConversation and UpdateConversationMessage calls.

Use this for the initial message when creating an incident channel.

func (*TeamsService) PostToConversation

func (s *TeamsService) PostToConversation(conversationID string, msg Message) (string, error)

PostToConversation posts a new message to an existing Bot Framework conversation. Use this for status updates, timeline notes, and bot replies after the initial PostToChannel has established the conversationID.

func (*TeamsService) SendDirectMessage

func (s *TeamsService) SendDirectMessage(userID string, msg Message) error

func (*TeamsService) SendOnCallDM

func (s *TeamsService) SendOnCallDM(aadObjectID string, incident *models.Incident, appURL string) error

SendOnCallDM sends a proactive Adaptive Card DM to the on-call responder via Bot Framework. aadObjectID is the Azure AD Object ID stored in users.teams_user_id. Unlike SendDirectMessage (which uses Graph API chat threads), this uses the Bot Framework relay so it works with the same client credentials scope — no Chat.ReadWrite delegation needed.

func (*TeamsService) UpdateConversationMessage

func (s *TeamsService) UpdateConversationMessage(conversationID, activityID string, msg Message) error

UpdateConversationMessage updates an existing message in a Bot Framework conversation. Used to keep the root incident Adaptive Card in sync with the current status.

func (*TeamsService) UpdateMessage

func (s *TeamsService) UpdateMessage(channelID, messageID string, msg Message) error

UpdateMessage updates an existing message. For Bot Framework updates the caller must pass the conversationID as channelID and the activityID as messageID. Used by postStatusUpdateToTeams via UpdateConversationMessage.

type TelegramService

type TelegramService struct {
	// contains filtered or unexported fields
}

TelegramService sends incident notification messages to a Telegram group/channel. Notification-only: no channel creation, no inbound command handling in v1.

func NewTelegramService

func NewTelegramService(botToken, chatID, appURL string) *TelegramService

NewTelegramService constructs a TelegramService. Returns nil if token or chatID is empty.

func NewTelegramServiceFromConfig

func NewTelegramServiceFromConfig(cfg *models.TelegramConfig, appURL string) *TelegramService

NewTelegramServiceFromConfig constructs from a DB config row. Returns nil if not configured.

func (*TelegramService) SendAISummary

func (s *TelegramService) SendAISummary(incident *models.Incident, summary string) error

SendAISummary posts the AI-generated summary for an incident to Telegram.

func (*TelegramService) SendIncidentCreated

func (s *TelegramService) SendIncidentCreated(incident *models.Incident) error

SendIncidentCreated posts an incident creation notification.

func (*TelegramService) SendStatusUpdate

func (s *TelegramService) SendStatusUpdate(incident *models.Incident, newStatus string) error

SendStatusUpdate posts a status change notification.

type TimelineSegment

type TimelineSegment struct {
	// Start is the beginning of this segment (inclusive).
	Start time.Time `json:"start"`
	// End is the end of this segment (exclusive).
	End time.Time `json:"end"`
	// UserName is the on-call user for this segment.
	UserName string `json:"user_name"`
	// IsOverride is true when this segment is covered by an explicit override.
	IsOverride bool `json:"is_override"`
	// LayerID is the layer this segment belongs to. nil for effective/merged timeline.
	LayerID *uuid.UUID `json:"layer_id,omitempty"`
}

TimelineSegment represents a contiguous window during which a single user is on call.

type UpdateActionItemParams

type UpdateActionItemParams struct {
	Title   *string
	Owner   *string
	DueDate *time.Time
	Status  *models.ActionItemStatus
}

type UpdateIncidentParams

type UpdateIncidentParams struct {
	Status      models.IncidentStatus
	Severity    models.IncidentSeverity
	Summary     string
	UpdatedBy   string
	ClientIP    string     // For audit logging
	AIEnabled   *bool      // Controls whether AI agents process this incident. nil = no change.
	CommanderID *uuid.UUID // nil = no change
}

UpdateIncidentParams holds parameters for updating an incident

type UpdatePostMortemParams

type UpdatePostMortemParams struct {
	Content *string
	Status  *models.PostMortemStatus
}

type UpdatePostMortemTemplateParams

type UpdatePostMortemTemplateParams struct {
	Name        *string
	Description *string
	Sections    []string
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL